🦙 llama.cpp 图解教程llama.cpp Visual Guide 第二部分 · 前置基础Part 2 · Foundations 06 / 40
第二部分 · 前置基础Part 2 · Foundations

量化入门Quantization, intuitively

量化是 llama.cpp 能把一个 7B、甚至 70B 大模型塞进消费级显卡 / 内存的"压缩术"。课 01 提过它有 4/5/8 bit 多种档位; 这一课讲清三件事:为什么能压、怎么压(块量化)、Q4_0 / Q8_0 / K-quant 各是什么。 硬核的字节级细节留到第三部分的"量化格式"课,这里先把直觉建立起来。

🔌 生活类比
量化很像把一张高清照片按小块压缩:每个小方块里,先记一个"基准亮度"(scale),块内每个像素只存"相对基准差几档"。 因为同一小块里的像素通常很接近,几档就够用,压完看起来还和原图差不多。把"像素"换成"模型权重",这就是块量化——块越小、基准越贴合,还原得越像。

为什么要量化:显存与带宽

训练好的大模型,本质是一大堆浮点数(权重)。默认用 16 位浮点(FP16 / BF16)存,每个权重 2 字节——一个 70 亿(7B)参数的模型, 光权重就要 约 14 GB,普通显卡和内存直接被劝退。量化就是用更少的位数近似地存这些权重:8 bit 砍掉一半、4 bit 再砍一半, 显存需求成倍下降。

FP16(原始)

每权重 2 字节
7B -> 约 14 GB

Q8_0(8-bit)

约 1 字节 / 权重
7B -> 约 7 GB

Q4_0(4-bit)

约 0.56 字节 / 权重
7B -> 约 3.9 GB

🌍 宏观理解
省显存只是其一。更关键的是带宽:课 04 说过,decode 阶段的瓶颈常常是"把权重从显存搬到计算单元"。权重越小,每生成一个 token 要搬的字节越少,速度直接变快。所以量化往往是"既省显存、又提速"的双赢,代价只是一点点精度损失——而大模型对这种损失通常相当宽容。

为什么大模型能被压到这么狠还能用?因为它的权重里有大量冗余:每个权重的具体数值并不需要那么高的精度,模型真正依赖的是这些权重 整体的统计规律。把每个数从"非常精确"降到"大致正确",单看一个损失明显,但成千上万个权重一起作用时,误差很大程度上互相抵消,最终输出几乎不受影响。 这也是为什么 4-bit 量化常常只让模型质量掉一点点,却换来几倍的体积和速度收益。

那为什么大家最常用 4-bit 而不是更狠的 2-bit、或更稳的 8-bit?这是个"甜点"问题:8-bit 几乎不掉质量,但省得不够多; 2-bit、3-bit 省得很狠,质量却开始明显滑坡。4-bit(尤其是 K-quant 的 4-bit)落在曲线的拐点上——体积压到原来的约四分之一,质量却只掉一点点, 于是成了社区下载量最大的档位。具体选哪档,还要看你的硬件、对质量的容忍度,以及模型本身大小(越大的模型,往往越扛得住激进量化)。

顺便补一点背景:前面反复出现的 FP32 / FP16 / BF16 都是浮点格式,区别在用多少位、以及怎么分配给"指数"和"尾数"。 FP32 是 4 字节的全精度,训练时常用;FP16 和 BF16 都是 2 字节的半精度,其中 BF16 牺牲一点尾数精度,换来和 FP32 一样大的表示范围,在大模型里很受欢迎。 量化则更进一步,把这些浮点直接换成更省的整数表示——可以理解为"在 FP16 已经省了一半的基础上,再往下狠压一截"。

💡 实战
把这个体量感再放大一点:一个 70B 模型,FP16 要 约 140 GB——这是好几张顶级显卡才装得下的量;而 Q4_K_M 量化后只要 约 40 GB 上下,一张 48 GB 显存的卡、或一台大内存的机器就能跑起来。正是量化,把"只有大公司机房玩得起"的大模型,变成了"发烧友在家也能折腾"的东西。

块量化:每块一个 scale

那"用更少位数近似"具体怎么做?最朴素的想法:给整个权重矩阵定一个统一的缩放系数(scale),把浮点数等比例映射到一个小整数范围。 但问题是,同一个矩阵里权重的大小可能差很多,用一个全局 scale,大值和小值没法兼顾,误差会很大。

块量化的办法是:把权重切成一个个小块(Q4_0 里每块 32 个权重),每块各自配一个 scale。块内动态范围小、scale 贴得准, 近似自然更精确。这就是"分而治之"在量化上的体现:

块量化:32 个浮点权重 -> 1 个 scale(半精度)+ 32 个低位整数
原始0.12-0.080.21-0.1532 个 fp
量化后d = scale971151 个 scale + 32 个 4-bit

再换个角度理解"为什么块内范围小就更准":量化的本质是"用有限的几档去近似连续的值",这几档要覆盖的范围越窄,每一档之间的间隔就越小、分得越细。 一整个矩阵里既有 0.001 也有 5.0,硬用 16 档去分,间隔必然很粗;可一旦切成小块,每块内部的数往往挤在相近的量级,同样 16 档分得就细多了。这就是块量化精度更高的根本道理。

落到代码上,Q4_0 的"一块"就是一个紧凑的结构体(来自 ggml/src/ggml-common.h):

#define QK4_0 32       // 一块 32 个权重
typedef struct {
    ggml_half d;            // scale (fp16), 每块一个
    uint8_t   qs[QK4_0/2];  // 32 个权重, 每个压成 4-bit, 两个挤一字节
} block_q4_0;              // 2 + 16 = 18 字节 -> 平均 4.5 bit/权重

算笔账:一块 32 个权重,用 2 字节存 scale(半精度浮点 ggml_half)、16 字节存 32 个 4-bit 量化值 (每个权重 4 bit,两个挤进一个字节),合计 18 字节。摊到每个权重就是 18 × 8 / 32 = 4.5 bit——这就是"4-bit 量化"实际占用稍多于 4 bit 的原因:那多出来的 0.5 bit,是每块都要分摊的那个 scale。

Q4_0 一块 = 18 字节,装下 32 个权重
布局d:2 字节 scaleqs:32 个 4-bit = 16 字节
合计2 + 16 = 18 字节摊到每权重 = 4.5 bit

用的时候要"解量化":把存的 4-bit 整数还原成近似的浮点值。Q4_0 的规则很简单(对应 ggml/src/ggml-quants.cdequantize_row_q4_0):

# 每个 4-bit 值 q 在 0..15, 还原成带符号的权重:
for i in range(32):
    q    = nibble(qs, i)     # 0..15
    x[i] = (q - 8) * d       # 减 8 居中到 0 附近, 再乘以这一块的 scale d
🔬 细节 / 源码对应
那个 -8 是把 0..15 的无符号范围平移到 -8..7,让它对称地分布在 0 两侧(权重有正有负);乘以 d则把这个小整数还原回原来的尺度。整个过程没有查表、没有分支,就是一个减法加一个乘法,非常适合在 CPU / GPU 上批量快速跑——这正是 Q4_0 这种"对称量化"格式简单高效的原因。

反过来,怎么从浮点权重得到那些 4-bit 整数?以对称量化为例:先在这一块里找出绝对值最大的那个权重,用它定出 scale——Q4_0 的具体做法是 d = max / -8max 是块内带符号的极值权重),相当于把这个极值锚定到量化范围的端点 q=0,从而用满 -8..7 整个范围;再把每个权重除以 d、四舍五入、加上偏移,压进 0..15。所以"量化"和"解量化"是一对互逆操作: 量化时 q = round(x/d) + 8,用时 x ≈ (q-8)*d。两次取整之间丢掉的那点零头,正是量化误差的来源。

🌍 宏观理解
注意那个 scale d 本身是用半精度浮点(fp16)存的,而不是再压成整数——因为每块只有一个 scale,占比很小,用 fp16 保住它的精度很划算,而它的准确度直接决定整块的还原质量。把视角拉远,你会看到一条清晰的"量化粒度"谱系:最粗的是整个张量共享一个 scale(per-tensor),中间是每块一个(per-block,如 Q4_0),最细的是超块里每个子块一个(K-quant)。粒度越细,精度越高,但要存的 scale 也越多——量化格式的演化,基本就是在这条线上找更好的平衡点。

顺带一问:块大小为什么常取 32?这又是一处权衡——块越小,scale 越贴合局部、精度越高,但要存的 scale 越多、压缩率下降;块越大则相反。 32 是精度和体积之间一个经过实践检验的折中,也正好契合硬件上常见的并行宽度,算起来顺手。K-quant 用 256 的超块再细分,则是想"既要大块的高压缩率、又要小块的高精度", 试图鱼和熊掌兼得。

把一组真实权重压一遍再还原,"有损但损得很小"就看得见了:

追踪一次量化往返:4 个权重压成 4-bit 再还原,看误差有多小(数字为示意)。
① 原始权重
0.46-0.120.31-0.40
块内 4 个 fp16
找最大
定 scale
② scale d
-0.058
d = max/-8 = 0.46/-8
量化
round(x/d)+8
③ 4-bit 码
010315
存进 0..15
反量化
(q-8)×d
④ 还原值
0.46-0.120.29-0.40
误差 ≤ 0.02

Q8_0 / K-quant:精度与压缩的取舍

Q4_0 只是量化大家庭里的一个。同样的"块 + scale"思路,换一换参数,就是不同档位:

类型每权重 bit块 / 超块特点
Q8_0约 8.5 bit32最接近 FP16,体积大,质量最稳
Q4_0约 4.5 bit32最轻最快,精度损失较明显
Q4_K约 4.5 bit超块 256子块各有 scale + 混合精度,同 bit 下更准

Q8_0 用 8 bit 存每个权重(块还是 32,结构是 d + 32 个 int8),约 8.5 bit/权重,体积大但精度最接近原始 FP16, 常用于对质量最敏感的场合。Q4_0 最轻,4.5 bit,速度快、省显存,但精度损失相对明显。

K-quant(名字里带 K,如 Q4_KQ5_K)是更聪明的一档:它用更大的"超块" (super-block,QK_K = 256 个权重),超块内再分成若干子块,每个子块有自己更精细的 scale 和 min,还会对不同张量混合用不同位宽。 结果是:在同样的平均 bit 数下,K-quant 的困惑度(perplexity,衡量模型预测好坏的指标,越低越好)通常明显低于对应的 Q4_0 / Q5_0。 今天大家从网上下载的 GGUF,大多就是 Q4_K_M 这类 K-quant 档位。

把这些名字连起来读就有规律了:字母 Q + 位数 + 可选的 K + 可选的档位后缀Q4_0 是"4-bit、对称、基础块", Q4_K_M 是"4-bit、K-quant、中档混合精度",Q8_0 是"8-bit、对称、基础块"。下次在下载页面看到一长串 Q3_K_SQ5_K_MQ6_K,你就能一眼读出它大概多大、多准了。

typedef struct {
    ggml_half d;          // 超块整体 scale
    ggml_half dmin;       // 超块整体 min (非对称)
    uint8_t scales[...];  // 各子块更细的 scale/min (6-bit, 已量化)
    uint8_t qs[...];      // 量化值
} block_q4_K;            // QK_K = 256, 简化自 ggml-common.h

还有两点值得知道。其一,llama.cpp 量化的主要是"权重",推理时流动的"激活值"通常仍用较高精度(如 fp16/fp32)计算——因为权重是静态的、 占绝大多数内存,最值得压;激活是动态的,过度量化更容易伤精度。其二,并非每个张量都用同一档:像词嵌入、输出投影这种对质量影响大的张量, 常被刻意保留在更高的位宽,这正是 K-quant 的 _M / _L 档在做的"混合精度"。

💡 实战
那这些量化文件是怎么来的?流程很直接:先用转换脚本把原始模型导出成一个高精度的 GGUF(通常是 fp16),再用 llama-quantize工具把它"压"成目标档位,比如 llama-quantize model-f16.gguf model-Q4_K_M.gguf Q4_K_M。量化是一次性的离线操作,压完得到一个更小的 GGUF,之后每次加载运行的都是这个小文件——所以量化的开销只在"制作"时付一次,运行时只享受它带来的省与快。

怎么衡量"量化掉了多少质量"?最常用的指标是困惑度(perplexity):拿一段标准文本,看模型对"下一个词"预测得有多准,困惑度越低越好。 社区常做的事,就是把同一个模型的各个量化档位都跑一遍困惑度、列成表对比——你会看到 Q8_0 几乎和 fp16 持平、Q4_K_M 只高一丁点,而 Q2_K 则明显抬高。 llama-perplexity 工具就是干这个的,第五部分的 L30 会专门讲怎么用它给量化"打分"。一个经验法则:在显存放得下的前提下,尽量选高一档,质量更有保障。

最后澄清一个边界:量化只压缩权重的表示方式(每个数用几位存),并不改变模型的结构——层数、维度、参数个数都原封不动。 这和剪枝(删掉一部分权重 / 神经元)、蒸馏(训练一个更小的新模型去模仿大模型)是完全不同的三条压缩路线。量化最大的好处就是 几乎免费、即插即用:不用重新训练,一个命令就能把现成模型变小变快,这也是它在本地推理里如此普及的原因。

⚠ 注意
量化的损失也不是对所有任务一视同仁。闲聊、续写这类容错高的任务,4-bit 几乎感觉不出差别;而代码生成、数学推理、长链条逻辑这类"一步错步步错"的任务,对精度更敏感,激进量化时更容易看出退步。所以同一个 Q4 模型,你拿它聊天觉得很好、拿它写复杂代码却偶有翻车,并不奇怪——这时往上换一档(如 Q5_KQ6_K 甚至 Q8_0)往往能找回不少。

还有个实用细节:不同硬件后端对量化格式的支持和优化程度并不一样。同一个 Q4_K 模型,在 CPU 上靠 SIMD 指令快速解量化、在 CUDA 上有专门的核函数, 速度表现可能差别不小。所以"选哪个量化档位"有时也要看你打算在什么硬件上跑——这部分在第六部分讲内核时会更具体。整体而言,主流的 Q4_K / Q8_0 在各后端都有良好支持,闭眼选基本不会错。

🌍 宏观理解
最后把这一课和课 02、课 05 串起来:一个量化后的 GGUF 文件,里面每个权重张量都带着自己的 type(课 05 讲过),有的标着 Q4_K、有的可能是 Q6_KF16。加载时,llama-model-loader按张量的 type 决定怎么读、怎么解量化;计算时,ggml 的算子(如矩阵乘)能直接吃量化权重,在乘法的内层即时解量化,省去"先整体还原成 fp32 再算"的开销。所以"量化"不是一个孤立的步骤,而是贯穿存储(GGUF)、加载(loader)、计算(ggml 算子)的一条完整链路

深入一点(选读)

下面三个问题,想深究的同学点开看;只想抓主线的可以先跳过。

1 Q4_0 / Q4_1 / Q8_0 后面的数字和 0/1 是什么意思? 点击展开

数字是每个权重的 bit 数:Q4 = 4 bit、Q8 = 8 bit。后缀 _0 / _1 区分量化的"对称性"。

_0对称量化,只存一个 scale、零点固定(就像前面 Q4_0 的 (q-8)*d);_1非对称量化,额外再存一个最小值 min(偏移量),还原公式变成 q*d + min。多存一个 min 让它能更贴合那些 "不以 0 为中心"的权重分布,精度略高,但每块要多占几字节。要不要这个 min,就是 _0_1 的区别。

2 K-quant(Q4_K_M 等)凭什么更准? 点击展开

关键在更细粒度的 scale。普通 Q4_0 是"32 个权重共享 1 个 scale";K-quant 用 256 个权重的超块,但超块内部再切成多个子块, 每个子块有自己的 scale 和 min(这些子块 scale 本身又被量化成 6-bit 存起来,省空间)。粒度越细,scale 越能贴合局部,误差越小。

名字里的 _S / _M / _L(small / medium / large)是不同的"混合精度"档: 对模型里更重要的层用稍高的位宽、不重要的用低位宽,在体积和精度之间取不同平衡。所以同样标着"4-bit",Q4_K_M 往往比 Q4_0 又准又只大一点点——这也是它成为主流下载格式的原因。

3 imatrix 是什么?和量化什么关系? 点击展开

imatrix(importance matrix,重要性矩阵)常被误解为"决定每个权重用几 bit"——其实不是。它是用一批校准数据 跑一遍模型,统计出"每个权重对最终输出的影响有多大"。

量化时,对更重要的权重,让它的量化误差更小(在选 scale、取整时更偏向保住它们)。换句话说,imatrix 改变的是"误差怎么分配", 让宝贵的精度用在刀刃上,而不改变位宽分配本身。它由 llama-imatrix 工具生成,再喂给 llama-quantize 一起用,通常能在不增大体积的前提下进一步降低困惑度。

✅ 关键要点
  • 量化 = 用更少 bit 近似存权重,省显存、省带宽、提速,代价是少量精度损失(大模型通常很宽容)。
  • 块量化:把权重切成小块、每块一个 scale;Q4_0 每块 32 个权重 = 2 字节 scale + 16 字节量化值 = 18 字节 = 4.5 bit/权重
  • 解量化就是 x = (q - 8) * d 这么简单(Q4_0 对称量化)。
  • Q8_0 准而大、Q4_0 轻而糙、K-quant(超块 + 子块 scale + 混合精度)同 bit 下更准,是当下主流。
  • imatrix 是误差加权、不是位宽分配。
💡 设计洞察
"每块一个 scale"——就这一个朴素的想法,把"浮点数动态范围太大"这个全局难题,拆成了无数个"块内范围很小"的局部小问题, 于是低到 4 bit 也能保住可用的精度。大模型能从数据中心走进你的笔记本,这一招居功至伟。记住一句话:量化不是把模型变笨, 而是把"过度精确"的浪费挤掉——用刚刚好的位数,装下模型真正需要的那部分信息。

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

1. 块量化(每块一个 scale)为什么比“整个张量共用一个 scale”更准?
  1. 因为完全不丢数据
  2. 因为用了更多 bit
  3. 因为压缩率更高
  4. 每块自带 scale,块内动态范围更小,近似误差更小
看答案与解析 点击展开
答案:D。全局一个 scale 没法兼顾大值和小值;切成小块、每块各配 scale,局部范围小、贴得准,误差自然更小。
2. Q4_0 每个权重平均约几 bit?(每块 32 权重 = 2 字节 scale + 16 字节量化值)
  1. 正好 4 bit
  2. 约 4.5 bit
  3. 2 bit
  4. 8 bit
看答案与解析 点击展开
答案:B。(2 + 16) 字节 × 8 / 32 = 4.5 bit;多出的 0.5 bit 是每块都要分摊的那个 scale。
3. imatrix(重要性矩阵)在量化里起什么作用?
  1. 它本身是一种新的量化格式
  2. 决定每个权重用几 bit
  3. 用校准数据按权重的重要性来加权量化误差,让重要权重更准;不改变位宽分配
  4. 它给模型增加显存占用
看答案与解析 点击展开
答案:C。imatrix 改变的是“误差怎么分配”,让精度用在重要权重上;位宽由量化档位(如 Q4_K)决定,与 imatrix 无关。
💭 发散思考(没有标准答案,动手或动脑想想)
  • 同样压到约 4bit,为什么 Q4_K 通常比 Q4_0 困惑度更低?(提示:super-block 与子块 scale)

Quantization is the "compression trick" that lets llama.cpp fit a 7B - or even 70B - model into consumer GPU / RAM. Lesson 01 mentioned its 4/5/8-bit tiers; this lesson nails down three things: why it compresses, how it compresses (block quantization), and what Q4_0 / Q8_0 / K-quant are. The hardcore byte-level details wait for Part 3's "quantization format" lesson; here we build the intuition first.

🔌 Analogy
Quantization is like compressing a high-res photo block by block: in each small block, record one "baseline brightness" (scale), and each pixel stores only "how many notches off the baseline". Since pixels in one block are usually close, a few notches suffice and the result still looks like the original. Swap "pixels" for "model weights" and that is block quantization - the smaller the block and the tighter the baseline, the closer the reconstruction.

Why quantize: memory and bandwidth

A trained model is essentially a huge pile of floating-point numbers (weights). Stored by default in 16-bit float (FP16 / BF16), each weight is 2 bytes - a 7-billion-parameter (7B) model needs about 14 GB for weights alone, which ordinary GPUs and RAM simply refuse. Quantization approximates these weights with fewer bits: 8-bit halves it, 4-bit halves it again, dropping memory needs several-fold.

FP16 (original)

2 bytes / weight
7B -> ~14 GB

Q8_0 (8-bit)

~1 byte / weight
7B -> ~7 GB

Q4_0 (4-bit)

~0.56 bytes / weight
7B -> ~3.9 GB

🌍 Big picture
Saving memory is only half of it. The bigger win is bandwidth: as lesson 04 noted, the decode bottleneck is often "moving weights from memory to the compute units". Smaller weights mean fewer bytes to move per generated token, so it is directly faster. Quantization is thus usually a win-win - less memory and more speed - at the cost of just a little accuracy, which large models tolerate quite well.

Why can a big model be squeezed this hard and still work? Because its weights carry a lot of redundancy: each weight's exact value need not be so precise; what the model really relies on is the overall statistical pattern of the weights. Dropping each number from "very precise" to "roughly right" looks lossy one at a time, but across thousands of weights the errors largely cancel, leaving the output almost unaffected. That is why 4-bit quantization often costs only a sliver of quality for several-fold gains in size and speed.

So why is 4-bit the go-to rather than more aggressive 2-bit or safer 8-bit? It is a "sweet spot" question: 8-bit barely loses quality but saves too little; 2-bit and 3-bit save a lot but quality starts to slide noticeably. 4-bit (especially K-quant 4-bit) sits at the knee of the curve - about a quarter the size, only a sliver of quality lost - so it is the most-downloaded tier. Which exact tier still depends on your hardware, your quality tolerance, and the model's own size (bigger models usually withstand aggressive quantization better).

A bit of background: the FP32 / FP16 / BF16 that keep coming up are all floating-point formats, differing in how many bits they use and how they split them between "exponent" and "mantissa". FP32 is 4-byte full precision, common in training; FP16 and BF16 are both 2-byte half precision, with BF16 trading some mantissa precision for the same wide range as FP32, which large models like. Quantization goes a step further, replacing these floats with cheaper integer representations - think of it as "squeezing further down, on top of the half FP16 already saved".

💡 Tip
To scale the intuition up: a 70B model in FP16 needs about 140 GB - several top-end GPUs' worth; quantized to Q4_K_M it needs only around 40 GB, runnable on a single 48 GB card or a big-RAM machine. Quantization is exactly what turned "only a corporate data center can afford it" models into something "an enthusiast can tinker with at home".

Block quantization: one scale per block

So how exactly do we "approximate with fewer bits"? The naive idea: pick one global scale for the whole weight matrix and map the floats proportionally into a small integer range. The problem: weight magnitudes within one matrix can vary a lot, and a single global scale cannot serve both large and small values well, so the error is big.

Block quantization's answer: cut the weights into small blocks (32 weights per block in Q4_0) and give each block its own scale. A block's dynamic range is small, the scale fits tightly, and the approximation is naturally more accurate. This is "divide and conquer" applied to quantization:

Block quantization: 32 float weights -> 1 scale (half-precision) + 32 low-bit integers
original0.12-0.080.21...-0.1532 floats
quantizedd = scale9711...51 scale + 32 4-bit

Another way to see "why a small in-block range is more accurate": quantization approximates continuous values with a few fixed levels, and the narrower the range those levels must cover, the smaller the gap between levels and the finer the quantization. A whole matrix holding both 0.001 and 5.0 forced into 16 levels has coarse gaps; but cut into small blocks, the numbers in each block usually cluster at a similar magnitude, so the same 16 levels resolve them far more finely. That is the root reason block quantization is more accurate.

In code, one Q4_0 "block" is a compact struct (from ggml/src/ggml-common.h):

#define QK4_0 32       // 32 weights per block
typedef struct {
    ggml_half d;            // scale (fp16), one per block
    uint8_t   qs[QK4_0/2];  // 32 weights, each 4-bit, two packed per byte
} block_q4_0;              // 2 + 16 = 18 bytes -> 4.5 bit/weight on average

Do the math: a block of 32 weights uses 2 bytes for the scale (half-precision ggml_half) and 16 bytes for 32 4-bit quants (4 bits each, two packed into a byte), totaling 18 bytes. Per weight that is 18 x 8 / 32 = 4.5 bit - which is why "4-bit quantization" actually takes a bit more than 4 bits: the extra 0.5 bit is the per-block scale, amortized over the block.

Q4_0 one block = 18 bytes, holding 32 weights
layoutd: 2-byte scaleqs: 32 x 4-bit = 16 bytes
total2 + 16 = 18 bytesper weight = 4.5 bit

To use it you "dequantize": restore the stored 4-bit integers to approximate floats. Q4_0's rule is simple (cf. dequantize_row_q4_0 in ggml/src/ggml-quants.c):

# each 4-bit value q in 0..15, restored to a signed weight:
for i in range(32):
    q    = nibble(qs, i)     # 0..15
    x[i] = (q - 8) * d       # subtract 8 to center near 0, then scale by the block's d
🔬 Details / source
The -8 shifts the unsigned 0..15 range to -8..7, placing it symmetrically around 0 (weights are positive and negative); multiplying by d restores the small integer to the original scale. The whole thing has no lookup table and no branches - just a subtract and a multiply - perfect for running fast in bulk on CPU / GPU. That is why a "symmetric quantization" format like Q4_0 is so simple and efficient.

Conversely, how do you get those 4-bit integers from float weights? For symmetric quantization: find the largest-magnitude weight in the block and use it to set the scale; Q4_0's recipe is d = max / -8 (max is the block's signed extreme weight), anchoring that extreme to the end of the range (q=0) so the full -8..7 range is used; then divide each weight by d, round, and add the offset to pack into 0..15. So "quantize" and "dequantize" are inverse operations: quantize with q = round(x/d) + 8, use with x ~= (q-8)*d. The little remainder lost between the two roundings is exactly where quantization error comes from.

🌍 Big picture
Note the scale d itself is stored in half precision (fp16), not further squeezed to an integer - since there is only one scale per block its overhead is tiny, and keeping its precision in fp16 is well worth it because its accuracy directly determines the whole block's reconstruction quality. Zooming out, you see a clear "granularity spectrum": coarsest is one scale for the whole tensor (per-tensor), middle is one per block (per-block, like Q4_0), finest is one per sub-block within a super-block (K-quant). Finer granularity means higher accuracy but more scales to store - the evolution of quantization formats is basically finding better balance points along this line.

A side question: why is the block size often 32? Another trade-off - smaller blocks fit the local scale better and raise accuracy but need more scales, lowering the compression ratio; larger blocks do the reverse. 32 is a practice-tested compromise between accuracy and size, and it also matches common hardware parallel widths, so it computes nicely. K-quant's 256-weight super-block with sub-division then tries to get "both the high compression of big blocks and the high accuracy of small ones".

Push a few real weights through and back, and "lossy but barely" becomes visible:

Tracing one quantization round-trip: 4 weights squeezed to 4-bit and restored - see how small the error is (numbers illustrative).
(1) original weights
0.46-0.120.31-0.40
4 fp16 in a block
find max
set scale
(2) scale d
-0.058
d = max/-8 = 0.46/-8
quantize
round(x/d)+8
(3) 4-bit codes
010315
stored in 0..15
dequant
(q-8)*d
(4) restored
0.46-0.120.29-0.40
error <= 0.02

Q8_0 / K-quant: trading accuracy against compression

Q4_0 is just one member of the quantization family. The same "block + scale" idea, with different parameters, gives different tiers:

Typebits / weightblock / super-blockcharacter
Q8_0~8.5 bit32closest to FP16, large, most stable quality
Q4_0~4.5 bit32lightest and fastest, more visible accuracy loss
Q4_K~4.5 bitsuper-block 256per-sub-block scales + mixed precision, more accurate at the same bits

Q8_0 stores each weight in 8 bits (block still 32, struct is d + 32 int8), ~8.5 bit/weight - large but closest to the original FP16, used where quality matters most. Q4_0 is lightest at 4.5 bit, fast and memory-saving, but with more visible accuracy loss.

And K-quant (the ones with K, like Q4_K, Q5_K) is a smarter tier: it uses a larger "super-block" (QK_K = 256 weights), splits it into several sub-blocks each with its own finer scale and min, and even mixes bit-widths across tensors. The result: at the same average bits, K-quant's perplexity (a measure of prediction quality, lower is better) is usually clearly lower than the matching Q4_0 / Q5_0. The GGUF files people download today are mostly K-quant tiers like Q4_K_M.

Read these names together and a pattern emerges: letter Q + bit count + optional K + optional tier suffix. Q4_0 is "4-bit, symmetric, basic block", Q4_K_M is "4-bit, K-quant, medium mixed precision", Q8_0 is "8-bit, symmetric, basic block". Next time you see a long list of Q3_K_S, Q5_K_M, Q6_K on a download page, you can read off roughly how big and how accurate each is.

typedef struct {
    ggml_half d;          // super-block overall scale
    ggml_half dmin;       // super-block overall min (asymmetric)
    uint8_t scales[...];  // finer per-sub-block scale/min (6-bit, quantized)
    uint8_t qs[...];      // quants
} block_q4_K;            // QK_K = 256, simplified from ggml-common.h

Two more things worth knowing. First, llama.cpp mainly quantizes "weights"; the "activations" flowing during inference are usually still computed in higher precision (fp16/fp32) - weights are static and dominate memory, so they are most worth compressing, while activations are dynamic and over-quantizing them hurts accuracy more easily. Second, not every tensor uses the same tier: high-impact tensors like the embedding and output projection are often deliberately kept at higher bit-widths - exactly the "mixed precision" the K-quant _M / _L tiers do.

💡 Tip
So where do these quantized files come from? The flow is direct: first export the original model to a high-precision GGUF (usually fp16) with the conversion script, then "compress" it to the target tier with the llama-quantize tool, e.g. llama-quantize model-f16.gguf model-Q4_K_M.gguf Q4_K_M. Quantization is a one-time offline operation; you get a smaller GGUF and run that small file every time afterwards - so you pay the quantization cost once at "manufacture", and only enjoy the savings and speed at runtime.

How do you measure "how much quality quantization cost"? The most common metric is perplexity: take a standard text and see how well the model predicts the "next word" - lower is better. A common community exercise is to run perplexity across a model's quant tiers and tabulate them - you will see Q8_0 nearly matching fp16, Q4_K_M only a hair higher, and Q2_K clearly higher. The llama-perplexity tool does exactly this, and Part 5 (L30) covers using it to "grade" quantization. A rule of thumb: pick one tier higher whenever memory allows, for safer quality.

Finally, a boundary to clarify: quantization only compresses the weights' representation (how many bits each number uses); it does not change the model's structure - layer count, dimensions, parameter count all stay. That makes it a different path from pruning (removing some weights/neurons) and distillation (training a smaller new model to mimic a big one). Quantization's big advantage is being nearly free and plug-and-play: no retraining, one command shrinks and speeds up an existing model - which is why it is so ubiquitous in local inference.

⚠ Heads-up
Quantization's loss is not uniform across tasks. Forgiving tasks like chat and free continuation barely show a difference at 4-bit; but code generation, math reasoning, long chains of logic - where one wrong step cascades - are more sensitive to precision and show regressions more readily under aggressive quantization. So it is not odd that the same Q4 model feels great chatting but occasionally trips on complex code - bumping up a tier (Q5_K, Q6_K, even Q8_0) often recovers a lot.

Another practical detail: different hardware backends support and optimize quant formats to different degrees. The same Q4_K model may perform quite differently dequantizing via SIMD on CPU versus a dedicated kernel on CUDA. So "which tier" sometimes also depends on what hardware you will run on - Part 6 on kernels gets concrete about this. Overall, the mainstream Q4_K / Q8_0 are well supported on all backends, so you can pick them blind without much risk.

🌍 Big picture
Finally, tying this lesson to lessons 02 and 05: a quantized GGUF carries, for each weight tensor, its own type (from lesson 05) - some marked Q4_K, some maybe Q6_K or F16. At load time, llama-model-loader reads and dequantizes per tensor type; at compute time, ggml's operators (like matmul) can consume quantized weights directly, dequantizing on the fly in the inner loop, skipping the cost of fully restoring to fp32 first. So "quantization" is not an isolated step but a full chain across storage (GGUF), loading (loader), and computation (ggml operators).

Going deeper (optional)

Three questions below; open them if you want depth, skip them if you only want the main line.

1 What do the numbers and the 0/1 in Q4_0 / Q4_1 / Q8_0 mean? click to expand

The number is bits per weight: Q4 = 4-bit, Q8 = 8-bit. The suffix _0 / _1 distinguishes the quantization's "symmetry".

_0 is symmetric - it stores only a scale with a fixed zero point (like Q4_0's (q-8)*d above); _1 is asymmetric - it stores an extra minimum (offset), so the formula becomes q*d + min. The extra min lets it fit weight distributions that are "not centered on 0", giving slightly better accuracy at a few extra bytes per block. Whether to keep that min is exactly the _0 vs _1 difference.

2 Why is K-quant (Q4_K_M etc.) more accurate? click to expand

The key is finer-grained scales. Plain Q4_0 is "32 weights share 1 scale"; K-quant uses a 256-weight super-block but cuts it into several sub-blocks, each with its own scale and min (these sub-block scales are themselves quantized to 6 bits to save space). Finer granularity means the scale fits the local data better, so error shrinks.

The _S / _M / _L (small / medium / large) in the name are different "mixed-precision" tiers: use slightly higher bits for the model's more important layers and lower bits for the rest, balancing size and accuracy differently. So even labeled "4-bit", Q4_K_M is often both more accurate and only slightly larger than Q4_0 - which is why it became the mainstream download format.

3 What is imatrix, and how does it relate to quantization? click to expand

imatrix (importance matrix) is often misread as "deciding how many bits each weight gets" - it is not. It runs the model over a batch of calibration data to measure "how much each weight influences the final output".

During quantization, more important weights are kept with smaller quantization error (the scale choice and rounding lean toward preserving them). In other words, imatrix changes how the error is distributed, spending precious precision where it matters, while not changing the bit-width allocation itself. It is produced by the llama-imatrix tool and fed to llama-quantize, usually lowering perplexity further without increasing size.

✅ Key points
  • Quantization = approximate weights with fewer bits, saving memory and bandwidth and gaining speed, at a small accuracy cost (large models tolerate it well).
  • Block quantization: cut weights into blocks, one scale per block; Q4_0 has 32 weights/block = 2-byte scale + 16 bytes of quants = 18 bytes = 4.5 bit/weight.
  • Dequantization is just x = (q - 8) * d (Q4_0 symmetric quantization).
  • Q8_0 accurate but large, Q4_0 light but coarse, K-quant (super-block + per-sub-block scales + mixed precision) more accurate at the same bits and is today's mainstream.
  • imatrix is error weighting, not bit-width allocation.
💡 Design insight
"One scale per block" - that single plain idea turns the global problem of "floats have too wide a dynamic range" into countless local problems of "a block's range is small", so even 4 bits can hold usable accuracy. That large models can walk out of the data center and onto your laptop owes much to this one move. Remember one line: quantization does not make the model dumber; it squeezes out the waste of "over-precision" - using just enough bits to hold the information the model actually needs.

🧪 Self-test - think about the design

1. Why is block quantization (one scale per block) more accurate than one scale for the whole tensor?
  1. Because it loses no data at all
  2. Because it uses more bits
  3. Because the compression ratio is higher
  4. Each block has its own scale, so its dynamic range is smaller and the approximation error is smaller
Show answer & explanation click to expand
Answer: D. One global scale cannot serve large and small values at once; per-block scales fit local ranges tightly, so error shrinks.
2. About how many bits per weight does Q4_0 use? (per 32-weight block = 2-byte scale + 16 bytes of quants)
  1. Exactly 4 bit
  2. About 4.5 bit
  3. 2 bit
  4. 8 bit
Show answer & explanation click to expand
Answer: B. (2 + 16) bytes x 8 / 32 = 4.5 bit; the extra 0.5 bit is the per-block scale amortized over the block.
3. What role does imatrix (importance matrix) play in quantization?
  1. It is itself a new quantization format
  2. It decides how many bits each weight gets
  3. It uses calibration data to weight quantization error by weight importance, keeping important weights more accurate; it does not change bit-width allocation
  4. It increases the model's memory footprint
Show answer & explanation click to expand
Answer: C. imatrix changes how error is distributed, spending precision on important weights; bit-width is set by the quant tier (e.g. Q4_K), independent of imatrix.
💭 Open questions (no single right answer - just think or try)
  • At roughly 4 bits, why does Q4_K usually have lower perplexity than Q4_0? (hint: super-block and per-sub-block scales)