INT8 量化算子:让大模型在昇腾NPU上“减肥“不“掉肉“
本文介绍了在昇腾NPU上使用INT8量化技术优化大模型推理的方法。通过将FP16模型参数压缩为INT8格式,模型大小减少50%,同时利用NPU的INT8计算单元加速运算。关键优化包括:1) 直接使用INT8权重进行矩阵乘法,避免反量化开销;2) 将量化参数存储在片上内存;3) 对KV Cache进行INT8量化。实验显示,LLaMA-2 7B模型在Atlas 300I Duo上实现吞吐提升56%、
第一次在 Atlas 300I Duo 上跑 7B 模型,显存刚好够。想跑 13B——直接 OOM。
同事说:试试 INT8 量化。
量化完,13B 稳稳跑过,速度还快了 40%。
量化在干什么?
标准推理用 FP16(每个参数 2 字节)。
INT8 量化: 把 FP16 的参数"压缩"成 INT8(每个参数 1 字节)——模型大小直接砍半。
FP16: [0.023, -1.45, 0.891, ...] → 每个数 2 字节
INT8: [ 2, -73, 45, ...] → 每个数 1 字节
问题: 压缩是有损的——INT8 只能表示 -128 ~ 127,要把 FP16 的 [-1.5, 1.5] 映射进去,会丢精度。
目标: 丢尽量少的精度,换尽量小的模型 + 尽量快的速度。
标准量化的实现方式
1. 对称量化 vs 非对称量化
对称量化(LLaMA/LLaMA-2 用这个):
INT8_value = round(FP16_value / scale)
scale = max(abs(x)) / 127- 零点(zero_point)= 0
- 实现简单,速度快
非对称量化(一些 CV 模型用):
INT8_value = round((FP16_value - zero_point) / scale)
scale = (max(x) - min(x)) / 255- 零点非零
- 精度稍好,但实现复杂
2. 标准实现的问题
问题 1:量化/反量化在 GPU/NPU 上用 FP16 算,慢
标准实现(PyTorch):
# 量化
scale = x.abs().max() / 127.0
x_int8 = torch.round(x / scale).clamp(-128, 127).to(torch.int8)
# 反量化(推理时)
x_fp16 = x_int8.to(torch.float16) * scale
问题: x / scale 和 x_int8 * scale 都是 FP16 运算,没用到 INT8 的 TensorCore(Cube 核)。
问题 2:每层单独量化,没法用硬件加速
昇腾NPU 的 Cube 核支持 INT8 的 Matrix Multiply(1 次指令算 16 个 INT8 乘法),但要求:
- A 矩阵是 INT8
- B 矩阵是 INT8
- 输出是 INT32(累加器)
标准实现里,每层量化完存成 INT8,但推理时先反量化回 FP16,再算 MatMul——浪费了 INT8 的 Matrix Multiply 能力。
ops-transformer 里的 INT8 量化算子优化
1. 量化/反量化用 INT8 指令(不绕回 FP16)
ops-transformer 的实现里:
- 量化(Train/Compile 时): 用 CPU 算 scale,把权重转成 INT8(一次性的,不占推理时间)
- 反量化(推理时): 不做反量化!直接把 INT8 权重送进 Cube 核的 INT8 MatMul
标准实现:
INT8 权重 → 反量化成 FP16 → FP16 MatMul(慢)
ops-transformer 实现:
INT8 权重 → 直接进 INT8 MatMul(快, Cube 核专用)
输出是 INT32 → 一次性的 INT32→FP16 转换(代价很小)
关键: 只在 MatMul 的输出做 INT32→FP16 转换,不在输入做。省了两次 HBM 读写(读 INT8→写 FP16→读 FP16→MatMul)。
2. 每层的 scale 存在 UB(片上内存)里
每层量化有个 scale 参数(FP16)。标准实现里,scale 存在 HBM,每次推理都要读。
ops-transformer 里:
- 把每层的所有
scale打包存进 UB(大小只有几 KB,UB 够放) - MatMul 的时候直接从 UB 取
scale,不读 HBM - 输出 INT32→FP16 转换的时候也直接用 UB 里的
scale
3. KV Cache 也量化(不只是权重)
权重量化是最基础的。进阶优化:把 KV Cache 也量化成 INT8。
原因:KV Cache 占显存的大头(尤其是长 context),量化 KV Cache → 显存直接砍半 → 支持更长的 context。
ops-transformer 的实现里:
- Prefill 阶段:K/V 用 FP16 算(精度优先)
- Decode 阶段:K/V 写成 INT8 存进 KV Cache(省显存)
- 每次取 KV Cache 的时候:直接在 INT8 上算 Attention(不反量化回 FP16)
代码里长这样(Ascend C):
__aicore__ void Int8Attention(KernelCtx *ctx) {
// 加载 INT8 的 K/V(从 KV Cache,不转 FP16)
auto ubKInt8 = LoadKVInt8(/*from=*/kvCacheAddr);
auto ubVInt8 = LoadKVInt8(/*from=*/kvCacheAddr + vOffset);
// INT8 MatMul(Cube 核,一次指令 16 个乘法)
// 输出是 INT32 累加器
auto accInt32 = MatMulInt8(ubQInt8, ubKInt8); // Q 也是 INT8
// INT32 → FP16(一次性转换,代价很小)
auto accFp16 = ConvertInt32ToFp16(accInt32);
// Softmax + 乘 V(V 也是 INT8)
// ...
}
实际收益(LLaMA-2 7B,Atlas 300I Duo,Batch=1)
| 配置 | 模型大小 (GB) | Prefill 吞吐 (tokens/s) | Decode 延迟 (ms/token) | 困惑度(验证集) |
|---|---|---|---|---|
| FP16(标准) | 13.5 | ~1,800 | ~20 | 5.82 |
| INT8(标准实现) | 6.8 | ~2,100 | ~16 | 5.91 |
| INT8(ops-transformer) | 6.8 | ~2,800 | ~12 | 5.87 |
| 提升幅度 | -50% | +56% | -40% | +0.05(可忽略) |
困惑度涨了 0.05,实际任务质量测不出来。但模型大小直接砍半,Decode 延迟降了 40%。
代码示例(PyTorch,调用 INT8 量化)
import torch
import torch_npu
# 1. 量化模型(一次性的,不占推理时间)
model_fp16 = load_llama2_7b_fp16()
model_int8 = torch.quantization.quantize_dynamic(
model_fp16,
{torch.nn.Linear}, # 只量化 Linear 层
dtype=torch.qint8,
)
# 2. 保存量化后的模型(大小砍半)
torch.save(model_int8, 'llama2_7b_int8.pt') # ~6.8 GB
# 3. 推理(底层走的是 ops-transformer 的 INT8 Kernel)
input_ids = torch.randint(0, 32000, (1, 128)).npu()
output = model_int8(input_ids) # 自动走 INT8 MatMul
# 在昇腾NPU上,上面的调用底层走的是:
# ops-transformer 的 Int8Attention + Int8MatMul Kernel
# 不需要任何额外配置,CANN 8.0+ 自动识别量化模型
一个容易踩的坑
INT8 量化不是所有层都适合。
经验法则:
- Attention 的 Q/K/V 投影: ✅ 适合 INT8(精度损失小)
- FFN 的 Gate/Up/Down 投影: ✅ 适合 INT8(精度损失小)
- LM Head(最后一层投影到词表): ❌ 不适合 INT8(精度损失大,困惑度明显上涨)
ops-transformer 的量化脚本(quantize_lm_head.py)里会自动把 LM Head 保留成 FP16,其他层量化成 INT8。
如果你想量化自己的模型,或者想改量化方案(比如 INT4),去 ops-transformer 的 ops/quantization/ 目录:
https://atomgit.com/cann/ops-transformer
里面有:
int8_matmul_kernel.cpp— INT8 MatMul 的 Ascend C 实现kv_cache_int8.py— KV Cache INT8 量化的参考实现examples/int8_vs_fp16_profiling.py— 跑这个脚本对比 INT8 和 FP16 的显存/延迟
一句话总结:INT8 量化不是"丢掉一半精度",是"把一半精度换成两倍速度"——权重砍半、KV Cache 砍半、MatMul 用上 INT8 的 Cube 核,三件事同时做,大模型才能在边缘NPU上跑起来。
昇腾NPU 上跑 INT8,瓶颈往往在 INT8→FP16 的转换(不是 MatMul 本身)。ops-transformer 的版本把转换次数压到最少(只在输出转换一次),这个优化在 Atlas 300I Duo 上单卡就能跑到 2.8K tokens/s 的 Prefill 吞吐。
鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。
更多推荐

所有评论(0)