简介《LLM.int8(): 8-bit Matrix Multiplication for Transformers at Scale》中文版论文聚焦大型语言模型推理显存占用过高的痛点面向自然语言处理学习者、大模型量化部署研究者及有毕业设计需求的读者。压缩包内含1个PDF文件大小约4.01MB便于在本地阅读与检索。内容系统梳理LLM.int8()的背景动机、既有小模型量化方法的局限以及矢量量化和混合精度分解两项核心技术前者为矩阵乘法的每个内积设置独立归一化常数后者将稀疏但系统性的极端异常值维度隔离到16位计算使超过99.9%的数值仍以8位相乘。论文还给出WinoGrande、HellaSwag、PIQA、LAMBADA等基准上的零样本精度结果说明175B参数模型量化后推理性能不降并讨论消费者级GPU部署与后续优化方向。已有215人学习适合作为理解8位矩阵乘法、异常值特征与大规模模型高效推理的入门与专题参考。1. 单卡跑 175B 的幻想这份 LLM.int8() 中文版论文到底解决了什么显存不够用的痛很多做模型部署的人都遇到过想把一个 175B 参数的大语言模型塞进一张 24GB 消费级显卡16 位权重光加载就要 350GB进程还没进入前向计算就被系统终止了。这份 LLM.int8() 中文版论文给出的结论很不寻常把前馈层和注意力投影层压到 8 位显存占用接近减半困惑度和零样本精度却与 16 位基线完全持平。它解决的核心问题不是“怎么把权重压小”而是“压小之后为什么会在 6.7B 这个参数规模上突然失效”以及怎样绕开那个失效点。适合两类人毕业设计要在单卡上跑十亿级模型、论文复现卡在显存上的同学以及想把推理服务单位成本压下来的工程开发者。2. 技术骨架一条矩阵乘法上的两级量化链路这份论文最值得借鉴的地方不在于最后那几个数字而在于它把“量化精度不够”这个模糊的现象拆成了两个可以分别处理的工程问题。整条链路只作用在 transformer 的线性层上输入端是 fp16 的隐藏状态和 fp16 权重输出端是 fp16 结果中间发生了一次 8 位量化、一次 int32 累加以及一次规模很小但必不可少的 16 位旁路计算。2.1 量化为什么偏偏在 6.7B 这个位置上失效先把参数量分布理清对于 6.7B 以上的模型前馈层和注意力的 Q/K/V/O 投影层贡献了约 95% 的参数Embedding 层占掉剩下的大部分norm 和 bias 只占极小一部分。也就是说只要把这几类线性层压到 8 位显存收益就基本到手了没必要去动 Embedding。问题出在最常用的 absmax 量化上。它的做法是拿整个张量的绝对值最大值去除 127得到缩放常数 s再乘回去。这个方案有一个天然弱点一个 20 倍于其他值的离群点会把 s 拉得很大导致其余 99.9% 的数值被压缩到接近 0 的量化区间里量化后直接变成 0信息等同于被抹掉。小模型上这种情况稀疏且偶发而参数规模上去之后离群特征变成系统性现象在 6B 到 6.7B 之间受影响的层数比例从 65% 跳到接近 100%受影响的序列维度从 35% 涨到 75%。这条曲线几乎和“8 位量化开始失效”的拐点重合作者把这个位置称为相变。更关键的一点是这些离群值高度集中。以一条长度 2048 的序列为例整个 transformer 里能统计到约 15 万个绝对值超过 6 的特征值但它们只落在 6 个隐藏维度上。把这些维度置零top-1 的 softmax 概率质量会掉 20% 以上验证困惑度上升 600% 到 1000%而随机删掉同样数量的维度困惑度只涨 0.1%。这说明那 6 个维度是必须用高精度保住的其余 99.9% 可以放心压。2.2 组件一矢量量化把缩放常数从 1 个变成 so 个既然单个离群点会污染整张量最直接的办法就是增加缩放常数的数量。论文的做法叫矢量量化把一个矩阵乘法看成一批独立的内积输入 X 按行序列维各给一个常数权重 W 按列输出维各给一个常数。这样X 是 s 个常数W 是 o 个常数离群点的影响被限制在它所在的那一行或那一列里。反量化的时候不需要逐元素处理把结果矩阵的每一行除以对应的 cx、每一列除以对应的 cw等价于除以 cx 和 cw 的外积。这一步在实现上就是一次广播除法没有额外开销。论文实测这个方案能撑到 2.7B 参数不掉点再往上就不行了因为离群值出现在特征维列上而 X 的量化是沿行做的方向刚好不对。2.3 组件二混合精度分解给 0.1% 的维度开一条 16 位旁路第二级处理就是绕开上面那个方向问题。做法是先按阈值 α 把输入里的大幅特征维度挑出来组成集合 O这些维度单独走一次 fp16 矩阵乘法剩下的维度照常做矢量量化后的 8 位乘法把结果反量化回来再和 fp16 分支的输出现加。由于 O 里通常不超过 7 个维度16 位分支的计算量占比约 0.1%显存额外开销也差不多在这个量级。阈值怎么定论文给的判定条件是特征绝对值 ≥ 6.0且在至少 25% 的层、至少 6% 的序列维度上出现。α 取 6.0 不是随手写的作者的结论是取到这个值时困惑度退化基本归零取得更小会把正常特征误判成离群速度直接腰斩取得更大则漏掉关键维度精度崩。还有一个容易被忽略的观察在分解之前零点量化非对称量化比 absmax 表现好很多因为离群值有严格的非对称性要么全正要么全负一旦上了混合精度分解这个优势就消失了说明被分解剩下的那部分分布是对称的。这个细节解释了为什么很多复现者只抄了一半方案就发现“零点量化没用了”。2.4 两个组件串起来之后的显存账一次前向的实际执行顺序是先扫一遍输入找 O 集合然后把 X 和 W 都拆成两块8 位块做 int8 乘加、int32 累加再用 cx 和 cw 的外积反量化成 fp1616 位块单独做一次小矩阵乘两块相加输出 fp16。显存收益主要来自权重常驻部分fp16 每参数 2 字节int8 每参数 1 字节理论上减半实测在 176B 模型上是 1.96 倍。这个数字比 2 差一点差额就是离群值那 0.1% 的 fp16 分支以及反量化过程中产生的临时张量。注意力函数本身没有被量化因为那里没有参数压它不省显存所以整条链路只覆盖线性层。3. 核心算法拆解把两级量化写成可运行的 PyTorch 代码论文里的公式不好直接对上代码我把两个组件拆成四个函数来实现每个函数只干一件事方便你单独拿去测。下面这段代码在 CPU 上就能跑不依赖任何量化后端目的是让你看清每一步的张量形状和数值范围。3.1 第一块砖absmax 缩放常数与矢量量化import torch def absmax_scale(x: torch.Tensor, dim: int) - torch.Tensor: 沿指定维度取绝对值最大映射到 int8 的 [0, 127] dim1 表示按行序列维算dim0 表示按列输出维算 clamp 防止全零行除零1e-8 只是个兜底值 max_abs x.abs().amax(dimdim, keepdimTrue).clamp(min1e-8) return 127.0 / max_abs # keepdim 保证能直接广播回原张量 def vector_quantize(x: torch.Tensor, dim: int): 矢量量化的核心每个内积一个常数 scale absmax_scale(x, dim) # (s,1) 或 (1,o) x_i8 torch.round(x * scale).clamp(-127, 127).to(torch.int8) return x_i8, scaledim这个参数是这里唯一的开关。对输入 X形状 s×h传dim1得到 s 个按行划分的常数对权重 W形状 h×o传dim0得到 o 个按列划分的常数。这两组常数最后做外积正好覆盖输出矩阵的每个位置。clamp(-127, 127)不能省round 之后偶尔会溢出到 128转 int8 时会绕回负数这个 bug 表现出的现象是“精度偶尔抖一下”很难查。3.2 第二块砖异常值检测把 α 和 6% 落成代码def find_outlier_dims(x, alpha6.0, seq_ratio0.06, max_dims7): 返回需要走 16 位分支的特征维度下标 alpha: 幅值阈值论文取 6.0 seq_ratio: 该维度至少在多少比例的序列位置上超阈值才算数取 6% max_dims: 兜底上限13B 以内实测不超过 7 个 hit x.abs() alpha # (s, h) 布尔矩阵 ratio hit.float().mean(dim0) # 每个特征维度的命中比例 (h,) idx torch.nonzero(ratio seq_ratio).flatten() if idx.numel() max_dims: # 阈值偏松时按命中比例截断 idx torch.topk(ratio, max_dims).indices.sort().values return idxseq_ratio这个门槛的作用是过滤偶发噪声。如果不加它一条长序列里随便一个 token 出现 8.0 的激活值就会被当成离群维度O 集合迅速膨胀到几十上百个16 位分支的计算量从 0.1% 涨到 5% 以上省下的显存又被吃掉一大块。max_dims是最后一道保险正常情况下不会触发一旦频繁触发说明 α 设小了该去调阈值而不是靠截断兜着。3.3 拼起来一次完整的 LLM.int8() 线性层def llm_int8_linear(x, w, alpha6.0, seq_ratio0.06): x, w x.half(), w.half() out torch.zeros(x.shape[0], w.shape[1], dtypetorch.float16, devicex.device) # 分支一离群维度走 16 位计算量占比约 |O|/h idx find_outlier_dims(x, alpha, seq_ratio) if idx.numel() 0: out torch.matmul(x[:, idx], w[idx, :]) # 分支二其余维度做矢量量化后的 int8 乘法 mask torch.ones(x.shape[1], dtypetorch.bool, devicex.device) mask[idx] False x_rest x[:, mask].contiguous() # (s, h)h h - |O| w_rest w[mask, :].contiguous() # (h, o) x_i8, cx vector_quantize(x_rest, dim1) # cx: (s, 1) w_i8, cw vector_quantize(w_rest, dim0) # cw: (1, o) # 演示用 float 承接 int8 乘加部署时换成 int8 GEMM int32 累加器 out_i32 torch.matmul(x_i8.float(), w_i8.float()) out (out_i32 / (cx * cw)).half() # 外积反量化广播成 (s, o) return out几个要点。x[:, idx]和w[idx, :]这两次索引必须是连续内存否则后面的 matmul 会退回慢路径这也是为什么两个contiguous()不能省。反量化那行out_i32 / (cx * cw)用的是广播cx 是 s×1cw 是 1×o乘出来就是 s×o 的完整缩放矩阵一次除法搞定不用循环。最后.half()再相加是为了让两个分支的累加精度一致混着 fp32 加会多出一份 s×o 的 fp32 中间张量长序列下这个临时张量能到几十 MB。3.4 验证误差量到什么程度才算复现成功def relative_error(x, w, alpha6.0): ref torch.matmul(x.half(), w.half()) # fp16 基线 approx llm_int8_linear(x, w, alpha) return ((ref - approx).norm() / ref.norm()).item() # 阈值扫描alpha 从 4.0 到 8.0看误差怎么走 x torch.randn(64, 4096).half() x[:, :3] * 20 # 人为埋 3 个离群特征维度 w (torch.randn(4096, 4096) * 0.02).half() for a in [4.0, 5.0, 6.0, 7.0, 8.0]: print(a, relative_error(x, w, a))跑这段你会看到一个很典型的形状α 从 4.0 抬到 6.0相对误差快速下降6.0 到 7.0 之间基本走平再往上又开始回升因为真正的离群维度被漏掉了。这条曲线就是论文里那句“α6.0 足以把退化降到接近零”的直观版本。测的时候别只看单层取十几层连起来看误差累积单层误差 0.5% 叠二十层就能把困惑度顶上去。4. 复现路线从 125M 到 13B 的对照实验怎么搭论文的说服力全在缩放曲线上所以复现的重点不是“跑通一个 8 位模型”而是“在不同规模上画出那条曲线”。如果你的毕业设计要做这个方向按下面这套口径搭实验台工作量可控结论也站得住。4.1 四个测量口径缺一不可第一个口径是困惑度。用一个清洗过的公开网页语料做验证集固定 token 数和上下文长度测 16 位基线和 8 位版本的差值。困惑度对量化退化极其敏感是最好用的指标。第二个口径是零样本下游精度。挑四到五个分类或补全类任务用同一套提示模板和同一套打分脚本跑重点看 8 位版本有没有掉到随机水平。论文里 absmax 基线在 6.7B 之后就是掉到随机水平的。第三个口径是峰值显存。一定要测峰值不要测稳定值。加载完模型后的稳定值看着很漂亮实际前向时反量化临时张量一开峰值能比稳定值高出一大截。用显存快照工具按 token 逐个记录找出最高点。第四个口径是延迟。量化本身有开销参数量小于 6.7B 的模型上8 位版本往往比 fp16 还慢因为这些模型本来就塞得进卡多出来的量化/反量化只是纯亏损。只有到了大矩阵乘法的量级8 位才真正跑出接近两倍的速度。4.2 规模扫描的清单和预期现象建议一次跑五个点每个点只改模型规模其余配置全部锁死。配置项包括序列长度、batch 大小、验证集样本数、随机种子、量化阈值 α。任何一个动了曲线就没法横向对比。规模8 位 absmax 的预期LLM.int8() 的预期主要观察点125M轻微退化与 fp16 持平基准是否搭对1.3B开始明显退化持平矢量量化是否生效2.7B明显退化持平矢量量化的能力边界6.7B掉到接近随机持平误差 0.5%相变是否出现13B完全失效持平O 集合 ≤ 7阈值 α 是否需要微调跑的时候把每一层的 O 集合大小也记下来。这张表比困惑度更能说明问题如果 6.7B 模型上你没观察到 O 集合从 0 突然跳到 5 到 7 个维度那大概率是加载的模型不是原版权重或者你测的根本不是同一批层。4.3 异常值统计怎么把“相变”画出来论文里那张相变图横轴是参数量纵轴是受影响层数占比和受影响序列维度占比。要复现它你需要在每个规模上挂一组 hook在每次前向时统计两件事某特征维度的绝对值是否超过 6.0以及这个维度在多少比例的层里重复出现。统计粒度要按“层类型”分开记。注意力的 Q/K/V/O 投影层和前馈的第一个扩展层是离群值的主要来源前馈的收缩层和注意力函数本身基本不出现。如果混在一起统计信号会被稀释曲线画出来是平的容易误判成“论文结论不成立”。还有一点不同训练框架训出来的模型离群值的分布差异不小。论文特意在三个框架、两种推理实现上验证过这个现象就是为了排除“这只是一个框架的 bug”。你复现的时候如果只在单一来源的权重上看到现象别急着下结论换一个来源的权重再跑一遍。5. 避坑与排查复现 8 位推理时最容易翻车的五个地方5.1 困惑度从 12 飙到 60但代码完全照抄现象8 位加载成功模型能出字但验证困惑度是基线的五倍下游任务全崩。原因量化范围选错了。把 Embedding 层和输出头也一起量化或者把 LayerNorm 的权重也塞进 8 位路径。这两部分参数占比很小压了省不了多少显存但精度损失是致命的Embedding 一乱后面所有层的输入分布全变。解决只替换注意力投影层和前馈扩展层Embedding、输出头、所有 norm 和 bias 一律保留 fp16。判断方法很简单统计一下被替换模块的参数量应该占总参数的 90% 以上如果只有 70%说明你漏了或者多替换了。5.2 显存只降了不到两成和论文的 1.96 倍差很远现象权重确实转成 int8 了但显存监控显示占用只少了 15%。原因反量化的临时张量没释放或者整个模型一次性反量化后再前向等于把 8 位权重又展开回 fp16 全量。另外注意力部分的 kv 缓存没动长上下文场景下这部分才是大头。解决按层做流式反量化前向到哪一层就反量化哪一层用完立刻释放测峰值显存而不是稳定显存单独统计 kv 缓存占用把它从“量化收益”里扣掉再算。5.3 小模型上速度掉一半O 集合里塞了三十多个维度现象在 1.3B 模型上跑 8 位推理比 fp16 慢 50%打印 O 集合发现每层都有二三十个维度。原因α6.0 这个阈值是在 6.7B 以上的模型上标定的。小模型的激活分布更平6.0 这条线会圈进大量正常特征16 位分支的计算量从 0.1% 涨到 5%得不偿失。解决按模型规模调阈值小模型上直接放弃混合精度分解只用矢量量化就够了。判断依据是 O 集合大小超过 7 个就说明阈值太松这时要么抬阈值要么换方案。5.4 单条短序列正常batch 一起来精度就崩现象batch size 为 1 的时候输出正常加到 8 或 16 之后部分样本的输出开始胡说。原因缩放常数是按行序列维算的batch 里的 padding token 会拉高某些行的绝对值最大值把真正有效 token 的量化区间压扁。batch 越大、padding 越多污染越严重。解决算 absmax 之前先把 padding 位置 mask 掉或者干脆按有效 token 逐条算常数再拼。这个坑很隐蔽因为 loss 曲线看着正常只有逐样本对比才发现。5.5 8 位矩阵乘法的计算 kernel 直接报错或静默回退现象代码不报错但速度一点没变甚至更慢或者直接抛出一个未实现的异常。原因8 位矩阵乘法的计算 kernel 对张量形状有对齐要求特征维度不是 8 或 16 的整数倍时会走慢路径batch 太小时kernel 启动开销比省下来的计算时间还多调度器会直接回退到 fp16。解决把特征维度 padding 到对齐边界小 batch 场景不要开量化参数量小于 6.7B 的模型本来就不缺那点显存。上线前先用一个最小复现脚本确认你确实走到了 int8 路径别凭速度变化猜。6. 二次开发把混合精度分解接进自己的推理服务真要把这套东西用起来改动点其实只有三个但每个都有讲究。第一个是封装成可直接替换的线性层。把原始线性层包一层forward里走llm_int8_linear权重在初始化时转成 int8 常驻bias 保持 fp16。这样你在模型里做一次模块遍历替换就能全量切换不用改模型定义。第二个是 O 集合的缓存策略。逐 token 重新扫一遍找离群维度是有开销的而实测在长序列上O 集合在层与层之间相当固定。我一般会按层缓存每处理 N 个 token 刷新一次N 取 64 到 128 之间精度损失基本看不出来省下的扫描开销很可观。第三个是明确边界这套方案只覆盖线性层kv 缓存和注意力计算仍然是 fp16。如果你的场景是长上下文显存大头在缓存上量化线性层的收益会被摊薄这时候该去看别的优化方向而不是硬调 α。改造点收益风险线性层替换为混合精度分解权重显存接近减半小模型上可能变慢O 集合按层缓存减少重复扫描开销序列分布突变时漏检阈值 α 按规模自适应避免误判离群维度需要额外的标定实验只量化线性层不碰缓存精度零损失长上下文收益有限验证方法我固定用三件套困惑度对照必须与 fp16 基线差值在 0.5% 以内、下游零样本精度不能掉出基线两个点、逐层输出余弦相似度找定位哪一层开始偏。三者都过才敢说改对了。尤其是第三项它能告诉你是哪一层的 O 集合出了问题比盯着总困惑度瞎猜高效得多。有个血泪教训我最早做验证只看最终困惑度结果一次改动后困惑度只涨了 0.3%我以为没事就上线了后来在长文本任务上才发现模型开始遗忘前文回头查是某一层的前馈收缩层被误量化了。从那以后我每次接量化都强制先跑一遍 125M 规模的对照实验把每一层的余弦相似度打出来看一遍再往大模型上推。小模型跑一遍只要几分钟能省掉上线后半夜被叫起来的麻烦。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?