☰
1.72比特三值化+Hadamard变换:低比特大模型量化与TensorSharp实操
2026/10/6 6:43:05 网站建设 项目流程

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.0
attention168.1B1.85
ffn3215.4B1.62
其他-1.3B2.0
合计-27B1.72

embedding 之所以是 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 和特殊 kernel
Binary 理论值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 H,H 满足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 / 2:

H4 = [[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 显卡也能勉强推理,只是需要更激进的内存换出。

我推荐的测试环境是这样:

  • CPU:x86_64 或 armv8,支持 AVX2 即可
  • GPU:NVIDIA 显卡,8GB 以上显存,支持 FP16
  • 系统:Linux 优先,macOS 的 MPS 后端也可以跑 Hadamard 算子
  • Python:3.10 以上
  • 关键库:PyTorch 2.1+,TensorSharp 二进制包

准备权重时,可以直接下载.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 的取值会考虑梯度的敏感性。

如果你不想用现成权重,想从零“训练后量化”,流程应该是:

  1. 加载原始 FP16 模型,逐层读取权重矩阵。
  2. 对每个可量化层按 block 做 Hadamard 变换。
  3. 在变换域找最优 scale 和 threshold。
  4. 将变换后的三值权重逆变换回原始空间,存成索引表。
  5. 用一小段验证集检查各中间层输出的最大绝对误差。

我在实际操作时发现,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 内部用递归蝶形算法避免这个问题,但如果你自己写脚本,很容易踩雷。

正确做法是写一个不需要显式矩阵的 Kernel:

def 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两个子命令,是你做任何改动之后最该先跑的东西——一个看结构,一个看结果,比来回改代码试错高效得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询