🧠 第十章 管理和部署
📄 一句话概括

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 秒实验

  1. 把矩阵从 256 × 256 改成 512 × 512,看 fp32 的耗时是不是按 4 倍涨。
  2. 把量化改成 int4(除以 7 而不是 127),看误差涨多少。
  3. 想一想:为什么批处理几乎不损失精度,而量化一定损失精度?

常见误区

  1. 「量化一定更快」——要看后端有没有量化算子,纯 Python 里反而更慢。
  2. 「平均误差 1% 就安全」——长尾输入的误差可能远超平均,要专门看边界样本。
  3. 「先上蒸馏最有效」——蒸馏最贵,先做批处理和量化往往够了。
自测三问(点开对答案) 1. 体积、延迟、吞吐三者为什么常常互相矛盾? —— 批处理提吞吐但拉高首字延迟,量化降体积但要付精度,目标不同手段就相反。 2. int8 量化为什么体积正好降到四分之一? —— 每个参数从 4 字节变 1 字节,参数量不变,所以是 4 倍。 3. 为什么在 numpy 里 int8 反而更慢? —— int8 矩阵乘法没有 BLAS 加速,走标量慢路径,而 fp32 走的是优化过的 BLAS。

部署上线了,事情才刚开始。下一节讲上线之后怎么发现它慢慢变坏。

相关笔记