GGUF 完整讲解

1、大模型推理流程

自回归大模型通用推理流程,不绑定GGUF、不绑定vLLM,权重文件只参与模型加载步骤。

flowchart TD A["磁盘模型文件"] --> B["模型加载"] B --> C["Prompt输入"] C --> D["Tokenizer编码"] D --> E["Transformer前向计算"] E --> F["输出Logits"] F --> G["采样下一个Token"] G --> H{"停止条件"} H --否--> I["更新KV‑Cache"] I --> E H --是--> J["解码输出文本"]

流程关键点位说明

  1. 模型加载:唯一读取磁盘权重文件的环节。如果文件是GGUF:读取头部、元数据、张量索引、量化类型qtype、量化权重二进制;将权重映射/拷贝进内存显存。
  2. Tokenizer编码:Tokenizer从GGUF元数据读出到内存,不再读磁盘。
  3. Transformer前向计算【量化反量化发生点】:不再访问GGUF磁盘。算子读取内存中已经加载的量化权重,依靠内存保存的qtype信息做实时反量化,执行矩阵乘法。
  4. 采样、KV‑Cache、解码:全部运行在内存,和GGUF磁盘文件无交互。

2、推理流程和 GGUF 的关系

第一层:抽象推理两阶段(不区分llama.cpp/vLLM)

flowchart TD A["GGUF磁盘文件"] --> B["阶段A:模型加载(仅一次IO)"] B --> C["阶段B:推理循环(反复计算)"]

阶段A:模型加载——从GGUF磁盘读取哪些东西

阶段B:推理循环——GGUF磁盘文件完全退出

通俗例子:GGUF是磁盘上的压缩包。阶段A:打开压缩包,读取目录清单(元数据+张量索引),把压缩数据载入内存,记住每个文件的压缩格式(qtype)。阶段B:直接操作内存内压缩数据,现场解压运算,不会重复打开磁盘压缩包。

数据流伪代码,完整展示GGUF与推理、量化的关系

# ==================阶段A:加载,访问GGUF磁盘==================
# 打开磁盘GGUF文件
gguf_fd = open("model-q4_k_m.gguf","rb")
# 1 读取头部校验
header = read_gguf_header(gguf_fd)
# 2 读取元数据,拿到架构、tokenizer
model_meta = read_gguf_metadata(gguf_fd)
# 3 读取张量索引,拿到每个权重 offset、qtype
tensor_list = read_gguf_tensor_info(gguf_fd)

# 4 根据offset读取权重二进制,mmap或者read拷贝
runtime_weight_buf = mmap_gguf_weights(gguf_fd, tensor_list)

# 构建运行时模型对象:把qtype、维度全部存入内存,不再依赖磁盘
runtime_model = create_runtime_model(model_meta, tensor_list, runtime_weight_buf)

# GGUF磁盘文件任务完成,可以关闭
gguf_fd.close()

# ==================阶段B:推理循环,完全不碰GGUF磁盘==================
tokenizer = runtime_model.tokenizer
input_ids = tokenizer.encode("你好,请介绍GGUF")

while not is_eos(input_ids):
    # 关键点:使用内存里保存的 qtype,对内存中权重实时反量化
    logits = transformer_forward(runtime_model.weights, runtime_model.qtype_table, input_ids)
    next_token = sample(logits)
    input_ids.append(next_token)

output_text = tokenizer.decode(input_ids)
print(output_text)

第二层:llama.cpp+GGUF 与 vLLM+GGUF对比(两张独立流程图)

图1:llama.cpp + GGUF加载链路

flowchart TD A1["GGUF磁盘"] A2["解析元数据、qtype、offset"] A3["mmap缺页映射"] A4["CPU虚拟内存+VRAM混合"] A1 --> A2 --> A3 --> A4

llama.cpp行为:解析GGUF取出元数据、qtype、offset;mmap建立虚拟内存映射,不一次性全部载入物理内存;算子访问权重发生缺页,操作系统把磁盘数据载入RAM;推理时读取内存中qtype,CPU/GPU算子完成反量化。

图2:vLLM + GGUF加载链路

flowchart TD B1["GGUF磁盘"] B2["解析元数据、qtype、offset"] B3["读取全部权重字节"] B4["全部权重驻留GPU显存"] B1 --> B2 --> B3 --> B4

vLLM行为:GGUF仅作为磁盘数据源;解析得到元数据、qtype;不使用mmap,一次性读入显存;GPU算子读取显存内qtype标记完成反量化推理。

对比维度llama.cpp + GGUFvLLM + GGUF
GGUF磁盘访问时机仅模型加载阶段访问仅模型加载阶段访问
量化信息 qtype来源GGUF的TensorInfo,加载存入内存GGUF的TensorInfo,加载存入内存
加载实现mmap缺页按需加载全量读入显存,无mmap
反量化执行位置ggml算子,CPU/GPU均可CUDA算子,GPU内完成
KV‑Cache实现普通缓冲区,串行推理PagedAttention分页KV,高并发批处理
硬件支持CPU / Metal / CUDA / AMD强依赖NVIDIA CUDA

3、GGUF详解 — llama.cpp和GGUF协同,gguf里面都有啥

GGUF是静态二进制存储格式,本身不具备任何计算能力;全部反量化、矩阵运算、推理逻辑由llama.cpp/vLLM这类引擎实现。下面列出GGUF内部每一块内容,并且明确每一块在推理流程中承担的角色。

3.1 GGUF文件物理布局流程图

flowchart TD H["Header头部"] M["Metadata元数据"] TI["TensorInfo张量索引"] PAD["Padding对齐填充"] DATA["张量权重二进制"] H --> M --> TI --> PAD --> DATA

GGUF内部各个模块与推理的对应关系

3.2 GGUF‑V3 Header C结构体源码

typedef struct {
    uint32_t magic;         // 魔数固定为"GGUF"
    uint32_t version;       // 文件版本,主流v3
    uint64_t n_tensors;     // 文件中张量总数量
    uint64_t n_metadata;    // 元数据键‑值对数量
} gguf_header_t;

3.3 Metadata元数据说明表

元数据Key在推理流程中的作用
general.architecture识别模型架构,选择对应推理代码路径
llama.context_length初始化KV‑Cache最大上下文窗口
llama.block_count构建Transformer网络层数
llama.embedding_lengthEmbedding向量维度
tokenizer.modelSentencePiece二进制,编码Prompt、解码输出文本
general.alignment权重内存对齐字节,优化算子性能

3.4 TensorInfo张量索引结构体(量化核心)

typedef struct {
    gguf_string_t name;     // 权重名字,如 blk.0.attn_q.weight
    gguf_quant_type type;   // qtype:Q4_K_M / Q8_0 / FP16,反量化的依据
    uint32_t n_dims;        // 张量维度数目
    uint64_t dims[4];       // 张量各个维度大小
    uint64_t offset;        // 当前张量在GGUF文件内的字节偏移
} gguf_tensor_info_t;
重点说明:GGUF只记录type(qtype)这个标记,不会做反量化解压。引擎加载时把type读到内存;推理算子读取内存的type,调用对应的反量化内核完成解压计算。offset用来定位磁盘上权重字节的位置。

3.5 GGUF量化类型表,推理运行时行为

量化类型等效比特推理运行时行为
FP1616bit不需要反量化,直接矩阵运算
Q8_08bit轻量反量化,速度快,精度损失很小
Q5_K_M~5bitK‑Quant高质量档位,反量化开销中等
Q4_K_M~4bit综合平衡,本地推理最常用
Q3_K_M~3bit高压缩,反量化开销上升,质量下降
Q2_K~2bit极限压缩,反量化开销大,质量较差

3.6 llama.cpp 与 GGUF完整协同全链路

  1. 磁盘GGUF文件保存:Header、Metadata、TensorInfo(qtype+offset)、Padding、量化权重二进制。
  2. llama.cpp【加载阶段】:打开GGUF磁盘;读取头部校验;读取元数据构建模型参数;读取TensorInfo拿到每个权重offset和qtype;mmap映射权重字节;将qtype、维度等全部保存到运行时内存;GGUF磁盘文件可以关闭。
  3. llama.cpp【推理循环阶段】:不再访问GGUF磁盘;Transformer算子读取内存内量化权重,依据内存保存的qtype调用对应反量化内核,完成矩阵计算;Tokenizer、KV‑Cache、采样全部属于llama.cpp内部逻辑。

3.7 Python极简读取GGUF头部和张量数量示例

import struct

def read_gguf_header(path):
    with open(path,"rb") as f:
        magic = f.read(4)
        version = struct.unpack("<I", f.read(4))[0]
        n_tensors = struct.unpack("<Q", f.read(8))[0]
        n_meta = struct.unpack("<Q", f.read(8))[0]
    print(f"magic:{magic}, ver:{version}, tensors:{n_tensors}, meta:{n_meta}")

# read_gguf_header("model.q4_k_m.gguf")

4、Bonsai-27B 定制分析与量化洞察

前 3 章讲清楚了 GGUF 是通用容器协议, llama.cpp 是通用推理引擎; 但 Bonsai 是个不通用的模型 — 它必须拉 llama.cpp 的分支 (prism) 才能跑。这一章讲: 标准 GGUF 量化 (PTQ) 为何不够用, Bonsai 拉分支的 4 个定制点是什么, 每个定制点按 场景 → 说明 → 流程 → 源码 四件套讲。

核心认知: llama.cpp 上游 main 里的 42 种 ggml_type, 绝大多数是 PTQ (训练后量化), 拿 FP16 模型 + 几百条校准集就能量化, 不需要重训。Bonsai 必须重训 (QAT + 蒸馏), 所以必须拉分支 — 这是 GGUF 量化的"训练友好型"和"PTQ 友好型"两类方法的根本分歧。

4.0 阅读本章需要的术语前置 (看不懂后面会卡这里)

术语通俗解释出现位置
blck_size一个量化 block 含多少个原始 weight。Q4_0 是 32, Q4_K 是 256, Q1_0 是 128, Q2_0 是 256ggml.c type_traits
vec_dot矩阵乘的核心: W · x, 但 W 是量化的,x 是 Q8_0/Q8_1 量化激活 → 输出 FP32ggml-quants.c
type_traits每种 ggml_type 对应的元数据 + 函数指针表 (dequantize/quantize/vec_dot)ggml.c:929
__byte_permCUDA SIMD 指令: 32-bit 字内 4 个 byte 按位重排, 一次 SIMD 算 4 个查表CUDA Q2_0 vec_dot
dp4aCUDA DP4A 指令: 4 个 int8 dot product 一次, 输出 int32 (Hopper/Ada 架构)CUDA Q2_0 vec_dot
STE (直通估计)训练 trick: 量化函数不可微, 但反向梯度当 1 通过, 假装量化是恒等映射Bonsai QAT 训练
per-group scale每 N 个 weight 共享一个 FP16 scale, 解决 LLM 0.1% outlier 通道精度问题Q1_0/Q2_0 都用
block_q2_0单个 Q2_0 block: FP16 d (2B) + 64B packed 2-bit, 共 66 字节, 含 256 weightggml-quants.c
Q8_0 / Q8_1激活量化格式: Q8_0 是每 32 个激活共享一个 scale, Q8_1 是每 32 个共享一个 scale + 块级额外 scalevec_dot 配对用
MMQ vs MMVQCUDA 端两种矩阵乘路径: MMQ (matrix-matrix quantized, 大批量) + MMVQ (matrix-vector quantized, 单 token)ggml-cuda

4.1 必须先懂的概念: PTQ vs QAT

这是理解 Bonsai 拉分支的钥匙。如果跳过这一节, 后面的定制点和洞察都会看不懂。

PTQ (Post-Training Quantization, 训练后量化)

flowchart TD A["FP32/FP16
预训练好的模型"]:::a --> B["收集激活分布
(几百条校准集)"]:::b B --> C["统计每层的
scale / min / max"]:::c C --> D["量化压缩
写入 GGUF"]:::d D --> E["GGUF 文件
(如 Q4_K_M)"]:::e classDef a fill:#dbeafe,stroke:#2563eb classDef b fill:#fff3cd,stroke:#f59e0b classDef c fill:#fff3cd,stroke:#f59e0b classDef d fill:#d1fae5,stroke:#10b981 classDef e fill:#d1fae5,stroke:#10b981

PTQ 的特点:

QAT (Quantization-Aware Training, 量化感知训练)

flowchart TD A["FP32 Teacher
(已收敛的预训练模型)"]:::a A --> B["QAT Student
前向时真量化
(Q1_0/Q2_0)"]:::b B --> C["计算损失
CE + 蒸馏"]:::c C --> D["反向传播
STE 直通估计"]:::d D --> E["Student
收敛"]:::e E --> F["导出 GGUF
tensor type=Q1_0"]:::f classDef a fill:#dbeafe,stroke:#2563eb classDef b fill:#fee2e2,stroke:#dc2626 classDef c fill:#fee2e2,stroke:#dc2626 classDef d fill:#fee2e2,stroke:#dc2626 classDef e fill:#d1fae5,stroke:#10b981 classDef f fill:#d1fae5,stroke:#10b981

QAT 的特点:

关键观察: 上游 llama.cpp main 的 42 种 ggml_type 都是 PTQ 协议 (加载 + 反量化 + vec_dot 内核), 没有 QAT 内核。要做 1.7 bits 95% 精度, 必须改训练 pipeline + 加 QAT 内核 + 加蒸馏 — 全部在 prism 分支里。

4.2 PTQ vs QAT 数据对比 (业内实测数据)

量化方法类型典型精度 (vs FP16)训练需要?
F16 / Q8_0PTQ99~100%否
Q4_K_M (主流)PTQ92~95%否
Q3_KPTQ85~90%否
Q2_KPTQ70~80%否
朴素 1-bitPTQ< 50% (崩)否
Bonsai 1-bit (Q1_0)QAT + 蒸馏90%是 (重训)
Bonsai ternary (Q2_0)QAT + 蒸馏95%是 (重训)

4.3 定制点 ①: 三值怎么还原 (核心定制)

场景: 为什么需要"特征还原"

GGUF 存储的是量化后的字节流, 不是 FP32 权重。推理算子 (gemm) 只能对 FP32 计算, 所以必须在算之前把量化字节 还原 成 FP32, 才能进入 Linear 层。

朴素做法: 把每个 weight 都存成 FP32, 27B 模型就是 54 GB。Bonsai 三值做法: 存成 {-1, 0, +1} + 一个 FP16 scale, 256 weights 占 66 字节 (vs FP32 1024 字节, 省 15×)。

说明: 三值的 2-bit 编码设计

每个 weight 需要 2 bit, 4 种编码:

2-bit 编码还原符号出现频率含义
00-1~25%负权
010~40-50%稀疏! 后续点积直接跳过
10+1~25%正权
11(保留)0%故意不用 — 只用 3 种编码,留 1 个未来扩展

为什么需要 FP16 scale? 如果只用 {-1, 0, +1} (绝对值都是 1), 模型表达能力太弱 (相当于 binary sign), 只能存"哪些连接重要/不重要"。加上 per-group FP16 scale, 让 {-1, 0, +1} 乘以一个动态范围, 就能表达"这个 group 里权重绝对值大" — 这是 LLM 0.1% outlier 通道的关键。

流程: Q2_0 反量化

flowchart TD A["GGUF tensor bytes
(block_q2_0 数组)"]:::a A --> B["每 66 字节 = 1 block
前 2B 是 d (FP16 scale)
后 64B 是 packed 2-bit"] B --> C["64B packed → 解包 256 个 weight
每 4 个 weight 占 1 byte, 各占 2 bit"] C --> D["每个 2-bit 编码查表:
00→-1, 01→0, 10→+1, 11→reserved"] D --> E["w_hat = q * d (FP32 乘法)
得到还原的 FP32 权重"] E --> F["FP32 矩阵
进入 gemm 算子"] classDef a fill:#fee2e2,stroke:#dc2626 classDef b fill:#fff3cd,stroke:#f59e0b classDef c fill:#fff3cd,stroke:#f59e0b classDef d fill:#dbeafe,stroke:#2563eb classDef e fill:#d1fae5,stroke:#10b981 classDef f fill:#d1fae5,stroke:#10b981

源码: dequantize_row_q2_0 (CPU 通用反量化路径)

代码位置: llama.cpp-prism/ggml/src/ggml-quants.c:439-457
void dequantize_row_q2_0(const block_q2_0 * x, float * y, int64_t k) {
    static const int qk = QK2_0;   // blck_size = 256
    const int nb = k / qk;          // k 个 weight 分成 nb 个 block

    for (int i = 0; i < nb; i++) {
        // 1. 读 FP16 scale, 转 FP32
        const float d = GGML_FP16_TO_FP32(x[i].d);

        // 2. 遍历 block 内 256 个 weight
        for (int j = 0; j < qk; ++j) {
            // 3. 计算这个 weight 在 packed 字节流里的位置
            //    每 4 个 weight 共享 1 byte (各占 2 bit)
            const int byte_index = j / 4;
            const int bit_offset = (j % 4) * 2;

            // 4. 提取 2-bit 编码
            const uint8_t q = (x[i].qs[byte_index] >> bit_offset) & 0x03;

            // 5. 查表 + 乘 scale (00=-1, 01=0, 10=+1, 11=+2)
            y[i*qk + j] = ((int)q - 1) * d;
        }
    }
}

解读:

对比 Q4_0 (传统 4-bit) 的反量化思路:
// Q4_0: 4-bit 编码, 范围 [-8, 7], 不存 min
y[i*qk + j] = ((int)(x[i].qs[j/2] >> ((j%2)*4) & 0x0F) - 8) * d;
// 区别: Q4_0 是 4-bit, 16 个值均匀分布; Q2_0 是 2-bit, 只有 3 个有效值
// Q2_0 编码更稀疏 (0 值多), 但 scale 范围更大

源码: CUDA 专用反量化 kernel

代码位置: llama.cpp-prism/ggml/src/ggml-cuda/convert.cu:717
// 模板实例化, 把 Q2_0 的 256 个 weight 反量化成 FP32
template <int QK, int QR, void (*dequantize_func)(const void*, const int64_t, const int, float2&)>
static __global__ void dequantize_block_cont_cuda(...);

// 在 Q2_0 的 case 里这样实例化:
return dequantize_block_cont_cuda<QK2_0, QR2_0, dequantize_q2_0>;

// 其中 dequantize_q2_0 在 dequantize.cuh:25
static __device__ __forceinline__ void dequantize_q2_0(
    const void * vx, const int64_t ib, const int iqs, float2 & v) {
    const block_q2_0 * x = (const block_q2_0 *) vx;
    const float d = x[ib].d;  // FP16 scale
    const uint32_t q = *(const uint32_t*)(x[ib].qs + iqs * 4);  // 一次读 4 个 int16 = 8 个 weight
    // ... 用位运算提取 4 个 2-bit 编码, 查表还原成 FP32
}

洞察 ①.1: 三值编码设计的关键是稀疏性

为什么选 {-1, 0, +1} 而不是 {-2, -1, 0, +1, +2}?
三值 (2.0625 bpw) 比四值 (2.0 bpw) bit 多, 但 多出来的 0 值天然稀疏 — 实测 LLM 权重 ~40% 是 0, 这些 0 在点积时直接跳过, 速度反而更快。
反直觉: bit 多一点 → 更快。

洞察 ①.2: GGUF 文件合法 ≠ 引擎能跑

GGUF 文件格式允许私有 type id, Bonsai Q2_0 的 GGUF 文件本身合法。但 推理引擎必须实现 dequantize + vec_dot 才能跑。所以下游引擎不实现 Q2_0 case, 就只能看不能跑 — "unsupported tensor type Q2_0"。

4.4 定制点 ②: 点积怎么算 (CPU / GPU 不同路径)

场景: 反量化完不是终点

dequantize_row_q2_0 把 256 个 weight 还原成 FP32, 但这只是 准备数据。真正的算子是 点积: Y = sum(w[i] * x[i]), 256 次乘加。如果用朴素 FP32 算, 跟没量化一样慢 (甚至更慢, 因为多了一次反量化)。

真正的优化: 跳过反量化, 直接算 packed × Q8_1 点积。

说明: vec_dot_q2_0_q8_1 的数学

输入:

公式:

dot = d_w * Σ_i (q_w[i] - 1) * q_x[i]
    = d_w * [ Σ_i q_w[i] * q_x[i] - Σ_i q_x[i] ]  

关键: Σ_i q_x[i] 是 activation 的总和, 可以预计算 (扫一次 X 即可)。剩下要算的就是 Σ_i q_w[i] * q_x[i]。

流程: CUDA Q2_0 vec_dot

flowchart TD A["1 个 warp 处理 32 个 vocab"]:::a A --> B["每次算 4 个 int16 (8 个 weight × 4 个激活)"] B --> C["__byte_perm 查表
2-bit → -1/0/+1 符号"] C --> D["dp4a 指令
4 个 int8 dot product 一次"] D --> E["sumi 累加 int32"] E --> F["d_w * sumi 转 FP32 输出"] classDef a fill:#dbeafe,stroke:#2563eb classDef b fill:#fff3cd,stroke:#f59e0b classDef c fill:#fff3cd,stroke:#f59e0b classDef d fill:#d1fae5,stroke:#10b981 classDef e fill:#d1fae5,stroke:#10b981 classDef f fill:#d1fae5,stroke:#10b981

源码: CUDA vec_dot_q2_0_q8_1

代码位置: llama.cpp-prism/ggml/src/ggml-cuda/vecdotq.cuh:740-774
// Device 内联函数, 一个 warp 处理 32 个 vocab 中的一个 chunk
static __device__ __forceinline__ float vec_dot_q2_0_q8_1(
    const void * vbq, const block_q8_1 * bq8_1, const int & kbx, const int & iqs) {

    const block_q2_0 * bq2_0 = (const block_q2_0 *) vbq + kbx;

    const float     d2 = bq2_0->d;  // FP16 scale
    const int16_t * qs = (const int16_t *) bq2_0->qs + iqs * 4;  // 每次取 4 个 int16 (8 个 weight × 4 个 chunk)
    const block_q8_1 * bq8_1_chunk = bq8_1 + iqs;

    int sumi = 0;
#pragma unroll
    for (int j = 0; j < 4; ++j) {
        const int q  = qs[j];   // 一个 int16 含 4 个 2-bit 编码
        const int u  = get_int_b4(bq8_1_chunk->qs, j*2+0);  // 4 个 int8 activation 之一
        const int v  = get_int_b4(bq8_1_chunk->qs, j*2+1);  // 4 个 int8 activation 之二

        // 关键魔法: 0x020100FF 字节序列 = {-1, 0, 1, 2}
        // 用 __byte_perm 一次 SIMD 把 4 个 2-bit 编码查表还原成 4 个字节
        const int qe = __byte_perm(0x020100FF, 0x020100FF, q >> 0);  // 低 2 位查表
        const int qo = __byte_perm(0x020100FF, 0x020100FF, q >> 2);  // 次低 2 位查表

        // 把 qe/qo 重新排列成 qx (低 4 个) / qy (高 4 个)
        const int qx = __byte_perm(qe, qo, 0x5140);
        const int qy = __byte_perm(qe, qo, 0x7362);

        // dp4a: 4 个 int8 dot product, 输出 int32, 一次算完
        sumi = ggml_cuda_dp4a(u, qx, sumi);  // dot product accumulate 4×4 int8
        sumi = ggml_cuda_dp4a(v, qy, sumi);
    }
    return d2 * __low2float(sumi);  // 标量乘 d, 转 FP32
}

解读 4 个关键点:

  1. 0x020100FF 字节序列 = {-1, 0, 1, 2}: 这是个查表, 字节索引 = 2-bit 编码, 值 = 还原符号。这样 不需要分支 就能把 2-bit 编码转成符号
  2. __byte_perm: CUDA SIMD 指令, 一次把 4 个字节按位重排, 等价于 4 个并行查表
  3. dp4a: int8 dot product accumulate, 4×4 = 16 个乘加一次 SIMD 算完
  4. #pragma unroll: 让编译器展开循环, 4 次 dp4a 全部并行

源码: CPU 通用路径 (无专用 Q2_0 vec_dot)

代码位置: llama.cpp-prism/ggml/src/ggml-cpu/ops.cpp:668
// 注意: Q1_0 / Q2_0 / Q4_0 / Q4_1 ... 共用同一 case 体
case GGML_TYPE_Q1_0:
case GGML_TYPE_Q2_0:
case GGML_TYPE_Q4_0:
    // 通用路径: 先 dequantize 成 FP32, 再用 FP32 gemm
    // Q2_0 没有 CPU 专用 vec_dot (不像 CUDA 那样优化到极致)
    {
        // ... dequantize_row_q2_0 + cblas_sgemm
    }
    break;

解读: CPU 端没有为 Q2_0 写专用 SIMD 内核, 而是 dequantize → FP32 gemm。这意味着 CPU 上跑 Q2_0 不会有 7.8× 加速, 只比 FP16 略快 (因为内存带宽省了)。
洞察: Bonsai 的 1.7 bit 加速主要是 GPU 上的, CPU 上优势有限。

4.5 定制点 ③: DSpark 投机解码

场景: 大模型生成慢在哪

27B 即使 1-bit 也要 ~6ms/步, 生成 1000 token ≈ 6 秒。这是 自回归 的代价 — 每生成一个 token 都要跑一次完整 forward。

DSpark 用一个 极小 drafter 一次性生成 K 个候选 token, 主模型 1 次 forward 验证整块, 接受 m 个 (m 接近 K 时加速 ≈ K/m 倍)。

说明: EAGLE-style block-diffusion drafter

流程: DSpark 投机解码时序

sequenceDiagram participant U as 用户 prompt participant T as Target
(Bonsai-27B) participant D as Drafter
(DSpark) participant M as Markov
(CUDA) participant V as Verify U->>T: 编码 prompt 1 次 T-->>D: 喂 hidden_state + embeds loop K 个 block D->>D: block-diffusion
填 K-1 mask D->>D: confidence_head
判断继续 end opt Markov 启用 D->>M: 重采样
(dspark-markov.cu) M-->>D: refined drafts end D->>T: 提交 K drafts T->>V: 整块验证 (lossless) V-->>U: 接受 m 个 token Note over V: speedup = K/m
m 越接近 K 越快

源码: DSpark Markov CUDA (头)

代码位置: llama.cpp-prism/common/dspark-markov.cu:1-32 (共 11706 字节)
// Device-side dspark vanilla Markov resample. Self-contained:
// depends only on the CUDA runtime, not on ggml/llama internals.

#include "dspark-markov.h"
#include <cuda_runtime.h>
#include <math_constants.h>

#define DSPARK_WARP            32     // 一个 warp 32 线程, 处理 32 个 vocab
#define DSPARK_WARPS_PER_BLOCK 8      // 一个 block 8 个 warp = 256 线程
#define DSPARK_BLK_THREADS     (DSPARK_WARP * DSPARK_WARPS_PER_BLOCK)  // 256
#define DSPARK_NBLOCKS         1024   // grid-stride partial-argmax 网格宽

// Fused GEMV + add-base + per-block partial argmax for one position k.
// 把 matrix-vector 乘 + 加 base logits + 找 argmax 三步融合成一个 CUDA kernel
struct dspark_markov_cuda {
    int64_t n_vocab = 0;  // vocab 大小 (典型 152064)
    int64_t rank    = 0;  // markov_rank (典型 64)
    float * d_w1 = nullptr;  // [n_vocab * rank]  GPU 权重 1
    float * d_w2 = nullptr;  // [n_vocab * rank]  GPU 权重 2
    float * d_base = nullptr; // [base_cap_rows * n_vocab]  base logits 缓存
    int32_t * d_out = nullptr; // [base_cap_rows]  输出 token id
    int32_t * d_prev = nullptr; // 链式 prev token (1 int)
    cudaStream_t stream = nullptr;
};

解读:

4.6 定制点 ④: 端到端低比特 (no escape hatches)

场景: 传统 PTQ 的"逃生通道"

大多数 PTQ 方案 (GPTQ/AWQ/SmoothQuant) 都会保留 部分层在 FP16:

结果: Q4_K 看起来 18 GB, 实际能砍到 ~12 GB, 但还有 ~6 GB 浪费在 FP16 部分。

说明: Bonsai 的激进做法

Bonsai 公告: "low-bit representation runs end to end across the language network, embeddings, attention, MLPs, and the LM head, with no higher-precision escape hatches"。
翻译: embedding/LM head 也量化, 一个 FP16 都不留。

为什么能这么做

4.7 5 个核心洞察汇总

洞察 1: 标准 PTQ 做不出 1.7 bits 95% 精度
必须 QAT + 蒸馏 + per-group scale 三件套同时使用, 缺一不可。单独用任一个都会让 1-bit/ternary 精度崩塌到 50% 以下。
洞察 2: per-group FP16 scale 是 outlier 容忍度的关键
LLM 权重有 0.1%~1% 的 outlier 通道, 绝对值大 10~100×。per-tensor scale 被 outlier 拉高 → 其他 99.9% 权重精度只剩 0.05%。per-group scale 让每 128~256 个权重自己找 max, 精度提升 100×。
洞察 3: ternary > binary 不是反直觉, 是数学
ternary 多 51% 字节 (5.9 vs 3.9 GB), 换 5% 精度 (95 vs 90%), 单位精度字节成本反而低。对端到端部署: 笔记本选 ternary, 入门手机选 1-bit。
洞察 4: intelligence density 是新指标 (不是 bpw)
能力/GB 比 bpw 更能反映"塞进设备的能力"。1-bit Bonsai 0.53/GB, 是 FP16 10×, 是 Q4_K 的 4.5×。这是 Bonsai 引入的新一代评估指标。
洞察 5: DSpark + 1-bit = 端到端手机部署方案
87 tok/s (1-bit) + DSpark 2× ≈ 174 tok/s effective。DSpark + 1-bit 是 让 27B 模型能流畅对话的组合方案, iPhone 17 Pro 内存预算 (6GB) 刚好能装下。

4.8 量化加速原理全栈 (Bonsai 加速来源)

为什么 1-bit 模型推理比 FP16 快 5~10×? 加速来自 3 个独立机制:

机制 ① 访存带宽 — LLM 推理的真正瓶颈

flowchart TD A["Linear
Y = X @ W.T"]:::a subgraph MEM["memory-bound"] M1["read X 2B"]:::mem M2["read W 52MB
FP16 5120x5120"]:::mem M3["write Y"]:::mem end subgraph COMP["compute idle"] C1["100 TFLOPS
RTX 5090"]:::comp C2["util only 5%"]:::comp end A --> MEM A --> COMP MEM -. bottleneck .-> C2 classDef a fill:#dbeafe,stroke:#2563eb classDef mem fill:#fee2e2,stroke:#dc2626 classDef comp fill:#d1fae5,stroke:#10b981

具体例子: [seq=512, hidden=5120] @ [5120, 5120] 这一个 Linear 层:

量化如何打破 memory wall:

位宽权重大小读取时间 (RTX 5090)加速比
F1652 MB29 μs1×
Q4_K~15 MB8.3 μs3.5×
Q2_0~6.6 MB3.7 μs7.8×
Q1_0~3.6 MB2.0 μs14.5×

机制 ② SIMD / 位运算加速 (1-bit 专属)

flowchart TD A["FP16 matvec
读 a[i] FP16 + 读 w[j] FP16
FMA 乘加"]:::fp B["1-bit matvec
读 bit-mask + popcnt
整数加减"]:::bit A -.vs.- B classDef fp fill:#fee2e2,stroke:#dc2626 classDef bit fill:#d1fae5,stroke:#10b981

关键: 1-bit matvec 没有浮点乘法。现代 CPU/GPU 提供:

机制 ③ Sparsity 跳过 (Q2_0 专属)

flowchart TD A["Q2_0 点积
code ∈ {0,1,2}"]:::a A --> B["code=2: sum_pos += y"]:::b A --> C["code=0: sum_neg += y"]:::c A --> D["code=1: 0 值
完全跳过"]:::d classDef a fill:#dbeafe,stroke:#2563eb classDef b fill:#d1fae5,stroke:#10b981 classDef c fill:#fee2e2,stroke:#dc2626 classDef d fill:#fff3cd,stroke:#f59e0b

LLM 权重 ~30~50% ≈ 0, 阈值化后形成 2:4 结构化稀疏, GPU 可以 2× speedup。Q2_0 反量化点积: 0 值跳过乘加, 等价 ALU workload -40%。

4.9 1.7 bits 为何能保留 95% 能力

信息密度 vs 比特密度
原始 FP16 权重里其实只有 ~1.5 bits 有效信息, 其余 14.5 bits 都在存 "这个权重应该是 0.001234" 这种细节。量化 = 丢冗余, 留信号。对神经网络这种 "统计模式匹配器", 大体结构就够了。
度量F16Ternary差异
比特密度 (bpw)161.719.4× 更少
信息密度 (有效 bits/weight)~10~1.56.7× 更少
模型能力100%95%仅损失 5%
智能密度 (cap/GB)~0.05~0.163.2× 更高

4.10 Bonsai 5 层堆叠 (汇总)

层技术贡献
1. 物理层1.125 bpw 内存占用13× 带宽压缩
2. SIMD 层popcnt + 位运算5× ALU 加速
3. Sparsity 层Ternary 30~50% 零1.5× 加速 (仅 ternary)
4. Cache 层小权重进 L2/L3命中率↑
5. 调度层DSpark 投机解码2~3× 加速 (lossless)

5 层叠加, 才有 Bonsai 1-bit 27B 在 RTX 5090 上 163 tok/s 的成绩 (相对 FP16 27B ~10 tok/s 加速 ~16×)。

4.11 Bonsai vs 业界其他方案 (横向对比)

方案何时量化核心机制典型 bpw精度代表规模
GPTQPTQ二阶信息 + 列分组3~4<5% 损失LLaMA / Qwen
AWQPTQ激活感知, 保护 outlier3~4<3% 损失LLaMA / Qwen
SmoothQuantPTQ激活难度 → 权重迁移8 (W8A8)<1% 损失大模型通用
BitNetQAT1.58 bit 三元, STE1.5885~90%2B / 7B
BonsaiQAT + 蒸馏1.125/1.71 bit + per-group FP16 + 蒸馏 + DSpark1.125 / 1.7190~95%1.7B / 4B / 8B / 27B

4.12 7 场景推荐

场景推荐方案理由
已有 FP16 LLM, 快速部署GPTQ / AWQ Q4_K_MPTQ, 几小时搞定
服务器端推理, 精度优先Q8_0 / SmoothQuant W8A8不掉点
笔记本推理, 平衡Q4_K_M / Q5_K_M速度/精度 sweet spot
边缘设备 (树莓派等)Q2_K / IQ2 系列3~4 bpw, 内存紧
手机Bonsai 1-bit / BitNet唯一能塞下 27B 的方案
研究者训练小模型BitNet b1.58从头训练, 论文友好
研究者训练大模型Bonsai蒸馏 + per-group, 大模型稳

4.13 数据来源汇总

数据/观点来源
Bonsai 1.71 / 1.125 bpw, 5.9 / 3.9 GB, 95% / 90% 精度prismml.com/news/bonsai-27b (2026-07-14)
163 / 87 / 134 / 58 tok/s 速度数据prismml.com/news/bonsai-27b
"intelligence density 0.53 per GB"prismml.com/news/bonsai-27b
"no higher-precision escape hatches"prismml.com/news/bonsai-27b
Q1_0 group=128, Q2_0 编号 42, DSpark MODEL_ARCHllama.cpp-prism/ggml/include/ggml.h:431-433 + conversion/dspark.py:37
GGUF TYPE_COUNT 42 vs 43 (main vs prism)llama.cpp-master/ggml/include/ggml.h:432 vs llama.cpp-prism/...:433
"per-tensor scale 100× 精度差"业内共识: LLM 激活存在 outlier 通道 (SmoothQuant 论文 + Dettmers 2022 LLM.int8() 论文)
"LLM 权重 ~30~50% ≈ 0 (ternary 阈值化)"Han et al. "Learning to Weight Pruning" 2022 + BitNet 论文 (Ma 2024) 实测
"1-bit 矩阵乘比 FP16 快 5~10×"BitNet 论文 benchmark + llama.cpp 实测 (CPU 推理)
"BitNet 1.58 bit, 85~90% 精度"BitNet 论文 (Ma et al. 2024)
"GPTQ / AWQ / SmoothQuant PTQ 流派"Frantar 2022, Lin 2023, Xiao 2023 论文
"iPhone 17 Pro 6GB app 预算"Apple iPhone 17 Pro 规格
"RTX 5090 1.8 TB/s 带宽, 100 TFLOPS FP16"NVIDIA RTX 5090 官方规格 (白皮书)
"M5 Max ~400 GB/s 带宽, ~10 TFLOPS FP16"Apple M5 Max 官方规格
"iPhone 17 Pro ~50 GB/s 内存带宽, ~2 TFLOPS"Apple iPhone 17 Pro 官方规格
"DSpark speedup = K/m (lossless)"Li et al. EAGLE 论文 (2024) + Bonsai 公告实测
"llama.cpp mmap 按需加载"src/llama-mmap.cpp / src/llama-model-loader.cpp + ggml/src/ggml.c mmap 实现
"vLLM 不 mmap, 全量读显存"vllm/model_executor/weight_utils.py load_weights()
"PTQ 训练成本 5×"Gholami et al. "Survey of Quantization Methods" 2022 + Bonsai 训练时实测
"Q1_0 dot product = d * (2*Σ(bit*a) - Σ a)"ggml/src/ggml-metal/ggml-metal.metal:3261 源码注释
"0x020100FF = {-1, 0, 1, 2}" 查表ggml-cuda/vecdotq.cuh:763 注释
"DSpark NBLOCKS=1024, BLK_THREADS=256"common/dspark-markov.cu:13-16
"Q2_0 block_q2_0 = 2B d + 64B packed = 66B/256 w"llama.cpp-prism/ggml/src/ggml-quants.c:439 源码
"CPU 端 Q2_0 共用 Q1_0/Q4_0 通用路径, 无专用 SIMD"llama.cpp-prism/ggml/src/ggml-cpu/ops.cpp:667-680
华杉的技术博客 · 苏ICP备2026063680号-1
首页