int8 量化把 256 × 256 的矩阵从 256 KB 压到 64 KB,输出误差 1.01%; 但单次耗时从 0.008 ms 涨到 0.072 ms——numpy 的 int8 矩阵乘法走慢路径, 延迟收益在这个环境里测不出来。
上一节把数据管住了。这一节面对部署时最现实的问题:模型太大、太慢、太贵,怎么优化。
一、部署时被卡住的三件事
体积:模型要装进显存或内存。 7B 参数的 fp16 模型约 14 GB,一张 24 GB 的卡装得下,两张就不一定了。 延迟:一次请求要多久。对话类应用超过 1 秒用户就开始等得烦。 吞吐:每秒能处理多少请求。它和延迟不是一回事——延迟可以靠加机器摊薄,吞吐直接决定成本。 三者常常互相矛盾。量化能降体积、可能降延迟,但要付精度;批处理能提吞吐,但会拉高首字延迟。 优化前必须先说清这次要优化哪一个。
二、四种手段,按性价比排
批处理:一次算多个请求。几乎不损失精度,吞吐能上去几倍,代价是单个请求的等待变长。 优先做这个,它最便宜。 量化:权重从 fp32 压到 int8 甚至 int4。体积直接降 4 倍或 8 倍,精度损失通常在 1% 以内。 部署里最常用的一招。 剪枝:删掉不重要的权重或整条通道。 收益不稳定,而且非结构化剪枝在通用硬件上不一定变快——稀疏矩阵未必有加速。 蒸馏:用大模型教一个小模型。效果最好也最贵,要重训,适合「确定要长期上线」的场景。 顺序建议:先测清楚瓶颈在哪,再批处理,再量化,最后才考虑剪枝和蒸馏。反过来先上蒸馏,往往白花几周。
三、实测:体积 4 倍是真的,延迟是假的
Demo 拿一个 256 × 256 的 fp32 矩阵做 int8 量化。 体积从 256 KB 降到 64 KB,正好四分之一——这是算术,跟用什么库无关。 精度损失 1.01%。 原因是每个权重被压到 256 个刻度上,步长 0.002735,量化误差的量级就是步长的一半。 但耗时那一列很反常识:int8 定点乘加 0.072 ms,比 fp32 的 0.008 ms 慢了 9 倍。 原因不在算法,在实现:numpy 的 int8 矩阵乘法没有 BLAS 加速,走的是标量慢路径; 而 fp32 走的是高度优化的 BLAS。真部署时靠的是专门的量化算子库或者推理引擎,不是这个。 所以这一节的结论要分两半:体积和带宽的收益是真的,能算出来; 延迟收益要看后端,在这个环境里测不出来,我不硬编一个好看的数字。
四、换算到真实规模
体积这一条可以直接放大:7B 参数的模型,fp16 是 14 GB,int8 是 7 GB,int4 是 3.5 GB。 这一条是算术,跟硬件无关。 延迟这一条不能直接放大。 真实推理的瓶颈往往不在矩阵乘法本身,而在显存带宽——量化降的是带宽需求,所以在大模型上收益反而明显。 还有一件常被忽略的事:量化对长尾输入更敏感。平均值误差 1%,不代表最差的那批输入也只差 1%。 上线前必须专门看一遍边界样本。 这就是上一节说的回归门禁——量化后的模型同样要过那道门。
五、动手 Demo:量化实测:体积降到四分之一,耗时反而涨 9 倍
造一个 256 × 256 的 fp32 权重矩阵和 256 维输入,做 per-tensor 的 int8 量化; 分别测 float32 原样、int8 反量化再算、int8 定点乘加三条路径的体积、 输出相对误差和单次中位耗时(各跑 300 次取中位数)。
scale = float(np.abs(W).max()) / 127.0
Wq = np.round(W / scale).astype(np.int8) # 量化
y32 = W @ x # 基准
yq = (Wq.astype(np.float32) * scale) @ x # 反量化再算
yfix = (Wq.astype(np.int32) @ xq.astype(np.int32)) * scale * xscale
实跑输出:
--------------------------------------------------------------------
部署前的性能优化:量化到 int8,体积降 4 倍,延迟不一定降
--------------------------------------------------------------------
路径 每参数字节 矩阵体积 输出相对误差 单次中位耗时 ms
-----------------------------------------------------------------------
float32 原样 4 256 KB 0.000% 0.008
int8 反量化再算 1 64 KB 1.009% 0.069
int8 定点乘加 1 64 KB 1.218% 0.071
量化误差 1.0085%,来自权重被压到 256 个刻度:步长是 0.002735。
体积确实降到四分之一。但耗时那一列很反常识:int8 定点乘加 慢于 float32 原样。
原因是 numpy 的 int8 矩阵乘法没有 BLAS 加速,走的是慢路径;真正部署时靠的是专门的量化算子库或推理引擎,不是这个。
所以这一节的结论要分两半:**体积和内存带宽的收益是真的**(4 倍,这是算术,跟库无关);**延迟收益要看后端**,在纯 Python 加 numpy 里测不出来。
换算到实际规模:70 亿参数的 fp16 模型约 14 GB,int8 之后约 7 GB,这一条和用哪个库无关。
体积是三行里唯一干净的结论:256 KB 到 64 KB,四倍。这条和硬件、和库都没关系。 两条 int8 路径的误差分别是 1.009% 和 1.218%。定点乘加更大,因为输入也量化了,两次量化误差叠加。 耗时那一列要看清楚:0.008、0.068、0.072 ms。int8 慢了一个数量级。 这不是「量化没用」,是「numpy 不是推理引擎」。 我把这个结果如实写出来,是因为网上很多文章直接说「量化能加速 2 到 4 倍」。 那句话在 GPU 加专门算子库的前提下成立,在纯 Python 里不成立。 换了环境就不成立的结论,必须连着环境一起说。
30 秒实验
- 把矩阵从 256 × 256 改成 512 × 512,看 fp32 的耗时是不是按 4 倍涨。
- 把量化改成 int4(除以 7 而不是 127),看误差涨多少。
- 想一想:为什么批处理几乎不损失精度,而量化一定损失精度?
常见误区
- 「量化一定更快」——要看后端有没有量化算子,纯 Python 里反而更慢。
- 「平均误差 1% 就安全」——长尾输入的误差可能远超平均,要专门看边界样本。
- 「先上蒸馏最有效」——蒸馏最贵,先做批处理和量化往往够了。
自测三问(点开对答案)
1. 体积、延迟、吞吐三者为什么常常互相矛盾? —— 批处理提吞吐但拉高首字延迟,量化降体积但要付精度,目标不同手段就相反。 2. int8 量化为什么体积正好降到四分之一? —— 每个参数从 4 字节变 1 字节,参数量不变,所以是 4 倍。 3. 为什么在 numpy 里 int8 反而更慢? —— int8 矩阵乘法没有 BLAS 加速,走标量慢路径,而 fp32 走的是优化过的 BLAS。部署上线了,事情才刚开始。下一节讲上线之后怎么发现它慢慢变坏。
- ☐ (本节完,下一节 10.1.4 流程4:持续监控和迭代改进)