前言

去年帮一个团队把Qwen2.5-72B部署到昇腾910B集群上。模型能跑,但decode吞吐320 TPS,客户说GPU上能到1800。差了5倍多。

跑msprof一看:算子执行占41%,算子调度开销23%,显存申请释放15%,Host-NPU搬运12%,Stream同步9%。NPU的Cube/Vector单元有59%的时间在等——等调度、等显存、等数据。

装上ATB,开FlashAttention + PagedAttention + W8A16量化,吞吐从320拉到1680 TPS。涨了425%。

不是昇腾算力不够,是默认配置没打开加速特性。

Transformer 推理瓶颈到底在哪

大模型推理分两个阶段:Prefill(首token生成,计算密集)和Decode(后续token逐个生成,访存密集)。

Prefill阶段,一次处理整个输入序列,矩阵乘的计算量大,Cube Unit是瓶颈。Decode阶段,每次只处理1个token,但要从HBM读整个KV Cache,HBM带宽是瓶颈。

实测数据(Qwen2.5-7B,910B单卡,FP16):

阶段 耗时占比 瓶颈
Attention(Prefill) 35% Cube(QK^T计算量大)
KV Cache读取(Decode) 42% HBM带宽(每token读57KB)
MoE Router + AllReduce 12% 通信 + 动态路由
算子调度 + 显存管理 11% Runtime开销

Decode阶段42%的时间在读KV Cache。为什么?7B模型FP16下每个token的KV Cache约57KB,batch=8、seq=2048时就是57KB × 2048 × 8 = 912MB。每生成一个token都要读一遍这912MB,910B的HBM带宽1.2TB/s,光读KV Cache就要0.76ms。

很多人以为推理优化就是"算子写快点",其实Decode阶段算子根本不是瓶颈——HBM带宽才是。

工程经验: Qwen2.5-72B在910B 4卡上,Decode阶段HBM带宽利用率只有38%。原因?KV Cache按连续地址分配,但推理时不同batch的序列长度不同,Cache里大量空洞。开了PagedAttention之后带宽利用率拉到79%,吞吐直接翻倍。

为什么 GPU 优化方案不能直接复用

三个核心差异:

1. 计算单元架构不同

GPU的SM能同时跑矩阵乘和逐元素运算。FasterTransformer可以把QK^T + Softmax + P×V融成一个巨型Kernel,中间结果全塞L2缓存(几十MB)。

昇腾是Cube干矩阵乘、Vector干逐元素,两个单元之间数据走L1缓存(1MB)。不能融成一个巨型Kernel——L1装不下,必须按Cube/Vector边界切分。

2. 内存层次不同

GPU的L2缓存是全局共享的,所有SM都能访问。昇腾的L1是每个AI Core独立的,跨核通信走HCCL。

这意味着GPU上的Tiling策略可以直接按L2容量规划,昇腾上必须按L1容量(1MB)规划tile大小。同一个FlashAttention,GPU上tile_q可以开到128,昇腾上只能开到64。

3. 调度模型不同

GPU的CUDA Stream调度是硬件级的(SM自动分配),开销<1μs。昇腾的ACL调度走软件调用链(ACL→GE→Runtime),开销12-15μs。7个小算子不融合,光调度就吃3ms。

为什么这样设计?因为达芬奇架构的Cube和Vector是物理独立的执行单元,不像SM是统一调度器管理。Cube/Vector的协作必须软件编排,这既是灵活性的来源(可以精细化控制流水线),也是调度开销的来源。

工程经验: GPU上优化MatMul就够(Cube利用率高),昇腾上要同时优化Cube和Vector——如果Vector太慢(比如softmax算得慢),Cube在等Vector,利用率掉一半。ATB的融合Kernel就是按这个原则设计的:Cube连续的计算塞一个Kernel,Vector操作批量处理,中间靠L1缓存桥接。

ATB 的核心能力

ATB在CANN架构中的定位:ops-transformer提供"砖头"(FlashAttention、MoE等基础算子),ATB把砖头搭成"墙"(融合算子编排),MindIE拿去盖"房子"(完整推理引擎)。

ATB管五件事:

能力 解决的问题 性能收益
Attention优化 QK^T + Softmax + P×V调度开销大 融合成1个Kernel,调度从3ms→0.05ms
KV Cache管理 显存碎片 + 带宽利用率低 PagedAttention消碎片,带宽利用率38%→79%
算子融合 LayerNorm+MatMul+残差反复读写HBM 融合后中间数据走L1,省3次HBM读写
动态Shape适配 不同batch/seq的shape不固定 运行时自动选Tiling策略
MoE加速 Router动态路由 + 通信瓶颈 GatingTopK融合,只算被激活的expert

Attention 优化

ATB封装了ops-transformer的FlashAttention,并针对达芬奇架构做了Cube/Vector流水线优化。

没有ATB时,Attention的一次前向要走7次ACL调用:

# 不复用ATB:7次ACL调用
Q = torch.matmul(x, Wq)  # ACL调用1
K = torch.matmul(x, Wk)  # ACL调用2
V = torch.matmul(x, Wv)  # ACL调用3
S = torch.matmul(Q, K.transpose(-2, -1))  # ACL调用4
S = S * scale + mask  # Vector运算,ACL调用5
P = torch.softmax(S, dim=-1)  # Vector运算,ACL调用6
O = torch.matmul(P, V)  # ACL调用7

# 7次调用,中间结果(Q、K、V、S、P)全部写回HBM再读出来

ATB的做法:把QKV投影融成1个MatMul(Q/K/V的输入都是x,权重拼接),再把S→P→O融成1个Kernel(FlashAttention)。总共2次ACL调用,中间结果走L1不落HBM。

# 用ATB:2次ATB调用,中间结果走L1
import torch_npu
from atb import AttentionOperation

# QKV投影融合(一次MatMul)
qkv_proj = AttentionOperation(
    type="QKVProjection",
    hidden_size=4096,
    head_num=32,
    head_dim=128,
)
q, k, v = qkv_proj.Run(x, [Wq, Wk, Wv], workspace)

# FlashAttention融合(一次Kernel)
attn_op = AttentionOperation(
    type="FlashAttention",
    head_num=32,
    head_dim=128,
    use_prefill_cache=True,
    sliding_window=4096,
)
output = attn_op.Run(q, k, v, mask, past_kv)

实测性能(Ascend 910B,FP16):

模型 未融合 ATB融合 提升
Qwen2.5-7B (seq=2048) 34 tokens/s 89 tokens/s +162%
Qwen2.5-72B (seq=4096, 4卡) 320 TPS 890 TPS +178%
DeepSeek-V3 (seq=4096) 580 TPS 1420 TPS +145%

为什么QKV投影能融成一个MatMul?因为Q、K、V的输入都是同一个x,三个权重矩阵 [d×d_q, d×d_k, d×d_v] 可以横向拼接成 [d×(d_q+d_k+d_v)],一次矩阵乘搞定。这招在GPU上也用,但昇腾上收益更大——省了2次ACL调用(24-30μs),GPU上只省了2次Kernel Launch(~2μs)。

工程经验: DeepSeek-V4有CSA(4倍压缩)和HCA(128倍压缩)两种Attention模式。128K序列下HCA把KV Cache从7.3GB压到57MB,TPOT < 10ms。但短序列(<4K)HCA反而比Full Attention慢12%——Compressor本身有计算开销。ATB的做法是按序列长度动态选策略:seq < 2048用Full Attention,2048-8192用CSA,>8192用HCA。

KV Cache 管理

KV Cache是Decode阶段最大的显存占用者。7B模型FP16,seq=2048、batch=32,KV Cache占3.6GB。

ATB提供三条优化路径:

1. PagedAttention

KV Cache分block(默认16 token一个block),按需分配,消碎片。没有PagedAttention时,不同batch的序列长度不同,连续分配会产生大量空洞。实测显存碎片率30-40%。开了PagedAttention碎片率降到<5%。

# MindIE配置:开PagedAttention
from mindie import MindIERuntime, ModelConfig

config = ModelConfig(
    model_path="/path/to/qwen2.5-72b",
    # PagedAttention:block_size=16
    kv_cache_mode="paged",
    block_size=16,
    # 预分配block池(避免运行时分配开销)
    paged_pool_size_gb=32,
)
runtime = MindIERuntime(config)
2. KV Cache量化

INT8存KV Cache,显存省一半。910B实测:3.6GB → 1.8GB,精度损失 < 0.3%(PPL变化 < 0.01)。

# KV Cache量化配置
config = ModelConfig(
    # KV Cache量化:INT8
    kv_cache_dtype="int8",
    # 可选:前几层FP16、后面INT8(混合策略)
    kv_cache_quant_layers="4:",  # 第4层开始量化
)

工程经验: KV Cache量化有个坑——不同层的KV Cache对量化的敏感度不一样。前几层(靠近embedding)的KV Cache量化后精度影响大,后面层影响小。我们试过"前4层FP16、后面INT8"的混合策略,精度和全INT8一样,但显存多省10%。ATB目前没内置这个策略,得自己改。

3. KV Cache压缩(DeepSeek-V4专用)

HCA 128倍压缩,128K序列KV Cache 57MB。

实测(Qwen2.5-7B,910B单卡,FP16):

配置 Batch 吞吐(tokens/s) 显存(GB)
无优化 8 72 20.1
+ PagedAttention 16 181 24.3
+ KV Cache INT8 + PagedAttention 32 294 28.1
+ KV Cache INT8 + PagedAttention + W8A16 48 318 31.2

算力没变,显存够用了,batch从8涨到48,吞吐涨341%。

算子融合

昇腾上每次算子下发走ACL→GE→Runtime调用链,开销12-15μs。30层Transformer几百次调用,光调度开销3-5ms。

ATB的融合策略:

LayerNorm + MatMul + 残差

三层算子融成一个Kernel。LayerNorm输出直接喂MatMul,MatMul输出加残差,中间数据走L1不落HBM。省2次HBM读写 + 2次ACL调用。

GatingTopK(MoE专用)

Router输出 + TopK + Expert分发融成一个Kernel。不融合时Router输出落HBM再给TopK读,融合后直接L1传数据。单层省18μs,40层省720μs。

SAS(DeepSeek-V4专用)

统一Window/Sparse/Compress Attention的融合Kernel。不同Attention类型共用同一个Kernel框架,按参数切换模式,省去Kernel切换开销。

// graph-autofusion配置(GE环境变量)
export GE_FUSION_PASS_MODE=aggressive  // 激进融合策略
export GE_ENABLE_OP_FUSION=1          // 开算子融合
export ATB_FUSION_MODE=aggressive     // ATB也开激进融合

// 融合效果验证:看GE编译日志
// 搜索"Fusion success",看哪些算子融了
grep "Fusion success" ge_compile.log | wc -l

实测融合收益(Qwen2.5-7B,seq=2048):

融合方案 省掉的HBM读写 省掉的ACL调用 性能提升
QKV投影融合 2次 2次 +8%
FlashAttention 3次 4次 +162%
LayerNorm+MatMul+残差 2次 2次 +15%
GatingTopK(MoE) 1次 2次 +22%

为什么不能把所有算子都融成一个巨型Kernel?两个原因:一是L1只有1MB,整层Transformer的中间结果装不下;二是Cube和Vector是物理独立的,跨单元的数据传递必须显式编排流水线,不像GPU的SM内部寄存器直接传。

动态 Shape

大模型推理的输入shape不是固定的——不同请求的序列长度不同,MoE每层的expert激活模式也不同。

ATB的做法:编译时生成多套Tiling策略,运行时按shape选最优的一套。

# 动态Shape配置(ATB)
from atb import DynamicShapeConfig

dynamic_config = DynamicShapeConfig(
    # 定义shape范围
    shape_ranges={
        "seq_len": [(1, 128), (129, 512), (513, 2048), (2049, 8192)],
        "batch": [(1, 1), (2, 4), (5, 16), (17, 64)],
    },
    # 为每个range生成对应的Tiling参数
    gen_tiling_for_ranges=True,
    # 运行时自动选最优Tiling
    auto_select_tiling=True,
)

编译阶段:

  • shape_range = [(1,128), (129,512), (513,2048), (2049,8192)]
  • 为每个range生成对应的Tiling参数和Kernel二进制

运行阶段:

  • 实际shape = (1, 1024)
  • → 落在(513, 2048)这个range
  • → 用第3套Tiling策略

为什么这样设计?因为Tiling参数影响Cube/Vector的利用率和L1缓存命中率。seq=128时tile_q=32就够了,seq=8192时tile_q要开到128才能填满Cube的MAC阵列。一套Tiling策略适配所有shape,要么短序列浪费算力,要么长序列L1溢出。

工程经验: 动态Shape有个隐藏开销——Tiling策略切换需要重新加载Kernel二进制(~50μs)。如果请求的序列长度频繁跳档(比如一会儿100 token,一会儿8000 token),切换开销会吃掉5-8%的吞吐。我们用"分桶策略"缓解:把请求按序列长度分到固定几个桶(0-256, 256-1024, 1024-4096),桶内统一padding到桶的上限,避免频繁切换。

Batch 调优

BatchSize对吞吐的影响不是线性的。

实测数据(Qwen2.5-7B,910B单卡,FP16,ATB全开):

BatchSize 吞吐(tokens/s) TTFT(ms) 显存(GB) HBM带宽利用率
1 38 4 5.2 21%
4 127 8 16.8 51%
8 178 12 20.1 68%
16 231 20 29.3 79%
32 268 38 39.2 85%
64 244 72 OOM 81%

BatchSize=32是拐点。再大,KV Cache占显存太多,开始swap到Host内存,HBM带宽利用率反而降了。

为什么不是越大越好?Decode阶段每个token都要读整个KV Cache。batch越大,要读的KV Cache越多,HBM带宽撑不住。batch=32时HBM带宽利用率85%,接近理论上限;batch=64时KV Cache太大,部分swap到Host内存,带宽利用率反而掉到81%。

场景选型:

  • 在线对话(TTFT < 200ms):batch=4-8
  • 离线批处理(吞吐优先):batch=16-32

工程经验: Batch调优不是只看吞吐和延迟。batch=32时吞吐最高,但如果请求的序列长度方差大(有的50 token,有的4000 token),短请求等长请求的时间会拉高P99延迟。我们实际部署时用batch=16,牺牲13%吞吐换P99延迟降40%。调优要看业务指标,不是看NPU指标。

踩坑实录

坑1:ATB融合后精度掉0.5%(Qwen2.5-72B,WikiText-2困惑度)

原因:LayerNorm+MatMul融合的时候,LayerNorm的epsilon参数(1e-5)在FP16下下溢成0,导致除零(近似)。

解决:设export ATB_LAYERNORM_EPSILON=1e-3(用更大的epsilon,避免下溢)。精度掉回到0.1%以内,性能不变。

坑2:MoE模型(Mixtral 8×7B)开ATB后吞吐反而掉20%

原因:expert数量太多(8个),ATB的GatingTopK融合在expert>4的时候反而不如逐算子调用(router的开销占比大)。

解决:expert数量>4的时候关掉MoE融合(export ATB_MOE_FUSION=0),只开Attention和FFN融合。关完吞吐涨25%。

坑3:batch=1推理,ATB融合反而慢10%

原因:batch=1的时候M维度太小(seq_len=1),Cube利用率掉得厉害,融合kernel的启动开销占比大。

解决:batch=1的时候关融合(export ATB_FUSION_MODE=none)。batch>=8再开融合。

坑4:动态Shape的Tiling切换开销吃掉5-8%吞吐

序列长度频繁跳档,Tiling策略频繁切换,每次切换~50μs。

解决:用分桶策略(bucket strategy),把请求按序列长度分到固定几个桶,桶内统一padding,避免频繁切换。export ATB_DYNAMIC_SHAPE_BUCKET="0-256,256-1024,1024-4096,4096-16384"

https://atomgit.com/cann/ascend-transformer-boost

https://atomgit.com/cann/ops-transformer

https://atomgit.com/cann/cann-recipes-infer

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐