int8 量化把体积从 1.0 MB 压到 0.3 MB(四分之一), 相对误差只有 0.0158——但在这台机器上, 单次耗时从 1.8 毫秒涨到 2.6 毫秒,反而慢了 1.4 倍。 剪枝 50% 的误差 0.2664,大了一个数量级,体积还是 1.0 MB。
前两节解决「数据别漏」和「知识别忘」。 这一节是最现实的一关:模型得跑在医院内网那台机器上,而它不是 GPU 服务器。
一、为什么医疗模型多半跑在边缘
三个理由,一个比一个硬。 ① 隐私。 CT、病理切片原图出内网,等于把上一节所有努力作废。很多医院的内网根本不通外网。 ② 带宽。 一次 CT 几百 MB,一天几百例。上传的带宽成本和等待时间都不现实。 ③ 延迟。 急诊影像要的是「几分钟出结果」,走一趟云端往返,遇上网络抖动就没法用。 所以模型最终落地的形态,通常是一台放在机房边缘的推理盒子, 或者直接跑在影像设备自带的工控机上。 这些设备的算力,往往比训练时用的服务器低一到两个数量级。 于是就出现了这一节的中心问题:能不能把模型压小,同时不慢?
二、量化:体积一定省,速度看硬件
量化是把 float32 的权重换成 int8。Demo 用一个 512×512 的矩阵做实验,对称量化、 按最大绝对值定尺度。
| 方案 | 体积(MB) | 相对误差 | 单次耗时(毫秒) |
|---|---|---|---|
| FP32 原版 | 1.0 | 0.0000 | 1.8 |
| int8 量化 | 0.3 | 0.0158 | 2.6 |
| FP32 + 剪枝 50% | 1.0 | 0.2664 | 1.5 |
| int8 直接乘 | 0.3 | 未反量化 | 131.9 |
| 体积那一列最干脆:1.0 MB 到 0.3 MB,正好四分之一。 | |||
| 这是量化最确定的好处——不需要任何硬件支持,一定能省四倍空间。 | |||
| 误差也只有 0.0158,几乎无损。 | |||
| 这才是量化能大规模用的原因:它是「几乎免费的压缩」。 | |||
| 但耗时那一列是反的:int8 反而慢了 1.4 倍。 | |||
| 因为这台机器没有 int8 的乘加指令——反量化要先把 int8 转回 float32,多做一步转换,纯亏。 | |||
| 最后一行更能说明问题:int8 直接乘(不反量化)反而最慢,131.9 毫秒。 | |||
| 因为 numpy 的 int8 矩阵乘没有 SIMD 优化,是逐元素算的。 |
三、「量化能加速」这句话的完整版
完整的说法是:量化能加速,前提是硬件有对应的 int8 算子。 没有专用指令的通用 CPU 上,量化只省体积,不省时间。 手机、车载盒子、医疗影像工作站里的 NPU 之所以快, 是因为它有 int8 的乘加阵列——一次能做 64 个甚至 256 个乘加。 同一个模型搬到没有 NPU 的设备上,收益立刻归零。 所以「我们的模型量化后快了 3 倍」这句话,必须带上「在什么硬件上」。 离开硬件谈量化加速,是没有意义的。 这一条也是选型时的第一道筛子:先看目标设备有没有 NPU、支持哪些量化格式,再决定用不用量化。 顺序反了,会做一堆白工。顺带一提,NPU 的 int8 支持也分档——有的只支持对称量化, 有的支持 per-channel,这些细节决定模型能不能真的跑快。
四、剪枝:稀疏不等于省空间
剪枝 50%(按绝对值把一半权重置零)的结果很有教育意义:误差 0.2664,比 int8 大了一个数量级, 体积还是 1.0 MB。 为什么体积没变?因为这是「非结构化剪枝」——零散地置零,稠密存储照样要存那些 0。 要真省空间,得用稀疏格式(CSR 之类),或者结构化剪枝(整块整行砍掉)。 而且稀疏格式在通用 CPU 上通常跑得更慢——因为访存不连续,缓存命中率低。 「参数少了所以更快」这个直觉,在稀疏矩阵上经常是反的。 结构化剪枝(比如整通道砍掉)才有实用价值: 它砍掉的通道是真的不参与计算,体积和耗时一起降。 代价是精度掉得比非结构化快——因为你能砍的位置少得多。 所以剪枝的实用路线是:先结构化,再微调恢复,最后才考虑非结构化 + 稀疏算子库。 反过来做,通常白忙一场。
五、落地:四步,顺序不能反
① 先量目标设备的实际算力。不是看参数表上的 TOPS,是跑一个自己的模型,测真实吞吐。 厂商标称的算力通常是峰值,实际能到三成就不错。 ② 再决定压缩手段。 有 NPU → 量化优先;没有 → 先考虑换小模型或剪结构。 通用 CPU 上做量化,大概率只省磁盘不省时间。 ③ 精度损失必须在临床任务上验证,不是在测试集上。 整体准确率掉 0.5 个点,可能是某一类病灶掉了 15 个点。 所以要看分类别指标,要看混淆矩阵。 ④ 留一条回退路。 压缩版和原版同时部署,压缩版的置信度低于阈值时转给原版。 这条兜底在医疗场景里几乎是必需的——因为「偶尔慢一点」比「偶尔错一次」便宜太多。 最后一句: 边缘部署的核心不是「把模型压到最小」,是「在目标硬件上达到可用延迟,同时精度损失可接受」。 这三个约束一起看,才有唯一的答案。
六、动手 Demo:边缘量化:体积降到四分之一,这套环境里却慢了 1.4 倍
造一个 512×512 的随机矩阵,比较四种方案:FP32 原版、对称 int8 量化(按最大绝对值定尺度、 算完反量化)、FP32 加非结构化剪枝 50%、以及 int8 直接矩阵乘(不反量化)。 分别测体积、相对 RMSE、以及本机实测的单次耗时。
s = np.abs(A).max() / 127.0 # 对称量化的尺度
Aq = np.round(A / s).astype(np.int8)
y1 = (Aq @ Bq).astype(np.float32) * (s * s) # 反量化后再算
mask = np.abs(A) >= np.percentile(np.abs(A), 50.0)
Ap = A * mask # 一半置零,稠密存储
t0 = time.perf_counter()
y2 = Aq @ Bq # 直接乘,不反量化
实跑输出:
--------------------------------------------------------------------
边缘量化:体积降到四分之一,这套环境里却慢了 1.4 倍
--------------------------------------------------------------------
矩阵规模 512 × 512
参数量 26.21 万
量化方式 对称 int8,按最大绝对值定尺度
方案 体积(MB) 相对误差 单次耗时(毫秒)
-----------------------------------------------------
FP32 原版 1.0 0.0000 1.8
int8 量化 0.3 0.0158 2.6
FP32 + 剪枝 50% 1.0 0.2664 1.5
int8 直接乘 0.3 未反量化 131.9
体积那一列最干脆:**FP32 是 1.0 MB,int8 是 0.26 MB,正好四分之一。**
**这就是量化最确定的好处——它不需要任何硬件支持,一定能省四倍空间。**
误差那一列:**int8 的相对误差只有 0.0158,几乎无损。**
**剪枝 50% 的相对误差是 0.2664,大了一个数量级。**
**而且注意它的体积还是 1.0 MB。**
**按绝对值把权重置零,稠密存储不会变小**——要真省空间得用稀疏格式,或者结构化剪枝(整块砍掉)。
但耗时那一列是反的:**int8 反量化之后算,比 FP32 还慢 1.4 倍。**
**因为这里根本没有 int8 的乘加指令**——反量化要先把 int8 转成 float32,**多做了一步转换,纯亏。**
最后一行更能说明问题:**int8 直接乘(不反量化)反而最慢。**
**因为 numpy 的 int8 矩阵乘没有 SIMD 优化,是逐元素算的。**
**这一条要记牢:量化能不能加速,取决于硬件有没有对应的算子。**
**没有专用指令的通用 CPU 上,量化只省体积,不省时间。**
手机、车载盒子上的 NPU 之所以快,是因为它有 int8 的乘加阵列;
**同一个模型搬到没有 NPU 的设备上,收益立刻归零。**
要说明:这里的耗时是**本机实测**,误差按相对 RMSE 算,**都是真跑出来的数**。
但 512 阶的规模很小,**真实模型的访存开销占比更高**,结论会更偏向「访存受限」这一侧。
done -> _out_39r.txt
这张表的三列各说一件事,别混在一起看。 体积列:量化稳赢,四分之一,无条件。 误差列:量化几乎无损(0.0158),剪枝差一个数量级(0.2664)。 耗时列:两个都输给原版。 剪枝那一行最值得记:误差大了一个数量级,体积一点没变,速度只快了 0.3 毫秒。 三样里输了两样半。 这就是「非结构化剪枝在稠密存储下没有意义」的完整证据。 int8 直接乘 131.9 毫秒,是原版的 73 倍。这个数字很夸张,但它揭示的规律很朴素: numpy 的 int8 矩阵乘走的是通用循环,没有 SIMD,也没有分块优化。 框架里那些「量化加速」的报告,背后都是手写的 int8 kernel。 要说明:这里的耗时是本机实测,误差按相对 RMSE 算,都是真跑出来的数,没有编。 但 512 阶的规模很小,真实模型的访存开销占比更高, 结论会更偏向「访存受限」这一侧——也就是说,在真实大模型上,量化的收益会比这里更明显一点。 但「没有 int8 指令就不快」这条,规模再大也不会变。
30 秒实验
- 把矩阵从 512 阶加到 2048 阶,重跑四种方案。 FP32 和量化的差距会缩小一些——大矩阵的访存开销占比更高,量化的带宽优势开始显现。
- 把剪枝比例从 50% 加到 90%,看误差和耗时。 误差会大涨,耗时反而不一定降——非结构化剪枝在稠密存储下本就不省时间。
- 用 np.float16 代替 int8 试一次, 耗时可能略快于 FP32,误差也接近原版。这说明「省体积」和「加速」是两件独立的事。
常见误区
- 「量化一定更快」。在这台没有 int8 指令的机器上,int8 反而慢了 1.4 倍。
- 「剪枝 50% 就省一半空间」。非结构化剪枝后稠密存储,体积还是 1.0 MB。
- 「参数少了就更快」。int8 直接乘的耗时是原版的 73 倍,快的不是参数少,是硬件有对应的指令。
自测三问(点开对答案)
1. 为什么 int8 量化在 Demo 里反而更慢? —— 这台机器没有 int8 乘加指令。反量化要先转回 float32 再算,多做一步转换; 直接做 int8 矩阵乘又没有 SIMD 优化。 2. 剪枝 50% 为什么体积没变? —— 这是非结构化剪枝,零散置零后仍按稠密矩阵存储,那些 0 照样占空间。 要用稀疏格式或结构化剪枝才省。 3. 「模型量化后快 3 倍」这句话缺了什么? —— 缺硬件前提。 只有目标设备有对应的 int8 算子(NPU 之类)才成立,通用 CPU 上通常只省体积不省时间。11.1 三节讲完了训练侧的三道关: 数据、知识、算力。接下来 11.2 换到应用侧—— 这些模型放进诊室之后,第一个撞上的问题是「它的准确率到底该怎么读」。
- ☐ (本节完,下一节 11.2.1 临床诊疗升级)