1. 量化基础:从浮点到定点的数学原理
1.1 线性量化公式解析
量化本质上是在有限精度表示和计算效率之间寻找平衡的艺术。在端侧AI部署中,我们最常用的量化公式可以表示为:
code复制量化(浮点 → 定点):q = clip(round(x/scale + zero_point), qmin, qmax)
反量化(定点 → 浮点):x_hat = (q - zero_point) * scale
其中关键参数的计算逻辑如下:
- scale:决定量化步长,计算公式为
(xmax - xmin) / (qmax - qmin) - zero_point:用于将浮点零点映射到整数域,计算公式为
round(qmin - xmin/scale)
不同位宽的量化范围:
- INT8:qmin=0, qmax=255(非对称)或 qmin=-128, qmax=127(对称)
- INT16:qmin=-32768, qmax=32767
- INT4:qmin=-8, qmax=7
注意:实际部署中,scale和zero_point需要作为量化参数保存在模型文件中。在Hexagon NPU上,这些参数会被编译进模型指令流,在运行时由硬件自动应用。
1.2 对称与非对称量化对比
对称量化的特点是zero_point固定为0,量化范围关于零点对称。这种方案计算效率高,特别适合权重量化:
python复制def symmetric_quantize(x, num_bits=8):
qmax = 2**(num_bits - 1) - 1 # 对于INT8是127
scale = np.max(np.abs(x)) / qmax
q = np.clip(np.round(x / scale), -qmax, qmax).astype(np.int8)
return q, q.astype(np.float32) * scale, scale
非对称量化则能更精确地利用整个量化范围,特别适合处理ReLU等非负激活函数:
python复制def asymmetric_quantize(x, num_bits=8):
qmin, qmax = 0, 2**num_bits - 1 # 对于INT8是0-255
scale = (np.max(x) - np.min(x)) / (qmax - qmin)
zero_point = np.round(qmin - np.min(x) / scale)
zero_point = np.clip(zero_point, qmin, qmax)
q = np.clip(np.round(x / scale + zero_point), qmin, qmax).astype(np.uint8)
return q, (q.astype(np.float32) - zero_point) * scale, scale, zero_point
实测对比(基于ReLU激活分布):
- 对称量化 MSE: 0.004215
- 非对称量化 MSE: 0.002107
- 误差降低约50%
1.3 量化粒度选择策略
量化粒度决定了共享同一组量化参数的张量范围,常见有三种粒度:
-
Per-Tensor:整个张量使用同一组scale/zero_point
- 优点:计算开销最小
- 缺点:精度损失最大
- 适用场景:对精度不敏感的全连接层
-
Per-Channel:卷积核的每个输出通道单独量化
- 优点:显著提升卷积层精度
- 缺点:增加少量计算开销
- 适用场景:卷积层权重量化(QNN默认推荐)
-
Per-Group:将通道分组后每组单独量化
- 优点:平衡精度和效率
- 缺点:实现复杂度高
- 适用场景:Transformer中的大矩阵乘法(group=128)
实测精度对比(同一模型):
| 量化粒度 | MSE | 相对精度 |
|---|---|---|
| Per-Tensor | 0.0124 | 基准 |
| Per-Channel | 0.0089 | +28% |
| Per-Group(128) | 0.0051 | +58% |
| Per-Group(32) | 0.0032 | +74% |
实操建议:在Hexagon HTP上,Per-Channel是卷积层的默认选择,Per-Group(128)则推荐用于Transformer的Linear层。V75及以上版本的NPU才支持Per-Group量化。
2. QNN量化实战:四种核心方案
2.1 基础训练后量化(PTQ)
PTQ是最基础的量化方案,只需校准数据无需重新训练:
bash复制qnn-onnx-converter \
--input_network model.onnx \
--output_path model_qnn.cpp \
--input_dim input 1,3,224,224 \
--input_list calibration/input_list.txt \
--act_bw 8 \ # 激活值8-bit
--weight_bw 8 \ # 权重8-bit
--bias_bw 32 \ # 偏置保持32-bit
--algorithms cle \ # 使用CLE优化
--use_per_channel_quantization
关键参数说明:
input_list.txt:包含100-1000个校准样本路径algorithms:支持cle(跨层均衡)、adaround(自适应舍入)等优化算法act_bw/weight_bw:可配置混合精度(如act_bw=16, weight_bw=8)
校准数据准备要点:
- 样本数量:至少200个,推荐500-1000
- 数据分布:需覆盖实际应用场景的所有边界情况
- 预处理:必须与推理时完全一致
2.2 增强型PTQ方案
通过quantization_overrides.json实现更精细的量化控制:
json复制{
"activation_encodings": {
"/model/layer.0/attention/Softmax_output_0": {
"dtype": "int16",
"is_symmetric": true
}
},
"param_encodings": {
"model.embed_tokens.weight": {
"dtype": "int8",
"is_symmetric": true
},
"model.layers.*.self_attn.q_proj.weight": {
"dtype": "int4",
"is_symmetric": true
}
},
"supergroups": [
{"op_list": ["Conv", "Relu"], "fuse": true}
]
}
典型增强策略:
- 敏感层保留高精度:如Softmax输出使用INT16
- 非敏感层激进量化:如中间层权重使用INT4
- 算子融合:将Conv+ReLU等模式融合为单个算子
2.3 混合精度量化
自动敏感度分析工具实现步骤:
python复制class MixedPrecisionSearcher:
def compute_layer_sensitivity(self, model, cal_loader):
baseline = evaluate(model, cal_loader)
sensitivities = {}
for name, module in model.named_modules():
if not isinstance(module, (nn.Linear, nn.Conv2d)):
continue
orig_weight = module.weight.clone()
quant_weight = self._simulate_quant(orig_weight)
module.weight.data = quant_weight
metric = evaluate(model, cal_loader)
sensitivities[name] = baseline - metric
module.weight.data = orig_weight
return sensitivities
def _simulate_quant(self, tensor, bits=8):
scale = tensor.abs().max() / (2**(bits-1)-1)
return torch.clamp(tensor/scale, -2**(bits-1), 2**(bits-1)-1) * scale
实施流程:
- 在全INT8模型上逐层计算量化敏感度
- 对敏感度最高的20%层保留INT16精度
- 生成混合精度配置文件并重新量化
2.4 量化感知训练(QAT)
QAT在训练过程中模拟量化误差,需要3-10个epoch的微调:
python复制from aimet_torch.quantsim import QuantizationSimModel
sim = QuantizationSimModel(
model=model,
dummy_input=torch.randn(1,3,224,224),
quant_scheme='tf_enhanced',
default_output_bw=8,
default_param_bw=8,
config_file='qnn_config.json'
)
# 校准
sim.compute_encodings(forward_callback, None)
# 微调
optimizer = torch.optim.SGD(sim.model.parameters(), lr=0.001)
for epoch in range(10):
for x, y in train_loader:
optimizer.zero_grad()
output = sim.model(x)
loss = criterion(output, y)
loss.backward()
optimizer.step()
QAT关键点:
- 学习率设为初始训练的1/10
- 使用SGD而非Adam优化器
- 校准数据应与训练数据同分布
- 最终导出时需要冻结量化参数
3. Hexagon NPU性能调优
3.1 NPU性能模型解析
Hexagon NPU的性能可由以下公式估算:
code复制NPU执行时间 = max(计算时间, 内存时间) + 调度开销
计算时间 = 总MAC数 / HTA吞吐量
内存时间 = 数据搬移量 / 内存带宽
骁龙8 Gen3(SM8650)关键指标:
| 指标 | 数值 |
|---|---|
| INT8算力 | 36 TOPS |
| INT16算力 | 18 TOPS |
| INT4算力 | 72 TOPS |
| 内存带宽 | 68.3 GB/s(理论) |
| 有效可用带宽 | ~50 GB/s |
| VTCM容量 | 8 MB |
3.2 Roofline模型实战
通过算术强度分析计算/内存瓶颈:
python复制class RooflineAnalyzer:
def __init__(self, peak_tops=36, mem_bw=50):
self.ridge_point = (peak_tops*1e12) / (mem_bw*1e9) # ops/byte
def analyze_layer(self, name, ops, data_bytes):
intensity = ops / data_bytes
compute_time = ops / (self.peak_tops*1e12)
memory_time = data_bytes / (self.mem_bw*1e9)
if intensity >= self.ridge_point:
return "计算瓶颈", compute_time
else:
return "带宽瓶颈", memory_time
典型层的分析结果:
| 层类型 | 算术强度(ops/byte) | 瓶颈类型 |
|---|---|---|
| 3x3卷积 | 4.5 | 计算瓶颈 |
| 1x1卷积 | 1.2 | 带宽瓶颈 |
| 全连接层 | 0.3 | 带宽瓶颈 |
| Depthwise卷积 | 0.8 | 带宽瓶颈 |
3.3 性能优化技巧
算子融合配置
json复制{
"graph_config": {
"enable_dlbc": true,
"tiling_config": {
"prefer_vtcm_tiling": true,
"max_tile_height": 64
}
}
}
内存布局优化
bash复制qnn-onnx-converter --input_layout input NHWC
零拷贝内存管理
cpp复制class ZeroCopyBuffer {
public:
bool allocate(size_t size) {
int ion_fd = open("/dev/ion", O_RDONLY);
struct ion_allocation_data alloc = {
.len = size,
.heap_id_mask = ION_HEAP_SYSTEM_MASK,
.flags = ION_FLAG_CACHED
};
ioctl(ion_fd, ION_IOC_ALLOC, &alloc);
buffer_ = mmap(nullptr, size, PROT_READ|PROT_WRITE,
MAP_SHARED, alloc.fd, 0);
return buffer_ != MAP_FAILED;
}
};
4. 实战案例:Llama 2量化部署
4.1 量化方案选择
| 方案 | 精度损失 | 推理延迟 | 实施难度 |
|---|---|---|---|
| INT8 PTQ | 1.8% | 45ms | ★ |
| INT8+混合精度 | 0.7% | 52ms | ★★ |
| INT4+组量化 | 1.2% | 38ms | ★★★ |
| QAT | 0.3% | 45ms | ★★★★ |
4.2 关键配置
json复制{
"param_encodings": {
"model.embed_tokens.weight": {"dtype": "int8"},
"lm_head.weight": {"dtype": "int8"},
"model.layers.*.self_attn.*_proj.weight": {
"dtype": "int4",
"group_size": 128
}
},
"activation_encodings": {
".*Softmax.*": {"dtype": "int16"}
}
}
4.3 性能优化结果
| 优化项 | 延迟改善 | 内存节省 |
|---|---|---|
| KV Cache量化 | 22% | 50% |
| 算子融合 | 15% | 30% |
| 零拷贝 | 8% | 90% |
| VTCM优化 | 12% | - |
最终在骁龙8 Gen3上实现Llama 2 7B的实时推理(~50ms/token)。