PicoLM量化反量化完全解析:Q4_K 4位权重如何恢复成FP32
【免费下载链接】picolmRun a 1-billion parameter LLM on a $10 board with 256MB RAM项目地址: https://gitcode.com/gh_mirrors/pi/picolm
PicoLM 是一个用纯 C 编写的极简 LLM 推理引擎,能在 $10 开发板(256MB RAM)上运行 10 亿参数大模型。它的核心秘密之一就是Q4_K 量化:把 4.4GB 的 FP32 权重压缩到 638MB,推理时再把 4 位整数反量化还原成浮点数参与计算。本文带你完整拆解这条"压缩 → 解压"流水线,看看每 144 字节里藏着的 256 个权重是如何一步步恢复成 FP32 的。
一、为什么需要量化:4.4GB 如何变成 638MB
TinyLlama 1.1B 有 11 亿个参数。如果每个参数用 FP32(4 字节)存储,光权重就要4.4GB——远超一块 $10 板子的全部内存。量化就是答案:用更少的位来"近似"每个权重。
PicoLM 支持的量化格式定义在 quant.h 中,各格式每 256 个权重的字节数对比(来自 gguf_type_quant_size):
| 格式 | 256 个权重占用 | 每权重位数 | 特点 |
|---|---|---|---|
Q2_K | 84 字节 | ~2.6 bit | 最小,质量损失较大 |
Q3_K | 110 字节 | ~3.4 bit | 偏小 |
Q4_K | 144 字节 | ~4.5 bit | 质量/体积最佳平衡(推荐) |
Q6_K | 210 字节 | ~6.5 bit | 高质量 |
Q8_0 | 32×34 字节 | 8 bit | 接近无损 |
可以看到,Q4_K 下每个权重平均只用144/256 ≈ 0.56 字节,不到 FP32 的 1/7。模型文件从 4.4GB 缩到 638MB,就能放在 SD 卡里、通过mmap按层流式读入内存。
二、Q4_K 块结构:256 个权重如何塞进 144 字节
Q4_K 的基本单位是一个"块"(block),每个块打包256 个 4 位量化值。它的内存布局定义在 quant.h 的 block_q4_K 结构体:
| 字段 | 大小 | 含义 |
|---|---|---|
d | 2 字节(FP16) | 超级块缩放因子(super-block scale) |
dmin | 2 字节(FP16) | 超级块最小值因子(super-block min) |
scales[12] | 12 字节 | 8 个子块的6 位缩放因子 + 6 位最小值(位级打包) |
qs[128] | 128 字节 | 256 个 4 位量化值(nibble,两两打包进 1 字节) |
设计思想是"两级缩放":
256 个权重(一个 Q4_K 块, 144 字节) ┌────────────────────────────────────────────────────┐ │ d(FP16) dmin(FP16) ← 超级块全局缩放/最小值 │ ├────────────────────────────────────────────────────┤ │ scales[12] ← 8 个子块 × (6bit scale + 6bit min) │ ├────────────────────────────────────────────────────┤ │ 子块0(32个) 子块1(32个) … 子块7(32个) │ │ 每个子块内部再分 2 组 × 16 个 4 位 nibble │ └────────────────────────────────────────────────────┘为什么是两级?因为 256 个权重的数值范围各不相同,只用一个全局缩放因子会损失精度。Q4_K 把它们切成 8 个子块(每 32 个一组),每个子块再各带 1 个 6 位缩放因子sc和 1 个 6 位最小值mn——共 16 个 6 位参数,巧妙挤进 12 个字节(8×12=96 bit = 12 B,刚好不浪费)。
三、反量化公式:4 位数字如何变回浮点数
反量化的核心公式极其简洁(见 dequantize_row_q4_K):
y = (d × sc) × q − (dmin × mn)
其中:
d、dmin:块头两个 FP16 因子,先经 fp16_to_fp32 软件位运算转成 FP32(无需硬件 FP16 支持);sc、mn:该 32 权重子块的 6 位缩放/最小值;q:0~15 的 4 位无符号整数,从qs字节中按低 4 位 / 高 4 位拆开(q[l] & 0xF与q[l] >> 4)。
也就是说,每个权重被近似成"缩放 × 4位整数 − 偏移"。4 位整数只有 16 档精度,但两级缩放让它能紧贴每个子块的真实数值范围,因此精度损失很小。
一个容易踩坑的细节:8 个 6 位参数无法直接存进字节对齐的位置,get_scale_min_k4 用位运算从scales[12]里"抠"出它们——前 4 个子块直接取 6 位(q[j] & 63),后 4 个子块的高 2 位被借存在前面字节的最高 2 位里(q[j-4] >> 6)。这种"位级借位"是 K-Quant 格式紧凑的关键。
四、融合点积:反量化与乘法一次完成 ⚡
既然推理只需要"权重 × 输入向量"的点积,何必先把整行权重解压进内存再读出来?PicoLM 的 vec_dot_q4_K_f32 采用融合(fused)策略:
- 按子块累加两个中间量:
Σ q·x(量化值与输入的加权和)与Σ x(输入分段和); - 最后一步才乘上
d·sc和dmin·mn合并出结果。
数学上等价,但反量化后的 FP32 权重从不落地内存——直接省掉一半访存量。在 ARM 平台上,该函数还有 NEON 向量化版本(一次处理 8 个字节:vmovl_u8 → vmovl_u16 → vcvtq_f32_u32),x86 则有 SSE2 版本,构建时自动检测(quant.h 的 SIMD 宏)。
这条融合点积正是矩阵乘法matmul的热路径:tensor.c 的 matmul 把输出行分给多个线程,每行调用一次vec_dot,在 4 核 Pi 上可近似线性加速。
五、在推理流水线中的位置
反量化并不只服务于矩阵乘法,PicoLM 中有两类调用点:
| 场景 | 入口 | 说明 |
|---|---|---|
| 矩阵乘法(Q/K/V/FFN 投影) | tensor.c 的 matmul →vec_dot | 走融合路径,不产生临时 FP32 缓冲 |
| 嵌入查表 / Norm 权重 | model.c 中 dequantize_row | 启动时预解压 RMSNorm 权重;每个 token 从token_embd解压一行嵌入 |
统一的分发逻辑在 dequantize_row:按 GGUF 头里声明的类型(GGUF_TYPE_Q4_K等)switch 到对应内核,所以换个 Q2_K/Q6_K 的模型文件无需改任何代码。
六、总结:一张表看懂 Q4_K 反量化
| 步骤 | 做什么 | 对应源码 |
|---|---|---|
| ① 读块头 | FP16 的d、dmin软转 FP32 | fp16_to_fp32 |
| ② 拆 6 位参数 | 从scales[12]位运算解出 8 组 sc/mn | get_scale_min_k4 |
| ③ 拆 nibble | 每字节拆成 2 个 4 位量化值 | dequantize_row_q4_K |
| ④ 还原权重 | y = d·sc·q − dmin·mn | 同上 |
| ⑤ 融合计算 | 边拆边乘,不落地 FP32 | vec_dot_q4_K_f32 |
核心要点:Q4_K 用"两级缩放 + 6 位打包参数 + 4 位量化值"三板斧,把 4 字节的 FP32 压进平均 0.56 字节;反量化时只需几次数乘和一次位拆分,就能在 $10 板子上实时恢复出参与矩阵运算的浮点权重——这正是 PicoLM 以 45MB 运行内存驱动 1B 模型的底层功臣。
【免费下载链接】picolmRun a 1-billion parameter LLM on a $10 board with 256MB RAM项目地址: https://gitcode.com/gh_mirrors/pi/picolm
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考