BitNet 深度拆解:当大模型被压到 1.58 bit——三值量化、BitLinear 与在 CPU 上跑 100B 的工程全貌(2026)
一句话结论:BitNet 不是"把模型压小"的又一个量化技巧,而是从训练第一天起就假设权重只有三个值 {-1, 0, +1} 的原生 1-bit 架构。它把"乘法"变成"加/减",把显存需求砍掉 90%+,让你在一台没有独显的笔记本上,以接近人眼阅读的速度跑起百亿参数模型。
一、背景:LLM 部署的三座大山
过去三年,我们见证了大模型参数规模从 7B 一路飙到 405B。但落地时才发现,参数量只是故事的一半,部署成本才是真正卡住工程团队的门槛。
第一座山:显存。 一个 FP16 的 7B 模型,权重本身就占 14GB(7×10⁹ × 2 字节);到了 70B,FP16 权重是 140GB,没有两张 A100 80G 根本加载不进来。更别提推理时的 KV Cache、激活值、框架开销——实际显存往往是权重的 1.3~1.8 倍。
第二座山:内存带宽。 哪怕你把模型塞进了显存,每一步生成 token 都要把全部权重从显存搬到计算单元。Transformer 是**带宽受限(memory-bound)**而非算力受限的:A100 的算力有 312 TFLOPS,但显存带宽只有 2TB/s。对于 7B 模型,每个 token 要搬运 14GB 权重,理论带宽上限约 143 token/s——这是天花板,和算法优化几乎无关。
第三座山:能耗。 FP16 矩阵乘的每次乘加都要走 IEEE 浮点单元,功耗高得吓人。数据中心跑一个大模型,电费可能比显卡本身还贵。
于是量化(Quantization)成了必然选择:FP16 → INT8 → INT4,权重越来越小。但问题是,这些量化都是"事后补救"——先训练一个高精度模型,再用 PTQ(训练后量化)或 QAT(量化感知训练)往低比特压。精度掉得厉害,INT2 以下基本就废了。
微软 2024 年的两篇论文《The Era of 1-bit LLMs》和《BitNet: Scaling 1-bit Transformers to Large Language Models》提出了一条更激进的路:直接从头训练一个权重只有 1.58 bit 的模型。这就是 BitNet。
二、核心概念:什么是 1.58 bit
2.1 为什么是 1.58,不是 1?
"1-bit" 这个名字其实有点误导性。BitNet b1.58 的权重被限制为三值:{-1, 0, +1}。
一个权重有 3 种可能取值,表达它需要多少 bit?答案是 log2(3) ≈ 1.58 bit。这就是为什么叫 b1.58 而不是 b1.0。
为什么不干脆压到纯 1-bit({-1, +1},二值)?论文做过对比:纯二值(BitNet b1.0)在规模变大后精度损失明显,而引入中间的 0 之后,三值网络在 3B 以上规模能几乎无损地逼近同尺寸 FP16 模型。0 值带来的稀疏性还能顺手做剪枝,一举两得。
2.2 三值权重到底意味着什么
传统 nn.Linear 做的是:y = xWᵀ + b,其中 W 是 FP16 矩阵,一次计算就是一次浮点乘加(FMA)。
BitNet 的 BitLinear 做的是:W 在存储和计算时只有 {-1, 0, +1}。那么 w · x 这个乘积里:
- 当
w = +1,乘积就是+x(一次加法) - 当
w = -1,乘积就是-x(一次减法) - 当
w = 0,直接跳过(一次空操作)
乘法消失了。 这看似是个小把戏,但在百亿参数的尺度上,它把最耗电、最占带宽的浮点乘法器换成了加减法器和查表,硬件友好度天差地别。
三、架构分析:BitLinear 是怎么炼成的
3.1 量化函数:absmean(权重)与 absmax(激活)
BitNet 的关键不在于"最后把权重变成三值",而在于训练过程中就用伪量化(fake quantization)让网络适应三值。核心是两个量化算子。
权重量化 Q_w(absmean 量化):
对每个权重矩阵,先按小组(group,典型 group_size=64 或 128)做-centered 的对称量化:
α = mean(W) # 组内均值,作为零点偏移
γ = (1/nm) · Σ|W - α| # 组内平均绝对值,作为缩放
W̃ = clip(round((W - α) / γ), -1, 1) # 四舍五入到 {-1,0,1}
Ŵ = W̃ · γ + α # 反量化回浮点,参与前向
注意这里用均值 α 而非 0 做 center,是因为权重分布往往有非零均值,先减均值能把更多值映射到 0(更稀疏)。
激活量化 Q_x(absmax 量化): 激活保留更多比特(论文用 INT8,即 b=8):
γ_x = max(|x|) / (2^(b-1) - 1) # 对 INT8,分母是 127
x_q = clip(round(x / γ_x), -127, 127)
激活不取三值,而是 8-bit,是因为激活的分布动态范围大、对精度更敏感;权重才是被压到 1.58 bit 的那个。
3.2 BitLinear 前向:混合精度矩阵乘
BitLinear 的巧妙之处在于:前向计算时权重是三值、激活是 INT8,但乘积结果会乘回缩放因子还原量级。
x_q = Q_x(x)
W_q = Q_w(W)
y = W_q · x_q # 三值 × INT8,本质是加减
y = y · (γ · γ_x) # 还原量级(γ 来自权重,γ_x 来自激活)
y = LayerNorm(y) + bias
推理时,γ · γ_x 可以预先融合成一个标量常量,所以真正的热路径只有"三值权重 × INT8 激活 + 一次标量缩放",这正是后面 CPU kernel 能起飞的原因。
3.3 为 1-bit 改掉的那些"默认设计"
直接把 BitLinear 替换进标准 Transformer 是不够的,论文还做了一系列架构适配(这些细节很多教程都略过了,但工程上很关键):
- 非线性从 GELU 换成 ReLU²(Square ReLU)。 GELU 依赖精确的浮点概率近似,对三值网络不友好;ReLU² 计算简单、非负、易于低比特实现,且实验表明在 1-bit 下效果更好。
- 去掉 LayerNorm 的 bias,改用 RMSNorm 思路。 减少低比特下的偏移累积。
- subln(sub-layer normalization)初始化。 用
1/√2N的注意力/FFN 残差初始化,保证深层网络在极端量化下仍稳定(这是 BitNet 能 scale 到 100B 的关键稳定性手段)。 - 更大的学习率 + 专门的 warmup。 1-bit 梯度噪声大,需要比 FP16 训练更大的 LR 和更长的 warmup。
3.4 训练如何"骗"过反向传播:Straight-Through Estimator
量化 round 和 clip 都是不可导的(梯度要么 0 要么断),怎么反向传播?答案是 STE(Straight-Through Estimator):前向走量化路径,反向时假装量化函数恒等(梯度直接透传),只在缩放/反量化那一步保留真实梯度。
换句话说,网络在训练时"以为"自己在做三值运算,但梯度按浮点版本回传,逼着浮点权重往"容易被量化成 {-1,0,1}"的方向收敛。
四、代码实战
4.1 自己实现一个 absmean 三值量化
先用 NumPy 写个最朴素的版本,把公式落到代码上,建立直觉:
import numpy as np
def ternary_quantize(W: np.ndarray, group_size: int = 64):
"""absmean 三值量化:把 FP 权重压成 {-1,0,1},并返回缩放因子。"""
out = np.zeros_like(W, dtype=np.int8)
gammas = []
rows, cols = W.shape
for r in range(rows):
for g in range(0, cols, group_size):
wg = W[r, g:g+group_size]
alpha = wg.mean()
gamma = np.mean(np.abs(wg - alpha)) + 1e-8
w_tilde = np.clip(np.round((wg - alpha) / gamma), -1, 1)
out[r, g:g+group_size] = w_tilde.astype(np.int8)
gammas.append(gamma)
return out, np.array(gammas)
# 演示:一组随机权重被压成三值
W = np.random.randn(2, 64).astype(np.float32)
Wq, g = ternary_quantize(W)
print("原始权重样例:", W[0, :5])
print("三值权重样例:", Wq[0, :5]) # 只会出现 -1 / 0 / 1
print("该组缩放因子:", g[0])
跑出来你会看到:权重被"钉"在三个离散值上,而且因为减了均值,0 的比例通常不低——这就是天然的稀疏性。
4.2 用 PyTorch 写 BitLinear(含 STE)
下面是一个可训练、可在反向传播中透传梯度的 BitLinear:
import torch
import torch.nn as nn
import torch.nn.functional as F
class BitLinear(nn.Module):
def __init__(self, in_features, out_features, group_size=64, bias=True):
super().__init__()
self.in_features = in_features
self.out_features = out_features
self.group_size = group_size
# 浮点权重是"宿主",训练时优化它,前向时量化
self.weight = nn.Parameter(torch.randn(out_features, in_features) * 0.02)
self.bias = nn.Parameter(torch.zeros(out_features)) if bias else None
def _quant_weight(self):
# 按 group 做 absmean 量化,返回三值权重 + 每组 gamma
w = self.weight
out = torch.zeros_like(w)
gammas = []
for i in range(w.shape[0]):
for s in range(0, w.shape[1], self.group_size):
blk = w[i, s:s+self.group_size]
alpha = blk.mean()
gamma = blk.abs().mean() + 1e-8
q = torch.clamp(torch.round(blk / gamma), -1, 1)
out[i, s:s+self.group_size] = q
gammas.append(gamma)
return out, torch.stack(gammas)
def forward(self, x):
w_q, gammas = self._quant_weight() # 三值权重 {-1,0,1}
# 激活 INT8 量化(简化版 absmax)
gamma_x = x.abs().amax(dim=-1, keepdim=True) / 127 + 1e-8
x_q = torch.clamp(torch.round(x / gamma_x), -127, 127)
# 三值 × INT8,本质是加减
y = F.linear(x_q, w_q)
# 还原量级:权重 gamma(每组) × 激活 gamma(标量)
scale = gammas.mean() * gamma_x.mean()
y = y * scale
if self.bias is not None:
y = y + self.bias
return y
注意:上面是教学版,为了可读性没做 STE 的显式实现(PyTorch 里
round/clamp的反向在torch.use_deterministic下会断,真实实现常用straight-through自定义 autograd Function 或借助bitblas的量化算子)。但它完整展示了"训练用浮点宿主、前向走三值"的核心思想。
4.3 最小的 1-bit Transformer block
把 BitLinear 塞进一个 Transformer block,注意非线性换成 relu²、加 subln 残差缩放:
class BitTransformerBlock(nn.Module):
def __init__(self, dim, ffn_dim, n_heads, group_size=64):
super().__init__()
self.n_heads = n_heads
self.attn = nn.MultiheadAttention(dim, n_heads, batch_first=True)
self.qkv = BitLinear(dim, 3 * dim, group_size)
self.proj = BitLinear(dim, dim, group_size)
self.fc1 = BitLinear(dim, ffn_dim, group_size)
self.fc2 = BitLinear(ffn_dim, dim, group_size)
self.ln1 = nn.RMSNorm(dim)
self.ln2 = nn.RMSNorm(dim)
self.scale = 1.0 / (2 * 1) ** 0.5 # subln 初始化常数
def forward(self, x):
h = self.ln1(x)
qkv = self.qkv(h)
q, k, v = qkv.chunk(3, dim=-1)
attn_out, _ = self.attn(q, k, v)
x = x + self.scale * self.proj(attn_out) # subln 残差
h = self.ln2(x)
x = x + self.scale * self.fc2(torch.relu(self.fc1(h)) ** 2) # ReLU²
return x
4.4 真正跑起来:用 BitNet.cpp 推理官方模型
自己写的是玩具,生产请用微软官方的 BitNet.cpp(纯 C++/CPU 推理框架,无第三方依赖)。它的 build + run 流程极其干净:
# 1. 克隆并编译(需要 CMake + Clang,纯 CPU,无 CUDA)
git clone https://github.com/microsoft/BitNet.git
cd BitNet
# 自动下载并量化一个官方 1-bit 模型(如 bitnet-b1.58-2B)
python3 setup_env.py -md model_weights -q i2_s
# 2. 直接对话(约 2B 参数,普通笔记本 CPU 即可流畅运行)
./build/bin/llama-cli \
-m model_weights/bitnet-b1.58-2B/ggml-model-i2_s.gguf \
-p "用一句话解释什么是 1-bit 大模型" \
-n 256 -t 8 # -t 8 表示用 8 个 CPU 线程
如果你习惯 Python,也可以通过 Hugging Face 加载官方 1-bit checkpoint:
from transformers import AutoModelForCausalLM, AutoTokenizer
model_id = "microsoft/bitnet-b1.58-2B"
tok = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForCausalLM.from_pretrained(model_id, torch_dtype="auto")
prompt = "解释一下三值量化为什么叫 1.58 bit?"
inputs = tok(prompt, return_tensors="pt")
out = model.generate(**inputs, max_new_tokens=128)
print(tok.decode(out[0], skip_special_tokens=True))
五、性能优化:为什么它在 CPU 上反而快
5.1 乘法变加减,带宽砍九成
这是 BitNet.cpp 性能神话的根。对比 FP16:
| 维度 | FP16 Llama-7B | BitNet b1.58-7B | 收益 |
|---|---|---|---|
| 权重存储 | 13.5 GB | ~1.4 GB | ↓ ~90% |
| 每 token 搬运量 | 13.5 GB | 1.4 GB | 带宽需求↓ 10× |
| 核心算子 | FMA(浮点乘加) | 加减 + 查表 | 能耗↓ 显著 |
| 单 CPU 速度 | 龟速/不可行 | 5~7 tok/s(7B) | 可用 |
权重只有 1.58 bit,意味着加载一个 token 要搬的数据量直接除以 10。由于 Transformer 是带宽受限的,这一刀砍在七寸上——哪怕 CPU 算力远不如 GPU,胜在数据量小,反而能跑出可用速度。
5.2 BitNet.cpp 的内核设计
官方框架为不同硬件后端实现了专门内核,核心思路一致:把三值权重打包成位,用整数加法/减法 + 移位实现矩阵乘。
- I2_S(Intel/ARM CPU)内核: 权重以 1.58-bit 打包,激活以 INT8/INT16 存储。内核先解包三值权重,再与激活做乘加;因为权重是 {-1,0,1},乘加被编译成条件加/减,无需浮点乘法器。
- TL1(NPU / Apple Neural Engine)内核: 针对端侧 NPU 的 INT1 张量核心优化,把整个层映射到 NPU 的原生 1-bit MAC 指令,实现更低功耗。
- 并行策略: 权重并行(按行/列分片到多核)+ 激活并行(解包成本低,可细粒度切分),吃满多核 CPU。
论文给出的官方基准(vs FP16 Llama,单 CPU):
- 延迟:3B 模型快约 2.4×,7B 快约 5.1×,13B 快约 5.1×,70B 快约 4.1× 到 6×。
- 能耗:同等任务下能耗降低 55%~82.8%。
- 规模上限:在单台 CPU 上,BitNet b1.58-100B 能跑到接近人类阅读速度(~5 tok/s),而同尺寸 FP16 模型根本加载不进内存。
数字来自微软原论文《BitNet.cpp: 1-bit LLM on CPU》,实际表现取决于 CPU 型号、线程数和内核后端,请以自己的压测为准。
5.3 工程落地的三个优化点
- KV Cache 仍是瓶颈。 权重小了,但 KV Cache 还是 FP16/FP8。长上下文场景下,KV Cache 可能比权重还大,记得开 PagedAttention 类的分页缓存或 KV 量化。
- 线程数 = 物理核数最稳。
-t设成物理核(而非超线程数)通常最优,避免线程迁移导致的 cache 抖动。 - i2_s vs i2_s 量化等级。 BitNet.cpp 支持不同量化等级(
i2_s是 1.58-bit 对称),在精度和体积间权衡;端侧追求体积选更激进等级,服务端追求精度选保守等级。
六、局限与陷阱:别神话它
写到这里必须泼盆冷水,否则就是营销号了。
- 它不是"任意模型一键压成 1-bit"。 BitNet 的威力来自从头训练。对已有的 FP16 大模型做 PTQ 到 1.58 bit,精度会崩。有后续研究(如 "One-bit ... post-training")在探索后量化路径,但成熟度和原生训练仍有差距。
- 训练成本被转嫁了。 你把显存/推理成本省了,但训练一个 BitNet 比训练同尺寸 FP16 更费心:更大学习率、专门 warmup、subln 初始化、STE 的梯度噪声……调参门槛不低。
- 精度天花板。 在超难推理/数学任务上,1.58-bit 仍略逊于同参数量 FP16,差距随规模增大而缩小,但在小模型上明显。它的最佳定位是端侧/边缘的通用对话与轻量任务,不是替代旗舰 GPU 集群。
- 生态还在早期。 相比 PyTorch/Transformers 的庞大生态,BitNet 的工具链、微调脚本、服务化方案仍在完善,生产化要走不少自己填坑的路。
七、总结与展望:SLM + 1-bit 是端侧 AI 的钥匙
把视角拉高一点:过去两年行业痴迷于"更大",但 2026 年的现实是——真正产生现金流的 AI,往往跑在手机、车机、工控机、笔记本这些没有独显的设备上。
BitNet 的意义不在于"又小了一点",而在于它证明了一件事:1-bit 不是精度的坟场,而是一个可训练、可扩展、可部署的架构范式。当权重只有三个值:
- 模型可以塞进任何设备的内存;
- 推理可以脱离 GPU,靠 CPU/NPU 的低功耗跑;
- 端侧隐私(数据不出设备)第一次变得可行而不妥协体验。
沿着这条线,微软后续又提出了 BitNet a4.8(把激活也压到 4.8-bit)、以及把 BitNet 思想融入 MoE 稀疏化的探索。可以预见,2026 之后的战场,会从"谁的模型参数多"转向"谁的模型在边缘设备上又好又便宜"。
如果你想动手,最稳的路径是:先用 BitNet.cpp 把官方 2B/7B 模型在本地跑通(第四节的命令复制即跑),感受一下"无显卡跑大模型"的快感;再用第四节的 BitLinear 在自己的小数据集上从头训一个玩具级 1-bit Transformer,理解 STE 和 subln 到底在做什么。等这些直觉建立了,再回头看动辄几百 GB 的 FP16 巨兽,你会有完全不同的部署思路。
一句话收尾: BitNet 把"大模型"从数据中心的奢侈品,变成了可以装进口袋的日用品——而这一切,只因为有人敢把权重限制成三个数。
参考资料:微软研究院《The Era of 1-bit LLMs: All Large Language Models are in 1.58 Bits》(arXiv:2402.17764)、《BitNet: Scaling 1-bit Transformers to Large Language Models》、《BitNet.cpp: 1-bit LLM on CPU》,以及 microsoft/BitNet 开源仓库。文中基准数字均来自上述论文,实际表现以你的硬件压测为准。