一个模型在 HuggingFace 上跑通和在生产环境部署,中间差了十万八千里。模型格式转换、图切分、KV Cache 管理、批量调度——cann-recipes-infer 就是把这些步做成标准化的菜谱。每个菜谱针对一个具体的模型,给出端到端的部署流程。

仓库里包含 30+ 个模型的推理菜谱,从 LLaMA、ChatGLM 到 Stable Diffusion、Whisper。每条菜谱里都有分步骤的配置、量化选择和性能调优参数。

一条 LLM 推理菜谱的完整流程

以 LLaMA-7B FP16 推理为例,菜谱的六个步骤:

步骤 1:模型格式转换

# step1_convert_model.py

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from cann_ascend.ir import export_to_ascend_ir

# 加载原始模型
model = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Llama-2-7b-hf",
    torch_dtype=torch.float16,
    low_cpu_mem_usage=True
)

# 转换为 Ascend IR(中间表示)
export_to_ascend_ir(
    model,
    "llama7b.ascend_ir",
    input_shape=(1, 2048),              # 最大序列长度
    dynamic_axes={"seq_len": "dynamic"}  # 序列长度动态
)

步骤 2:图切分

# step2_split_graph.py

from cann_ascend.graph_ir import GraphSplitter

splitter = GraphSplitter("llama7b.ascend_ir")

# 按层切分:每两层一个子图
# 原因:一个子图太大(40 层全部在一个图里)→ L2 缓存覆盖不了 → 频繁换入换上
splitter.split_by_layers(
    layers_per_subgraph=2,
    strategy="memory_balanced"  # 显存均衡策略
)

splitter.save("llama7b_split.ascend_ir")

步骤 3:图量化(可选)

# step3_quantize.py

from cann_ascend.quantization import Quantizer

quantizer = Quantizer("llama7b_split.ascend_ir")

# W8A16 量化:权重 int8,激活 float16
quantizer.set_strategy("w8a16")
quantizer.calibrate(
    "calibration_data.json",  # 校准数据集
    num_samples=128           # 128 个样本校准
)

quantizer.save("llama7b_w8a16.ascend_ir")

步骤 4:编译生成算子

# step4_compile.py

from cann_ascend.compiler import Compiler

compiler = Compiler("llama7b_w8a16.ascend_ir")
compiler.set_target("Ascend910")
compiler.set_options({
    "fusion_level": "O2",           # 算子融合级别
    "memory_optimizer": "recompute", # 重计算换显存
    "max_workspace": "2GB"          # workspace 上限
})

compiler.compile("llama7b_compiled.bin")

步骤 5:部署推理服务

# step5_deploy.py

from cann_ascend.inference import InferenceServer

server = InferenceServer("llama7b_compiled.bin")
server.set_batch_size(8)           # 动态 batch 上限
server.set_max_seq_len(2048)
server.set_kv_cache_policy("paged")  # PagedAttention KV Cache

# 启动服务
server.start(port=8000)

# 客户端调用
import requests
response = requests.post("http://localhost:8000/generate", json={
    "prompt": "Explain quantum computing in simple terms.",
    "max_new_tokens": 256,
    "temperature": 0.7
})

步骤 6:性能调优

# step6_tune.sh

# 调整 batch 大小 → 吞吐达到最优
export ASCEND_BATCH_SIZE=8

# 调整 NPU 频率 → 在功耗和延时之间平衡
export ASCEND_NPU_FREQ=high  # high/medium/low

# 开启 operator cache → 缩减冷启动时间
export ASCEND_OP_CACHE=on

# KV Cache 预分配(避开运行时碎片)
export ASCEND_KV_CACHE_SIZE=4GB

图切分的显存优化策略

LLM 推理的最大瓶颈是 KV Cache 占用的 HBM。一个 7B 模型的 KV Cache 计算方法:

每个 token 的 KV Cache = 2 × layers × hidden_dim × dtype_size
                     = 2 × 32 × 4096 × 2 (FP16)
                     = 512 KB / token

2048 token 序列 = 512 KB × 2048 = 1 GB
batch=8 = 1 GB × 8 = 8 GB

一张 32GB HBM 的 Ascend 910 要同时装模型权重(14GB FP16)和 KV Cache(8GB batch=8),只剩 10GB 给中间激活。cann-recipes-infer 菜谱里的优化策略:

// cann-recipes-infer/utils/kv_cache_manager.py

struct KVCacheConfig {
    // PagedAttention:把 KV Cache 切成 256 token 的页
    int page_size = 256;

    // 每页的 HBM 大小
    int bytes_per_page(int layers, int hidden_dim, int dtype_size) {
        return 2 * layers * hidden_dim * dtype_size * page_size;
        // 7B: 2 * 32 * 4096 * 2 * 256 = 128 MB/page
    }

    // 显存分配策略
    enum Strategy {
        PRE_ALLOCATE,   // 预分配满,避免运行时碎片
        ON_DEMAND,      // 按需分配,更省但碎片风险
        POOL            // 内存池复用,折中方案
    };
};

踩坑一:推理精度和训练精度的不一致

HuggingFace 的模型权重通常是 FP16,但量化到 W8A16 后,推理的 logits 和原始 FP16 可能差 0.5-1 个 token 的预测。

错误写法

# 错误:量化后不做精度校验,直接上线
quantizer.calibrate("calibration.json", num_samples=64)
# 64 个样本可能不够覆盖所有 token 分布
# 上线后某些输入出现乱码输出

正确写法:量化后用大量样本校验精度。

# 正确:用 512 个样本做校准
quantizer.calibrate("calibration.json", num_samples=512)

# 量化后做精度回测:和 FP16 baseline 对比
evaluator = QuantEvaluator(
    fp16_model="llama7b_split.ascend_ir",
    int8_model="llama7b_w8a16.ascend_ir",
    eval_dataset="wikitext-2"
)

# per-token accuracy:和 FP16 的输出 token 对齐度
# 目标 > 99.5%
accuracy = evaluator.compare_token_accuracy(num_samples=1000)
assert accuracy > 0.995, f"精度下迭:{accuracy:.4f}"

踩坑二:动态 seq_len 和静态 kv_cache 冲突

如果编译时设 max_seq_len=2048 但实际推理只有 128 个 token 的输入,预分配的 KV Cache(8GB)有 7.5GB 浪费。

错误配置

# 编译时写了死必须的最大 seq_len
export ASCEND_MAX_SEQ_LEN=2048
export ASCEND_KV_CACHE_SIZE=8GB    # 为 2048 长度预分配

# 实际推理只有 128 tokens → KV Cache 只用了 0.5GB
# 浪费了 7.5GB HBM

正确配置:用 PagedAttention + 动态分配。

# PagedAttention:按 256 token/page 分配
# 128 tokens 用 1 page = 128 MB(不是 8GB)
export ASCEND_KV_CACHE_POLICY=paged
export ASCEND_PAGE_SIZE=256

# batch size 上限预留
export ASCEND_MAX_BATCH=32
# 运行时参数
server.set_kv_cache_policy("paged")
server.set_max_seq_len(4096)       # 硬上限
server.set_dynamic_seq_len(True)   # 实际按输入分配

踩坑三:O2 融合的 hidden state 精度丢失

编译器的 O2 融合级别会把 LayerNorm + MatMul + GeLU 融合成一个算子。中间的 hidden state 不写回 HBM——但在 FP16 下,连续跳过两到三个写回会导致精度累积下降。

现象:推理日志里没有报错,但生成文本在第 200-300 token 后开始跑偏——某些层的 hidden state 的 FP16 精度在融合 Pipeline 里被连续截断。

缓解:用 O1 融合(只做相邻两层的融合,不做 pipeline 级别的多层链式融合)。

# O1:保守融合,每一层 hidden state 都写回 HBM
compiler.set_options({
    "fusion_level": "O1",          # 不是 O2
    "memory_optimizer": "recompute"
})

# O2 的好处是 10-15% 性能提升,但对于长文本推理,
# hidden state 的累积精度损失可能比性能收益更大

实际性能数据

Ascend 910 单卡上 LLaMA-7B W8A16 推理性能:

指标 数值
吞吐(batch=1) 48 tokens/s
吞吐(batch=8) 210 tokens/s
首 token 延迟(batch=1, 128 input) 98 ms
单 token 延迟(batch=1, decode) 21 ms
显存占用(batch=1) 16.2 GB
显存占用(batch=8) 24.5 GB

cann-recipes-infer 的价值不在算法创新,在工程细节。一条菜谱代表了一个模型的部署配置——量化策略、图切分、融合级别、KV Cache 策略——这些参数一旦配错了,推理服务的吞吐和延迟可能差 2-3 倍。菜谱就是这些参数的最佳值,每条都经过了测试和验证。

Logo

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

更多推荐