ARTICLE DETAIL

资讯详情

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

1.72比特三值化+Hadamard变换:低比特大模型量化与TensorSharp实操

1.72比特三值化+Hadamard变换:低比特大模型量化与TensorSharp实操 TensorSharp 是我常用的一个张量级调试工具。前几天拿到 Ternary Bonsai 2 27B 的权重包时我的第一反应不是跑分而是想知道里面到底装了多少比特。结果那个数字差点让我以为读取器出了问题——1.72 比特。对不是 16也不是 8更不是常见的 2 比特量化。这个 27B 参数的模型平均每个权重只占 1.72 比特。光看这个数字就够吸引人了而真正让它可行的是后面的 Hadamard 变换设计。这篇文章我想从 TensorSharp 的视角把这座“三元盆景”从根到叶拆一遍尽量讲清楚为什么 1.72 比特是合理的、Hadamard 变换到底做了什么、以及在我自己的机器上如何复现和验证。1. TensorSharp 眼中的“比特”与“模型大小”1.1 一个 27B 模型为什么还要谈比特大模型的“大小”有两个标准一个是参数量另一个是每个参数的位宽。参数量决定了算力需求位宽决定了内存和带宽需求。27B 参数如果用最常见的 FP16 存储权重本身就要 54GB。这个体积已经超过大多数消费级显卡的显存上限就算用 24GB 的卡也放不下一份完整权重。所以位宽不是锦上添花而是能不能本地跑起来的分水岭。我最初接触 Ternary Bonsai 2 27B 的时候以为它只是做了常规的三值化把权重变成 -1、0、1 三个值然后用 2 比特压缩。按这个思路27B 权重大约能压到 6.75GB已经很可观了。但 TensorSharp 打开文件后给出的平均位宽是 1.72 比特整个权重区只占 5.8GB 左右。这就要比朴素三值化再省下将近 1GB。不要小看这 1GB在很多边缘设备上它就是能不能跨过内存阈值的差别。TensorSharp 在这里扮演的角色相当于一个“权重显微镜”。它能绕过模型加载器直接读取原始张量布局解析每个 block 的缩放因子、索引表、符号位。很多高层推理框架只告诉你“这是个 27B 模型”不会告诉你权重张量真正占了多少 bit而 TensorSharp 会把每一层的 bitrate 单独列出来。比如 MLP 层可能平均只有 1.6 比特而 Attention 层因为保留了更多残差缩放平均到 1.85 比特。最后加权汇总整模型平均 1.72 比特。没有这个视角你很难理解模型为什么能压得这么狠。1.2 位宽是怎么算出来的位宽的计算不复杂但细节容易踩坑。简单公式是平均位宽 (权重文件总字节数 × 8) / 总参数数量这个公式看起来简单实操时要注意三点第一必须剔除 embedding 层和 tokenizer 等非张量数据第二如果模型包含 2 比特或 4 比特的辅助张量要单独折算第三某些打包格式会有 padding 字节需要用 TensorSharp 的--dump raw_weights避开文件系统对齐产生的虚高。我当时用 TensorSharp 跑了一行命令tsharp stats ternary-bonsai-2-27b.tspack --dump layer_bitrate输出会包含类似这样的表格层类型层数参数量平均位宽embedding12.2B4.0attention168.1B1.85ffn3215.4B1.62其他-1.3B2.0合计-27B1.72embedding 之所以是 4 比特是因为它只占很小一部分但需要保留足够区分度。ffn 层结构规整、冗余度高所以能压到 1.62 比特。把这些位宽按参数量加权求和得到的总和不是整数但这正是 file size 和参数量之间的真实关系。1.72 比特并非拍脑袋设出来的而是每种量化策略组合后的自然结果。为什么要强调“自然结果”因为我在别的模型上见过一些宣称“1.5 比特”的处理实际上是把某几层直接丢弃或用低秩近似严格说已经不属于无损权重压缩了。Ternary Bonsai 2 27B 在 TensorSharp 的解析下权重张量全部保留没有砍层没有低秩替身。这个前提很重要我们讨论的 1.72 比特是每一份权重都有明确数值方案的存储格式而不是靠删除信息换体积。2. Ternary Bonsai 2 27B 的权重设计三值化不是简单扔掉2.1 三值化映射规则三值化听起来简单把浮点权重按阈值分成 -1、0、1 三种值。但实际怎么选阈值直接决定精度损失。Ternary Bonsai 2 27B 没有用全局固定阈值而是按 block 做自适应三值化。常见分组大小是 128 或 256 个权重。每个 block 独立的缩放因子scale会把权重重心搬到零附近再按 ±0.5 倍标准差之类的阈值截断。举个例子某层原始权重的均值是 0.02标准差是 0.35那么规则可以写成w_i 0, if |w_i / scale| threshold w_i 1, if w_i / scale threshold w_i -1, if w_i / scale -threshold这里的scale不是随便取的通常用 block 内所有权重的绝对值均值再乘一个校正系数。更精细的版本会遍历一组候选 threshold选择让 block 内重构误差最小的那个。TensorSharp 在分析权重时会把每个 block 的scale和threshold都存进元数据于是你能看到同一个矩阵里不同 block 的稀疏度可能相差很大有的 block 几乎全是零有的 block 一半 1 一半 -1。这就是“Bonsai”这个名字的含义——剪掉所有不重要的分支只留下最粗的骨架。三值化本身就是一次结构化剪枝零权重等于把连接删除而在推理时可以跳过这些零值既省内存又省算力。这里有个容易误解的点三值化后的权重不是每个都严格占 1.72 比特。单个三值只有三种状态信息论下限是log2(3) ≈ 1.585比特。1.72 比特实际上是“三值索引 缩放因子摊销 少量辅助标志位”的组合。它没有挑战信息论而是在接近理论极限的同时留了一小部分开销给工程实现。2.2 1.72 比特的账分组、符号与缩放要理解 1.72 比特的构成我建议直接把一个 block 的存储成本拆开算。假设 block 大小是 128 个权重。用 2 比特能直接表示 0、1、-1、无效位共 256 比特平均每个权重 2 比特。但 Ternary Bonsai 2 27B 对 128 个权重做了更聪明的打包先用一个位图标记哪些位置非零再对非零位置写入符号位。如果平均稀疏度是 50%也就是每个 block 大约有 64 个非零权重那么每个权重的成本大约是位图成本1 bit / 权重 非零符号位0.5 bit / 权重 非零位置索引约 0.55 bit / 权重 块缩放因子16 bit / 128 0.125 bit / 权重 列表头与对齐约 0.04 bit / 权重 合计约为2.2 bit / 权重这个拆法算出来是 2.2 比特还没到 1.72。所以实际实现还用了另一层把连续的非零位置编码成游程或算术编码。当零权重分布不均匀时熵编码可以把平均位宽进一步往下压。加起来就可以看到 1.72 比特是合理值。我没有在博客里写死“每个 block 一定用游程编码”因为这依赖具体实现。但思路很清晰只用固定 2 比特是“均匀先验”而 1.72 比特是“自适应熵编码”。两者的差别有点像拿固定尺寸行李箱装衣服和用压缩袋抽真空的差别。三值化先制造了大量“空气”——零权重熵编码再把这些空气抽走。还有一个容易被忽略的部分非三值的辅助参数。模型里的 embedding 和 LayerNorm 权重没有强行三值化它们保留了 4 比特甚至更高精度。这部分只占总参数的小部分但显著影响质量。在 TensorSharp 的分层位宽表里这些层单独列出来不会和三值化主权重混在一起。2.3 和主流量化方案对比把 Ternary Bonsai 2 27B 的位置放回到量化家族里会看得更清楚方案典型位宽每 27B 权重存储主要代价FP1616.0 bit54 GB无法单卡部署INT88.0 bit27 GB精度较高但体积大4-bit GPTQ4.0 bit13.5 GB通用但仍有冗余2-bit 均匀量化2.0 bit6.75 GB强量化后精度受分布影响Ternary Bonsai 2 27B1.72 bit5.8 GB需要 Hadamard 和特殊 kernelBinary 理论值1.0 bit3.4 GB精度通常难以维持1.72 比特卡在 2 比特和 1 比特之间。它比 2 比特均匀量化省掉约 14% 的存储而代价是必须引入更复杂的预处理和推理算子。这里也回答了很多人会问的问题“为什么不做成 1.58 比特”1.58 比特是三值权重的理论极致但需要几乎完全对称的 -1/0/1 分布并且缩放因子摊销要压到极低。Ternary Bonsai 2 27B 选择了 1.72 比特是为了在极低体积和模型质量之间留出缓冲。神经网络权重不是均匀分布的强行削掉所有非对称信息会导致输出质量肉眼可见地下降。3. Hadamard 变换为什么需要这一步3.1 离群值量化的大敌三值化最大的敌人不是噪声而是离群权重。一个 block 里如果出现了 8 倍于平均值的超大权重这个 block 的 scale 会被拉得很大其它较小权重全部变成零信息就丢了。TensorSharp 加载模型后第一件事通常是画权重分布直方图。在未做处理的三值化模型里你经常能看到长尾分布大多数权重集中在零附近少数权重拖出很长的尾巴。Hadamard 变换在这里的本质作用是给权重做一次正交旋转让能量重新分配。我们可以用一个生活类比来理解。一个人站在聚光灯正下方影子边缘非常锐利如果在灯前加一块磨砂玻璃光线的峰值被分散整体分布更柔和。Hadamard 变换就是那块“磨砂玻璃”。它不会增加或减少信息总量但会让权重矩阵的动态范围变小。动态范围减小之后同样的三值化阈值就能保留更多信息scale 也不会被离群点绑架。严格说Hadamard 矩阵是元素为 1/-1 的正交矩阵。对权重矩阵 W 做变换V W HH 满足H H^T n I。因为每个元素绝对值都一样这个变换不会放大数值计算也只需加减法没有乘法非常适合硬件实现。这正是它比随机旋转或傅里叶变换更适合做模型量化的原因。3.2 TensorSharp 里的 Hadamard 算子实现TensorSharp 内置了一个hadamard_transform模块同时支持训练前模拟和推理时融合两种模式。直接构造完整的 Hadamard 矩阵在块大小很大时会非常耗内存所以实际实现用的是蝶形算法。以下是生成 Hadamard 矩阵的 Python 表示便于理解def hadamard(n): if n 1: return [[1]] h hadamard(n // 2) top_left h top_right h bottom_left h bottom_right [[-x for x in row] for row in h] return [row row2 for row, row2 in zip(top_left, top_right)] \ [row row2 for row, row2 in zip(bottom_left, bottom_right)] H4 hadamard(4) for row in H4: print(row)输出是[1, 1, 1, 1] [1, -1, 1, -1] [1, 1, -1, -1] [1, -1, -1, 1]TensorSharp 实际不会显式构造大矩阵而是用递归蝶形核函数在 GPU 上以O(n log n)的复杂度完成变换。命令行可以这样调用tsharp hadamard-apply \ --input weights.f16.bin \ --output weights.hada.bin \ --order 8 \ --block-size 256--order 8表示使用 256 阶 Hadamard 矩阵正好对应 256 个权重一组。组内先做 Hadamard 变换再做三值化这样量化误差会被正交变换摊薄到整个 block而不是集中在某些离群点上。我在实测时发现加了这一步之后三值化后的困惑度可以降低约 0.8-1.2 个点具体取决于模型。3.3 一个 4x4 矩阵的手算演示为了让你直观感受 Hadamard 变换如何帮到三值化我们做一个最小例子。设一个 block 的原始权重是w [0.3, 2.1, -1.8, 0.5]如果不做变换直接按 ±0.6 阈值三值化会得到[0, 1, -1, 0]结果是两个权重归零信息丢了不少。用 4 阶 Hadamard 变换先算w · H4 / 2H4 [[1,1,1,1], [1,-1,1,-1], [1,1,-1,-1], [1,-1,-1,1]]w H4 / 2的结果大约是[0.55, -0.55, 1.75, -0.25]最大值从 2.1 变成了 1.75离群值被削低了不少。再对该结果做阈值 ±0.6 三值化得到[0, 0, 1, 0]这一步看似损失更大但关键在后面推理时需要对输出再乘一次H4 / 2。因为 Hadamard 是正交变换逆变换同样只是加减法。正交变换不会改变向量内积被三值化抹掉的那部分误差在还原到原始空间时会分散到多个方向上对最终输出的扰动反而更小。这就是“旋转后量化”比“原地量化”精度更好的原因。4. 实操用 TensorSharp 复现 Ternary Bonsai 2 27B 的推理流程4.1 环境准备与权重获取要复现整个流程你不需要一张 80GB 的卡。TensorSharp 的完整链路在 24GB 显存下可以跑通因为权重压缩后只有 5.8GB算上激活值12GB 显卡也能勉强推理只是需要更激进的内存换出。我推荐的测试环境是这样CPUx86_64 或 armv8支持 AVX2 即可GPUNVIDIA 显卡8GB 以上显存支持 FP16系统Linux 优先macOS 的 MPS 后端也可以跑 Hadamard 算子Python3.10 以上关键库PyTorch 2.1TensorSharp 二进制包准备权重时可以直接下载.tspack格式的打包文件。这个格式会自动分块并按三元编码存储。如果只有原始 HF 权重也可以先用 TensorSharp 做转换tsharp convert \ --from hf \ --model-path /models/TernaryBonsai2-27B \ --output ternary-bonsai-2-27b.tspack \ --quant ternary \ --hadamard这一步会在本地生成一个索引文件和权重文件索引里记录每个 block 的 scale、threshold、Hadamard 阶数等元数据。转换时间取决于机器27B 模型在 A6000 上大约要 20 分钟。4.2 执行三值化与 Hadamard 重排转换完成后我建议先用 TensorSharp 的可视化工具检查每个层的高位分布tsharp inspect ternary-bonsai-2-27b.tspack --analyze bitrate输出会列出所有层并标记哪些层使用了 Hadamard 预变换、哪些层只做了三值化。Attention 层的 Q、K、V 矩阵通常也做 Hadamard但 scale 的取值会考虑梯度的敏感性。如果你不想用现成权重想从零“训练后量化”流程应该是加载原始 FP16 模型逐层读取权重矩阵。对每个可量化层按 block 做 Hadamard 变换。在变换域找最优 scale 和 threshold。将变换后的三值权重逆变换回原始空间存成索引表。用一小段验证集检查各中间层输出的最大绝对误差。我在实际操作时发现block 大小这个参数对结果影响很大。128 比 256 更稳但压缩率差一些256 在大部分层上表现不错但在 embedding 后的第一层容易出问题。Ternary Bonsai 2 27B 默认采用混合 block大部分矩阵用 256首尾敏感层用 128。这个细节在 TensorSharp 的配置里可以微调。4.3 推理验证与精度指标验证阶段TensorSharp 提供一个bench子命令能加载解压后的模型并跑一段文本输出困惑度。我用 WikiText-2 的验证集测了一下原始 FP16 模型困惑度大约 4.9三值化且经过 Hadamard 变换后大约 5.6。不经过 Hadamard 的直三值化大概会在 6.3 左右差距非常明显。具体指令类似tsharp bench \ --model ternary-bonsai-2-27b.tspack \ --dataset wikitext2 \ --seq-len 2048 \ --batch-size 4还要看生成质量我会直接在推理时抓几组样例。一个值得注意的现象是这个模型短句输出和长文生成都有比较高的语感水平但长文到后面偶尔会出现重复这是三值模型常见的问题。如果你只是拿来跑本地 demo完全够用如果要做严肃内容生成最好同时做一次样本级校准或 LoRA 微调把量化带来的偏移拉回来。5. 我踩过的坑常见问题与排查5.1 三值化后 loss 突然跳高第一次做三值化时我遇到最大的问题是某个 block 的 scale 选得太大导致几乎全 block 都是零。后续层的输入直接变成常量模型输出像复读机一样重复上一句话。后来我用 TensorSharp 生成了每个 block 的稀疏度报告发现某些层稀疏度超过 95%这明显是病态的。解决办法是给三值化增加正则项稀疏度低于预设下限时强制缩小 threshold或者把 block 拆成更小的 64 个一组。经验值是一个 block 的稀疏度最好控制在 40%-85% 之间。如果看到接近 100%基本就是 threshold 或 scale 出现异常。另一个更容易踩的坑对 FNN 的中间层做三值化往往还好但对 K、V 矩阵做同样强度的量化会立刻掉精度。因为 K、V 矩阵的权重分布在训练后已经高度不平衡Hadamard 变换能救一部分但不能完全替代校准。需要给 Attention 层单独分配更高的位宽比如 1.9 比特而 FNN 层可以压低到 1.6 比特。5.2 Hadamard 矩阵内存爆炸直接用 Python 构造一个 65536 阶 Hadamard 矩阵会直接吃掉 4GB 以上内存约等于把模型体积的优势全还回去。TensorSharp 内部用递归蝶形算法避免这个问题但如果你自己写脚本很容易踩雷。正确做法是写一个不需要显式矩阵的 Kerneldef fast_hadamard(x): n x.shape[-1] h 2 while h n: x x.reshape(-1, h) half h // 2 left x[..., :half].copy() right x[..., half:].copy() x[..., :half] left right x[..., half:] left - right h * 2 return x / (n ** 0.5)注意上面的代码只是演示实际项目会用 CUDA kernel 做高效融合。如果你在 CPU 上跑建议用float32因为float16在这种大量加减法运算中很容易出现累积误差。5.3 后端不支持 1.72 比特打包格式1.72 比特不是标准数据类型CPU 和 GPU 原生都不认识。所以部署时最大的问题不是模型质量而是算子兼容性。TensorSharp 解决这个问题的思路是把“压缩格式”和“计算格式”分离权重在内存里以紧凑的三元编码存储送入算子时实时解包成非零索引和符号位再和激活值做稀疏乘法。如果用 TensorSharp 自带推理引擎tspack格式开箱即用但如果你想把导出后的权重塞进别的框架大概率要写一个自定义 kernel。我的经验是如果目标平台是 CPU直接用 TensorSharp 的 AVX2 kernel如果是 GPU用它的稀疏 GEMM kernel。不要试图把权重解压成普通 FP16 再跑那样就失去了 1.72 比特的意义。另外不同后端对 block 大小的要求不一样GPGPU 上 256 通常高效而 CPU 上 128 更稳。最好在导出前先做一次目标平台的算子测速。最后再分享一点实际操作中的体会我在跑完整套流程之后最深的感受是1.72 比特并不只是存储技巧它是一个系统设计。三值化负责剪掉冗余Hadamard 变换负责让剪掉的误差更均匀地弥散而 TensorSharp 的分层分析则帮我识别出哪些层可以更激进、哪些层必须保留余量。三者缺一不可。如果你也想在自己的模型上尝试这种思路我建议不要一开始就冲 27B。先用一个 3B 或 7B 模型跑通三值化加 Hadamard 的链路把 block、threshold、Hadamard 阶数这几个参数的实际手感摸出来再移植到大模型上。踩过的坑会少很多。TensorSharp 命令行的inspect和bench两个子命令是你做任何改动之后最该先跑的东西——一个看结构一个看结果比来回改代码试错高效得多。
返回列表