你在对话框按下回车后,昇腾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 仓库源码理解。

CANN Runtime 仓库

Graph Engine 图执行详解

ops-transformer FlashAttention 实现

CANN 端到端推理示例


为什么 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 的页表命中率够不够高。理解了全链路,才能针对自己的负载模式做精细化调优。

参考仓库

CANN Runtime
Graph Engine
ops-transformer
cann-recipes-infer

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐