你在对话框按下回车后,昇腾NPU里发生了什么
文章摘要: 本文详细解析了大模型在昇腾NPU上的推理全链路过程,分为Prefill和解码两个阶段。Prefill阶段一次性处理完整输入序列,计算量大但只需执行一次;解码阶段则逐Token生成输出,依赖KV Cache机制降低计算复杂度。文章重点分析了KV Cache对显存资源的占用问题,并介绍了CANN在算子融合、动态Batch管理和分页缓存等方面的优化技术。最后总结了不同阶段的性能瓶颈及应对方案
你在对话框按下回车后,昇腾NPU里发生了什么
打开一个对话 AI,输入"用 Python 写一个快速排序",按下回车。等一两秒,屏幕上开始一个字一个字地往外蹦结果。这个过程中,你的输入走了一条什么样的路,才变成一段通顺的代码输出?CANN 在中间扮演了什么角色?
大模型推理在昇腾NPU上的全链路,正是这篇文章要讲的事情——不堆概念,从你发消息到 Token 一个个生成为止。
这篇文章不堆概念,而是从用户的每一次输入出发,讲清楚大模型推理的全链路——从你发消息到 Token 一个个生成为止。
Prefill:读完整段输入
你发了一条消息:“用 Python 写一个快速排序”。这一行文本首先要被 Tokenizer 切成一个整数序列:比如 [45, 892, 312, 67, 1289, ...]。这个序列就是模型的第一输入。
然后进入 Prefill 阶段——把整段输入一次性喂给模型做计算。
输入 Token 序列 [45, 892, 312, 67, 1289, ...] → Embedding
→ Transformer Block × N
→ 最后一个 Token 的 Logits → Softmax → 第一个输出 Token
这个过程走了模型的全部 Decoder Block。每个 Block 里都做了 Attention + FFN。计算量很大但只需要做一次。
CANN 在 Prefill 阶段扮演的角色:
- AscendCL 把 Token 数据拷进 NPU 显存
- Graph Engine 在模型加载阶段已经做好了算子融合——Attention 用 FlashAttention 融合算子替代了三个分离的 BMM+Softmax+BMM
- Runtime 把融合后的算子序列分派到 AI Core 执行
- 整个 Prefill 在 NPU 上是纯计算流水线,中间不需要跟 CPU 交互
Prefill 完成后模型内部的状态变了——每层的 Key 和 Value 被缓存下来,这就是 KV Cache。比如一个 32 层的 LLaMA 模型,每层缓存两个矩阵(K 和 V),大小都是 [输入长度, 头维度 × 头数]。
解码:一个 Token 一个 Token 地生
Prefill 得到第一个输出 Token 后,进入解码阶段。这个阶段是逐 Token 的:
当前 Token → Embedding → Transformer Block × N → Logits → 下一个 Token
↑ |
└────────── KV Cache ──────────────┘
每生成一个 Token,就把这个 Token 对应的 K 和 V 追加到对应层的 Cache 末尾。下一轮计算时只处理新 Token 的 Q,用 Q 跟完整的 KV Cache 做 Attention。
为什么这样分两阶段? Prefill 把输入序列完整算一遍,Cache 积累完毕。解码阶段每次只算新来的一个 Token,复杂度从 Prefill 的 O(n²·d) 降到 O(n·d)。Token 越长,解码阶段的单步延迟越低。
CANN 在解码阶段扮演的角色:
- Runtime 管理 KV Cache 的动态增长——每步生成后更新 Cache 偏移地址
- GE 的图优化确保解码阶段的 Attention 走 Paged FlashAttention 路径,只搬运新增的 K/V
- Runtime 的 Stream 机制让计算和 KV Cache 更新并行
如果不用 KV Cache,解码阶段也要从所有历史 Token 重新算 Attention——复杂度退回到 O(n²·d),输出 1000 个 Token 的累计代价是 Prefill 的 1000 倍。用 Cache 后解码阶段的计算量只跟序列长度线性相关。
KV Cache 为什么是关键资源
大模型推理对显存的需求主要来自两个地方:模型参数和 KV Cache。
以 LLaMA-13B 为例(FP16 精度):
- 模型参数:约 26GB
- KV Cache 每层:
[2 × 序列长度 × 4096 × 40](40 个头) - 32 层:总 Cache 大小 ≈
2 × n × 4096 × 40 × 32 × 2 bytes = ~20MB × n
序列长度 n=4096 时,KV Cache 占用约 80GB。这已经超过了模型参数本身的三倍。
KV Cache 的管理直接决定了推理服务的并发度。 一张 Ascend 910 的显存一般是 32GB。装下 13B 模型后剩余约 6GB,只够 n=4096 时服务的并发度极低。
优化手段集中在几个方向:
- Cache 量化:把 FP16 的 K/V 量化成 INT8,占用减半
- Paged Attention:把 Cache 切成不连续的物理页,通过页表映射逻辑地址,避免碎片
- Prefix Cache:共享相同前缀的请求复用 Cache,聊天对话的 system prompt 部分不用重复计算
CANN Runtime 在 8.0 版本后原生支持 Paged KV Cache 管理——K/V 的物理地址由 Runtime 分配和管理,算子代码通过页表接口访问,不需要应用层操心分页逻辑。
推理全链路:从输入到输出
用一张流程图串起来:
用户输入文本
↓ Tokenizer 编码
Token ID 序列
↓ AscendCL aclmdlExecute
GE 图执行(Prefill 分支——FlowAttention 融合)
↓ Runtime 调度
NPU Prefill 计算(全部 Decoder Block 跑一次)
↓ 缓存 KV Cache
第一个输出 Token
↓ 进入解码循环(逐 Token)
新 Token → AscendCL → GE → Runtime → NPU(单 Block 计算)
→ 更新 KV Cache → 下一个 Token
↓ 重复直到 [EOS] Token 或达到最大长度
输出 Token 序列
↓ Tokenizer 解码
最终文本
从 CANN 视角看,Prefill 和解码两个阶段在 AscendCL 接口上没有区别——都是调用 aclmdlExecute。区别在于 GE 在执行时根据输入 Shape 的变化自动切换到对应的执行分支。
Runtime 如何调度解码
解码阶段 NPU 的工作模式跟前文的 YOLO 推理完全不同:
- 单次计算量极小——一个 Token 的 Attention + FFN,可能只有几十到几百 TFLOPs
- 调用频率极高——每个 Token 都要调一次,输出 1000 个 Token 就是 1000 次推理
- 数据依赖连续——上一步的结果决定下一步的输入
这对 Runtime 的调度提出了特殊要求。
Stream 空泡问题。 解码阶段每次推理的计算量小,每次 aclmdlExecute 调用都有 Runtime 的调度开销。如果 Stream 没有 pipeline 好,NPU 会在调度间隙空转。实测中不做流水线优化的场景下解码时 Runtime 的开销占总延迟的 15-25%。
Batch 策略不同。 解码阶段不同 request 的解码进度不同——request A 出第 5 个字时 request B 可能才出第 2 个字。连续 Batch(Continuous Batching)是应对方案:NPU 每步推理时,在 Batch 维度上混入不同进度的 request,只要每个 request 的计算形状一致就可以合并。Runtime 需要支持 Batch 维度在推理步间动态变化。
CANN Runtime 的 Batch Launch 接口在连续 Batch 场景下能显著降低调度开销——一次提交多个 request 的算子,Runtime 内部做合并调度。
大模型推理学习路线
如果看完这篇文章想系统地掌握 CANN 大模型推理,建议按这个顺序学习:
第一阶段:跑通推理。 从 AscendCL 入手——装好 CANN Toolkit,配环境,调一个 OM 模型做推理(Prompt 8 的内容)。不需要理解底层细节,先看到输出。
第二阶段:理解图优化。 学 Graph Engine 的原理——算子融合、内存优化、图执行链路。这阶段你会理解为什么 ATC 转换是关键步骤,以及自动优化做了哪些事。
第三阶段:掌握 Runtime。 深入 Device-Context-Stream 模型,理解内存管理和异步执行机制。这阶段要学会自己编排多 Stream 流水线和检查 Memory 碎片。
第四阶段:优化大模型推理。 FlashAttention 融合、Paged KV Cache、Continuous Batching、动态 Shape 管理——这些都是大模型专有的优化技术。建议结合 ops-transformer 仓库源码理解。
ops-transformer FlashAttention 实现
为什么 KV Cache 这么占显存
KV Cache 的大小跟模型层数、注意力头数、头维度、序列长度成正比。以 LLaMA-70B 为例:
| 模型 | 参数 | 每层 KV 大小 | 层数 | 总 Cache (n=4096) |
|---|---|---|---|---|
| LLaMA-7B | ~13GB | 4MB × n | 32 | 512MB × n → ~2GB |
| LLaMA-13B | ~26GB | 6.4MB × n | 40 | 768MB × n → ~3GB |
| LLaMA-70B | ~140GB | 8MB × n | 80 | 1.9GB × n → ~7.8GB |
注意这些是单 request 的缓存。一台推理服务器通常要并发处理多个 request。100 个并发 request,LLaMA-70B 的 KV Cache 总量接近 800GB——远超单张 NPU 的显存。
这就是分页 KV Cache 和 Cache 量化的意义——一个把缓存页化管理消除碎片,一个把精度从 FP16 降到 INT8 直接减半。再加上 Prefix Cache 让共享前缀的 request 复用同一段缓存,实际部署中的显存利用率可以从 30% 提到 80% 以上。
大模型推理的瓶颈总结
从上面整个链路可以看出,大模型推理在昇腾上的瓶颈在不同阶段是不同的:
| 阶段 | 瓶颈 | 应对手段 |
|---|---|---|
| Prefill | 计算量密集,n² 复杂度 | FlashAttention 融合 + 大 Batch |
| 解码单步 | Memory Bound,搬运 KV Cache | Paged Attention + Cache 量化 |
| 整链 | Cache 增长,显存压力 | Continuous Batching + Prefix Cache |
| 调度 | Runtime 每次 Launch 开销 | Batch Launch + 多 Stream 流水线 |
这四个瓶颈的优化手段都不是独立的。FlashAttention 减少搬运提升了单步延迟,但同时降低了 Cache 的访问模式复杂度。Paged KV Cache 解决了显存碎片问题,但页表查找增加了访存延迟——需要跟更大粒度的 Cache 搬运策略配合。
在实际部署中,这些优化不是"开了某个开关就完事",而是一个系统工程:理解每层瓶颈在哪、当前负载模式偏 Prefill 还是解码、Cache 的页表命中率够不够高。理解了全链路,才能针对自己的负载模式做精细化调优。
参考仓库
更多推荐




所有评论(0)