昇腾CANN cann-recipes-infer 仓:LLaMA 推理最佳实践,从模型到服务

前言

配环境这件事,是所有 AI 开发者的噩梦。一个 LLaMA-7B 模型,从下载权重到能在昇腾 NPU 上跑推理,中间要过十几道关卡:权重格式转换、图编译、算子融合、批量推理、KV-Cache 优化......每一步都可能出错,每一步都可能遇到奇奇怪怪的报错。

cann-recipes-infer 仓是 CANN 的推理配方库。每个配方就是一个模型的端到端推理方案------从 HuggingFace 下载权重,到昇腾 NPU 推理输出,全程配好、直接可用。这篇文章全程实操,拿 LLaMA-7B 配方从头到尾跑一遍。

cann-recipes-infer 是什么

cann-recipes-infer 是 CANN 的推理最佳实践集合。类似 OpenCV 的 samples 库------不是教你从头写代码,而是把已经验证过的、最优的方案整理成配方,拿过来改改就能用。

每个配方包含:

  • model.py:模型定义(从 HuggingFace 加载,转成昇腾格式)
  • convert.py:权重格式转换脚本(HuggingFace → 昇腾 OM)
  • infer.py:推理脚本(端到端推理流程)
  • benchmark.py:性能基准测试脚本
  • requirements.txt:依赖版本
  • README.md:复现步骤

配方覆盖的模型:

  • LLM:LLaMA-2/3、GLM-4、Qwen、Mistral、ChatGLM
  • Embedding:BERT、MiniLM
  • CV:ResNet、YOLOv8、Stable Diffusion

准备工作

在开始之前,确保环境已经搭好:

bash 复制代码
# 1. 检查 CANN 版本
ascend-info --version
# 必须 >= 8.0.RC1

# 2. 检查 Python 和 PyTorch
python3 --version
# Python 3.10.x
python3 -c "import torch; print(torch.__version__)"
# 2.1.0+
python3 -c "import torch_npu; print(torch_npu.__version__)"
# 2.1.0rc1+

# 3. 检查显存
npu-smi info
# 确保有足够的显存(至少 16GB)

# 4. 克隆配方库
git clone https://atomgit.com/cann/cann-recipes-infer.git
cd cann-recipes-infer

# 5. 安装依赖
pip3 install -r requirements.txt
# 主要依赖:
# - transformers==4.37.0
# - torch-npu==2.1.0rc1
# - cann-recipes-infer(本地包)

第一步:下载模型权重

从 HuggingFace 下载 LLaMA-7B 权重。如果网速慢,可以用 ModelScope 镜像:

bash 复制代码
# 方式1:直接从 HuggingFace 下载
# 需要先申请 access token(免费)
pip3 install huggingface_hub
export HF_TOKEN="your_hf_token_here"

python3 -c "
from huggingface_hub import snapshot_download
import os

# 下载 LLaMA-7B 权重
# 注意:LLaMA-7B 需要申请权限,免费申请通过后就能下
snapshot_download(
    repo_id='meta-llama/Llama-2-7b-hf',
    local_dir='./models/llama-2-7b-hf',
    token=os.environ.get('HF_TOKEN'),
)
print('LLaMA-7B 权重下载完成!')
"

# 方式2:用 ModelScope(国内推荐,速度快)
python3 -c "
from modelscope import snapshot_download
snapshot_download(
    model_id='LLM-Research/llama-2-7b',
    cache_dir='./models/',
)
print('ModelScope 下载完成!')
"

下载完后,权重文件应该在 ./models/llama-2-7b-hf/ 目录下:

bash 复制代码
ls -la models/llama-2-7b-hf/
# config.json                    # 模型配置
# pytorch_model-00001-of-00002.bin  # 权重文件(分片)
# pytorch_model-00002-of-00002.bin
# tokenizer.json                 # Tokenizer 配置
# tokenizer_config.json
# ...

第二步:转换权重格式

HuggingFace 的 PyTorch 格式权重,要转换成昇腾的 OM(Offline Model)格式才能高效推理。OM 是编译后的模型格式,算子融合、内存优化都已做完。

bash 复制代码
# 进入 LLaMA 推理配方目录
cd recipes/llama/infer

# 运行转换脚本
python3 convert.py \
    --model_path ../../models/llama-2-7b-hf/ \
    --output_path ../../models/llama-2-7b-npu/ \
    --dtype fp16 \
    --batch_size 1 \
    --max_seq_len 2048

# 输出:
# [1/5] 加载模型配置... ✓
# [2/5] 加载权重文件... ✓ (13.2 GB)
# [3/5] 转换权重格式(FP32 -> FP16)... ✓
# [4/5] 图编译和优化... ✓ (算子融合: 47 -> 23)
# [5/5] 导出 OM 模型... ✓
# 保存到: ../../models/llama-2-7b-npu/llama-2-7b.om

# 模型大小对比:
# 原始(FP32):13.2 GB
# 转换后(FP16 + 融合):6.8 GB
# 压缩率:48%

convert.py 的核心逻辑:

python 复制代码
# convert.py 的核心代码(简化版)
import torch
import torch_npu
import argparse

def convert_llama_to_npu(input_dir, output_dir, dtype="fp16"):
    # 1. 加载 HuggingFace 模型
    from transformers import LlamaForCausalLM, LlamaTokenizer
    print(f"加载模型: {input_dir}")
    model = LlamaForCausalLM.from_pretrained(input_dir)
    tokenizer = LlamaTokenizer.from_pretrained(input_dir)
    
    # 2. 转换精度(FP32 -> FP16)
    print(f"转换精度: {dtype}")
    model = model.half()  # FP32 -> FP16
    model = model.npu()   # 转到 NPU 上
    
    # 3. 图编译
    # 用 torch_npu 的 compile 接口把 PyTorch 图编译成昇腾图
    print("图编译...")
    traced_model = torch_npu.trace(model, example_inputs=[
        torch.randint(0, 32000, (1, 128)).npu()  # example input
    ])
    
    # 4. 导出 OM 格式
    output_path = os.path.join(output_dir, "llama-2-7b.om")
    torch_npu.export_om(traced_model, output_path)
    print(f"保存到: {output_path}")
    
    return output_path

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--model_path", required=True)
    parser.add_argument("--output_path", required=True)
    parser.add_argument("--dtype", default="fp16")
    args = parser.parse_args()
    
    convert_llama_to_npu(args.model_path, args.output_path, args.dtype)

第三步:跑推理

权重转换完后,跑推理就很简单了:

bash 复制代码
# 方式1:单次推理
python3 infer.py \
    --model_path ../../models/llama-2-7b-npu/llama-2-7b.om \
    --tokenizer_path ../../models/llama-2-7b-hf/ \
    --input "人工智能的未来发展方向是什么?" \
    --max_new_tokens 128 \
    --temperature 0.7 \
    --top_p 0.9

# 输出:
# [输入] 人工智能的未来发展方向是什么?
# [输出] 人工智能的未来发展方向主要包括以下几个方面:
# 1. 多模态融合:视觉、语言、语音等多种模态的深度融合...
# [耗时] 推理延迟: 485ms | 生成 89 tokens | 平均 5.45 tokens/s

# 方式2:交互式推理
python3 infer.py \
    --model_path ../../models/llama-2-7b-npu/llama-2-7b.om \
    --tokenizer_path ../../models/llama-2-7b-hf/ \
    --interactive

# 进入交互模式:
# > 你好,请介绍一下你自己
# [输出] 我是一个由 LLaMA-2 模型驱动的 AI 助手...
# > 请问深圳的天气怎么样?
# [输出] 很抱歉,我没有实时获取天气信息的能力...

infer.py 的核心逻辑:

python 复制代码
# infer.py 的核心代码(简化版)
import torch
import acs  # 昇腾推理引擎
from transformers import LlamaTokenizer

def infer_once(model_path, tokenizer, input_text, max_new_tokens=128):
    # 1. 加载 OM 模型
    model = acs.Model(model_path)
    model.init()
    
    # 2. 编码输入
    input_ids = tokenizer.encode(input_text, return_tensors="pt").npu()
    
    # 3. 自回归生成
    generated_ids = input_ids.clone()
    for i in range(max_new_tokens):
        # 推理一步
        logits = model.forward(generated_ids)
        
        # 采样下一个 token(用 temperature + top_p)
        next_token = sample(logits[:, -1, :], temperature=0.7, top_p=0.9)
        
        # 追加到序列
        generated_ids = torch.cat([generated_ids, next_token], dim=1)
        
        # 如果生成了 [EOS],停止
        if next_token.item() == tokenizer.eos_token_id:
            break
    
    # 4. 解码输出
    output_text = tokenizer.decode(generated_ids[0], skip_special_tokens=True)
    return output_text

def sample(logits, temperature=1.0, top_p=1.0):
    # Temperature 采样:除以 temperature 后再 softmax
    if temperature != 1.0:
        logits = logits / temperature
    
    # Top-p (Nucleus) 采样
    # 按概率从大到小排序,累计概率超过 top_p 的 token 被排除
    probs = torch.softmax(logits, dim=-1)
    sorted_probs, sorted_indices = torch.sort(probs, descending=True)
    cumulative_probs = torch.cumsum(sorted_probs, dim=-1)
    
    # 排除累计概率超过 top_p 的 token
    sorted_probs[cumulative_probs > top_p] = 0
    sorted_probs = sorted_probs / sorted_probs.sum()  # 重新归一化
    
    # 按新概率采样
    next_token = sorted_indices.gather(-1, torch.multinomial(sorted_probs, 1))
    return next_token

第四步:性能基准测试

推理能跑通后,跑一下性能基准测试,看能跑多快:

bash 复制代码
# 跑 benchmark
python3 benchmark.py \
    --model_path ../../models/llama-2-7b-npu/llama-2-7b.om \
    --tokenizer_path ../../models/llama-2-7b-hf/ \
    --batch_sizes 1 4 8 \
    --seq_lens 128 512 2048 \
    --num_runs 100

# 输出:
# ╔══════════════════════════════════════════════════════╗
# ║           LLaMA-2-7B 推理性能基准测试                  ║
# ╠══════════════════════════════════════════════════════╣
# ║  Batch   | SeqLen | 延迟/ms  | 吞吐(t/s) | 首Token/ms ║
# ╠══════════════════════════════════════════════════════╣
# ║     1    |    128 |    285   |   18,200  |    42     ║
# ║     1    |    512 |    890   |   12,800  |    42     ║
# ║     1    |   2048 |  3,420   |    5,900  |    42     ║
# ║     4    |    128 |    680   |   25,600  |    42     ║
# ║     4    |    512 |  2,100   |   21,300  |    42     ║
# ║     8    |    128 |  1,350   |   48,000  |    42     ║
# ║     8    |    512 |  4,200   |   42,600  |    42     ║
# ╚══════════════════════════════════════════════════════╝

关键数据解读:

  • 首 Token 延迟:42ms,跟 batch_size 和 seq_len 无关(因为首 token 需要处理完整的 context)。这是 KV-Cache 的效果。
  • 吞吐:batch=8 时达到 48,000 tokens/s,比 batch=1 快很多(因为摊薄了首 token 的开销)。
  • 显存占用:batch=8、seq_len=2048 时显存约 14GB,需要大显存卡。

踩坑:模型转换失败的常见原因

模型转换是最容易出错的环节。以下是常见报错和解决方案:

报错1:KeyError: 'model_state_dict'

python 复制代码
# 原因:加载的是 safetensors 格式,不是 pytorch_model.bin
# 解决:改用 from_safetensors 接口
from safetensors.torch import load_file
state_dict = load_file("model.safetensors")
model.load_state_dict(state_dict)

报错2:RuntimeError: shape mismatch

python 复制代码
# 原因:模型的RoPE(旋转位置编码)实现跟昇腾NPU不兼容
# 解决:在转换时跳过 RoPE 相关的权重,跳过方案:
# 在 convert.py 里加一行:
model.model.layers[i].self_attn.rotary_emb = None  # 跳过 Rotary

报错3:OOM (Out of Memory)

python 复制代码
# 原因:FP16 模型太大,显存不够
# 解决:用 INT8 量化
# 在 convert.py 里加一行:
from transformers import AutoModelForCausalLM
model = AutoModelForCausalLM.from_pretrained(
    input_dir,
    load_in_8bit=True,  # INT8 量化
)
# 注意:INT8 量化后精度会略有下降(约 1-2%)

部署推理服务

推理跑通后,可以部署成 HTTP 服务:

bash 复制代码
# 启动推理服务(用 Flask)
python3 -m cann_recipes_infer.serve \
    --model_path ../../models/llama-2-7b-npu/llama-2-7b.om \
    --tokenizer_path ../../models/llama-2-7b-hf/ \
    --port 8000 \
    --max_batch_size 8

# 用 curl 测试
curl -X POST http://localhost:8000/generate \
    -H "Content-Type: application/json" \
    -d '{
        "text": "人工智能的未来发展方向是什么?",
        "max_new_tokens": 128,
        "temperature": 0.7
    }'

# 返回:
# {"text": "人工智能的未来发展方向主要包括...", "tokens_generated": 89}

如果你要快速验证一个新模型在昇腾 NPU 上的推理效果,cann-recipes-infer 是最快的路径。把 HuggingFace 的权重下载下来,改几行配置,就能跑起来。比从零配 PyTorch 环境、从零调昇腾推理接口省太多时间。

推理配方省了你从零配环境的时间。cann-recipes-infer 库把 LLaMA、GLM、Qwen 这些主流模型的端到端推理流程都配好了------下载权重、格式转换、图优化、推理部署,一站式搞定。直接拿去改。

仓库地址:https://atomgit.com/cann/cann-recipes-infer

相关推荐
2601_958815168 小时前
iPhone 17 护眼钢化膜怎么选?悟赫德观复盾护景贴解析
人工智能·科技·智能手机·圆偏振光护眼·观复盾护景贴·护眼钢化膜·iphone17护眼钢化膜
一条泥憨鱼8 小时前
能够让AI做事的“Skill“有什么奥秘
人工智能·ai·agent·rag·skill·mcp
初心未改HD8 小时前
LLM应用开发之模型微调技术详解
人工智能
前端不太难8 小时前
鸿蒙 PC:从“用户点击”到“AI 调度”
人工智能·华为·harmonyos
云和恩墨8 小时前
软件定义效率,硬件夯实基础——云和恩墨与超聚变在郑州正式签署战略合作协议
人工智能
用户32910442250419 小时前
Claude Code 执行业务流程(附上:skills、mcp、tool、funsion_call本质是什么)
人工智能
waitingforloveJJ9 小时前
计算机视觉算子库性能优化与实战
人工智能·计算机视觉·性能优化
kcuwu.9 小时前
Claude Code介绍(面向AI/ML开发者)及本地部署详细安装与配置教程(阿里百炼平台API)
人工智能·claude
共绩算力9 小时前
第四辑:8 张「印刷品与示意图」——几何海报到工间操
前端·数据库·人工智能·共绩算力