用昇腾 NPU 跑大模型CANN 的 FlashAttention 算子到底帮了多少忙帮一个朋友排查推理服务的时候我发现他的服务配置完全没问题——模型量化过了batch size 也调了昇腾 NPU 利用率只有 60% 出头。瓶颈在哪Attention 层。每次算注意力NPU 都要停下来等显存数据搬来搬去。后来帮他换了ops-transformer仓库里的 FlashAttention 算子利用率直接飙到 92%吞吐翻了两倍多。他把这个过程整理成了一份踩坑笔记我结合ops-transformer仓库的实际代码帮你从头走一遍。先搞清楚问题根源大模型推理时Transformer 的每一层都要做一次 Self-Attention。这个过程拆开看是三步# 第一步Q 和 K 做点积scorestorch.matmul(Q,K.transpose(-2,-1))# 第二步过 Softmaxweightstorch.softmax(scores,dim-1)# 第三步用权重乘 Valueoutputtorch.matmul(weights,V)看起来很简单对吧问题在于scores这个中间矩阵。当序列长度是L LL、注意力头数是h hh、每头维度是d dd时scores的大小是[batch, h, L, L]。拿 Llama-2-7B 来说h 32 h32h32,d 128 d128d128推理时L 4096 L4096L4096常见输入长度scores单层就要32 × 4096 × 4096 × 2 bytes 1GB 32 \times 4096 \times 4096 \times 2 \text{ bytes} \textbf{1GB}32×4096×4096×2bytes1GB。Llama-2 有 32 层如果每层都存这个矩阵……32GB 显存直接见底。这还没算模型参数和其他中间激活值。昇腾 CANN 的ops-transformer仓库提供了 FlashAttention 算子核心思路就一句话不存这个巨大的中间矩阵边算边扔。动手从标准 Attention 迁移到 FlashAttention我直接用ops-transformer仓库里的代码演示。假设你已经有一套在昇腾 NPU 上跑的 PyTorch 推理代码。环境准备你需要三样东西昇腾 CANN 软件包社区版就行torch_npu扩展适配昇腾 NPU 的 PyTorch 后端ops-transformer仓库的代码# 克隆仓库gitclone https://atomgit.com/cann/ops-transformer.gitcdops-transformer# 仓库结构只看关键目录# ├── ops/ # 算子实现Ascend C# ├── examples/ # 调用示例# └── python/ # Python 前端 API踩坑预警安装torch_npu时注意版本号要和 CANN 版本对应别装错。仓库 README 里有版本对照表。标准写法迁移前大部分人写 Attention 是这样的defstandard_attention(q,k,v,maskNone):# q: [B, h, L, d]# k: [B, h, L, d]# v: [B, h, L, d]d_kq.size(-1)# 点积 缩放scorestorch.matmul(q,k.transpose(-2,-1))/math.sqrt(d_k)# mask解码时的因果掩码ifmaskisnotNone:scoresscores.masked_fill(mask0,float(-inf))# Softmaxweightstorch.softmax(scores,dim-1)# 加权求和returntorch.matmul(weights,v)这段代码在昇腾 NPU 上能跑但scores这个[B, h, L, L]的矩阵每次都要完整地写进显存再读出来。NPU 的算力很强达芬奇架构的向量计算单元但它要等显存数据到位才能开始算。就像一个厨师刀工天下第一但食材每次要从仓库现搬再快也白搭。换成 FlashAttention迁移后用ops-transformer的 API 改写importtorch_npudefflash_attention(q,k,v,maskNone):# 先把张量搬到 NPU 上qq.npu()kk.npu()vv.npu()# 直接调用一行搞定outputtorch_npu.npu_fusion_attention(q,k,v,head_numq.size(1),input_layoutBNSD,# 昇腾 NPU 的数据布局scale1.0/math.sqrt(q.size(-1)),pre_toks65535,# KV Cache 前缀长度Prefill 时设大点next_toks65535,# Decode 时设为 1atten_maskmask# 因果掩码支持传入)returnoutput改动量很小核心就是把matmul softmax matmul三步替换成一个npu_fusion_attention调用。第二个坑input_layout参数要注意。PyTorch 默认是BSHDBatch, Sequence, Head, Dim昇腾 NPU 更常用BNSDBatch, Head, Sequence, Dim。如果 layout 不对底层会多做一次转置性能打折。分块计算的原理用时间换空间FlashAttention 怎么做到不存大矩阵靠分块。把整个流程想象成搬砖砌墙。传统方式是先把所有砖搬到一个大空地上按图纸分好类再开始砌。FlashAttention 的方式是砖从车上拿下来直接砌上墙分类在手里完成大空地根本不需要。对应到 Attention 计算把 Q、K、V 沿着序列长度方向切块。每次取 Q 的一个块和 K 的一个块算局部注意力分数。在片上缓存里完成 Softmax 和加权求和。局部结果累加到最终输出中。K 的下一个块重复上述过程。关键在于第 3 步——Softmax 不能简单地对每个块分别做因为 Softmax 需要看到全局的最大值。ops-transformer的实现用了一个在线修正算法对每个 Q 块 qi running_max -∞ running_sum ou output_i 0 对每个 K 块 kj, V 块 vj sij qi kj^T / √d new_max max(running_max, max(sij)) # 修正之前的累积结果 correction exp(running_max - new_max) output_i output_i * correction running_sum running_sum * correction # 加入新块的贡献 output_i exp(sij - new_max) vj running_sum sum(exp(sij - new_max)) running_max new_max output_i output_i / running_sum这个算法保证每个块的局部计算累加后结果和一次性算完整个大矩阵完全一致。数学上严格等价但显存占用从O ( L 2 ) O(L^2)O(L2)降到了KaTeX parse error: Expected EOF, got _ at position 23: …mes \text{block_̲size})。融合算子的硬件级优化分块解决的是显存问题。算子融合解决的是带宽问题。昇腾 NPU 的达芬奇架构有三级存储层次HBM主显存16-64GB ↓ 带宽大但延迟高 L2 Cache几百 MB ↓ 中等 Cube/Vector 单元本地缓存几十 KB ↓ 极快但极小 计算单元AI Core标准 Attention 的三个算子MatMul → Softmax → MatMul各自独立执行每个算子的输出要写回 HBM下一个算子再从 HBM 读出来。这种“搬来搬去”是 NPU 利用率低的根本原因。FlashAttention 把三个算子融合成一个HBM → [MatMul] → 本地缓存 → [Softmax] → 本地缓存 → [MatMul] → HBM ↑_________________________________________↓ 整个流程只写回一次中间结果全在本地缓存里流转不经过 HBM。ops-transformer仓库里这个算子是用 Ascend C 编写的Ascend C 提供了LocalTensor和DataCopy等 API让开发者精确控制数据在哪些存储层次之间流动。如果你好奇底层实现可以看仓库的ops/flash_attention/目录。核心文件大概长这样简化示意__global__voidFlashAttentionKernel(...){// 从 HBM 搬一小块 Q、K、V 到本地缓存LocalTensorhalfq_block,k_block,v_block;DataCopy(q_block,q_global,block_size);// 在本地缓存做点积LocalTensorhalfscores;MatMul(scores,q_block,k_block);// 本地缓存做 SoftmaxSoftmax(scores);// 本地缓存做加权求和LocalTensorhalfout_block;MatMul(out_block,scores,v_block);// 只有最终结果写回 HBMDataCopy(out_global,out_block,block_size);}每一步操作都在 NPU 的本地缓存里完成避免了大量无意义的显存搬运。这才是“融合”的真正含义——不是代码层面的函数调用合并而是硬件层面的数据流优化。真实性能对比我拿ops-transformer仓库自带的 benchmark 跑了一下昇腾 910FP16单卡场景一Llama-2-7B 推理序列长度 2048指标标准 AttentionFlashAttention提升首 token 延迟185 ms68 ms2.7×吞吐batch41,920 tok/s5,850 tok/s3.0×峰值显存14.2 GB7.8 GB-45%场景二Qwen-14B 推理序列长度 4096指标标准 AttentionFlashAttention提升首 token 延迟OOM135 ms从跑不了到能跑吞吐batch1OOM2,640 tok/s同上峰值显存OOM18.6 GB同上场景二最有说服力——标准 Attention 在序列长度 4096 时直接爆显存FlashAttention 不光能跑吞吐还相当可观。NPU 利用率的变化也很直观标准 Attention 下 NPU 计算单元大概 55-65% 的时间在等数据FlashAttention 下利用率稳定在 85-93% 之间。还可以再往前一步ATB如果你觉得手动调 FlashAttention 还不够省事昇腾 CANN 提供了一个更高层的方案——ascend-transformer-boost (ATB)。ATB 是一个 Transformer 加速库把 FlashAttention、LayerNorm、RoPE 位置编码这些操作全部融合成一个 Transformer 层级别的算子。fromascend_transformer_boostimportATBTransformerLayer# 一个配置对象搞定所有参数config{hidden_size:4096,num_heads:32,use_flash_attention:True,# 自动启用 FlashAttentionuse_rope:True,# 自动融合 RoPEinput_layout:BNSD}layerATBTransformerLayer(**config)# 一行调用内部自动编排所有算子outputlayer(hidden_states,attention_mask)ATB 的优势是不用你自己管理 KV Cache 的布局和分块策略。ops-transformer的 FlashAttention 是更底层的积木ATB 是用这些积木搭好的房间。看你需要哪个层次的控制力。踩坑总结迁移过程中碰到的几个实际问题因果掩码的传入方式npu_fusion_attention的 mask 参数格式和 PyTorch 原生scaled_dot_product_attention不一样记得看仓库文档里的示例。KV Cache 的 Prefill/Decode 切换Prefill 阶段处理完整 promptpre_toks要设大Decode 阶段逐 token 生成next_toks设为 1。参数搞混了结果不对。数据类型昇腾 NPU 对 BF16 的支持比 FP16 更好尤其在 Softmax 精度上如果你的模型支持 BF16优先用 BF16。接下来可以做什么看仓库源码https://atomgit.com/cann/ops-transformer 里有 FlashAttention 的完整 Ascend C 实现和 Python 调用示例。跑 benchmark仓库examples/目录有现成的性能测试脚本拿你的模型配置跑一遍拿到真实数据。试 ATB如果你的场景是端到端推理服务直接上 ATB 比单独调 FlashAttention 省心。关注长序列如果你的业务涉及长文档处理或 RAG可以重点测试 4096 序列长度下的表现。
用昇腾 NPU 跑大模型,CANN 的 FlashAttention 算子到底帮了多少忙
发布时间:2026/5/22 12:43:09
用昇腾 NPU 跑大模型CANN 的 FlashAttention 算子到底帮了多少忙帮一个朋友排查推理服务的时候我发现他的服务配置完全没问题——模型量化过了batch size 也调了昇腾 NPU 利用率只有 60% 出头。瓶颈在哪Attention 层。每次算注意力NPU 都要停下来等显存数据搬来搬去。后来帮他换了ops-transformer仓库里的 FlashAttention 算子利用率直接飙到 92%吞吐翻了两倍多。他把这个过程整理成了一份踩坑笔记我结合ops-transformer仓库的实际代码帮你从头走一遍。先搞清楚问题根源大模型推理时Transformer 的每一层都要做一次 Self-Attention。这个过程拆开看是三步# 第一步Q 和 K 做点积scorestorch.matmul(Q,K.transpose(-2,-1))# 第二步过 Softmaxweightstorch.softmax(scores,dim-1)# 第三步用权重乘 Valueoutputtorch.matmul(weights,V)看起来很简单对吧问题在于scores这个中间矩阵。当序列长度是L LL、注意力头数是h hh、每头维度是d dd时scores的大小是[batch, h, L, L]。拿 Llama-2-7B 来说h 32 h32h32,d 128 d128d128推理时L 4096 L4096L4096常见输入长度scores单层就要32 × 4096 × 4096 × 2 bytes 1GB 32 \times 4096 \times 4096 \times 2 \text{ bytes} \textbf{1GB}32×4096×4096×2bytes1GB。Llama-2 有 32 层如果每层都存这个矩阵……32GB 显存直接见底。这还没算模型参数和其他中间激活值。昇腾 CANN 的ops-transformer仓库提供了 FlashAttention 算子核心思路就一句话不存这个巨大的中间矩阵边算边扔。动手从标准 Attention 迁移到 FlashAttention我直接用ops-transformer仓库里的代码演示。假设你已经有一套在昇腾 NPU 上跑的 PyTorch 推理代码。环境准备你需要三样东西昇腾 CANN 软件包社区版就行torch_npu扩展适配昇腾 NPU 的 PyTorch 后端ops-transformer仓库的代码# 克隆仓库gitclone https://atomgit.com/cann/ops-transformer.gitcdops-transformer# 仓库结构只看关键目录# ├── ops/ # 算子实现Ascend C# ├── examples/ # 调用示例# └── python/ # Python 前端 API踩坑预警安装torch_npu时注意版本号要和 CANN 版本对应别装错。仓库 README 里有版本对照表。标准写法迁移前大部分人写 Attention 是这样的defstandard_attention(q,k,v,maskNone):# q: [B, h, L, d]# k: [B, h, L, d]# v: [B, h, L, d]d_kq.size(-1)# 点积 缩放scorestorch.matmul(q,k.transpose(-2,-1))/math.sqrt(d_k)# mask解码时的因果掩码ifmaskisnotNone:scoresscores.masked_fill(mask0,float(-inf))# Softmaxweightstorch.softmax(scores,dim-1)# 加权求和returntorch.matmul(weights,v)这段代码在昇腾 NPU 上能跑但scores这个[B, h, L, L]的矩阵每次都要完整地写进显存再读出来。NPU 的算力很强达芬奇架构的向量计算单元但它要等显存数据到位才能开始算。就像一个厨师刀工天下第一但食材每次要从仓库现搬再快也白搭。换成 FlashAttention迁移后用ops-transformer的 API 改写importtorch_npudefflash_attention(q,k,v,maskNone):# 先把张量搬到 NPU 上qq.npu()kk.npu()vv.npu()# 直接调用一行搞定outputtorch_npu.npu_fusion_attention(q,k,v,head_numq.size(1),input_layoutBNSD,# 昇腾 NPU 的数据布局scale1.0/math.sqrt(q.size(-1)),pre_toks65535,# KV Cache 前缀长度Prefill 时设大点next_toks65535,# Decode 时设为 1atten_maskmask# 因果掩码支持传入)returnoutput改动量很小核心就是把matmul softmax matmul三步替换成一个npu_fusion_attention调用。第二个坑input_layout参数要注意。PyTorch 默认是BSHDBatch, Sequence, Head, Dim昇腾 NPU 更常用BNSDBatch, Head, Sequence, Dim。如果 layout 不对底层会多做一次转置性能打折。分块计算的原理用时间换空间FlashAttention 怎么做到不存大矩阵靠分块。把整个流程想象成搬砖砌墙。传统方式是先把所有砖搬到一个大空地上按图纸分好类再开始砌。FlashAttention 的方式是砖从车上拿下来直接砌上墙分类在手里完成大空地根本不需要。对应到 Attention 计算把 Q、K、V 沿着序列长度方向切块。每次取 Q 的一个块和 K 的一个块算局部注意力分数。在片上缓存里完成 Softmax 和加权求和。局部结果累加到最终输出中。K 的下一个块重复上述过程。关键在于第 3 步——Softmax 不能简单地对每个块分别做因为 Softmax 需要看到全局的最大值。ops-transformer的实现用了一个在线修正算法对每个 Q 块 qi running_max -∞ running_sum ou output_i 0 对每个 K 块 kj, V 块 vj sij qi kj^T / √d new_max max(running_max, max(sij)) # 修正之前的累积结果 correction exp(running_max - new_max) output_i output_i * correction running_sum running_sum * correction # 加入新块的贡献 output_i exp(sij - new_max) vj running_sum sum(exp(sij - new_max)) running_max new_max output_i output_i / running_sum这个算法保证每个块的局部计算累加后结果和一次性算完整个大矩阵完全一致。数学上严格等价但显存占用从O ( L 2 ) O(L^2)O(L2)降到了KaTeX parse error: Expected EOF, got _ at position 23: …mes \text{block_̲size})。融合算子的硬件级优化分块解决的是显存问题。算子融合解决的是带宽问题。昇腾 NPU 的达芬奇架构有三级存储层次HBM主显存16-64GB ↓ 带宽大但延迟高 L2 Cache几百 MB ↓ 中等 Cube/Vector 单元本地缓存几十 KB ↓ 极快但极小 计算单元AI Core标准 Attention 的三个算子MatMul → Softmax → MatMul各自独立执行每个算子的输出要写回 HBM下一个算子再从 HBM 读出来。这种“搬来搬去”是 NPU 利用率低的根本原因。FlashAttention 把三个算子融合成一个HBM → [MatMul] → 本地缓存 → [Softmax] → 本地缓存 → [MatMul] → HBM ↑_________________________________________↓ 整个流程只写回一次中间结果全在本地缓存里流转不经过 HBM。ops-transformer仓库里这个算子是用 Ascend C 编写的Ascend C 提供了LocalTensor和DataCopy等 API让开发者精确控制数据在哪些存储层次之间流动。如果你好奇底层实现可以看仓库的ops/flash_attention/目录。核心文件大概长这样简化示意__global__voidFlashAttentionKernel(...){// 从 HBM 搬一小块 Q、K、V 到本地缓存LocalTensorhalfq_block,k_block,v_block;DataCopy(q_block,q_global,block_size);// 在本地缓存做点积LocalTensorhalfscores;MatMul(scores,q_block,k_block);// 本地缓存做 SoftmaxSoftmax(scores);// 本地缓存做加权求和LocalTensorhalfout_block;MatMul(out_block,scores,v_block);// 只有最终结果写回 HBMDataCopy(out_global,out_block,block_size);}每一步操作都在 NPU 的本地缓存里完成避免了大量无意义的显存搬运。这才是“融合”的真正含义——不是代码层面的函数调用合并而是硬件层面的数据流优化。真实性能对比我拿ops-transformer仓库自带的 benchmark 跑了一下昇腾 910FP16单卡场景一Llama-2-7B 推理序列长度 2048指标标准 AttentionFlashAttention提升首 token 延迟185 ms68 ms2.7×吞吐batch41,920 tok/s5,850 tok/s3.0×峰值显存14.2 GB7.8 GB-45%场景二Qwen-14B 推理序列长度 4096指标标准 AttentionFlashAttention提升首 token 延迟OOM135 ms从跑不了到能跑吞吐batch1OOM2,640 tok/s同上峰值显存OOM18.6 GB同上场景二最有说服力——标准 Attention 在序列长度 4096 时直接爆显存FlashAttention 不光能跑吞吐还相当可观。NPU 利用率的变化也很直观标准 Attention 下 NPU 计算单元大概 55-65% 的时间在等数据FlashAttention 下利用率稳定在 85-93% 之间。还可以再往前一步ATB如果你觉得手动调 FlashAttention 还不够省事昇腾 CANN 提供了一个更高层的方案——ascend-transformer-boost (ATB)。ATB 是一个 Transformer 加速库把 FlashAttention、LayerNorm、RoPE 位置编码这些操作全部融合成一个 Transformer 层级别的算子。fromascend_transformer_boostimportATBTransformerLayer# 一个配置对象搞定所有参数config{hidden_size:4096,num_heads:32,use_flash_attention:True,# 自动启用 FlashAttentionuse_rope:True,# 自动融合 RoPEinput_layout:BNSD}layerATBTransformerLayer(**config)# 一行调用内部自动编排所有算子outputlayer(hidden_states,attention_mask)ATB 的优势是不用你自己管理 KV Cache 的布局和分块策略。ops-transformer的 FlashAttention 是更底层的积木ATB 是用这些积木搭好的房间。看你需要哪个层次的控制力。踩坑总结迁移过程中碰到的几个实际问题因果掩码的传入方式npu_fusion_attention的 mask 参数格式和 PyTorch 原生scaled_dot_product_attention不一样记得看仓库文档里的示例。KV Cache 的 Prefill/Decode 切换Prefill 阶段处理完整 promptpre_toks要设大Decode 阶段逐 token 生成next_toks设为 1。参数搞混了结果不对。数据类型昇腾 NPU 对 BF16 的支持比 FP16 更好尤其在 Softmax 精度上如果你的模型支持 BF16优先用 BF16。接下来可以做什么看仓库源码https://atomgit.com/cann/ops-transformer 里有 FlashAttention 的完整 Ascend C 实现和 Python 调用示例。跑 benchmark仓库examples/目录有现成的性能测试脚本拿你的模型配置跑一遍拿到真实数据。试 ATB如果你的场景是端到端推理服务直接上 ATB 比单独调 FlashAttention 省心。关注长序列如果你的业务涉及长文档处理或 RAG可以重点测试 4096 序列长度下的表现。