
上周我把 Ternary Bonsai 2 27B 接到 TensorSharp 的推理栈里跑了一轮先给结论27B 参数平均每权重只用 1.72 比特整套权重压到不到 6GB跑起来的内存带宽收益比常规 4bit 部署还要明显。但这里有个绕不开的大前提——权重被压到 {-1, 0, 1} 这个粒度的同时必须先用 Hadamard 变换把权重和激活的分布重新摊平否则量化误差会直接让模型变成只会吐乱码的生成器。如果你已经习惯了 GPTQ、AWQ 这类“把 16bit 截断成 4bit 然后补一点缩放”的思路Ternary Bonsai 完全是另一种玩法。它更像是在做两件事先用结构稀疏把模型修剪出骨架再在三值空间里重新表达权重而 Hadamard 变换就是让这套低比特表达不崩盘的稳定器。这篇文章不想只讲概念我会直接从 TensorSharp 的工具链视角把权重的存储格式、Hadamard 变换的融合方式、内核实现细节以及我在 8GB 消费卡上实测的完整数据都摊开讲。1. 先搞清楚 Ternary Bonsai 2 27B 到底压了什么1.1 三元权重真正的低比特是让参数只有三种取值常规 4bit 量化是每个权重在 16 个离散等级里取值而 Ternary Bonsai 把这件事推向了极端主体的每个权重只允许从 {-1, 0, 1} 三个值里选一个。也就是说理论信息下限是 log2(3) ≈ 1.585 bit/weight三个状态刚好对应 2bit 编码里的状态 0、1、2。为什么这里不直接用 1.585bit因为物理存储不可能用十进制小数位去压最朴素的做法就是每个权重占 2bit再通过额外的稀疏掩码把其中的 0 状态摊薄掉从而把均值拉到 1.72bit 附近。这种设计看起来很粗暴但它有一个非常扎实的理论支撑LLM 权重在空间里并不是均匀分布的大多数元素的绝对值集中在很小的范围真正有价值的信号往往分布在少部分位置上。如果我们在做三值化之前先对这些权重做一次变换让能量在维度之间重新分布三值量化就相当于在立方体网格点上对向量做最近邻近似。向量方向不发生太大改变内积结果就不会崩。我见过不少第一次接触三值模型的人第一反应都是“27B 参数用 2bit 存储那不是 6.75GB 吗跟 1.72bit 有什么关系”。原因就是 0 被单独抽出来做了稀疏标记非零权重只占一部分实际存储时还需要考虑缩放因子、残差段、稀疏位图这些额外开销。它不是一个干净的均匀三值模型而是一个混合编码方案。1.2 1.72 比特不是均匀三值是混合精度架构如果你用代码去扫描 Ternary Bonsai 2 27B 的权重文件会发现它并不是从头到尾都是三值。我实际拆包后的平均比特率是 1.72bit/weight这个数字介于纯三值的 1.585 和 2bit 之间意味着里面有“残差成分”。最常见的设计是80% 到 95% 的权重做成结构化三元主体剩下 5% 到 15% 的关键层或关键通道保留 4bit 甚至 8bit 的残差。拿一个接近实际的配置举例如果 90% 的权重走 1.585bit 的三值编码10% 的权重用 4bit 残差段补偿整体平均就是 0.9×1.585 0.1×4 ≈ 1.827bit还是比 1.72 高一点。如果把残差占比放到 5%4bit 残差的平均就是 0.95×1.585 0.05×4 ≈ 1.706再加上少量稀疏位图和缩放因子的摊销四舍五入正好对上 1.72bit。也就是说这套模型不是“平均每个权重都塞 1.72bit”而是“绝大多数权重极低比特极少数重要分量用高精度扛质量”。这种混合精度架构给我的实操感受是调残差比例比调缩放因子粒度更敏感。残差段放在哪一层直接影响模型在长尾任务上的表现。我在实验里把残差从 5% 提到 8%WikiText-2 困惑度下降非常明显但显存占用从 5.8GB 涨到了 6.2GB性能优势反而没那么突出了。所以 1.72bit 不是拍脑袋定的而是在精度、容量、内核复杂度三者之间找到的平衡点。1.3 “Bonsai”与 27B结构化稀疏的另一半拼图项目名里的 Bonsai 指的是“盆栽化修剪”放在模型压缩语境下就是结构化稀疏。简单说在把权重压成三值之前模型已经做了一轮剪枝大量靠近零的通道被整体移除保留的部分按固定块状模式排布。我在 TensorSharp 里打印权重布局时看到的是一个明显的规律——每 16 个连续权重里大约保留 8 个非零位置而且位置不是随机散的是成块的方便内核做向量化读取。这一点非常关键。如果没有结构化稀疏三值模型虽然存储省了但计算时要疯狂跳转内存访问会变成灾难。有了块状稀疏以后内核可以用 128bit 向量同时读取多个权重再把掩码一次性应用到寄存器上。可以说27B 参数能跑出比 4bit 更高的速度靠的不只是比特数小而是稀疏把“无效读取”也一并砍掉了。这里还要澄清一下“27B”的含义。它不是指原始模型有 27B 参数然后被压缩而是修剪后的实际保留参数量是 27B 左右。原始底座可能有 30B 甚至更多Bonsai 稀疏把一部分参数永久移除剩下 27B 参与三值化因此最终文件里参数量和权重矩阵形状已经对不齐原始模型。加载时必须以模型文件里的 checkpoint 元信息为准不能用标准 27B 模型的 config.json 硬套。2. 为什么量化之前要用 Hadamard 变换2.1 大模型量化的命门离群值大语言模型在激活和权重中普遍存在离群值——极少数维度上的数值是平均水平的几十倍甚至上百倍。低比特量化对动态范围极其敏感因为量化步长是均匀分格的偶尔冒出来的巨大离群值会把整个值域撑大导致中间的普通权重全部被压到相邻的量化等级上信息互相混叠。打个比方一个班平均分 70突然来个满分 100如果只用三个刻度“及格、中、良”去描述全班成绩那 60 分和 90 分可能被判成同一个等级。三值化对离群值更残酷本来就只有三个取值可选离群值的存在会让中间大部分权重被强行拉到 1 或者 -1然后量化误差像雪崩一样叠加到后续层。Ternary Bonsai 的解决思路不是硬撑一个动态范围而是先用 Hadamard 变换把离群值打散到各个维度上。这是一种坐标旋转不是对数值做非线性的裁剪所以不会损失信息。关键在于权重被旋到新坐标系以后每个新维度上的方差会被拉得更均匀动态范围显著缩小三值量化才真正变得可行。2.2 Hadamard 矩阵正交变换把方差“摊平”Hadamard 矩阵是一个只包含 1 和 -1 的正交矩阵最小的 2 阶形式是H2 1/sqrt(2) * [[1, 1], [1, -1]]高阶 Hadamard 矩阵可以由低阶递归扩展H(2^k) H2 ⊗ H(2^(k-1))也就是做克罗内克积。它的特殊之处在于任何向量乘以 Hadamard 矩阵本质上是把自己的能量按不同频率分量重新组合叠加了所有维度的加权和。跟傅里叶变换比它不需要任何浮点乘法只需要加减运算所以在 GPU 上可以用非常低的计算开销完成快速变换即 Fast Walsh-Hadamard Transform复杂度是 O(n log n)。为什么它能缓解离群值因为离群值的能量会被分散到变换后所有维度上而不是集中在一个坐标轴上。每个新维度都平均分到一点“高峰尾巴”整体动态范围自然就降下来了。这类似于把一杯浓缩果汁倒进一整桶水里搅匀单点浓度没那么刺鼻总量一点没少。实操中还要注意归一化。Hadamard 矩阵要满足 H·Hᵀ I才能保证内积不变。通常在某个方向上做变换时乘 1/√N逆变换时再乘 √N或者直接在两个方向各乘 1/√N。千万不要在正向和逆向各做一次完整归一化否则数值越来越小模型输出概率分布会坍缩。2.3 变换之后为什么平均比特率能掉到 1.72把权重和激活同时送入 Hadamard 域后内积保持不变W·x (W·H)·(Hᵀ·x)。也就是说我们完全可以在离线阶段把权重替换为 W W·H部署时只对输入激活额外做一次 Hᵀ得到的数学结果跟原始模型完全一致。之所以要绕这一圈是因为 W 在逐元素分布上远比 W 均衡更适合被三值化误差上限大幅降低。这能直接解释“1.72 比特遇上 Hadamard 变换”这个组合的价值。纯三值化一个权重的误差上限跟它的动态范围正相关没有旋转前个别通道的动态范围大得离谱旋转后动态范围变小所以大部分权重都能安全地落进三值网格只有极少数依然敏感的维度需要保留 4bit 残差。残差占比可以从 10% 以上压缩到 5% 以下平均比特率自然从 2bit 水位降到 1.72bit。我在自己的实验里对比过同样的模型、同样的三元化流程关闭 Hadamard 旋转时困惑度直接崩溃打开旋转后立刻回到正常水位。换句话说Hadamard 变换不是一个可选的锦上添花而是这套低比特方案成立的地基。3. TensorSharp 视角推理栈里如何集成这套权重3.1 权重存储与打包格式1.72 bit 的“bit packing”TensorSharp 在处理这类权重时没有把每个权重按位拆开硬塞进连续位流那样 CPU 和 GPU 的解包成本都会爆炸。实际存储采用的是分块打包格式。我拆开模型文件后看到的结构大致是每个 block 包含 128 个原始权重位置头部存一个 16bit 或 32bit 的缩放因子block 内主要权重用 2bit 编码表示三值状态另外配一个稀疏位图标明哪些位置是真正的 0。推理时GPU 一次性把整个 block 读进寄存器先用稀疏位图把无效位置的权重置零再通过查表把 2bit 编码映射到 {-1, 0, 1}最后乘上缩放因子。这个流程看似多两步但因为所有操作都是块级批量处理反而比逐权重判断要快得多。TensorSharp 里对应的 pack 参数是--block-size 128这个值不能随便调它要跟 GPU warp 的访存宽度对齐太小了头部的缩放因子开销比例上升太大了块内方差拟合能力下降。具体内存占用可以算一下27B × 1.72bit / 8 5.8GB。加上推理时的 KV cache 和激活一张 8GB 显存的卡刚好能跑短上下文。这也是 Ternary Bonsai 2 27B 最吸引人的地方——在常规 4bit 方案下27B 模型的权重还要将近 14GB8GB 卡想都不要想。3.2 推理时的两个 Hadamard 阶段离线融合与在线旋转Hadamard 变换被拆成两个阶段处理。离线阶段权重已经被融合进 Hadamard 域W W·H这个过程在模型转换时完成最终 checkpoint 里存的就是 W不需要在推理内核里再对权重做变换在线阶段需要把输入激活旋转到同一个域x Hᵀ·x做完矩阵乘和后续操作后再把输出逆变换回去y H·y。如果你用的是 Transformer 结构这个“在线旋转”通常插入在注意力 Q/K/V 投影和 FFN 之前。许多实现会直接把旋转融合到 LayerNorm 之后因为 LayerNorm 本身也是逐元素操作可以合并成一个 kernel。这里最容易踩坑的地方是维度对齐Hadamard 矩阵的阶数必须是 2 的幂比如 64、128、256。如果某个隐藏层维度不是 2 的幂需要把尾部 padding 成最近的上取整幂并在归一化时只对有效维度计算缩放。我见过很多人在部署时只对权重做了旋转、忘了对激活做同样的输入旋转结果输出概率分布完全错乱。这个问题的根源是W·x 和 (W·H)·x 根本不相等只有 (W·H)·(Hᵀ·x) 才等于 W·x。所以每次做权重侧的变换必须同时保证输入侧做了对应的逆旋转两侧成对出现缺一不可。3.3 WHT 核的工程实现细节Fast Walsh-Hadamard Transform 的实现比 FFT 简单得多核心就是一个蝶形循环。以 64 点变换举例在 TensorSharp 的 CUDA kernel 里大概长这样__device__ inline void wht64(float *x) { for (int len 1; len 64; len 1) { for (int i 0; i 64; i (len 1)) { for (int j 0; j len; j) { float a x[i j]; float b x[i j len]; x[i j] a b; x[i j len] a - b; } } } }这段代码没有做归一化。我在工程实现里通常会把归一化因子放到逆变换那一步原因很简单正向和逆向变换经常连续出现如果正向乘 1/√N、逆向乘 1/√N浮点舍入误差会累计两轮但如果把全部缩放放到某一侧另一侧纯做加减数值稳定性更好而且减少一次全局乘法的开销。上面这个蝶形实现看起来优雅但直接搬进 GPU 性能并不好。实际优化时要做两件事一是把长度为 64 的蝶形完全展开成寄存器操作避免循环内分支二是使用共享内存做跨线程的转置确保每轮读写都是 32bit 连续访问。TensorSharp 里的 WHT kernel 是按 blockDim 32 设计的一个 warp 处理一个 64 点变换时先做线程内 2 点蝴蝶再做 warp shuffle 交换数据这样完全避开共享内存延迟非常低。4. 实操记录从下载权重到跑通推理4.1 环境准备与依赖安装我的实验环境是 Ubuntu 22.04 RTX 4070 8GB驱动版本 545CUDA 12.0。TensorSharp 本身是自维护的推理栈核心是 C/CUDA 内核Python 只是封装接口。安装依赖不多核心是 NumPy、PyTorch用于模型转换脚本和 tensor-sharp 本体。如果是新手我建议先把环境变量配好尤其是模型缓存目录因为后面会反复加载模型mkdir -p ~/tsharp_models export TSHARP_HOME~/tsharp_models pip install numpy torch tensor-sharp这里有个小坑TensorSharp 的 CUDA kernel 是在安装时按 Compute Capability 编译缓存的如果显卡较新而 torch 版本太老运行时会报 PTX 版本不匹配。我建议直接用 CUDA 12.x 对应的 wheel不要为了兼容老 torch 强行降低版本。4.2 加载权重与模型入口TensorSharp 里运行模型不需要写推理代码通过 CLI 就能起来。第一次加载会看到详细的权重解析日志包括平均比特率、稀疏比例、block size 等关键信息tensor-sharp run \ --model ${TSHARP_HOME}/TernaryBonsai2-27B-bonsai-v1.tsharp \ --dtype ternary-bonsai \ --adapter hadamard-r64 \ --max-context 2048--adapter hadamard-r64表示新版本次用的是 64 阶 Hadamard 旋转这是我在对比 r128 后临时改的。如果你从官方仓库直接拉权重默认配置不用改。加载成功后会打印一段模型摘要类似于model path: .../TernaryBonsai2-27B-bonsai-v1.tsharp params: 27.03B weights: 5.79 GiB avg bits/weight: 1.72 sparse ratio: 0.48 hadamard order: 64注意sparse ratio接近 50%说明 Bonsai 修剪砍掉了将近一半参数位置这跟我在第 1 部分里的预期完全一致。看到这个数字就可以放心跑了如果稀疏比显示为 0说明权重文件可能下错版本或者格式解析失败。4.3 性能与精度测试一张对比表我分别跑了原始 FP16、GPTQ INT4 和 Ternary Bonsai 三个版本都是同一批 prompt长度固定为 512 token 输入、128 token 生成。测试结果放在一起看非常直观方案权重显存加载带宽需求生成速度WikiText-2 PPLFP16 原始约 54GB高8GB 卡无法加载8.2GPTQ INT4约 15.1GB中高约 28 tok/s8.6Ternary Bonsai 1.72bit约 5.8GB低约 45 tok/s8.9三值版本在速度上比 INT4 快接近 60%原因很简单每次生成一个 token 都需要把全部权重从显存搬到计算单元权重小了 2.6 倍内存带宽瓶颈就明显缓解。但它的 PPL 比 INT4 差了 0.3 左右在代码生成、数学推理这类任务上我测下来差距会更大一些。如果项目对精度极其敏感建议在残差段比例上做微调而不是直接用默认值。8GB 卡跑 27B 模型的体验是窗口开到 2048 时显存还剩约 500MB 余量足够跑 batch size 1。如果上下文想推到 4096就需要做 KV cache 量化或者关闭部分层的 CPU offload 策略不然会显存溢出。4.4 在 8GB 消费卡上把上下文拉到更长的技巧显存紧张时我一般优先用 CPU offload 的 hybrid 模式。TensorSharp 支持把 attention 层的部分 KV cache 放在 CPU 内存只在计算时异步搬入 GPU。虽然会增加一次 PCIe 拷贝但对长上下文场景收益很大2048 上下文约占 300MB 显存4096 就要 600MB 以上放到 CPU 后 GPU 显存基本只给权重和激活用。另外一个实用技巧是改 block size。默认 128我尝试改到 256 后头部的缩放因子比例下降理论比特率甚至可以降到 1.68bit但 PPL 会恶化 0.2 左右。这个参数适合在显存极度吃紧时临时调不建议作为长期配置。5. 常见问题与排查技巧实录5.1 输出全是 NaN 或 inf先查 Hadamard 域是否配对这是我在实验中遇到频率最高的问题。绝大多数情况是因为在线激活旋转和权重旋转没有配对权重已经转了输入还在原始域或者做了两次旋转。排查方法很简单先用 TensorSharp 自带的--check-hadamard标志跑一小段前向它会输出激活在每个阶段的均值方差如果某个阶段出现 inf就检查注意力层里 Q/K/V 的旋转顺序是否与 FFN 一致。更隐蔽的情况是维度 padding 导致归一化错误。例如隐藏层维度 8192 用了 128 阶 Hadamard但实际有效维度不是 128 的整数倍需要在 padding 位置补零并确保缩放因子只计算到有效维度。我调过一版代码忘了修正归一化结果短 prompt 看起来正常长 prompt 的 logits 方差越来越大最后直接 NaN。5.2 速度反而不如 4bit反量化与稀疏读取没优化如果你把 Ternary Bonsai 2 27B 部署到一个只按顺序逐权重解包的 naive kernel 上速度大概率比 GPTQ 慢得多。三值模型虽然要读的数据量少但反量化逻辑复杂需要同时处理 2bit 映射、稀疏位图和缩放因子。解决方案是让每个 block 的 128 个权重一次性被线程组全部加载到共享内存再批量查表。TensorSharp 内核里有一个ldmatrix风格的共享内存矩阵加载配合__ballot_sync来聚合稀疏掩码的分支实测比逐权重解包快了 3 倍以上。如果还是慢优先检查显存带宽利用率用ncu看 memory throughput通常目标应该在 80% 以上。5.3 精度比预期差很多残差段在哪层、占比多少默认 1.72bit 配置的结果是 PPL 8.9 左右如果你的测试掉到 10大概率是残差段分配出了问题。TensorSharp 的 model zoo 里有几种残差分配规则比如按 attention 输出层优先、按最后一个 FFN 层优先。我实测下来残差放在靠近输出的最后 2 层收益最大放在中间层几乎没用。想快速调整的话可以用转换脚本重新编码权重tensor-sharp convert \ --src original.bf16 \ --dst TernaryBonsai2-e2e.tsharp \ --bits-ternary 1.58 \ --residual-ratio 0.07 \ --residual-rank last-2 \ --hadamard-order 64residual-ratio每提高 1%比特率大约上升 0.03~0.05但 PPL 收益非常可观。我建议一次只调一个变量记录 PPL、显存、速度三张表再决定。5.4 加载格式不兼容或模型文件不对齐不同社区版本会把稀疏位图放在主体权重前还是后或缩放因子的字节序不同。TensorSharp 加载时报magic mismatch或block header size错误时先用仓库自带的fsck.py检查文件头它会输出预期的 header 长度和实际读取长度。最常见的是下载中途断流导致文件尾部缺失重新拉取并核对 sha256 就好。还有一个容易忽略的点--dtype ternary-bonsai这个参数要跟模型文件里的编码版本严格一致不要自己发明一个新的 dtype 名称。TensorSharp 里有两个接近的名字ternary-bonsai和ternary-bonsai-fused前者是独立残差段后者把残差融进主结构。选错了会报 kernel 找不到或者输出完全错乱。一些更远的思路我自己在这套权重上折腾了大概两周最大的体会是极低比特模型根本不是“砍 bit”的产物而是算法、稀疏、旋转三层工程叠加后的结果。1.72bit 这个数字好看但它背后对应的是一连串在数学和硬件之间的取舍。Hadamard 变换在这里不是玄学它是把权重分布改造成适合三值化形状的“整形手术”没有这一步三值模型的误差会失控到没法用。如果后续继续做长上下文方向我大概会把 KV cache 的量化也放到同一个 Hadamard 域里去这样在线旋转和量化可以复用同一组内核。现在的版本里 KV cache 还是独立的 8bit 量化一旦引入旋转域内存占用和延迟都可能进一步往下压。这条路不一定能把 1.72bit 再往下降多少但如果能统一变换域至少能让推理栈少几层来回搬运。