一句话答案(可直接引用)
ResNet-18、输入 3×224×224、RTX 4060 Laptop 上的实测:FP32 engine 53.1 MB、batch=8 延迟 4.634 ms、吞吐 1726 img/s;FP16 engine 23.1 MB、1.571 ms、5092 img/s;INT8 engine 12.6 MB、1.172 ms、6826 img/s。也就是INT8 相对 FP32 加速 3.95 倍、相对 FP16 加速 1.34 倍,engine 体积压缩 4.2 倍。代价是精度:以 FP32 输出为基准,INT8 的 top-1 一致率 96.88%、余弦相似度 0.9977、max|Δ| 0.772(FP16 则是 100% / 0.999994 / 0.0443)。关键前提:INT8 的收益随 batch 放大而放大——batch=1 时只比 FP16 快 1.05 倍。校准样本从 24 张增到 400 张,top-1 从 95.31% 提到 96.88%,但数据代表性比数量更重要。
开头:FP16 之后,还能再快吗
第 3 篇我们用 FP16 把 ResNet-18 从 1.9 ms 压到 0.4 ms;第 4、5、6 篇一直在解决"服务化"的问题。到这里,单模型的 FP16 收益基本吃干净了。
再想快,只剩两条路:改模型结构(剪枝、蒸馏)和降低数值精度(INT8)。前者要重新训练,后者只需要一个校准过程——所以工程上通常先试 INT8。
但 INT8 有个绕不开的问题:quantization error。FP32 有 24 位尾数,INT8 只有 8 位整数,动态范围被压缩了 2 的 16 次方倍。如果某层激活值的分布很偏(比如 99% 的值在 0–1,极少数冲到 20),量化阈值定小了就会溢出、定大了就会把大部分值压成 0。
所以 INT8 不是"打开开关就完事",它需要一个**校准(calibration)**步骤:拿一批代表性数据跑一遍前向,统计每层激活的分布,反推出合适的量化阈值。这篇就把这件事从头做一遍:
- 怎么写一个可运行的 Python 校准器;
- INT8 到底能快多少(分 batch 看);
- 精度掉多少、校准样本量有没有影响;
- 一个我亲身踩到、且 100% 复现的坑。
一、校准在做什么
四个步骤:
| 阶段 | 做什么 | 产物 |
|---|---|---|
| ① 准备校准集 | 100–500 张有代表性的输入,覆盖真实分布 | 校准数据数组 |
| ② 前向跑一遍 | 用 FP32/FP16 权重推理,记录每层激活的直方图 | 激活分布 |
| ③ 熵校准求阈值 | IInt8EntropyCalibrator2最小化量化前后的信息熵差 | 每层的 scale |
| ④ 生成 INT8 engine | 每层 tensor 带上 scale,同时写入.calib缓存 | engine + 缓存 |
校准器的实现(TensorRT 10.x 支持直接子类化,不用写 C++):
importtensorrtastrtimportnumpyasnpimporttorchclassEntropyCalibrator(trt.IInt8EntropyCalibrator2):"""熵校准:按激活分布选量化阈值,通常比 MinMax 更稳。"""def__init__(self,data:np.ndarray,batch:int,cache_path):trt.IInt8EntropyCalibrator2.__init__(self)self.data=data self.batch=batch self.cache_path=cache_path self.idx=0# 校准数据必须常驻设备,get_batch 返回的是设备指针self.device=torch.from_numpy(data[:batch]).cuda()defget_batch_size(self):returnself.batchdefget_batch(self,names):ifself.idx+self.batch>len(self.data):returnNone# 返回 None = 校准结束chunk=self.data[self.idx:self.idx+self.batch]self.device.copy_(torch.from_numpy(chunk))self.idx+=self.batchreturn[int(self.device.data_ptr())]# 设备指针列表defread_calibration_cache(self):returnself.cache_path.read_bytes()ifself.cache_path.exists()elseNonedefwrite_calibration_cache(self,cache):self.cache_path.write_bytes(bytes(cache))# 构建 INT8 enginecfg=builder.create_builder_config()cfg.set_flag(trt.BuilderFlag.INT8)cfg.int8_calibrator=EntropyCalibrator(calib_data,batch=8,cache_path=cache)serialized=builder.build_serialized_network(network,cfg)三个容易写错的点:
get_batch必须返回设备指针(int),不是 numpy 数组。所以校准数据要提前.cuda(),并在同一块显存上反复覆盖;- 返回
None是"校准结束"的信号,不是错误。别忘了这个终止条件,否则会一直循环到数据耗尽才停; .calib缓存是校准结果的持久化。有了它,第二次构建不用再跑校准集——这也正是下一个坑的来源。
二、三方实测:快多少、小多少
同一个 ONNX、同一个max_batch=32的 profile,只改精度标志:
| 精度 | engine 体积 | 构建耗时 | batch=1 | batch=8 | batch=8 吞吐 |
|---|---|---|---|---|---|
| FP32 | 53.1 MB | 6.6 s | 1.861 ms | 4.634 ms | 1726 img/s |
| FP16 | 23.1 MB | 21.7 s | 0.544 ms | 1.571 ms | 5092 img/s |
| INT8 | 12.6 MB | 31.1 s | 0.517 ms | 1.172 ms | 6826 img/s |
换算成倍数:
| 对比 | batch=1 | batch=8 | 吞吐 |
|---|---|---|---|
| INT8 vs FP32 | 3.60× | 3.95× | 3.95× |
| INT8 vs FP16 | 1.05× | 1.34× | 1.34× |
| FP16 vs FP32 | 3.42× | 2.95× | 2.95× |
这张表里有三个必须注意的事实:
① INT8 的收益高度依赖 batch。batch=1 时只比 FP16 快 5%(0.517 vs 0.544 ms),基本可以忽略;batch=8 时才有 1.34 倍的差距。原因很直接:小 batch 下推理是延迟受限(kernel 启动、调度开销占主导),大 batch 下才是带宽/算力受限,而 INT8 的核心优势恰好是省带宽。
所以如果你的场景是"在线单请求、batch=1",上 INT8 几乎白忙;如果是"离线批处理、batch≥8",收益很实在。
② 构建时间反而变长了。FP32 6.6 s → FP16 21.7 s → INT8 31.1 s。因为 INT8 构建要额外跑一遍校准集。这也是为什么工程上一定要把 engine 落盘缓存——别每次启动都重新校准。
③ engine 体积压缩 4.2 倍(53.1 → 12.6 MB)。这个对显存紧张或多模型并存的场景很有价值:第 6 篇算过context 显存 ≈ 3 × 激活体积,而激活体积本身也跟着精度下降——INT8 的 context 显存也会比 FP16 小,不只是文件小。
三、精度代价:96.88% 意味着什么
评测方法要严格:校准集与评测集用不同的随机种子生成,完全分开,否则测出来的精度会虚高。
以 FP32 输出为基准,在 64 张独立评测图上逐样本比对:
| 指标 | FP16 vs FP32 | INT8 vs FP32 |
|---|---|---|
| top-1 一致率 | 100.0% | 96.88% |
| 余弦相似度 | 0.999994 | 0.997701 |
| max|Δ| | 0.0443 | 0.7722 |
| mean|Δ| | 0.006022 | 0.114888 |
怎么解读这三个数:
- top-1 一致率 96.88%:64 张里有 2 张的 top-1 预测发生了改变。对分类任务来说,这是"需要在验证集上确认"的信号,不是"可以直接上线"的信号;
- 余弦相似度 0.9977:整体方向几乎一致,说明量化没有把输出结构搞乱;
- max|Δ| 0.7722:在最敏感的样本上有接近 0.8 的绝对偏差(logits 量级约 O(1)),这就是那 2 张翻车的样本。
这里必须强调:本文用的是合成图像 + 随机权重模型,绝对精度没有业务意义。这三个指标的用途是给你一个可复用的评测方法——真实业务里要换成"验证集上的 top-1 / mAP / PSNR"等业务指标,并且把这项对比写进 CI。
校准样本量的影响
(同一张图右侧的曲线就是这组数据,这里单独列表便于复制)
| 校准样本 | 构建耗时 | max|Δ| | 余弦 | top-1 一致率 |
|---|---|---|---|---|
| 24 张 | 27.7 s | 0.9020 | 0.99751 | 95.31% |
| 96 张 | 30.3 s | 0.8771 | 0.99772 | 95.31% |
| 400 张 | 21.0 s | 0.7722 | 0.99770 | 96.88% |
样本从 24 增到 400,max|Δ| 从 0.902 降到 0.772、top-1 从 95.31% 提到 96.88%——有提升,但不到一个数量级。
结论:校准数据的"代表性"比"数量"重要得多。400 张覆盖真实分布的图,远好过 4000 张分布不对的图。TensorRT 官方文档给的量级建议也是几百张,不是几万张。
(顺带说:构建耗时不随样本量单调变化,24 张反而比 400 张慢——因为构建时间主要花在内核搜索上,校准只占一小部分,单次测量波动很大。不要用构建时间来判断校准质量。)
四、最阴的坑:换了校准集,engine 却没变
这个坑我是在写本文的实验脚本时踩到的,而且100% 复现。
第一次跑样本量扫描时,我用的是这样的缓存路径:
cache=MODEL/f"resnet18_int8_{calib_batch}.calib"# 只带 batch,不带样本数结果三档样本量(24 / 96 / 400 张)跑出来的指标完全一样:
样本 24 → max|Δ| 0.7719 cos 0.99770 top1 96.88% 样本 96 → max|Δ| 0.7719 cos 0.99770 top1 96.88% 样本 400 → max|Δ| 0.7719 cos 0.99770 top1 96.88%一开始我以为是"样本量没影响",差点写成结论。后来发现是缓存命中:三次构建的缓存文件名相同,read_calibration_cache()直接返回了第一次的校准结果,write_calibration_cache()再也没被调用过。
于是我又专门设计了一个对照实验来验证:
第一次:校准集 A(24 张) → cache = resnet18_int8_stale_demo_b8.calib 结果 max|Δ| 0.9020 top1 95.31% 第二次:校准集 B(400 张新数据)→ 同一个 cache 路径 结果 max|Δ| 0.9020 top1 95.31% 两次结果完全相同: True 缓存文件是否被重写: False400 张全新数据喂进去,什么也没发生。engine 用的还是 24 张时代的量化阈值。
为什么这很危险
它不像崩溃那样显眼,而是静默失效:
- 你以为"更新了校准数据,精度应该变好了",实际上 engine 一个字节都没变;
- 更糟的情况是反过来的:你换了一版更差的校准数据(比如某次数据采集出错),却发现指标没变化,误以为"数据质量无所谓";
- 在多环境部署时,
.calib一旦被提交进仓库,A 机器产生的缓存会被 B 机器复用,而 B 机器的数据分布可能完全不同。
正确做法
# ① 缓存名带「校准集指纹」:样本数 + 数据版本 + 预处理版本fingerprint=f"n{len(calib)}_v{data_version}_pre{preprocess_version}"cache=MODEL/f"{model_name}_int8_{fingerprint}_b{calib_batch}.calib"# ② 换数据时强制重算(显式开关,别靠记忆)ifforce_recalibandcache.exists():cache.unlink()# ③ 把 .calib 纳入版本管理,并在 CI 里校验它与数据版本一致而本次实验修完之后的真实数据(每档独立缓存):
| 校准样本 | max|Δ| | top-1 |
|---|---|---|
| 24 张 | 0.9020 | 95.31% |
| 96 张 | 0.8771 | 95.31% |
| 400 张 | 0.7722 | 96.88% |
这才是真正的曲线——有提升,但不是数量级。
五、什么时候该用 INT8
适合用
- 吞吐优先的离线批处理(batch ≥ 8):实测 1.34 倍相对 FP16、3.95 倍相对 FP32;
- 显存紧张的多模型服务:engine 体积压到 1/4.2,context 显存也随之下降(第 6 篇的结论);
- 判别式任务(分类、检测):对数值误差相对宽容,top-1/mAP 的小幅波动通常可接受;
- 有真实校准数据且能维护版本:这是前提,不是可选项。
不建议用
- 在线单请求、延迟敏感:batch=1 时只快 5%,收益被服务层开销淹没(第 4 篇实测框架开销占 96%);
- 回归型任务:超分、深度估计、语音合成这类输出连续值的任务,INT8 的误差会直接体现在 PSNR/SSIM 上;
- 校准集与线上分布差异大:量化误差会被放大,而且很难通过"多给点样本"补救;
- 团队没有精度回归能力:没有 CI 的量化等于蒙眼上线。
三项成本(本文实测)
| 成本项 | 实测 | 说明 |
|---|---|---|
| 构建时间 | FP32 6.6 s → FP16 21.7 s → INT8 31.1 s | INT8 要跑校准集,最慢 |
| 校准数据 | 400 张覆盖真实分布的输入 + 版本管理 | 数据漂移会导致量化误差放大 |
| 精度回归 | 验证集指标对比 + top-1 一致率 | 必须进 CI,否则上线才发现掉点 |
常见问题(FAQ)
Q1:INT8 比 FP16 快多少?
取决于 batch。实测 ResNet-18:batch=1 时 INT8 0.517 ms vs FP16 0.544 ms,只快 1.05 倍;batch=8 时 1.172 vs 1.571 ms,快 1.34 倍;吞吐维度 6826 vs 5092 img/s,也是 1.34 倍。小 batch 下推理是延迟受限,INT8 的带宽优势发挥不出来。
Q2:INT8 相对 FP32 能快多少?engine 能小多少?
实测 batch=8 时 4.634 ms → 1.172 ms,吞吐 1726 → 6826 img/s,加速 3.95 倍;engine 从 53.1 MB 压到 12.6 MB,压缩 4.2 倍。构建时间则是 6.6 s → 31.1 s,因为要额外跑校准。
Q3:INT8 的精度会掉多少?
以 FP32 输出为基准、在 64 张独立评测图上:top-1 一致率 96.88%(64 张里 2 张预测改变)、余弦相似度 0.9977、max|Δ| 0.772。作为对照,FP16 是 100% / 0.999994 / 0.0443。真实业务必须换成验证集上的 top-1 / mAP / PSNR 来判断是否可接受。
Q4:校准需要多少张图?
实测 24 / 96 / 400 张三档:max|Δ| 0.902 / 0.877 / 0.772,top-1 95.31% / 95.31% / 96.88%。样本增加有提升,但不是数量级差别。TensorRT 官方建议量级是几百张,关键是覆盖真实分布,而不是堆数量。
Q5:为什么我换了校准数据,engine 的精度指标却没变?
大概率是命中了旧的.calib校准缓存。校准器的read_calibration_cache()如果返回了缓存内容,TensorRT 就直接复用,不会重新跑校准。实测把 400 张新数据喂给同一缓存路径,结果与 24 张时完全一致、缓存文件也没被重写。解决方式是让缓存文件名带上校准集指纹,或换数据时先删缓存。
Q6:.calib缓存文件应该提交到代码仓库吗?
可以提交,但必须与数据版本绑定。建议在文件名里带样本数、数据版本、预处理版本,并在 CI 里校验它与当前数据版本一致。否则会出现"A 机器生成的缓存被 B 机器复用,而 B 的线上分布不同"的静默错误。
Q7:INT8 和 FP16 能混用吗?
可以,用逐层精度控制:config.set_flag(trt.BuilderFlag.OBEY_PRECISION_CONSTRAINTS)后,对敏感层设置layer.precision = trt.float16(或 float32),其余层走 INT8。常见做法是把第一层和最后一层保持高精度,中间的卷积/矩阵乘走向量化。
Q8:TensorRT 10.x 的cfg.int8_calibrator提示 deprecated 是什么意思?
TensorRT 10.1 起推荐"显式量化"(explicit quantization):用QuantizationSpec/set_dynamic_range直接给出每层的量化范围,而不是依赖隐式校准器。功能上int8_calibrator仍可用(会打印 DeprecationWarning),新项目建议评估显式量化路线;本文为保持与大多数存量代码一致,仍使用隐式校准器。
小结
- INT8 相对 FP32 加速 3.95 倍、相对 FP16 加速 1.34 倍,engine 压缩 4.2 倍;但batch=1 时只比 FP16 快 5%——收益随 batch 放大。
- 精度代价量化清楚:top-1 一致率 96.88%、余弦 0.9977、max|Δ| 0.772(FP16 是 100% / 0.999994 / 0.0443)。这三个指标是方法,真实业务要换业务指标并进 CI。
- 校准样本量:24 → 400 张让 max|Δ| 从 0.902 降到 0.772、top-1 从 95.31% 到 96.88%;代表性 > 数量。
- 最阴的坑是校准缓存:换了校准集但缓存名没改,engine 会静默复用旧阈值(实测 100% 复现、结果完全相同、缓存未重写)。缓存名必须带校准集指纹。
- 该不该用 INT8 一句话判断:吞吐优先 + batch 大 + 判别式任务 + 能维护校准数据 → 用;在线单请求 + 回归任务 + 无精度回归能力 → 别碰。
下一篇我们转回"工程交付":TensorRT 服务的可观测性与压测体系——指标怎么埋(本地耗时 / 端到端 / 分位)、日志怎么设计、怎么用一条命令跑出容量报告,以及把前六篇的结论沉淀成一套可复用的诊断脚本。