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_batchdynamic_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层:显存优化

大模型推理的显存瓶颈主要是两部分:

  1. 模型权重:7B 模型 FP16 约 14GB,72B 模型约 144GB
  2. 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
Logo

鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。

更多推荐