🦙 llama.cpp 图解教程llama.cpp Visual Guide 第三部分 · ggml 引擎Part 3 · The ggml engine 12 / 40
第三部分 · ggml 引擎Part 3 · The ggml engine

量化格式细节Quantization formats in detail

课 06 我们建立了量化的直觉——"每一小块权重共享一个 scale,块内只存相对差"。这一课我们钻进字节级:打开 ggml/src/ggml-common.h, 看 block_q4_0block_q4_K 这些块在内存里到底每个字节装了什么,再看解量化函数怎么把这堆字节还原成浮点, 最后看 ggml_type_traits 怎么把几十种量化类型统一接进引擎

L06 回答的是"为什么能压、压完省多少",这一课回答的是"压完在内存里长什么样、源码怎么把它读回来"—— 两课正好是同一件事的"直觉面"与"实现面"。如果 L06 的块量化直觉你还有点模糊,建议先回头扫一眼;这一课会直接落到结构体的每一个字段上。

🔌 生活类比
一个量化 block 很像一个压缩包:开头几个字节是"解压参数"(scale、min),告诉你怎么把后面的数据放大还原;后面一长串是"压缩数据"(一个个挤在一起的低位量化值)。 解量化,就是照着开头的参数,把后面每个小整数乘回它该有的大小。不同的量化格式,无非是"解压参数怎么记、数据怎么打包"的不同约定——读懂了字节布局,你就读懂了一种格式。

打开一个 block:q4_0 与 q8_0

先看最经典的两个块。q4_032 个权重打成一块:开头 2 字节是一个 half(FP16 精度)的 scale,紧跟着 16 字节装下 32 个 4-bit 量化值 (每字节塞两个 nibble)。算下来一块 18 字节q8_0 同样 32 个权重一块,但每个权重用一整字节的 int8 存、不打包——一块 34 字节 (2 字节 scale + 32 字节量化值),更准也更大。

q4_0 / q8_0 字节布局:开头是 scale,后面是量化值;q4_0 把 4-bit 打包,q8_0 用 int8 不打包
q4_0d : 2Bqs : 16B(32 个 4-bit)= 18 B / 32 权重
q8_0d : 2Bqs : 32B(32 个 int8)= 34 B / 32 权重

这些 block 在文件和内存里是一块紧挨着一块、连续排放的:一个权重矩阵就是一长串 block。知道了"一块多少字节",就能算出"第 n 块在哪"——这正是后面解量化时能随机定位、并行处理的基础。 布局规整带来的好处,远不止省空间——它让"按需取一小块来解量化"成为可能,这也是后面"边算边解"能做到的前提。

对应到源码,结构体几乎和上图一一对应(ggml/src/ggml-common.h):

// 简化自 ggml/src/ggml-common.h
#define QK4_0 32
typedef struct { ggml_half d; uint8_t qs[QK4_0/2]; } block_q4_0;  // 2 + 16 = 18 B
typedef struct { ggml_half d; int8_t  qs[QK8_0];   } block_q8_0;  // 2 + 32 = 34 B

逐个字段看:d 是那个块共享的 scale,ggml_half 就是 2 字节的半精度浮点(L06 说的"基准");qs 是量化值数组。 q4_0 的 qs[QK4_0/2]=qs[16]——32 个值只占 16 字节,因为每个值才 4 bit,两个挤进一个字节(一个放高 nibble、一个放低 nibble)。 q8_0 的 qs[QK8_0]=qs[32]——32 个值占满 32 字节,一个值独占一字节,不用拆位。

🔬 细节 / 源码对应
有人会问:那个 scale 为什么用 2 字节的 half,而不是更精确的 4 字节 float?因为 scale 每块才一个,它的精度对最终结果影响有限,却要乘进整块的体积里——用 half 省下的这 2 字节,摊到 32 个权重上虽小,乘以一个模型里成千上万个块,省下来的就很可观了。这是一个典型的工程取舍:在"几乎不影响精度"的地方能省则省,把字节预算留给真正重要的量化值。

为什么 q8_0 更准也更大?因为 4-bit 只能表示 16 个档位、8-bit 能表示 256 个档位,同一个权重,8-bit 能贴得更近,量化误差更小。代价是体积翻倍:每权重从 0.5 字节涨到 1 字节。 这正是 L06 那张"显存对照表"背后的字节级真相——你在命令行里选 Q4_0 还是 Q8_0,本质就是在这两种 block 布局之间二选一。

🌍 宏观理解
这里值得停下来想一个问题:为什么要分块,不干脆整个权重矩阵共享一个 scale?因为一个几千乘几千的大矩阵里,权重的大小范围差异很大——某些行很大、某些行很小。若全矩阵共用一个 scale,就得迁就那个最大值,小权重会被压得几乎只剩 0、精度全丢。分成 32 个一块后,每块各自找自己的 scale,大块用大 scale、小块用小 scale,量化误差立刻小一大截。这就是"块量化"四个字的全部用意:用"每块一个 scale"的少量开销,换回"局部精度"的大幅提升。

还有个常被忽略的细节:q4_0 一块 18 字节、装 32 个权重,平均每权重 4.5 bit,而不是正好 4 bit。多出来的 0.5 bit,就是那 2 字节 scale 摊到 32 个权重头上的开销。 块越大,这份"管理开销"摊得越薄——这也为下一节 K-quant 用 256 的大超块埋下了伏笔。

super-block:K-quant 的两层 scale

q4_0 每 32 个权重配一个 scale,已经不错了,但还能更准。K-quant(带 K 的格式,如 q4_Kq6_K)的思路是:用一个 256 个权重的"超块", 里面再切成若干个小子块,做成两层 scale——超块给一个"整体基准",每个子块再各自记一个"细调 scale"。这样既摊薄了基准开销,又保住了局部精度。

q4_K 为例:256 个权重切成 8 个子块(每子块 32 个权重)。超块层有 ddmin 两个 half;子块层有 8 组 6-bit 的 scale/min, 打包进 12 字节的 scales[];再加 128 字节装 256 个 4-bit 量化值。

超块d, dmin(各 1 个 half)
整个 256 权重共享的"整体 scale / 整体 min"
子块scales[12]:8 组 6-bit scale/min
每 32 个权重一组,各自细调,比单层 scale 贴得更准
数据qs[128]:256 个 4-bit 量化值
真正的权重量化值,打包存放

那 12 字节的 scales[] 是怎么来的?8 个子块、每块要存一个 scale 和一个 min,本可以各用一字节、共 16 字节;但 K-quant 把它们再压成 6-bit, 8×(6+6)=96 bit=12 字节,又省下 4 字节。连"管理用的 scale"本身都要量化——这种把每一处冗余都榨干的抠门劲,正是量化格式设计的日常。

两层 scale 是 K-quant 的灵魂。第一层(超块的 d/dmin)定个大致范围;第二层(子块的 6-bit scale/min)在这个范围里做局部微调—— 哪一小段权重偏大偏小,子块 scale 立刻跟上。这就是 L06 spark 说的"同样的 bit 数,K-quant 往往更准"的字节级来源:精度不是靠多花 bit,而是靠更聪明地分配 scale

// 简化自 ggml/src/ggml-common.h(QK_K = 256, K_SCALE_SIZE = 12)
typedef struct {
    ggml_half d;                   // 超块整体 scale
    ggml_half dmin;                // 超块整体 min
    uint8_t   scales[K_SCALE_SIZE];// 8 个子块的 6-bit scale/min(无 qh)
    uint8_t   qs[QK_K/2];          // 256 个 4-bit 量化值
} block_q4_K;

把 q4_K 的字节数也算一下:2(d)+ 2(dmin)+ 12(scales)+ 128(qs)= 144 字节装 256 个权重,平均每权重 144×8/256 = 4.5 bit——和 q4_0 一样的位宽,精度却更高。 这就是"两层 scale"最直接的回报:没多花一个 bit,纯靠结构更精巧把误差压了下去。

⚠ 注意
注意 q4_K没有 qh——只有 d、dmin、scales、qs 四个字段,这点很容易记错(见深挖 2)。而 q6_K有 qh:6-bit 的量化值被拆成"低 4 位"放 ql、"高 2 位"放 qh,再配 16 个 8-bit 的子块 scale 和一个超块 d。不同 K-quant 的字段并不统一,别想当然套用

顺带认识一下整个 K-quant 家族:从 q2_Kq3_K 一直到 q6_K,名字里的数字是大致的位宽,K 则代表都用"超块 + 两层 scale"这套结构。 位宽越低(如 q2_K)压得越狠、精度越险,往往只敢用在不那么敏感的层上;位宽越高(如 q6_K)越接近原始精度。这也是为什么实际下载模型时,你会看到 Q4_K_MQ5_K_S 这种带后缀的名字—— 它们是把不同层用不同 K-quant 档位混搭出来的方案,在体积和质量之间取不同的平衡点。

🔬 细节 / 源码对应
多说一句那个 dmin。q4_0 只有一个 scale,量化是对称的(围绕 0);而 q4_K 多了一个 min,做的是带偏移的量化——还原公式更像 x = scale · q + min(源码注释写的就是 "weight is represented as x = a·q + b")。为什么要这个偏移?因为很多权重的分布并不以 0 为中心,有了 min 就能让量化区间整体平移去贴合真实分布,进一步压低误差。这是 K-quant 比 q4_0 更准的另一半原因:不只 scale 更细,连"零点"都能调。

为什么超块要选 256 这么大?因为超块越大,"整体 d/dmin"这份固定开销摊到的权重越多、每权重的开销越小;而局部精度由更细的子块 scale 兜底,不会因为块大而变糊。 大块负责"高压缩"、子块负责"高精度",两者兼得——这就是 K-quant 在相同位宽下能比 q4_0 更准的设计动机(详见深挖 1)。

解量化:把字节还原成浮点

权重以量化字节存着,可真要参与计算(比如喂进 L11 的 mul_mat),得先还原成浮点。这一步叫解量化(dequantize),由每种格式各自的 dequantize_row_* 函数负责 (在 ggml/src/ggml-quants.c)。以 q4_0 为例,逻辑出奇地简单:

// 伪代码, 对应 ggml/src/ggml-quants.c 的 dequantize_row_q4_0
for each block:
    d = half_to_float(block.d)          // 取回这块的 scale
    for j in 0..15:                     // 每字节两个 nibble
        q0 = (block.qs[j] & 0x0F) - 8   // 低 4 位, 减 8 回到有符号
        q1 = (block.qs[j] >> 4)   - 8   // 高 4 位, 减 8 回到有符号
        y[j]      = q0 * d              // x = (q - 8) * d
        y[j + 16] = q1 * d
qs 取一个 nibble
q = 0..15
->
q - 8
平移成 -8..7
(有符号)
->
× d
乘这块的 scale
->
float x
x = (q - 8) · d

拿一个真实的块走一遍,上面这条链路就具体了:

追踪一次解量化:一个 q4_0 块的几个字节怎么还原成浮点(d=0.05 为示意)。
① 字节 qs[j]
0x960xC3
两个量化字节
& 0x0F
>>4
② 拆 nibble
69312
每字节拆出两个 4-bit 码
q - 8
③ 减 8
-2+1-5+4
平移成有符号 -8..7
× d
d=0.05
④ × d
-.10+.05-.25+.20
还原出近似浮点

核心就一行:x = (q - 8) * dq 是那个 0..15 的 4-bit 量化值,减 8 把它平移成 -8..7 的有符号数,再乘上这块的 scale d, 就还原出近似的原始浮点。q8_0 更直接:x = q * d,因为 int8 本身就是有符号的,不用减偏移。

🔬 细节 / 源码对应
那个"减 8"也值得多想一层。4-bit 能存 0..15 共 16 个数,但权重有正有负,所以约定让 8 代表"0"、比 8 小的是负、比 8 大的是正——减去 8,就把 0..15 平移成 -8..7 这个大致对称的区间。这样正负权重都能表示,且 0 附近分布得最密(量化最准),恰好契合"大多数权重都集中在 0 附近"的事实。一个小小的减法,背后是对权重分布的理解。

反过来,量化(把浮点压成字节)就是解量化的逆运算。q4_0 的参考实现 quantize_row_q4_0_ref 里,scale 取 d = max / -8——找出这块绝对值最大的权重, 让它对应到量化区间的端点(-8),其余权重按比例缩放取整。除以 -8 而不是 8 是个容易看走眼的细节:它和解量化时的"减 8"配套,保证一来一回数值对得上。

🌍 宏观理解
顺势说清一件事:解量化是有损的——还原出来的浮点和原始权重并不完全相等,差的那一点就是量化误差。但为什么模型还能照常工作?因为深度网络对权重的微小扰动相当宽容:单个权重差一点点,经过成百上千次累加和非线性,整体输出几乎察觉不到。量化格式的全部艺术,就是在"压得更狠"和"误差还能被模型容忍"之间找平衡——这也是为什么会有 q4_0、q4_K、q6_K 这么多档位,让你按对精度的需求挑一个合适的折中点。

还有一点值得记住:解量化不一定提前一次性把整个权重张量铺开成浮点(那会占很多内存)。在矩阵乘这种热点里,ggml 常常是边算边解——在内层循环里即时把用到的那一小块还原成浮点、立刻参与点积, 算完就丢。所以量化省的不只是显存,连"解压后的浮点"也大多不落地,带宽和缓存都跟着受益(呼应 L06 说的"省显存又提速")。

接进引擎:ggml_type_traits

问题来了:q4_0、q8_0、q4_K、q6_K…… 几十种量化格式,字节布局各不相同,难道每种都要在 mul_mat、解量化里写一遍特判?当然不。ggml 用一张 类型特征表ggml_type_traits) 把每种类型的"基本参数 + 怎么转 float"登记成一行,算子只管查表,不关心具体是哪种量化。

tensor.type
= GGML_TYPE_Q4_K
->
type_traits[Q4_K]
blck_size / type_size
to_float = dequantize_row_q4_K
->
算子
按 traits 解量化后计算

张量只在 type 字段记着"我是 Q4_K"(L05 的 type 字段,到这里终于派上大用场)。算子要用它时,拿这个 type 去查 type_traits 表,就能拿到"这种类型一块多少元素、多少字节、用哪个函数转 float", 照着做即可——算子代码里没有一个 if 在判断量化类型

// 简化自 ggml/include/ggml.h(每种 type 登记一行)
struct ggml_type_traits {
    const char * type_name;     // "q4_K"
    int64_t       blck_size;     // 一块多少元素, 如 256
    size_t        type_size;     // 一块多少字节, 如 144
    bool          is_quantized;
    ggml_to_float_t   to_float;       // 解量化函数指针
    ggml_from_float_t from_float_ref; // 量化函数指针(参考实现)
};

关键就在那两个函数指针to_float 指向这种类型的解量化函数,from_float_ref 指向量化(参考)函数。算子拿到张量,按 traits 调 to_float 解量化、再计算; 要存盘时按 from_float_ref 量化。注意字段名是 from_float_ref(带 _ref,参考实现),别记成 from_float。于是加一种新量化格式,主要工作就是填一行 traits + 写好这两个函数, mul_mat、解量化那些算子代码基本不用动——这正是 L05 "结构不变、类型可换"在量化上的兑现。

退一步看,这一课其实把"量化"这件事讲全了三个层次:布局(block 里每个字节装什么)、还原(解量化怎么把字节变回浮点)、接入(traits 表怎么让算子统一处理)。 这三层正好对应你用 llama.cpp 时会碰到的三种场景:挑量化档位(布局决定体积与精度)、跑推理(解量化在背后默默进行)、以及读懂源码(traits 是所有量化类型的总入口)。 把这三层串起来,你就不再把 Q4_K_M 这种名字当成黑盒,而能说清它在内存里到底是什么、算的时候发生了什么。把一个量化名字拆解到字节这一层,你对"模型怎么落到磁盘和内存里"的理解,就又扎实了一截。

1 为什么 q4_K 用 256 的超块,而不是像 q4_0 那样 32? 点击展开

核心是摊薄固定开销 + 不牺牲局部精度。q4_0 每 32 个权重就得花 2 字节存一个 scale;超块做到 256,"整体 d/dmin"这份固定开销摊到的权重多了 8 倍,每权重的管理开销明显下降。

但只把块放大、还用单层 scale 的话,局部精度会变差——256 个权重共享一个 scale,太粗了。K-quant 的解法是再加一层子块 scale:超块定大范围、子块定细节,于是"块大带来的高压缩"和"子块带来的高精度"同时拿到。

这也解释了为什么不无脑把超块做到更大(比如 1024):子块 scale 本身也要占空间,块太大、子块太多,第二层开销又上来了。256 + 8 子块是工程上压缩率与精度的一个甜点,经过大量实测调出来的。

2 q4_K 有 qh 吗?q6_K 呢? 点击展开

q4_K 没有 qh。它的字段只有四个:ddminscales[12]qs[128]。4-bit 量化值正好一个 nibble,直接打包进 qs,不需要再拆高低位。

q6_K有 qh。6-bit 一个值放不进一个 nibble,于是拆开:低 4 位存进 ql、高 2 位存进 qh,再加 16 个 8-bit 的子块 scale 和一个超块 d。 所以 q6_K 的结构和 q4_K 长得很不一样。

这是初学者最容易栽的坑之一:以为所有 K-quant 字段都一样、把 q4_K 想象成"带 qh"。读这些结构体时一定对着源码逐字段核对,别凭格式名想当然——位宽不同、打包方式就不同,字段自然不同。

3 算子怎么做到不为每种量化各写一遍? 点击展开

type_traits 里的 to_float / from_float_ref 这两个函数指针。算子拿到一个量化张量,不去判断"这是 q4_0 还是 q4_K",而是直接调它 traits 里登记的 to_float 解量化,再算。

在 mul_mat 这种热点里更讲究:往往不是先整块解量化、再乘,而是在内层循环里即时解一小段、立刻点积,甚至为某些量化类型配了专门的高速点积内核。但对外暴露的接口是统一的——都是"按 traits 拿到怎么转 float"。

结果就是:加一种新量化类型,算子代码一行都不用改,只要在 traits 表里填一行、写好解/量化函数。这种"用函数指针把差异收进一张表"的做法,正是 ggml 能容纳几十种量化格式还不臃肿的关键,和 L11 "switch(op) 派发算子"是同一种解耦思路。

✅ 关键要点
  • q4_0 = [d 2B][qs 16B] = 18 B / 32 权重(4-bit 打包);q8_0 每权重一个 int8,一块 34 B,更准更大。
  • K-quant 用 256 的超块 + 两层 scale(超块 d/dmin + 子块 6-bit),同位宽下比 q4_0 更准。
  • q4_K 无 qh(只有 d/dmin/scales/qs);q6_K 才有 ql+qh。别想当然套用。
  • 解量化核心一行:x = (q - 8) * d;量化方向 d = max / -8
  • ggml_type_traitsto_float / from_float_ref 函数指针把每种类型接进引擎,算子无需为每种量化各写一遍
💡 设计洞察
把"每种量化类型长什么样、怎么转 float"全收进一张 traits 表、用函数指针暴露出来——于是几十种量化格式能共用同一套张量结构和同一批算子。L05 说"结构不变、类型可换", 到这里你看到了它在字节级的兑现:换格式只是换一行表项,引擎的主干纹丝不动。下一课,我们把视线从"一个张量怎么存"抬到"整个模型文件怎么存"——GGUF 格式。

🧪 自测 · 想一想为什么这么设计

1. q4_0 的一块 18 字节是怎么构成的?
  1. 2 字节的 half scale + 16 字节装 32 个 4-bit 量化值
  2. 全是 int8,没有 scale
  3. 18 个权重,每个 1 字节
  4. 16 字节 scale + 2 字节量化值
看答案与解析 点击展开
答案:A。block_q4_0 = {ggml_half d; uint8_t qs[16]}:2 字节 scale + 16 字节装 32 个 4-bit 值(每字节两个 nibble),共 18 字节、平均每权重 4.5 bit。
2. K-quant(如 q4_K)为什么在相同位宽下比 q4_0 更准?
  1. 它完全不丢失任何信息
  2. 用 256 的超块:整体 d/dmin 之外,每个子块还有更细的 scale,局部更贴合
  3. 它的量化值不打包
  4. 它其实用了更多的 bit
看答案与解析 点击展开
答案:B。q4_K 是两层 scale:超块 d/dmin 定大范围、8 个子块各有 6-bit scale/min 做局部微调,加上 dmin 的偏移量化,同样 4.5 bit 却比 q4_0 单层 scale 更准。
3. ggml 怎么让算子统一处理几十种量化类型?
  1. 运行时为每种类型即时编译代码
  2. 为每种量化类型写一个专门的算子
  3. 把所有权重都转成 F32 存盘
  4. 用 ggml_type_traits 表 + to_float/from_float_ref 函数指针,算子按 traits 解量化,无需为每种类型各写一遍
看答案与解析 点击展开
答案:D。ggml_type_traits 为每种 type 登记一行(blck_size、type_size、to_float、from_float_ref 等);算子查表、调函数指针解量化,加新类型只需填一行 + 写解/量化函数。
💭 发散思考(没有标准答案,动手或动脑想想)
  • q4_K 和 q4_0 都是约 4-bit,但内存布局差别很大。试着说出至少两点结构上的不同。(提示:超块/两层 scale/dmin/字节数)

Lesson 06 built the intuition of quantization - "each small block of weights shares one scale, and stores only relative offsets inside the block". This lesson goes down to the byte level: we open ggml/src/ggml-common.h to see what every byte of blocks like block_q4_0 and block_q4_K actually holds, then how the dequantize functions restore those bytes back to floats, and finally how ggml_type_traits wires dozens of quantization types uniformly into the engine.

L06 answered "why we can compress, and how much it saves"; this lesson answers "what it looks like in memory afterward, and how the source reads it back" - the two are the "intuition side" and the "implementation side" of one thing. If L06's block-quantization intuition is still fuzzy, glance back first; this lesson lands directly on every struct field.

🔌 Analogy
A quantization block is much like a zip archive: the first few bytes are the "decompression parameters" (scale, min) that tell you how to scale the following data back up; the long run after is the "compressed data" (low-bit quantized values packed together). Dequantizing means following those leading parameters to multiply each small integer back to its true size. Different quantization formats are just different conventions for "how the parameters are recorded and how the data is packed" - read the byte layout and you have read the format.

Open one block: q4_0 and q8_0

Start with the two most classic blocks. q4_0 packs 32 weights into a block: the first 2 bytes are a half (FP16) scale, followed by 16 bytes holding 32 4-bit quantized values (two nibbles per byte). That is 18 bytes a block. q8_0 also groups 32 weights, but stores each as a full byte of int8, unpacked - 34 bytes a block (2-byte scale + 32-byte values), more accurate and larger.

q4_0 / q8_0 byte layout: scale up front, values after; q4_0 packs 4-bit, q8_0 keeps int8 unpacked
q4_0d : 2Bqs : 16B (32 x 4-bit)= 18 B / 32 weights
q8_0d : 2Bqs : 32B (32 x int8)= 34 B / 32 weights

These blocks are laid out one right after another, contiguously in file and memory: a weight matrix is just a long run of blocks. Knowing "how many bytes per block" lets you compute "where block n is" - exactly the basis for random addressing and parallel processing during dequantization later. A regular layout brings benefits far beyond saving space - it makes "fetch one small block on demand and dequantize it" possible, the very precondition for the "dequantize on the fly" mentioned earlier.

In source, the struct matches the figure almost one-to-one (ggml/src/ggml-common.h):

// simplified from ggml/src/ggml-common.h
#define QK4_0 32
typedef struct { ggml_half d; uint8_t qs[QK4_0/2]; } block_q4_0;  // 2 + 16 = 18 B
typedef struct { ggml_half d; int8_t  qs[QK8_0];   } block_q8_0;  // 2 + 32 = 34 B

Field by field: d is the block's shared scale, and ggml_half is exactly a 2-byte half-precision float (L06's "baseline"); qs is the array of quantized values. q4_0's qs[QK4_0/2]=qs[16] - 32 values in only 16 bytes, because each value is just 4 bits, two squeezed into one byte (one high nibble, one low). q8_0's qs[QK8_0]=qs[32] - 32 values filling 32 bytes, one value per byte, no bit-splitting.

🔬 Details / source
One might ask: why is that scale a 2-byte half rather than a more precise 4-byte float? Because there is only one scale per block, its precision has limited impact on the final result, yet it counts against the whole block's size - the 2 bytes saved by using half, small per 32 weights, multiplied by the tens of thousands of blocks in a model, add up to a lot. This is a classic engineering trade-off: save wherever you can in places that "barely affect precision", leaving the byte budget for the quantized values that truly matter.

Why is q8_0 both more accurate and larger? Because 4 bits can represent only 16 levels while 8 bits represent 256 levels, so for the same weight 8-bit can sit much closer, with smaller quantization error. The cost is double the size: from 0.5 byte per weight to 1 byte. This is the byte-level truth behind L06's "VRAM table" - choosing Q4_0 vs Q8_0 on the command line is, in essence, picking between these two block layouts.

🌍 Big picture
It is worth pausing on one question here: why block at all, instead of sharing one scale across the whole weight matrix? Because in a matrix of thousands by thousands, weight magnitudes vary widely - some rows large, some small. With one scale for the whole matrix you must accommodate the largest value, and small weights get crushed almost to 0, losing all precision. Split into blocks of 32, each block finds its own scale - big blocks use a big scale, small blocks a small one - and quantization error drops sharply at once. That is the entire point of "block quantization": spend the small overhead of "one scale per block" to buy a large gain in local precision.

One often-missed detail: a q4_0 block is 18 bytes for 32 weights, averaging 4.5 bits per weight, not exactly 4. The extra 0.5 bit is the 2-byte scale amortized across 32 weights. The bigger the block, the thinner this "management overhead" spreads - which already foreshadows why the next section's K-quant uses a big 256-weight super-block.

Super-block: the two-level scale of K-quant

q4_0's one scale per 32 weights is already decent, but it can be more accurate. The idea of K-quant (the K formats, such as q4_K and q6_K) is to use a 256-weight "super-block", sliced into several sub-blocks, with a two-level scale - the super-block gives an "overall baseline", and each sub-block records its own "fine-tuning scale". This both thins the baseline overhead and preserves local precision.

Take q4_K: 256 weights split into 8 sub-blocks (32 weights each). The super-block level has two halves, d and dmin; the sub-block level has 8 groups of 6-bit scale/min packed into a 12-byte scales[]; plus 128 bytes holding 256 4-bit quantized values.

superd, dmin (one half each)
the "overall scale / overall min" shared by all 256 weights
subscales[12]: 8 groups of 6-bit scale/min
one group per 32 weights, each fine-tuned, sitting closer than a single-level scale
dataqs[128]: 256 4-bit quantized values
the actual quantized weight values, packed

Where does the 12-byte scales[] come from? 8 sub-blocks, each needing a scale and a min, could use one byte each for 16 bytes total; but K-quant squeezes them to 6 bits, 8x(6+6)=96 bits=12 bytes, saving another 4 bytes. Even the "management" scales themselves are quantized - this squeezing of every last bit of redundancy is the daily business of quantization-format design.

The two-level scale is the soul of K-quant. The first level (the super-block's d/dmin) sets a rough range; the second level (the sub-block's 6-bit scale/min) fine-tunes locally within that range - wherever a small run of weights skews high or low, the sub-block scale follows immediately. This is the byte-level source of L06's spark "for the same bit count, K-quant is usually more accurate": precision comes not from spending more bits, but from allocating scale more cleverly.

// simplified from ggml/src/ggml-common.h (QK_K = 256, K_SCALE_SIZE = 12)
typedef struct {
    ggml_half d;                   // super-block overall scale
    ggml_half dmin;                // super-block overall min
    uint8_t   scales[K_SCALE_SIZE];// 8 sub-blocks' 6-bit scale/min (no qh)
    uint8_t   qs[QK_K/2];          // 256 4-bit quantized values
} block_q4_K;

Compute q4_K's byte count too: 2 (d) + 2 (dmin) + 12 (scales) + 128 (qs) = 144 bytes for 256 weights, averaging 144*8/256 = 4.5 bits per weight - the same bit width as q4_0, yet higher precision. That is the most direct payoff of the "two-level scale": not one extra bit spent, error pushed down purely by a smarter structure.

⚠ Heads-up
Note that q4_K has no qh - only the four fields d, dmin, scales, qs, which is easy to misremember (see Dig deeper 2). q6_K, by contrast, does have qh: its 6-bit values are split into "low 4 bits" in ql and "high 2 bits" in qh, plus 16 8-bit sub-block scales and one super-block d. Fields differ across K-quants, so do not assume.

Worth meeting the whole K-quant family: from q2_K and q3_K up to q6_K, the number in the name is the rough bit width, while K means they all use this "super-block + two-level scale" structure. The lower the bit width (like q2_K) the harder the compression and the riskier the precision, so it is often used only on less sensitive layers; the higher (like q6_K) the closer to original precision. This is why, downloading real models, you see suffixed names like Q4_K_M and Q5_K_S - schemes that mix different K-quant levels across different layers, striking different balances between size and quality.

🔬 Details / source
A word on that dmin. q4_0 has only a scale, so its quantization is symmetric (around 0); q4_K adds a min, doing offset quantization - the restore formula is more like x = scale * q + min (the source comment reads "weight is represented as x = a*q + b"). Why the offset? Because many weight distributions are not centered on 0, and a min lets the quantization range shift as a whole to fit the real distribution, pushing error down further. This is the other half of why K-quant beats q4_0: not only finer scale, but even the "zero point" is adjustable.

Why pick a super-block as big as 256? Because the bigger the super-block, the more weights the fixed "overall d/dmin" overhead spreads across, and the smaller the per-weight overhead; meanwhile local precision is backstopped by the finer sub-block scales, so a big block does not get blurry. The big block handles "high compression", the sub-blocks handle "high precision", and you get both - this is the design motive for K-quant being more accurate than q4_0 at the same bit width (see Dig deeper 1).

Dequantize: restoring bytes to floats

Weights sit in memory as quantized bytes, but to actually take part in computation (say, fed into L11's mul_mat) they must first be restored to floats. That step is dequantization, handled by each format's own dequantize_row_* function (in ggml/src/ggml-quants.c). For q4_0 the logic is surprisingly simple:

// pseudocode, mirrors dequantize_row_q4_0 in ggml/src/ggml-quants.c
for each block:
    d = half_to_float(block.d)          // fetch this block's scale
    for j in 0..15:                     // two nibbles per byte
        q0 = (block.qs[j] & 0x0F) - 8   // low 4 bits, minus 8 back to signed
        q1 = (block.qs[j] >> 4)   - 8   // high 4 bits, minus 8 back to signed
        y[j]      = q0 * d              // x = (q - 8) * d
        y[j + 16] = q1 * d
take a nibble from qs
q = 0..15
->
q - 8
shift to -8..7
(signed)
->
x d
times this block's scale
->
float x
x = (q - 8) * d

Run one real block through it and the chain above gets concrete:

Tracing one dequant: how a few bytes of a q4_0 block become floats (d=0.05 illustrative).
(1) byte qs[j]
0x960xC3
two quantized bytes
& 0x0F
>>4
(2) split nibble
69312
two 4-bit codes per byte
q - 8
(3) minus 8
-2+1-5+4
shift to signed -8..7
x d
d=0.05
(4) x d
-.10+.05-.25+.20
restored approximate floats

The core is one line: x = (q - 8) * d. q is the 0..15 4-bit value; subtracting 8 shifts it to a signed -8..7, then multiplying by this block's scale d restores the approximate original float. q8_0 is even more direct: x = q * d, since int8 is already signed and needs no offset.

🔬 Details / source
That "minus 8" deserves a second thought too. 4 bits store 0..15, sixteen values, but weights are both positive and negative, so the convention makes 8 mean "0", below 8 negative, above 8 positive - subtracting 8 shifts 0..15 into the roughly symmetric range -8..7. This represents both signs, and is densest (most accurate) near 0, matching the fact that "most weights cluster near 0". A tiny subtraction encodes an understanding of the weight distribution.

In reverse, quantization (compressing floats to bytes) is the inverse. In q4_0's reference implementation quantize_row_q4_0_ref, the scale is d = max / -8 - find the weight with the largest magnitude in the block, map it to the endpoint of the quantization range (-8), and scale-and-round the rest proportionally. Dividing by -8 rather than 8 is an easy-to-misread detail: it pairs with the "minus 8" at dequantize time, ensuring the round trip lines up numerically.

🌍 Big picture
To make one thing explicit: dequantization is lossy - the restored floats are not exactly equal to the original weights, and that small gap is the quantization error. So why does the model still work? Because deep networks are quite tolerant of tiny perturbations to weights: a single weight off by a hair, after hundreds or thousands of accumulations and nonlinearities, is almost imperceptible in the overall output. The whole art of a quantization format is balancing "compress harder" against "error the model can still tolerate" - which is exactly why there are so many levels like q4_0, q4_K, q6_K, letting you pick a suitable trade-off for your precision needs.

One more thing worth remembering: dequantization does not necessarily expand the whole weight tensor to floats up front (that would cost a lot of memory). In hotspots like matmul, ggml often dequantizes on the fly - restoring just the small block it needs in the inner loop, feeding it into the dot product immediately, and discarding it. So quantization saves not only VRAM; even the "decompressed floats" mostly never land, benefiting bandwidth and cache too (echoing L06's "saves VRAM and speeds up").

Wiring into the engine: ggml_type_traits

Here is the problem: q4_0, q8_0, q4_K, q6_K... dozens of quantization formats, each with a different byte layout - must every one be special-cased inside mul_mat and dequantize? Of course not. ggml uses a type-traits table (ggml_type_traits) to register each type as one row of "basic parameters + how to convert to float", and operators just look it up, never caring which quantization it actually is.

tensor.type
= GGML_TYPE_Q4_K
->
type_traits[Q4_K]
blck_size / type_size
to_float = dequantize_row_q4_K
->
operator
dequantize per traits, then compute

A tensor only records "I am Q4_K" in its type field (L05's type field finally earns its keep here). When an operator needs it, it looks that type up in the type_traits table to get "how many elements per block, how many bytes, which function converts to float" and just follows it - there is not a single if in the operator code branching on quantization type.

// simplified from ggml/include/ggml.h (one row per type)
struct ggml_type_traits {
    const char * type_name;     // "q4_K"
    int64_t       blck_size;     // elements per block, e.g. 256
    size_t        type_size;     // bytes per block, e.g. 144
    bool          is_quantized;
    ggml_to_float_t   to_float;       // dequantize function pointer
    ggml_from_float_t from_float_ref; // quantize function pointer (reference)
};

The key lies in those two function pointers: to_float points to this type's dequantize function, from_float_ref to its (reference) quantize function. An operator takes a tensor, calls to_float per its traits to dequantize, then computes; when saving, it quantizes via from_float_ref. Note the field name is from_float_ref (with _ref, the reference implementation), not from_float. So adding a new quantization format is mainly filling one traits row + writing these two functions, while mul_mat and the dequantize operators need almost no change - exactly L05's "structure fixed, type swappable" cashed out for quantization.

Stepping back, this lesson actually covers "quantization" at three levels: layout (what each byte in a block holds), restoration (how dequantize turns bytes back into floats), and wiring (how the traits table lets operators handle everything uniformly). These three map neatly onto the three situations you meet using llama.cpp: picking a quantization level (layout decides size and precision), running inference (dequantization happens quietly behind the scenes), and reading the source (traits is the single entry point for all quantization types). String the three together and you stop treating a name like Q4_K_M as a black box - you can say exactly what it is in memory and what happens when it computes. Breaking a quantization name down to the byte level makes your grasp of "how a model lands on disk and in memory" that much more solid.

1 Why does q4_K use a 256 super-block instead of q4_0's 32? Click to expand

The core is amortizing fixed overhead without sacrificing local precision. q4_0 spends 2 bytes on a scale for every 32 weights; bringing the super-block to 256 spreads the fixed "overall d/dmin" overhead over 8x as many weights, clearly lowering per-weight management cost.

But simply enlarging the block with a single-level scale would hurt local precision - 256 weights sharing one scale is too coarse. K-quant's answer is to add a second sub-block scale: the super-block sets the broad range, the sub-blocks set details, so you get both "high compression from the big block" and "high precision from the sub-blocks".

This also explains why we do not blindly make the super-block even bigger (say 1024): sub-block scales themselves take space, and too big a block with too many sub-blocks raises the second-level overhead again. 256 + 8 sub-blocks is an engineering sweet spot between compression ratio and precision, tuned through extensive measurement.

2 Does q4_K have qh? What about q6_K? Click to expand

q4_K has no qh. It has only four fields: d, dmin, scales[12], qs[128]. A 4-bit value is exactly one nibble, packed straight into qs, with no need to split high and low bits.

q6_K is the one that has qh. A 6-bit value does not fit in one nibble, so it is split: the low 4 bits go into ql, the high 2 bits into qh, plus 16 8-bit sub-block scales and one super-block d. So q6_K's struct looks quite different from q4_K's.

This is one of the most common beginner traps: assuming all K-quant fields are identical and imagining q4_K "with a qh". When reading these structs, always check field by field against the source, never guessing from the format name - different bit widths mean different packing, and thus different fields.

3 How do operators avoid being rewritten for each quantization? Click to expand

Through the two function pointers to_float / from_float_ref in type_traits. Given a quantized tensor, an operator does not test "is this q4_0 or q4_K"; it directly calls the to_float registered in its traits to dequantize, then computes.

In hotspots like mul_mat it is subtler: rather than dequantizing the whole block first and then multiplying, it often dequantizes a small run on the fly and dot-products immediately, even with dedicated fast dot-product kernels for certain quantization types. But the exposed interface is uniform - all "get how to convert to float from traits".

The result: adding a new quantization type needs not a single line changed in operator code, only one traits row plus the dequant/quant functions. This "fold the differences into a table of function pointers" approach is the key to ggml hosting dozens of quantization formats without bloat - the same decoupling idea as L11's "switch(op) dispatch".

✅ Key points
  • q4_0 = [d 2B][qs 16B] = 18 B / 32 weights (4-bit packed); q8_0 is one int8 per weight, 34 B a block, more accurate and larger.
  • K-quant uses a 256 super-block + two-level scale (super-block d/dmin + sub-block 6-bit), more accurate than q4_0 at the same bit width.
  • q4_K has no qh (only d/dmin/scales/qs); q6_K is the one with ql+qh. Do not assume.
  • Dequant core, one line: x = (q - 8) * d; quantize direction d = max / -8.
  • ggml_type_traits wires each type into the engine via to_float / from_float_ref function pointers, so operators need not be rewritten per quantization.
💡 Design insight
Folding "what each quantization type looks like and how to convert it to float" into one traits table, exposed via function pointers - so dozens of quantization formats can share the same tensor structure and the same operators. L05 said "structure fixed, type swappable"; here you see it cashed out at the byte level: switching format is just switching a table row, while the engine's trunk does not move at all. Next lesson, we lift our gaze from "how one tensor is stored" to "how a whole model file is stored" - the GGUF format.

🧪 Self-test - think about the design

1. How are the 18 bytes of a q4_0 block made up?
  1. a 2-byte half scale + 16 bytes holding 32 4-bit quantized values
  2. all int8, no scale
  3. 18 weights, one byte each
  4. 16 bytes of scale + 2 bytes of values
Show answer & explanation click to expand
Answer: A. block_q4_0 = {ggml_half d; uint8_t qs[16]}: a 2-byte scale + 16 bytes holding 32 4-bit values (two nibbles per byte), 18 bytes total, 4.5 bits per weight on average.
2. Why is K-quant (e.g. q4_K) more accurate than q4_0 at the same bit width?
  1. It loses no information at all
  2. It uses a 256 super-block: beyond the overall d/dmin, each sub-block has a finer scale, fitting locally better
  3. Its quantized values are not packed
  4. It actually uses more bits
Show answer & explanation click to expand
Answer: B. q4_K has a two-level scale: super-block d/dmin set the broad range, 8 sub-blocks each carry a 6-bit scale/min for local fine-tuning, plus dmin's offset - the same 4.5 bits but more accurate than q4_0's single scale.
3. How does ggml let operators handle dozens of quantization types uniformly?
  1. JIT-compile code for each type at runtime
  2. Write a dedicated operator for each quantization type
  3. Convert all weights to F32 on disk
  4. A ggml_type_traits table + to_float/from_float_ref function pointers; operators dequantize per traits, no need to rewrite per type
Show answer & explanation click to expand
Answer: D. ggml_type_traits registers one row per type (blck_size, type_size, to_float, from_float_ref, ...); operators look it up and call the function pointers, so a new type needs only one row + its dequant/quant functions.
💭 Open questions (no single right answer - just think or try)
  • q4_K and q4_0 are both ~4-bit, yet their memory layouts differ a lot. Name at least two structural differences. (hint: super-block / two-level scale / dmin / byte count)