ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

27B大模型压缩至1.72比特:三值化与Hadamard变换实战解析

27B大模型压缩至1.72比特:三值化与Hadamard变换实战解析 第一次看到“Ternary Bonsai 2 27B”这个模型的评测表时我盯着“平均 1.72 比特/权重”那一栏看了很久。一个 27B 参数的大模型FP16 存储要占 54GB 左右压到 1.72 比特之后权重部分只需要 5.8GB 上下几乎可以直接在单张消费级显卡上跑推理。更让我感兴趣的是这个压缩比不是单纯“降低位宽”就能做到的——三值化、结构化稀疏、Hadamard 变换、分组缩放好几套技术叠在一起最后还得靠 TensorSharp 这类工具在张量层面把它们串成能落地的算子。这篇东西我想从 TensorSharp 的视角把这件事拆开讲清楚1.72 比特是怎么算出来的Hadamard 变换在其中到底解决了什么问题真跑起来是什么体验以及我实际踩过的几个坑。如果你对极低比特量化感兴趣或者想在小显存机器上跑大模型这篇应该能给你一条完整的参考路径。1. 1.72 比特这笔账到底怎么算的三值化、稀疏与缩放摊销1.1 先把“三值化”说透三值化Ternary Quantization的意思是每个权重只能取三个值通常是 {-1, 0, 1}。理论上每个权重只需要 log2(3) ≈ 1.585 比特就能表示比 2 比特还低一点点这就是 1.72 比特里最核心的那部分“本金”。但三值化不是简单地把权重往 [-1, 0, 1] 上硬凑。你肯定会问一个训练出来的浮点权重数值都是连续分布的直接取符号或者归零那精度不崩了吗所以实际做法通常分两步第一步先用一个缩放因子把权重整体拉到一个合适的量级。比如某个矩阵里大部分值落在 [-0.02, 0.02] 区间你直接做符号函数那所有正数都变成 1负数都变成 -1信息全丢。正确做法是先算出一个组内的平均绝对值把它作为 scale然后让权重除以 scale、四舍五入、再截断到 {-1, 0, 1}。这一步本质上是在“可表达范围”和“数值分辨率”之间做权衡。第二步决定 0 的位置。很多人直觉以为 0 是正负之间的分界线但真实权重分布往往不是零对称的而且还存在大量接近 0 的小值。如果把接近 0 的值也强行压成 1 或 -1累积误差会很大。所以很多三值化方案会把阈值设在一个不为零的位置比如保留绝对值最大的 20% 权重作为 ±1其余全部置 0。这个阈值怎么选是三值化里最影响最终精度的超参数之一后面我会专门讲。1.2 Bonsai 里的“修剪”意味着什么模型代号里的“Bonsai”其实是“修剪”的意思——盆景嘛剪掉枝桠。Ternary Bonsai 2 27B 不是单纯所有参数一律三值化而是先对权重矩阵做结构化稀疏把一部分元素直接剪掉再对剩下的非零元素做三值化。目前常见的结构化稀疏是 2:4 模式每四个元素里只保留两个非零且非零位置是固定的、规则的。这种模式的好处是硬件可以利用“半结构化”布局直接跳过零元素不需要动态掩码计算效率很高。稀疏和三值化叠加的效果非常有意思。算一笔账如果只用纯三值化理论下界是 1.585 比特。但如果先用 2:4 稀疏每四个原始元素里有 2 个被剪掉那么只需存储约一半位置的三值权重再加上 4 比特的掩码信息来标记哪两个位置非零折算下来每个原始元素大概 1.29 比特。这个结合之所以划算是因为稀疏化把不重要的元素直接剔除了三值化再把剩下的重要元素用极低精度编码每一层都在用“不重要的部分省空间”。不过要注意位成本不能只看理论下界。2:4 掩码本身在存储时不一定是你想的“4 比特对应 4 个位置”实际库实现可能会用字节对齐、位打包等方式存储导致额外开销。TensorSharp 在处理这类布局时我印象最深的是它把“掩码 三值数据”作为一个整体张量来处理而不是分开存成两个数组这样可以连续按字节读取减少访存开销。1.3 scale 因子和“结构开销”才是拉开差距的关键三值本身是 1.585 比特但模型的存储开销还有别的部分每个组要存一个缩放因子、embedding 层通常不量化、LayerNorm 的 weight 和 bias 往往保留高精度。这些“非三值部分”的折算会显著影响平均比特数。举个例子。假设我们对权重每 128 个元素分成一组每组共享一个 fp16 的 scale那么每个权重要摊上 16/128 0.125 比特的 scale 开销。这个数看起来不大但和三值的 1.585 加在一起就是 1.71 比特——已经非常接近 1.72 了。再加上 embedding 层和 norm 层按 8 比特或 16 比特保留整体折算下来正好落在 1.72 比特附近。所以当你看到“平均 1.72 比特”这个数字时它不是一个孤立的位宽而是一个混合存储账本的结果组成部分存储方式单参数位成本示例权重主体2:4 稀疏 三值化 掩码约 1.29 比特每组缩放因子fp16每 128 元素组约 0.125 比特Embedding 层8 或 16 比特保留折算后少量摊销LayerNorm 等参数fp16/32 保留折算后少量摊销合计全模型平均约 1.72 比特/权重这个账本也说明了极低比特量化的一个基本原则不要幻想所有参数都拿到极低比特关键是让“大头”极低“小头”保持精度整体折算下来数字就会很好看而且精度也不会崩。1.4 为什么选择 1.72 而不是直接压到 1.6理论下界 1.585 比特就在那儿为什么最终落在 1.72因为 1.585 只考虑了“理想化的三值纯权重”没有算 scale 和结构开销。工程实现里你要留出余量scale 位宽不能太低否则组间差异表达不出来embedding 这类对输出质量影响极大的层不能激进压缩稀疏掩码如果为了对齐方便多占几个比特也会推高平均位。所以 1.72 不是一个“数学上最优”的值而是一个“精度、显存、kernel 复杂度”三方都能接受的经验值。我在实际项目里也试过把组大小从 128 改成 256scale 摊销降到 0.0625 比特平均位成本看似更低了但精度掉了不少最后发现问题就出在组内权重分布差异太大一个 scale 盖不住。2. Hadamard 变换为什么能和 1.72 比特“配对”旋转、异常值与误差再分配2.1 极低比特量化真正的敌人是异常值如果只是把权重三值化精度损失会非常明显。最大的问题不是“精度不够”而是大语言模型里的权重矩阵普遍存在“异常值”一小部分元素的绝对值远远大于周围元素。这些异常值会霸占 scale 的计算结果导致整体量化步长变大普通数值反而被压碎精度断崖式下跌。这就像大家站在同一个秤上称体重来了一位 200 公斤的重量级选手你为了让他能上秤把整个秤的量程调到了 400 公斤其他人的读数自然就变得不精确了。Hadamard 变换要解决的就是这个问题——在量化之前先把这些异常值的“能量”打散分散到整个向量的各个维度里去。2.2 Hadamard 变换的数学直觉把重量摊到每个人肩上Hadamard 变换本质上是一个正交线性变换。它的变换矩阵 H 由 1 和 -1 组成满足 H·H^T n·I也就是说它相当于一个旋转/反射操作不会放大或缩小向量的总能量除了一个固定的缩放系数。关键性质是一个向量经过 Hadamard 旋转后它的能量会在各个维度之间重新分布少数几个维度特别大、其他维度特别小的分布状况会被打散。具体实现上Hadamard 变换可以用递归结构组成标准形式是H_2 [[1, 1], [1, -1]] H_{2n} H_n ⊗ H_2这里 ⊗ 是克罗内克积。因为矩阵元素只有 ±1而且这一矩阵是递归定义的变换本身不需要真的存储整块矩阵而是可以用类似 FFT 的蝶形结构快速计算复杂度只有 O(n log n)。对于一个 128 维的块做一次 Hadamard 变换开销极低在 kernel 里甚至可以完全展开这是它能在推理时反复使用而不拖垮性能的原因。2.3 对三值化来说旋转带来的三个直接好处第一异常值被抹平了。权重向量先乘以 Hadamard 矩阵能量会被重新分配极端尖峰变小、小值变大一点分布变得更“圆润”。对三值化而言这直接降低了阈值选择对异常值的敏感度scale 估计也稳定很多。第二量化误差的上界从“跟最大绝对值相关”变成“跟向量总能量相关”。不做旋转时三值化对某个向量的误差可能被单个异常值主导旋转之后误差被分摊各个维度都“差不多难”反而让三值化有机会保留更多信息。用比较直观的话说原来信息集中在少数维度你把它们弄坏了就全坏了旋转之后信息被均匀地抹在整个向量上坏掉一小部分也只损失一小部分。第三Hadamard 变换在推理阶段可以被“吸收”掉一部分。在标准 attention 或 MLP 结构里如果你先把输入旋转一下量化后再接一个反变换在数学上可以把反变换接到后续的权重矩阵里从而让实际推理时几乎不增加额外算子。TensorSharp 里对这种旋转吸收的处理做得比较细后面我会展开讲。2.4 代价和边界条件Hadamard 变换不是免费的午餐。它的维度必须是 2 的幂而神经网络的矩阵维度往往是任意值比如 4096、8192 是 2 的幂没问题但 6144 这类就不行。处理方式是把维度块拆成 2^k 的子块分别变换或者做少量 padding这样会带来一些额外的边界开销需要 kernel 层面对齐处理。还有一个容易忽略的问题Hadamard 变换本身需要使用 float 运算。在纯 int 推理设备上你没法直接享受它的好处因为需要在某个环节先还原出浮点。这也是为什么“三值化配 Hadamard”目前主要出现在以 FP16/BF16 计算为基础的 GPU 推理流程里——变换本身用少量浮点计算权重和计算过程切成低比特非常划算。3. TensorSharp 在这一套方案里实际管的是哪几件事3.1 先从“张量视角”理解问题同样一套压缩方案从模型视角看是“每层怎么改结构”从张量视角看是“每个张量在内存里长什么样、计算时怎么访存”。TensorSharp 的核心工作就是在张量层面把三值化、稀疏和 Hadamard 变换串成一致的数据流。比如三值权重存储。{-1, 0, 1} 在数学上是 3 个状态但在计算机里你没法直接用 1.585 比特寻址。实际存储时有两种主流方案一种是每个元素用 2 比特打包浪费 0.415 比特但换来对齐和快速解码另一种是结合稀疏掩码用位打包的方式把三值数据压缩得更紧。TensorSharp 对这两种模式都做了 kernel 级支持而且内置了布局转换不需要用户手动去拆字节。我实际测试下来2 比特对齐方案的扩展性更好虽然存储上比理论多了一点但 GPU kernel 的访存边界容易满足解码时可以用位运算一次处理多个元素。对 1.72 比特这个目标来说那点浪费完全可以靠稀疏部分找补回来。3.2 自动校准与缩放搜索三值模型不是“训好之后直接压”就行的通常需要一个校准阶段喂一批真实数据统计每个权重矩阵的分布然后确定每组的 scale、三值化阈值、甚至 Hadamard 变换的块大小。这一步决定了压缩后的模型到底是在原有能力基础上“轻微受损”还是直接“崩掉”。TensorSharp 的校准流程和我之前用过的一些量化工具类似但它有两点做得比较顺手一是逐层搜索 scale 时会同时考虑前后层量化后的分布而不是孤立地最小化单层误差二是支持把 Hadamard 旋转参数也纳入校准过程也就是说它会同时搜索“旋转之后的三值化阈值”和“旋转大小”两者一起调优。搜索目标通常是让量化前后输出的均方误差最小同时约束激活分布不发生明显漂移。在实操中校准集的选择往往比校准算法更关键。用普通文本做校准很容易让模型在推理和数学任务上掉分更好的做法是混合指令数据、代码、数学题和通用文本而且样本量不需要很多几百到一千条就足够。关键是覆盖任务类型要广而不是量要大。3.3 自研算子的核心逻辑解码、反量化、Hadamard 融合模型真正推理时TensorSharp 会做三件事把打包的三值权重解码成实际计算需要的值、把缩放因子乘回去、在需要的地方执行 Hadamard 变换及其逆变。这些操作如果拆成独立的 kernel 来做性能会很难看——每一步都是额外的显存访问和 kernel 启动开销。所以实际实现必须做算子融合。最核心的融合逻辑是把“Hadamard 反变换 矩阵乘法”合并到一个 kernel 里。因为权重经过旋转之后数学上等效于乘了一个正交矩阵这个正交矩阵可以被吸收到相邻层的参数里让推理时不需要显式执行 Hadamard 变换。TensorSharp 在核验哪些 layer 可以吸收、哪些必须在计算时临时做变换上花了很大功夫。如果吸收做得好调用方的代码几乎感觉不到量化过程如果吸收不彻底每一层都会额外多一个变换 kernel吞吐会掉 20% 甚至更多。3.4 为什么“模型视角”和“张量视角”会差很多从模型视角看三值化 Hadamard 只是一个“算法 trick”。但从张量视角看你要处理的问题多得多张量形状不是 2 的幂怎么办、矩阵乘法里哪个维度该做变换、稀疏掩码和打包数据怎么交织存储、CUDA kernel 里如何做到 warp 级对齐……这一层才是让极低比特量化“真的能跑起来”和“只存在于论文里”的分水岭。我自己刚接触这一套时也走过弯路我先是把精力放在“怎么压得更狠”结果跑到 kernel 阶段发现显存确实小了但访存模式乱到不行GPU 利用率反而下降了。后来才意识到极低比特量化的收益从来不只是“文件小了”而是“访存带宽压力小了、计算能更密集”——这个目标的实现完全取决于张量层布局和 kernel 的质量也就是 TensorSharp 这类工具的存在意义。4. 27B 压到 1.72 比特之后真实运行到底什么水平4.1 显存账终于可以在单卡上跑 27B先算硬账。27B 参数平均 1.72 比特权重部分大概是 27e9 × 1.72 / 8 ≈ 5.8GB。Fp16 版本是 54GB差了 9 倍多。当然模型推理时还要额外算 KV cache、激活值和中间缓冲但权重这一大头的压力被大幅缓解了。我测试的场景是 4090 24GB 显卡上下文长度设到 4096batch size 为 1 的情况下模型权重之外还会占 6-10GB 左右的 KV cache 和激活空间。也就是说1.72 比特的权重让整个模型可以比较从容地放进 24GB 显存这在 FP16 下完全不可能。如果你把 KV cache 也做低比特量化模型可以继续往 16GB 显存甚至 12GB 显存上塞但那样尾部精度损失会比较明显需要额外进行校准。4.2 吞吐带宽瓶颈下“减肥”就是提速大模型推理通常不是算力瓶颈而是访存带宽瓶颈。一次 token 生成需要把所有权重从显存搬到计算单元权重越小搬运时间越短。三值化 稀疏化在这方面收益非常大。我在同样硬件上用 FP16 baseline 和 1.72 比特版本对比batch size 为 4 时吞吐能提升 2-3 倍batch size 更大时提升没那么夸张因为计算强度上来了反而会暴露出“三值解码 反缩放”的固定开销。这里有一个很重要的取舍如果你的任务没有长上下文、也没有大的 batch那么极低比特量化的收益几乎全部来自显存变小如果你追求高吞吐那么三值化带来的带宽红利会非常明显但要小心 kernel 融合质量否则解码三值权重的开销会把带宽收益抵消掉。TensorSharp 在融合做得好时解码开销几乎是隐藏的因为它把“读权重位 → 符号展开 → 乘 scale → 做矩阵乘”前几步直接按硬件指令展开减少了中间寄存器压力。4.3 质量损失掉点可控甚至有意外之喜任何一个量化方案你都要问质量到底损失多少我拿公开 benchmark 和一些任务集测试了一下结果大致是这样的任务类型FP16 基线1.72 比特版本主观感受通用对话 / 摘要正常基本持平无明显损失代码生成 / 逻辑推理正常轻微下降复杂场景能感知到数学解题正常有所下降多步推理错误率上升简单分类 / 抽取正常甚至更好少数任务因稀疏正则化受益“某些任务反而更好”不是玄学。极低比特压缩相当于给模型加了一个极强的正则化约束去掉了大量冗余信息之后模型在简单任务上更容易给出稳定输出幻觉也有轻微减少。但涉及多步推理的复杂任务三值化的信息瓶颈就会暴露出来尤其是在“需要精确记忆常数、日期、具体数字”的场景里。校准集的质量对这几个维度的平衡非常关键。我遇到过一种情况只用通用文本校准模型的中文对话很流畅但做代码题时几乎崩掉。把代码数据加入校准集后代码能力回来了但对话风格变得有点死板。最后我用 6:1 的通用文本和代码比例做混合校准才找到一个相对平衡的点。5. 实操中我踩过的四个坑以及对应的绕法5.1 坑一Hadamard 变换维度不对齐kernel 直接错位我第一次在 TensorSharp 里对 attention 的 QKV 投影矩阵做 Hadamard 旋转时矩阵维度是 6144。Hadamard 要求 2 的幂而 6144 3 × 2048。直接对全维度做变换CUDA kernel 里索引全乱跑出来的结果完全不对甚至出现非法内存访问。绕法把维度拆成 2048 的 3 块分别做变换或者先做 2048 维旋转剩下的 1/3 保持原样。注意拆块处理后缩放因子和上下层结构的吸收关系都要跟着调整。TensorSharp 里如果开启了自动布局它会为这种非 2 幂维度生成分块方案但你需要额外检查相邻层的参数吸收是否匹配否则单层误差会沿层堆叠。5.2 坑二三值化阈值选不好精度比 2-bit 量化还差最初我把阈值简单地设为“保留绝对值前 20%”结果模型在复杂任务上掉点严重。后来改用最小化每个输出通道的量化均方误差才稳定下来。具体做法是对每个权重矩阵先统计分布然后在“保留 1%、5%、10%、15%、20%、25% 非零”这个网格上做 try-and-error选择让该层输出误差最小的比例。实现上不需要真的跑完整模型评估逐层搜就行。阈值的选择还要和 scale 联动。最稳妥的组合是先选一个合适的 scale 让数值落到 [-1, 1] 量级再做稀疏裁剪最后用少量校准数据验证。如果顺序反过来先裁剪再定 scale容易出现“裁剪后剩余权重波动太大scale 无法覆盖”的问题。5.3 坑三校准集过拟合模型“背”住了校准数据有一版校准我用了几百条质量很高的指令数据每一条都让模型反复看。结果模型在跟这些数据相近的任务上表现很好换个领域就明显退化。这是典型的量化后过拟合校准过程把权重往校准集的分布上“搓”了太狠损失了对广泛分布的适应性。解决办法是校准数据尽量杂句子覆盖不同领域训练轮次不要过多校准完成后增加一个“留出验证集”来检测通用性下降。留出验证集建议包含若干种与校准集没见过的任务类型。5.4 坑四KV cache 没量化长上下文显存照样爆权重省下的显存在长上下文场景下会被 KV cache 的膨胀吃掉。27B 模型 context 拉到 32K 时KV cache 可能要占 10GB 甚至更多这时如果你只用 1.72 比特权重、KV cache 保持 fp16总显存可能比一个普通 8 比特量化模型还紧张。绕法对 KV cache 也做低比特量化比如 6 比特或 8 比特非对称量化。不过 KV cache 的量化不能用太激进的位宽因为它是逐 token 积累的长期误差会被放大。稳妥的顺序是先量化权重、固定之后再单独做 KV cache 校准。两者是相对独立的但一起做的时候尾段推理质量的下降不是线性叠加而是会集中爆发在长上下文任务上。5.5 实操顺序建议如果你打算复现这一套流程建议按照这个顺序推进先把基线 FP16 模型和推理代码跑通记录各任务指标和显存占用。做三值化 稀疏化不加入 Hadamard先看纯量化损失。加入 Hadamard 旋转逐层检查异常值分布是否被抹平损失应该明显减小。用 TensorSharp 或同类工具做校准先搜 scale 再搜阈值每次只改一个参数。最后做 KV cache 量化和长上下文测试确认显存与质量都达标。每一步都要留好回滚点因为变量太多的时候一旦出问题你很难判断是旋转、稀疏还是阈值选择导致的。说回我自己的感受。1.72 比特这个数字在一开始确实让我觉得有点“魔幻”但当我把整个链路从权重压缩、布局转换、kernel 融合到校准跑下来之后它其实是一个数学约束和硬件实现的折中点。Hadamard 变换和 TensorSharp 这类工具的价值不在于让你把比特数压得更低而在于帮你把“压缩”转化为“可运行的高效推理”。如果你也想在小显存显卡上跑 27B 级别的模型建议从 1.72 比特这条路线入手先跑通再慢慢调校准集——这个过程踩过的坑基本就是我上面写的这几类。
返回列表