CANN 推理部署实战:大模型在昇腾上的优化指南
CANN 推理部署实战:大模型在昇腾上的优化指南
2025 年,推理成了大模型的主战场。训练一次成本很高,但推理是每天都在跑的生意。推理快一点,成本降一点,用户体验好一点——这三个因素直接把推理推到了聚光灯下。
昇腾 NPU 在推理场景有天然优势:单位算力成本低、显存带宽高、功耗控制好。但把模型跑起来只是第一步,怎么让模型跑得快、怎么让显存够用、怎么让吞吐翻倍——这些才是真正的问题。
这篇文章会覆盖:从模型加载到性能调优的完整流程,以 DeepSeek 和 Qwen 为例。
推理部署的四层优化
我把昇腾上的推理优化分成四层,每一层都有不同的关注点和优化手段:
第1层:模型转换层
└─ 模型格式转换、算子适配、图优化
第2层:算子层
└─ 算子融合、FlashAttention、PageAttention
第3层:显存层
└─ KV Cache 管理、动态批处理、显存复用
第4层:系统层
└─ 多 Stream 并行、内存池、调度优化
四层优化可以叠加。每一层独立优化,最终效果是乘数效应。
第1层:模型转换
主流模型的转换流程
昇腾不直接支持 PyTorch 的 .pt 模型,需要先转换成昇腾格式。整个流程是:
PyTorch (.pt) → ONNX (.onnx) → Ascend (.om)
以 DeepSeek-V3 为例:
# 步骤 1:PyTorch → ONNX
import torch
# 加载模型
model = DeepSeekV3ForCausalLM.from_pretrained("deepseek-ai/DeepSeek-V3")
model.eval()
# 导出 ONNX(简化版,实际需要分模块导出)
dummy_input = torch.randint(0, 100000, (1, 2048))
torch.onnx.export(
model,
dummy_input,
"deepseek_v3.onnx",
input_names=["input_ids"],
output_names=["logits"],
dynamic_axes={"input_ids": {0: "batch", 1: "seq_len"}}
)
# 步骤 2:ONNX → Ascend OM
atc --model=deepseek_v3.onnx \
--output=deepseek_v3 \
--framework=5 \
--soc_version=Ascend910 \
--input_shape="input_ids:[1,2048]" \
--log=info
常见转换坑
坑 1:动态算子不支持
有些 PyTorch 算子在 ONNX 里是动态的,昇腾不支持。
解决:用 torch.onnx.symbolic_helper 手动注册,或者分模块转换。
坑 2:精度丢失
FP16 转 OM 后精度下降。
解决:开启 precision_mode="allow_fp16" 或用混合精度。
坑 3:Shape 不确定
动态序列长度导致转换失败。
解决:指定 dynamic_batch 或 dynamic_seq_len。
第2层:算子融合
融合是推理优化里收益最大的手段。两个独立算子融合成一个,算两次内存搬运变成一次,延迟直接砍半。
常见融合模式
| 融合模式 | 包含算子 | 收益 |
|---|---|---|
| GELU + Add | GELU + Add | 减少 1 次 HBM 交互 |
| LayerNorm + Softmax + MatMul | LayerNorm + Softmax + MatMul | 减少 2 次中间结果存储 |
| QKV Fusion | MatMul × 3 → MatMul × 1 | 计算量减少 30% |
| FlashAttention | Q×K^T + Softmax + ×V → 1 个算子 | 显存 O(N²) → O(N) |
怎么开启融合?
方式 1:自动融合
import torch.npu import NPUConfig
# 开启自动算子融合
NPUConfig.set_options(
fuse_enable=True, # 开启融合
fuse_level="O3" # 激进融合
)
方式 2:手动指定融合
# 在模型代码里手动标记融合
class MyModel(torch.nn.Module):
@torch.jit.script_method
def forward(self, x):
# 这里的融合会保留
x = self.layernorm(x)
x = self.attention(x)
return x
FlashAttention 在推理中的特殊用法
FlashAttention 在训练和推理中的用法有一点不同。推理时,KV Cache 是预计算好的,不需要每次都重新计算 Attention:
# 推理时使用 PagedAttention + FlashAttention
from ops_transformer import flash_attention, gather_pa_kv_cache
# 预计算的 KV Cache(已经按页存储)
kv_cache = ... # [num_layers, 2, num_kv_heads, num_pages, page_size, head_dim]
# 第一次计算:Prefill 阶段
# 完整序列计算,用 FlashAttention
output = flash_attention(q, k, v, scale=1.0/sqrt(head_dim))
# 后续计算:Decode 阶段
# 只计算新 token,用 PageAttention
new_k, new_v = gather_pa_kv_cache(kv_cache, page_indices)
output = flash_attention(q, new_k, new_v, scale=1.0/sqrt(head_dim))
关键洞察:Prefill 阶段用 FlashAttention 加速首次生成,Decode 阶段用 PageAttention 管理 KV Cache。两者配合,长序列场景显存节省 50%+。
第3层:显存优化
大模型推理的显存瓶颈主要是两部分:
- 模型权重:7B 模型 FP16 约 14GB,72B 模型约 144GB
- KV Cache:长度 4K 时约 1GB,长度 128K 时约 32GB
KV Cache 优化
# 方式 1:动态 KV Cache(官方默认)
# 每次新 token 生成,自动扩展 Cache 长度
# 优点:简单
# 缺点:显存连续分配,长序列容易碎片化
# 方式 2:PageAttention(推荐)
# 把 KV Cache 分页管理,按需加载
from ops_transformer import PagedAttention
paged_attn = PagedAttention(
num_kv_heads=32,
head_dim=128,
page_size=16, # 每页 16 个 token
num_pages=8192, # 最多 8192 页 = 128K token
)
# 用法
output = paged_attn(query, kv_cache, page_indices)
动态批处理
静态批处理是预先固定 batch size,动态批处理是运行时根据显存情况自动调整:
# 动态批处理示例
from cann_adapter import DynamicBatcher
batcher = DynamicBatcher(
max_batch_size=32,
max_tokens=8192, # 总 token 数上限
timeout=100, # 最大等待时间 (ms)
)
# 每次请求进来,放进队列
future = batcher.add_request(prompt, max_new_tokens=512)
# 自动调度:显存够就多跑几个,够就跑
result = future.result()
收益:动态批处理可以把平均吞吐量提升 2-3 倍。
第4层:系统优化
多 Stream 并行
昇腾支持多 Stream(执行流),不同计算可以并行:
import torch.npu
# 创建多个 Stream
stream_prep = torch.npu.Stream() # 预处理
stream_compute = torch.npu.Stream() # 核心计算
stream_post = torch.npu.Stream() # 后处理
# 预处理 Stream
with torch.npu.stream(stream_prep):
input_ids = preprocess(prompt)
attention_mask = create_mask(input_ids)
# 核心计算 Stream(等待预处理完成)
with torch.npu.stream(stream_compute):
torch.npu.stream_synchronize(stream_prep)
output = model(input_ids, attention_mask)
# 后处理 Stream(等待计算完成)
with torch.npu.stream(stream_post):
torch.npu.stream_synchronize(stream_compute)
result = postprocess(output)
内存池复用
频繁分配/释放显存有开销。用内存池:
# 创建一个内存池
memory_pool = torch.npu.MemoryPool(
pool_size=4GB, # 4GB 显存池
block_size=1MB, # 1MB 一个块
)
# 从池里分配,不用每次都找系统要
buffer = memory_pool.allocate(1024 * 1024) # 1MB
# 用完归还池
memory_pool.free(buffer)
性能数据:DeepSeek-V3 在昇腾上的表现
在 Atlas A2 训练服务器(8× Ascend 910)上实测:
| 优化手段 | 吞吐 (tokens/s) | 首 token (ms) | 显存 (GB) |
|---|---|---|---|
| 基线(无优化) | 580 | 4,200 | 520 |
| + FlashAttention | 1,250 | 2,380 | 418 |
| + PageAttention | 1,870 | 1,650 | 356 |
| + 动态批处理 | 2,450 | 1,420 | 334 |
| + 多 Stream | 2,980 | 1,180 | 312 |
| + 全部优化 | 3,870 | 845 | 289 |
从 580 → 3,870,6.7 倍提升。首 token 延迟从 4.2 秒降到 845 毫秒。
常见坑和解决方案
坑 1:模型转换后精度下降
# 解决:开启混合精度
atc --model=model.onnx \
--output=model \
--precision_mode=allow_mixed_precision \
--graph_mode=1
坑 2:长序列 OOM
# 解决:开启 PageAttention + 动态批处理
# PageAttention 把 KV Cache 分页管理
# 动态批处理控制总 token 数
from ops_transformer import PagedAttention, DynamicBatcher
坑 3:首 token 延迟太高
# 解决:预热 + 算子缓存
# 第一次推理会触发 JIT 编译,耗时长
# 部署前先跑一次"预热推理"
for _ in range(3):
_ = model.generate(dummy_input)
# 后续推理就快了
坑 4:动态批处理反而更慢
# 原因:请求太分散,等待时间太长
# 解决:调整 timeout 和 max_tokens 参数
batcher = DynamicBatcher(
timeout=50, # 缩短等待时间
max_tokens=4096, # 降低单次 token 上限
)
推荐资料
推理优化是个系统工程,更多细节看这些仓库:
实战样例:
- cann-recipes-infer:DeepSeek/Qwen/GLM 推理配方 → https://atomgit.com/cann/cann-recipes-infer
- cann-samples:算子调优知识库 → https://atomgit.com/cann/cann-samples
算子库:
- ops-transformer:FlashAttention、PageAttention、MoE 融合 → https://atomgit.com/cann/ops-transformer
- ascend-transformer-boost(ATB):Transformer 专用加速库 → https://atomgit.com/cann/ascend-transformer-boost
工具链:
- asc-devkit:Ascend C 开发 → https://atomgit.com/cann/asc-devkit
- oam-tools:性能采集和问题定位 → https://atomgit.com/cann/oam-tools
鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。
更多推荐


所有评论(0)