Lesson 22 of 73
k-quants, i-quants, and the Importance Matrix
A model file is a very long list of numbers called weights. Quantization stores each weight with fewer bits so the file is smaller and fits in memory. The price is a small rounding error in every weight. Building Q8_0 and Q4_0 Like ggml showed the simplest scheme: cut the weights into blocks of 32 and give every block its own scale, which is the number that converts small integer codes back into real weights.
This lesson covers the formats that most downloaded GGUF files use today. Names such as Q4_K_M and IQ4_XS look like codes, but each part has a meaning. The lesson explains how those formats shrink the cost of scales, how one file can use different formats for different parts of the model, how the quantizer can be told which weights matter most, and how the newer i-quants squeeze weights into 2 or 3 bits.
After the lesson, you can read a quant name, predict the file size for a given model, explain why two files that are both called "4-bit" differ in quality, and pick a file for a given amount of memory.
| Idea | What it means in plain words |
|---|---|
| Super-block | A group of 256 weights that shares one or two FP16 numbers (16-bit decimal numbers) |
| Sub-block scale | A small integer (4, 6, or 8 bits) for each 16 or 32 weights, multiplied by the super-block number to give the real scale |
| Quant mix | One file that uses different formats for different tensors (the _S, _M, and _L suffixes) |
| Importance matrix (imatrix) | A list that says how large the inputs to each weight usually are, so the quantizer protects the weights that matter most |
| Non-linear grid | 16 fixed levels that sit closer together near zero (IQ4_NL, IQ4_XS) |
| Codebook | A table of 8-value patterns. Each group of 8 weights stores only the position of its closest pattern (IQ2_*, IQ3_*) |
Layouts, block sizes, and constants come from ggml/src/ggml-common.h, ggml/src/ggml-quants.c, and src/llama-quant.cpp at the pinned llama.cpp commit, and from gguf-py 0.19.0 constants.py. ggml is the tensor library inside llama.cpp, and a tensor is a table of numbers. Every Java program uses seeded random weights (the random generator always starts from the same number), so the output is the same on every run.
Two measuring terms appear throughout. Bits per weight (bpw) is the file size in bits divided by the number of weights, so it includes the cost of scales. RMSE (root mean squared error) is the typical size of the difference between the original weight and the weight after rounding. A lower RMSE means a more faithful copy.
Example 1: two-level scales and what they save
Every 4-bit format stores each weight as a code from 0 to 15, which takes half a byte. The code alone is not enough, because the reader also needs to know what real number each code stands for. That information is the scale (and sometimes a minimum), and storing it costs extra bits. The problem in this example is how to keep 4-bit codes but pay less for the scales and minimums.
The program counts the bytes for four layouts that all store 4-bit codes. Look at the last two numbers on each line: the total bits per weight and the part of that total spent on scales.
import java.util.Locale;
public class SuperBlockStorage {
static void row(String name, int bytes, int weights, int scaleBytes) {
System.out.println(String.format(Locale.ROOT, "%-34s %4d bytes / %3d weights = %.4f bpw (scales %.4f)",
name, bytes, weights, bytes * 8.0 / weights, scaleBytes * 8.0 / weights));
}
public static void main(String[] args) {
// 4-bit codes take 0.5 byte per weight in every layout below.
row("Q4_0: FP16 scale per 32", 2 + 16, 32, 2);
row("Q4_1: FP16 scale + min per 32", 2 + 2 + 16, 32, 4);
row("flat: FP16 scale + min per 32, x8", 8 * (2 + 2) + 128, 256, 32);
row("Q4_K: 6-bit scale + min per 32", 2 + 2 + 12 + 128, 256, 16);
System.out.println("6-bit pairs: 8 sub-blocks x 12 bits = " + (8 * 12) + " bits = " + (8 * 12 / 8) + " bytes");
}
}Q4_0: FP16 scale per 32 18 bytes / 32 weights = 4.5000 bpw (scales 0.5000)
Q4_1: FP16 scale + min per 32 20 bytes / 32 weights = 5.0000 bpw (scales 1.0000)
flat: FP16 scale + min per 32, x8 160 bytes / 256 weights = 5.0000 bpw (scales 1.0000)
Q4_K: 6-bit scale + min per 32 144 bytes / 256 weights = 4.5000 bpw (scales 0.5000)
6-bit pairs: 8 sub-blocks x 12 bits = 96 bits = 12 bytesAll four layouts spend 0.5 byte per weight on the 4-bit codes. They differ only in what the scales cost. Here is how to read each line.
Q4_0stores one FP16 scale (2 bytes, which is 16 bits) for every 32 weights. The overhead is 16 bits / 32 weights = 0.5 bits per weight. The total is 4 + 0.5 = 4.5 bpw.Q4_1adds an FP16 minimum. A minimum lets the 16 levels cover a range that does not sit centered on zero, for example from -0.1 to 0.4. The overhead doubles to 1 bpw.- The "flat" row is eight
Q4_1blocks side by side. Putting them in a bigger group saves nothing, because each block still has its own two FP16 numbers. Q4_Kkeeps a scale and a minimum for each 32-value sub-block, but stores them as 6-bit integers instead of FP16. Two FP16 numbers per 256 weights, calleddanddmin, turn those integers back into real values: sub-block scale =d * lsand sub-block minimum =dmin * lm, wherelsandlmare the stored 6-bit integers.
A small case shows why this saves space. Suppose a sub-block needs the scale 0.0123. Instead of storing 0.0123 as a 16-bit number, Q4_K stores ls = 40 in 6 bits. If d is 0.0003075, then d * ls = 0.0123. Eight sub-blocks share the same d, so the cost of the FP16 number is spread over 256 weights instead of 32.
A super-block is the group of 256 weights (QK_K = 256 in ggml-common.h) that shares d and dmin. This is quantization applied twice: first the weights are quantized with per-sub-block scales, then the scales themselves are quantized with a per-super-block scale. The result has the flexibility of Q4_1 (a scale and a minimum for every 32 weights) at the cost of Q4_0 (4.5 bpw).
super-block of 256 weights
d, dmin 2 FP16 numbers, shared by all 8 sub-blocks
sub-block 0 32 weights ls0, lm0 (6 bits each)
sub-block 1 32 weights ls1, lm1
...
sub-block 7 32 weights ls7, lm7Example 2: the Q4_K block layout and its 12 scale bytes
The previous example said that a Q4_K block stores eight 6-bit scales and eight 6-bit minimums. That is sixteen 6-bit numbers, and the question is how they fit into a clean number of bytes. block_q4_K in ggml-common.h is a structure with four parts: d (FP16), dmin (FP16), scales[12] (12 bytes), and qs[128] (128 bytes of 4-bit codes).
Sixteen numbers of 6 bits is 96 bits, which is exactly 12 bytes. The packing is not a plain stream of bits, though, because ggml wants the unpacking to be fast. The program packs the numbers exactly as quantize_row_q4_K_ref does and unpacks them with a copy of get_scale_min_k4, the ggml function that reads them back. Watch that every pair comes back unchanged.
public class Q4KScales {
// Packs 8 six-bit scales and 8 six-bit mins into 12 bytes, as quantize_row_q4_K_ref does.
static byte[] pack(int[] ls, int[] lm) {
int[] s = new int[12];
for (int j = 0; j < 8; j++) {
if (j < 4) {
s[j] = ls[j];
s[j + 4] = lm[j];
} else {
s[j + 4] = (ls[j] & 0xF) | ((lm[j] & 0xF) << 4);
s[j - 4] |= (ls[j] >> 4) << 6;
s[j] |= (lm[j] >> 4) << 6;
}
}
byte[] out = new byte[12];
for (int i = 0; i < 12; i++) out[i] = (byte) s[i];
return out;
}
// get_scale_min_k4 from ggml-quants.c
static int[] unpack(int j, byte[] b) {
int[] q = new int[12];
for (int i = 0; i < 12; i++) q[i] = b[i] & 0xFF;
if (j < 4) return new int[] {q[j] & 63, q[j + 4] & 63};
return new int[] {(q[j + 4] & 0xF) | ((q[j - 4] >> 6) << 4), (q[j + 4] >> 4) | ((q[j] >> 6) << 4)};
}
public static void main(String[] args) {
int[] ls = {63, 40, 12, 5, 33, 50, 17, 1};
int[] lm = {0, 7, 20, 63, 31, 2, 45, 9};
byte[] packed = pack(ls, lm);
StringBuilder hex = new StringBuilder();
for (byte v : packed) hex.append(String.format("%02x ", v & 0xFF));
System.out.println("12 scale bytes: " + hex.toString().trim());
for (int j = 0; j < 8; j++) {
int[] sm = unpack(j, packed);
System.out.println("sub-block " + j + ": scale " + sm[0] + ", min " + sm[1]);
}
System.out.println("offsets: d=0 dmin=2 scales=4 qs=16 end=" + (16 + 128));
}
}12 scale bytes: bf e8 4c 05 40 07 94 3f f1 22 d1 91
sub-block 0: scale 63, min 0
sub-block 1: scale 40, min 7
sub-block 2: scale 12, min 20
sub-block 3: scale 5, min 63
sub-block 4: scale 33, min 31
sub-block 5: scale 50, min 2
sub-block 6: scale 17, min 45
sub-block 7: scale 1, min 9
offsets: d=0 dmin=2 scales=4 qs=16 end=144Sixteen 6-bit numbers are 96 bits, which is exactly 12 bytes, so nothing is wasted. The layout works like this:
- Sub-blocks 0 to 3 put their scale in the low 6 bits of bytes 0 to 3 and their minimum in the low 6 bits of bytes 4 to 7. Each of those bytes still has 2 unused bits at the top.
- Sub-blocks 4 to 7 put the low 4 bits of the scale and the low 4 bits of the minimum together in bytes 8 to 11. Their top 2 bits go into the unused top 2 bits of bytes 0 to 7.
Byte 0 in the output is bf, which is 1011 1111 in binary. The low 6 bits, 111111, are 63, the scale of sub-block 0. The top 2 bits, 10, are the high bits of sub-block 4's scale. The number 33 is 100001 in 6 bits, so its low 4 bits are 0001 and its high 2 bits are 10. Byte 8 is f1: the low half 1 is the low 4 bits of the scale 33, and the high half f (1111) is the low 4 bits of the minimum 31. The unpacker reverses all of this, and the eight printed pairs match the inputs.
The offsets line describes the whole block. d is at byte 0, dmin at byte 2, the scales at byte 4, and the 128 bytes of 4-bit codes at byte 16, for 144 bytes in total. The 4-bit codes are also arranged differently from Q4_0. Each group of 32 bytes holds 64 weights, with weight j in the low nibble (the low 4 bits of a byte) and weight j + 32 in the high nibble. Anyone reading the raw bytes must follow that order.
Example 3: a simplified Q4_K quantizer against Q4_0
The extra flexibility of Q4_K should help, but the claim needs a measurement. The program quantizes 4,096 random weights with a bell-shaped (Gaussian) distribution and a standard deviation of 0.02, which is a typical size for the weights of a trained model. It uses the Q4_0 method from Building Q8_0 and Q4_0 Like ggml and a simplified Q4_K method. Both formats cost 4.5 bpw, so any difference in error comes from the design alone.
import java.util.Locale;
import java.util.Random;
public class SimpleQ4K {
static float f16(float x) { return Float.float16ToFloat(Float.floatToFloat16(x)); }
static float[] q4_0(float[] x) { // lesson 18
float[] out = new float[x.length];
for (int b = 0; b < x.length; b += 32) {
float amax = 0, max = 0;
for (int j = 0; j < 32; j++) if (Math.abs(x[b + j]) > amax) { amax = Math.abs(x[b + j]); max = x[b + j]; }
float d = max / -8f, id = d != 0 ? 1f / d : 0f, dq = f16(d);
for (int j = 0; j < 32; j++) out[b + j] = (Math.min(15, (int) (x[b + j] * id + 8.5f)) - 8) * dq;
}
return out;
}
static float[] q4_K(float[] x) { // simplified: no scale search
float[] out = new float[x.length];
for (int sb = 0; sb < x.length; sb += 256) {
float[] scale = new float[8], min = new float[8];
float maxScale = 0, maxMin = 0;
for (int s = 0; s < 8; s++) {
float lo = x[sb + 32 * s], hi = lo;
for (int j = 0; j < 32; j++) { lo = Math.min(lo, x[sb + 32 * s + j]); hi = Math.max(hi, x[sb + 32 * s + j]); }
lo = Math.min(lo, 0f);
scale[s] = (hi - lo) / 15f;
min[s] = -lo;
maxScale = Math.max(maxScale, scale[s]);
maxMin = Math.max(maxMin, min[s]);
}
float d = f16(maxScale / 63f), dmin = f16(maxMin / 63f);
for (int s = 0; s < 8; s++) {
int ls = Math.min(63, (int) Math.rint(scale[s] * 63f / maxScale));
int lm = maxMin > 0 ? Math.min(63, (int) Math.rint(min[s] * 63f / maxMin)) : 0;
float ds = d * ls, ms = dmin * lm;
for (int j = 0; j < 32; j++) {
int i = sb + 32 * s + j;
int q = ds == 0 ? 0 : Math.max(0, Math.min(15, (int) Math.rint((x[i] + ms) / ds)));
out[i] = ds * q - ms;
}
}
}
return out;
}
static double rmse(float[] a, float[] b) {
double s = 0;
for (int i = 0; i < a.length; i++) s += (a[i] - b[i]) * (a[i] - b[i]);
return Math.sqrt(s / a.length);
}
public static void main(String[] args) {
Random rnd = new Random(42);
float[] w = new float[4096];
for (int i = 0; i < w.length; i++) w[i] = (float) (rnd.nextGaussian() * 0.02);
System.out.println(String.format(Locale.ROOT, "Q4_0 (4.5 bpw) RMSE %.6f", rmse(w, q4_0(w))));
System.out.println(String.format(Locale.ROOT, "Q4_K simplified (4.5 bpw) RMSE %.6f", rmse(w, q4_K(w))));
}
}Q4_0 (4.5 bpw) RMSE 0.001662
Q4_K simplified (4.5 bpw) RMSE 0.001543The simplified Q4_K method follows the structure of quantize_row_q4_K_ref:
- For each 32-value sub-block, take the minimum (never above zero) and the maximum. The sub-block scale is
(max - min) / 15, so codes 0 to 15 span the whole range. - Find the largest sub-block scale and the largest minimum in the super-block. Then
d = maxScale / 63anddmin = maxMin / 63, rounded to FP16. - Store each sub-block scale as the 6-bit integer
round(scale * 63 / maxScale), and each minimum the same way. - Quantize each weight with the quantized scale and minimum, so the codes match what a reader will reconstruct later.
The output shows a lower error for Q4_K (0.001543 against 0.001662, about 7 percent less). The reason is that each sub-block uses all 16 levels between its own minimum and maximum. Q4_0 centers its grid on zero and derives the step from the largest magnitude, so when a block leans to one side, part of the grid goes unused.
The real quantize_row_q4_K_ref adds a search on top of this. Its helper make_qkx2_quants tries 21 candidate scales around the first guess (rmin = -1, rdelta = 0.1, nstep = 20). For each candidate it refits the scale and minimum with weighted least squares (a standard way to find the numbers that give the smallest weighted error), and it keeps the candidate with the lowest weighted error. The weights are av_x + |x|, where av_x is the root mean square of the sub-block, so large weights count more. Example 8 shows the idea of a weighted scale search on 8 values.
Example 4: the k-quant family: sizes from gguf-py block sizes
Q4_K is one member of a family: Q2_K, Q3_K, Q4_K, Q5_K, Q6_K, and Q8_K. The number is the main bit width and the K marks the super-block design. This example answers how many bits per weight each member really costs.
The program computes the bytes in one block with the same formulas as GGML_QUANT_SIZES in gguf-py constants.py, then prints bits per weight. Compare the bpw column with the nominal number in the type name.
import java.util.List;
import java.util.Locale;
public class KQuantSizes {
record QType(String name, int id, int blockSize, int typeSize, String layout) {
double bpw() { return typeSize * 8.0 / blockSize; }
}
public static void main(String[] args) {
int qk = 256;
List<QType> types = List.of(
new QType("Q2_K", 10, qk, 2 + 2 + qk / 16 + qk / 4, "16 x 16, 4-bit scale+min"),
new QType("Q3_K", 11, qk, 2 + qk / 4 + qk / 8 + 12, "16 x 16, 6-bit scale"),
new QType("Q4_K", 12, qk, 2 + 2 + qk / 2 + 12, "8 x 32, 6-bit scale+min"),
new QType("Q5_K", 13, qk, 2 + 2 + qk / 2 + qk / 8 + 12, "8 x 32, 6-bit scale+min"),
new QType("Q6_K", 14, qk, 2 + qk / 2 + qk / 4 + qk / 16, "16 x 16, 8-bit scale"),
new QType("Q8_K", 15, qk, 4 + qk + qk / 8, "activations only"));
System.out.println("type id block bytes bpw sub-blocks");
for (QType t : types)
System.out.println(String.format(Locale.ROOT, "%-5s %3d %5d %5d %6.4f %s",
t.name(), t.id(), t.blockSize(), t.typeSize(), t.bpw(), t.layout()));
}
}type id block bytes bpw sub-blocks
Q2_K 10 256 84 2.6250 16 x 16, 4-bit scale+min
Q3_K 11 256 110 3.4375 16 x 16, 6-bit scale
Q4_K 12 256 144 4.5000 8 x 32, 6-bit scale+min
Q5_K 13 256 176 5.5000 8 x 32, 6-bit scale+min
Q6_K 14 256 210 6.5625 16 x 16, 8-bit scale
Q8_K 15 256 292 9.1250 activations onlyThe type ids are the numbers stored in a GGUF tensor info record to say which format a tensor uses (The GGUF Format reads them). Each type has a block size of 256 and a different byte count. The bpw column is the byte count times 8, divided by 256. In every row it is higher than the number in the name, because scales and minimums cost bits too. The table lists what one block holds.
| Type | What one block holds |
|---|---|
Q2_K |
16 bytes of 4-bit scale and 4-bit minimum pairs, 64 bytes of 2-bit codes, FP16 d and dmin |
Q3_K |
32 bytes of high bits (hmask), 64 bytes of low 2 bits, 12 bytes of 6-bit scales, FP16 d |
Q4_K |
as in Example 2 |
Q5_K |
Q4_K plus 32 bytes holding the fifth bit of every weight |
Q6_K |
128 bytes of low 4 bits, 64 bytes of high 2 bits, 16 signed 8-bit scales, FP16 d |
Q8_K |
FP32 scale, 256 int8 codes, 16 int16 sums of groups of 16 |
Q3_K and Q6_K have no minimum: their codes are symmetric around zero, like Q4_0. Q2_K, Q4_K, and Q5_K have minimums. The comments in ggml-common.h give the same numbers as the output: "Effectively 2.625 bits per weight" for Q2_K, 3.4375 for Q3_K, 4.5 for Q4_K, 5.5 for Q5_K, and 6.5625 for Q6_K.
Q8_K is not a storage format for weights. During inference (running the model to produce text), the CPU backend converts the activations to Q8_K before it multiplies them with k-quant weights. An activation is a number computed while the model runs, as opposed to a weight, which is stored in the file. The stored sums of each 16 codes (bsums) let the dot product apply the weights' sub-block minimums and offsets with one multiplication per 16 activations instead of one per value.
Example 5: quant mixes: what _S, _M, and _L change per tensor
A model is made of many tensors. A tensor here is one table of weights, such as the matrix that produces the attention values in layer 5. Not all tensors are equally sensitive to rounding. A file called Q4_K_M is therefore not all Q4_K: the letters S, M, and L (small, medium, large) tell llama-quantize to give some tensors more bits than the base type.
The program copies a subset of llama_tensor_get_type_impl from src/llama-quant.cpp, the function that picks a type for each tensor. It does this for a Llama-style model with 32 layers and prints one digit per layer: 4 for Q4_K, 6 for Q6_K, and so on. Read each row as a strip of 32 layers from first to last.
The tensor names matter here. attn_v is the attention tensor that produces values, ffn_down is the last matrix of the feed-forward part of a layer, attn_q produces attention queries, and output is the final matrix that turns the model's internal state into scores for every token.
public class QuantMixRules {
// A subset of llama_tensor_get_type_impl for a Llama-style model:
// grouped-query attention with 4 query heads per KV head, no experts, rows divisible by 256.
static boolean useMoreBits(int i, int n) { return i < n / 8 || i >= 7 * n / 8 || (i - n / 8) % 3 == 2; }
static String type(String ftype, String tensor, int i, int n) {
String base = ftype.substring(0, 4); // Q3_K, Q4_K, Q5_K, Q6_K
if (tensor.equals("output")) return "Q6_K";
switch (tensor) {
case "attn_v" -> {
if (ftype.equals("Q3_K_M")) return i < 2 ? "Q5_K" : "Q4_K";
if (ftype.equals("Q3_K_L")) return "Q5_K";
if ((ftype.equals("Q4_K_M") || ftype.equals("Q5_K_M")) && useMoreBits(i, n)) return "Q6_K";
if (ftype.equals("Q4_K_S") && i < 4) return "Q5_K";
}
case "ffn_down" -> {
if (ftype.equals("Q3_K_M")) return i < n / 16 ? "Q5_K" : "Q4_K";
if (ftype.equals("Q3_K_L")) return "Q5_K";
if ((ftype.equals("Q4_K_M") || ftype.equals("Q5_K_M")) && useMoreBits(i, n)) return "Q6_K";
if (ftype.equals("Q4_K_S") && i < n / 8) return "Q5_K";
}
case "attn_output" -> {
if (ftype.equals("Q3_K_M")) return "Q4_K";
if (ftype.equals("Q3_K_L")) return "Q5_K";
}
default -> { }
}
return base;
}
public static void main(String[] args) {
int n = 32;
for (String ftype : new String[] {"Q3_K_M", "Q4_K_S", "Q4_K_M", "Q5_K_M"}) {
for (String tensor : new String[] {"attn_v", "ffn_down", "attn_q"}) {
StringBuilder line = new StringBuilder();
for (int i = 0; i < n; i++) line.append(type(ftype, tensor, i, n).charAt(1));
System.out.printf("%-6s %-8s %s%n", ftype, tensor, line);
}
}
System.out.println("output tensor: " + type("Q4_K_M", "output", 0, n));
}
}Q3_K_M attn_v 55444444444444444444444444444444
Q3_K_M ffn_down 55444444444444444444444444444444
Q3_K_M attn_q 33333333333333333333333333333333
Q4_K_S attn_v 55554444444444444444444444444444
Q4_K_S ffn_down 55554444444444444444444444444444
Q4_K_S attn_q 44444444444444444444444444444444
Q4_K_M attn_v 66664464464464464464464464466666
Q4_K_M ffn_down 66664464464464464464464464466666
Q4_K_M attn_q 44444444444444444444444444444444
Q5_K_M attn_v 66665565565565565565565565566666
Q5_K_M ffn_down 66665565565565565565565565566666
Q5_K_M attn_q 55555555555555555555555555555555
output tensor: Q6_KThe name before the suffix sets the default type (llama_ftype_get_default_type): Q3_K_S, Q3_K_M, and Q3_K_L all start from Q3_K, and Q4_K_S and Q4_K_M start from Q4_K. The suffix then raises the type of selected tensors. The attn_q rows show an unchanged default: every layer keeps the base type. The attn_v and ffn_down rows show the raised types.
| Mix | attn_v |
ffn_down |
attn_output |
output |
|---|---|---|---|---|
Q3_K_S |
Q3_K |
Q3_K |
Q3_K |
Q6_K |
Q3_K_M |
Q5_K in layers 0 and 1, then Q4_K |
Q5_K in the first 1/16 of layers, then Q4_K |
Q4_K |
Q6_K |
Q3_K_L |
Q5_K |
Q5_K |
Q5_K |
Q6_K |
Q4_K_S |
Q5_K in the first 4 |
Q5_K in the first 1/8 |
Q4_K |
Q6_K |
Q4_K_M |
Q6_K where use_more_bits |
Q6_K where use_more_bits |
Q4_K |
Q6_K |
Q5_K_M |
Q6_K where use_more_bits |
Q6_K where use_more_bits |
Q5_K |
Q6_K |
use_more_bits(i, n) decides whether layer i of n layers gets the larger type. It is true for the first eighth of the layers, the last eighth, and every third layer in between: i < n/8 || i >= 7*n/8 || (i - n/8) % 3 == 2. For 32 layers, n/8 is 4, so layers 0 to 3 and 28 to 31 qualify (8 layers), and the middle rule adds layers 6, 9, 12, 15, 18, 21, 24, and 27 (8 more). That is 16 of 32 layers, which matches the 666644644... pattern in the output. Tensors that are not listed in the table (attn_q, attn_k, ffn_gate, ffn_up, token_embd) keep the default type, as the attn_q rows show.
The real function has more branches than this copy. Some depend on the architecture (Falcon is one family), on mixture-of-experts models with 8 experts, on grouped-query attention (n_gqa() >= 4 moves attn_v to Q4_K for Q2_K), and on whether an importance matrix is present. Two rules apply to every type:
- Tensors with fewer than 2 dimensions (norm weights and biases) are never quantized.
- A tensor whose row length is not a multiple of the type's block size falls back to a type with a smaller block (
Q4_KtoQ5_0,Q6_KtoQ8_0).
Making, Quantizing, and Testing Your Own GGUF shows that fallback in a real llama-quantize log.
Example 6: the average bits per weight of a mixed model
Because a mix uses several types, the real size of a file is not the size of its base type. This example computes the size of an 8B model (8 billion weights) at every mix. The program applies the Example 5 rules to each tensor of a model with the Llama 3 8B shapes, adds up the bits, and divides by the number of weights.
The shapes are: hidden size 4096 (the width of the model's internal vectors), FFN size 14336 (the width inside the feed-forward part), 32 layers, 8 KV heads of 128 dimensions (the groups used by grouped-query attention), a vocabulary of 128256 tokens, and separate input and output embeddings (the tables that turn a token into a vector and back). The norm tensors stay in F32 because they are never quantized.
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
public class MixedModelSize {
static boolean useMoreBits(int i, int n) { return i < n / 8 || i >= 7 * n / 8 || (i - n / 8) % 3 == 2; }
static String type(String ftype, String tensor, int i, int n) { // same rules as Example 5
if (ftype.equals("Q8_0")) return "Q8_0";
if (tensor.equals("output")) return "Q6_K";
boolean m = ftype.equals("Q4_K_M") || ftype.equals("Q5_K_M");
if (tensor.equals("attn_v")) {
if (ftype.equals("Q3_K_M")) return i < 2 ? "Q5_K" : "Q4_K";
if (ftype.equals("Q3_K_L")) return "Q5_K";
if (m && useMoreBits(i, n)) return "Q6_K";
if (ftype.equals("Q4_K_S") && i < 4) return "Q5_K";
} else if (tensor.equals("ffn_down")) {
if (ftype.equals("Q3_K_M")) return i < n / 16 ? "Q5_K" : "Q4_K";
if (ftype.equals("Q3_K_L")) return "Q5_K";
if (m && useMoreBits(i, n)) return "Q6_K";
if (ftype.equals("Q4_K_S") && i < n / 8) return "Q5_K";
} else if (tensor.equals("attn_output")) {
if (ftype.equals("Q3_K_M")) return "Q4_K";
if (ftype.equals("Q3_K_L")) return "Q5_K";
}
return ftype.substring(0, 4);
}
static final Map<String, Double> BPW = Map.of("Q3_K", 3.4375, "Q4_K", 4.5, "Q5_K", 5.5, "Q6_K", 6.5625, "Q8_0", 8.5);
public static void main(String[] args) {
// Llama 3 8B shapes: hidden 4096, FFN 14336, 32 layers, 8 KV heads of 128, vocabulary 128256.
long d = 4096, ff = 14336, kv = 1024, vocab = 128256; int n = 32;
Map<String, Long> perLayer = new LinkedHashMap<>();
perLayer.put("attn_q", d * d); perLayer.put("attn_k", d * kv); perLayer.put("attn_v", d * kv);
perLayer.put("attn_output", d * d); perLayer.put("ffn_gate", d * ff); perLayer.put("ffn_up", d * ff);
perLayer.put("ffn_down", d * ff);
long norms = (2L * n + 1) * d; // F32, never quantized
for (String ftype : new String[] {"Q3_K_S", "Q3_K_M", "Q3_K_L", "Q4_K_S", "Q4_K_M", "Q5_K_S", "Q5_K_M", "Q6_K", "Q8_0"}) {
double bits = norms * 32.0;
long params = norms;
bits += vocab * d * BPW.get(type(ftype, "token_embd", 0, n)) + vocab * d * BPW.get(type(ftype, "output", 0, n));
params += 2 * vocab * d;
for (int i = 0; i < n; i++)
for (var e : perLayer.entrySet()) {
bits += e.getValue() * BPW.get(type(ftype, e.getKey(), i, n));
params += e.getValue();
}
System.out.println(String.format(Locale.ROOT, "%-7s %.3f bpw %.2f GiB (%d params)",
ftype, bits / params, bits / 8 / (1L << 30), params));
}
}
}Q3_K_S 3.643 bpw 3.41 GiB (8030261248 params)
Q3_K_M 3.996 bpw 3.74 GiB (8030261248 params)
Q3_K_L 4.298 bpw 4.02 GiB (8030261248 params)
Q4_K_S 4.667 bpw 4.36 GiB (8030261248 params)
Q4_K_M 4.894 bpw 4.58 GiB (8030261248 params)
Q5_K_S 5.570 bpw 5.21 GiB (8030261248 params)
Q5_K_M 5.704 bpw 5.33 GiB (8030261248 params)
Q6_K 6.563 bpw 6.14 GiB (8030261248 params)
Q8_0 8.501 bpw 7.95 GiB (8030261248 params)The parameter count, 8,030,261,248, is the one published for Llama 3 8B, which confirms the shapes. The sizes match the table that llama-quantize --help prints for Llama-3-8B at the pinned commit to within 0.01 GiB: 3.41G for Q3_K_S, 3.74G for Q3_K_M, 4.58G for Q4_K_M, 5.33G for Q5_K_M, 6.14G for Q6_K, and 7.96G for Q8_0. The small remaining differences come from metadata such as the tokenizer, which this calculation leaves out. (A GiB is 2^30 bytes, about 1.07 billion bytes.)
Check one row by hand. Q4_K_M averages 4.894 bpw over 8,030,261,248 weights, which is 39.30 billion bits, or 4.91 billion bytes, or 4.58 GiB. Two facts follow from the numbers.
- A "4-bit" file is 4.67 to 4.89 bpw, not 4.0. The 0.5 bpw of scales is part of every
Q4_Kblock, and the mix adds more on top. Memory estimates in Memory Math must use the mix's average, not the nominal bit width. Q4_K_Mcosts 0.22 GiB more thanQ4_K_S(5 percent). All of that goes to half of theattn_vandffn_downtensors.
Example 7: sensitive tensors and the cost of extra bits
The source gives tensors such as output, attn_v, and ffn_down more bits. Extra bits are only a good deal when the tensor is small. This example measures what each tensor group costs.
The program prints each tensor group's share of the 8B model's weights, its size at Q4_K in MiB (2^20 bytes), and how many MiB it would add to move that group from Q4_K to Q6_K. The last column is the price of upgrading.
import java.util.Locale;
public class SensitiveTensorCost {
public static void main(String[] args) {
long d = 4096, ff = 14336, kv = 1024, vocab = 128256; int n = 32;
String[] names = {"token_embd", "output", "attn_q (all layers)", "attn_k (all layers)", "attn_v (all layers)",
"attn_output (all layers)", "ffn_gate (all layers)", "ffn_up (all layers)", "ffn_down (all layers)"};
long[] params = {vocab * d, vocab * d, n * d * d, n * d * kv, n * d * kv, n * d * d, n * d * ff, n * d * ff, n * d * ff};
long total = 0;
for (long p : params) total += p;
System.out.println("tensor group share Q4_K MiB +MiB for Q6_K");
for (int i = 0; i < names.length; i++) {
double q4 = params[i] * 4.5 / 8 / (1 << 20);
double q6 = params[i] * 6.5625 / 8 / (1 << 20);
System.out.println(String.format(Locale.ROOT, "%-26s %5.1f%% %8.1f %8.1f",
names[i], 100.0 * params[i] / total, q4, q6 - q4));
}
}
}tensor group share Q4_K MiB +MiB for Q6_K
token_embd 6.5% 281.8 129.2
output 6.5% 281.8 129.2
attn_q (all layers) 6.7% 288.0 132.0
attn_k (all layers) 1.7% 72.0 33.0
attn_v (all layers) 1.7% 72.0 33.0
attn_output (all layers) 6.7% 288.0 132.0
ffn_gate (all layers) 23.4% 1008.0 462.0
ffn_up (all layers) 23.4% 1008.0 462.0
ffn_down (all layers) 23.4% 1008.0 462.0The table explains the choice of tensors in terms of cost:
attn_vis 1.7 percent of the weights. With grouped-query attention it has 1024 rows against 4096 forattn_q, so moving all of it toQ6_Kadds only 33 MiB. A comment inllama-quant.cppmakes the same argument for the 70B model:attn_vis 8 times smaller thanattn_q, so more bits give "a nice boost in quantization accuracy with nearly negligible increase in model size".outputis 6.5 percent. It maps the final internal state to the logits, the raw scores for every possible next token, at every step. An error there changes the scores the sampler reads directly, without later layers in between to smooth it out. Every k-quant mix stores it asQ6_K(129 MiB more thanQ4_Khere).- The
ffn_*tensors hold 70 percent of the weights. Raising all offfn_downcosts 462 MiB, soQ4_K_Mraises only half of its layers, with the first and last layers always included.
The source does not give a single measured reason for each rule. The comments describe the choices as found by trying them ("Guard against craziness in the first few ffn_down layers"). The facts that can be stated are the cost (this table) and the measured effect. The effect is measured with perplexity (how surprised the model is by a test text, where lower is better) and KL divergence (a number for how far the quantized model's predictions drift from the original's). Making, Quantizing, and Testing Your Own GGUF measures both with llama-perplexity.
Example 8: importance-weighted error and the best scale
A quantizer must pick one scale per block. A larger scale covers bigger values but rounds small ones more coarsely, so the choice is a trade. The problem is that some weights in the block matter more to the model's output than others. This example shows how importance changes the best scale.
The program quantizes 8 values to 4 bits with three scales: the plain absmax scale (the largest absolute value divided by the largest code), the scale with the lowest plain squared error, and the scale with the lowest importance-weighted squared error. The array w holds the importance of each weight. Weights 1, 3, and 6 are marked as important (values 9.0, 8.0, and 7.5).
import java.util.Locale;
public class WeightedScale {
static double error(float[] x, float[] w, float scale, boolean weighted) {
double e = 0;
for (int i = 0; i < x.length; i++) {
int q = Math.max(-8, Math.min(7, (int) Math.rint(x[i] / scale)));
double diff = x[i] - q * scale;
e += (weighted ? w[i] : 1.0) * diff * diff;
}
return e;
}
public static void main(String[] args) {
float[] x = {0.90f, -0.35f, 0.12f, 0.51f, -0.77f, 0.05f, 0.33f, -0.18f};
float[] w = {0.1f, 9.0f, 0.2f, 8.0f, 0.1f, 0.3f, 7.5f, 0.2f}; // importance of each weight
float absmaxScale = 0.90f / 7;
float bestPlain = absmaxScale, bestWeighted = absmaxScale;
for (int k = 0; k <= 60; k++) {
float s = absmaxScale * (0.70f + 0.01f * k);
if (error(x, w, s, false) < error(x, w, bestPlain, false)) bestPlain = s;
if (error(x, w, s, true) < error(x, w, bestWeighted, true)) bestWeighted = s;
}
for (float s : new float[] {absmaxScale, bestPlain, bestWeighted}) {
System.out.println(String.format(Locale.ROOT, "scale %.5f plain error %.5f weighted error %.5f",
s, error(x, w, s, false), error(x, w, s, true)));
}
}
}scale 0.12857 plain error 0.00962 weighted error 0.03620
scale 0.12600 plain error 0.00910 weighted error 0.02602
scale 0.16714 plain error 0.01368 weighted error 0.00502Importance-weighted error is sum_i w_i * (x_i - s * q_i)^2, where x_i is the weight, s is the scale, q_i is its integer code, and w_i is how much weight i matters. With every w_i = 1 it is the ordinary squared error. For one weight, for example, a rounding error of 0.1 with importance 9 costs 9 * 0.01 = 0.09, while the same error with importance 0.1 costs only 0.001.
- The absmax scale (0.12857) puts 0.90 exactly on code 7, but it is not the best even by plain error.
- The best plain scale (0.12600) lowers the plain error from 0.00962 to 0.00910.
- The best weighted scale (0.16714) is very different. This scale places -0.35, 0.51, and 0.33 (the important weights) close to grid points. Its plain error is higher (0.01368), but its weighted error is 7 times lower than the plain choice's (0.00502 against 0.03620).
So a quantizer that knows which weights matter makes a different trade: it lets unimportant weights carry more error so that important ones carry less. All ggml quantizers that accept quant_weights (quantize_q4_K, quantize_iq4_xs, and the others) work this way: their searches minimize a weighted error. The next example shows where the weights come from.
Example 9: a toy importance matrix from activation statistics
The previous example assumed that someone hands over the importance of each weight. This example shows how to measure it. The idea: a weight matters when the input it multiplies is large. A weight that always meets an input near zero contributes almost nothing, so rounding it badly costs almost nothing.
The program builds an importance matrix from 512 "calibration" activation vectors (sample inputs, here random numbers). In a few columns, called "hot" channels, the inputs are much larger. The program then quantizes 64 weight rows with and without the importance values, and measures the error of the layer output on 512 different "test" activations that were not used to build the matrix. Look at the three imatrix numbers first, then at the two error lines.
import java.util.Locale;
import java.util.Random;
public class ToyImatrix {
static float[] quantize(float[] w, double[] imp) { // 4-bit symmetric, one block, best scale by search
float amax = 0;
for (float v : w) amax = Math.max(amax, Math.abs(v));
float best = amax / 7; double bestErr = Double.MAX_VALUE;
for (int k = 0; k <= 80; k++) {
float s = amax / 7 * (0.6f + 0.01f * k);
double e = 0;
for (int i = 0; i < w.length; i++) {
double diff = w[i] - s * Math.max(-8, Math.min(7, Math.rint(w[i] / s)));
e += imp[i] * diff * diff;
}
if (e < bestErr) { bestErr = e; best = s; }
}
float[] out = new float[w.length];
for (int i = 0; i < w.length; i++) out[i] = best * (float) Math.max(-8, Math.min(7, Math.rint(w[i] / best)));
return out;
}
static double[][] activations(Random rnd, int tokens, int cols) {
double[][] x = new double[tokens][cols];
for (double[] row : x)
for (int j = 0; j < cols; j++) row[j] = rnd.nextGaussian() * (j % 8 == 3 ? 6.0 : 1.0); // hot channels
return x;
}
static double outputError(float[] w, float[] q, double[][] x) {
double s = 0;
for (double[] row : x) {
double y = 0, yq = 0;
for (int j = 0; j < w.length; j++) { y += w[j] * row[j]; yq += q[j] * row[j]; }
s += (y - yq) * (y - yq);
}
return s / x.length;
}
public static void main(String[] args) {
Random rnd = new Random(7);
int cols = 32;
double[][] calib = activations(rnd, 512, cols), test = activations(rnd, 512, cols);
double[] imatrix = new double[cols], ones = new double[cols];
for (double[] row : calib) for (int j = 0; j < cols; j++) imatrix[j] += row[j] * row[j] / calib.length;
java.util.Arrays.fill(ones, 1.0);
System.out.println(String.format(Locale.ROOT, "imatrix: col0 %.2f col3 %.2f col11 %.2f", imatrix[0], imatrix[3], imatrix[11]));
double plain = 0, weighted = 0;
for (int r = 0; r < 64; r++) { // 64 rows of a weight matrix
float[] w = new float[cols];
for (int j = 0; j < cols; j++) w[j] = (float) (rnd.nextGaussian() * 0.02);
plain += outputError(w, quantize(w, ones), test);
weighted += outputError(w, quantize(w, imatrix), test);
}
System.out.println(String.format(Locale.ROOT, "output MSE without imatrix: %.3e", plain / 64));
System.out.println(String.format(Locale.ROOT, "output MSE with imatrix: %.3e", weighted / 64));
}
}imatrix: col0 0.96 col3 36.73 col11 34.65
output MSE without imatrix: 4.453e-04
output MSE with imatrix: 2.864e-04For a weight row w and input x, the layer output is y = sum_j w_j * x_j. An error e_j in weight j changes y by e_j * x_j. Over many inputs, the expected squared change is e_j^2 * mean(x_j^2). So mean(x_j^2), the average of the squared input for column j, is the natural importance of every weight in input column j.
The importance matrix (imatrix) is that number for each input column of each weight matrix in the model. llama-imatrix computes it by running the model on calibration text and adding x[j] * x[j] for every token in every matrix multiplication (acc[j] += x[j] * x[j] in tools/imatrix/imatrix.cpp), then dividing by the token count.
In the toy, every eighth column is a hot channel with standard deviation 6, so its imatrix value is about 36 (36.73 and 34.65 in the output) against about 1 for normal columns (0.96 for column 0). The number 36 is 6 squared, as the formula predicts. Quantizing with those weights cuts the output error from 4.453e-04 to 2.864e-04, a 36 percent reduction, with the same 4 bits and the same storage. The test activations were not used to build the imatrix, which shows the statistics carry over to new inputs from the same distribution.
ggml does not use the raw imatrix value as the weight. In quantize_row_q4_K_impl and quantize_row_iq4_nl_impl the weight of value x[j] is qw[j] * sqrt(sigma2 + x[j]^2), where qw is the imatrix entry and sigma2 is twice the mean square of the super-block. Large weights in hot columns therefore get the most protection. The imatrix changes only the search for scales and codes. The file format is the same with or without it, so any program that reads GGUF can read an imatrix-quantized file.
Example 10: IQ4_NL: a non-linear 16-value grid
A 4-bit code can name 16 values. The formats so far space those 16 values evenly, so the gap between neighboring values is the same everywhere. But trained weights are not spread evenly: most are close to zero and few are large. The problem is whether evenly spaced levels are the best use of 16 codes.
IQ4_NL replaces the grid -8 .. 7 with the 16 values of kvalues_iq4nl from ggml-common.h, which are packed closer together near zero. The formats whose names start with IQ are called i-quants. The program implements quantize_row_iq4_nl_impl for 32-value blocks, as quantize_iq4_nl calls it, and compares the error with Q4_0. The last line prints the gaps between neighboring table values.
import java.util.Locale;
import java.util.Random;
public class Iq4Nl {
static final int[] VALUES = {-127, -104, -83, -65, -49, -35, -22, -10, 1, 13, 25, 38, 53, 69, 89, 113};
static float f16(float x) { return Float.float16ToFloat(Float.floatToFloat16(x)); }
static int bestIndex(float x) { // best_index_int8
if (x <= VALUES[0]) return 0;
if (x >= VALUES[15]) return 15;
int lo = 0, hi = 15;
while (hi - lo > 1) { int mid = (lo + hi) / 2; if (x < VALUES[mid]) hi = mid; else lo = mid; }
return x - VALUES[hi - 1] < VALUES[hi] - x ? hi - 1 : hi;
}
static float[] iq4nl(float[] x) { // quantize_row_iq4_nl_impl, block 32, ntry 7
float[] out = new float[x.length];
for (int b = 0; b < x.length; b += 32) {
float amax = 0, max = 0;
for (int j = 0; j < 32; j++) if (Math.abs(x[b + j]) > amax) { amax = Math.abs(x[b + j]); max = x[b + j]; }
float bestD = 0, best = 0;
for (int itry = -7; itry <= 7; itry++) {
float id = (itry + VALUES[0]) / max, sumqx = 0, sumq2 = 0;
for (int j = 0; j < 32; j++) {
float w = x[b + j] * x[b + j], q = VALUES[bestIndex(id * x[b + j])];
sumqx += w * q * x[b + j];
sumq2 += w * q * q;
}
if (sumq2 > 0 && sumqx * sumqx > best * sumq2) { bestD = sumqx / sumq2; best = bestD * sumqx; }
}
float d = f16(bestD), id = d != 0 ? 1f / d : 0f;
for (int j = 0; j < 32; j++) out[b + j] = d * VALUES[bestIndex(id * x[b + j])];
}
return out;
}
static float[] q4_0(float[] x) { // lesson 18
float[] out = new float[x.length];
for (int b = 0; b < x.length; b += 32) {
float amax = 0, max = 0;
for (int j = 0; j < 32; j++) if (Math.abs(x[b + j]) > amax) { amax = Math.abs(x[b + j]); max = x[b + j]; }
float d = max / -8f, id = d != 0 ? 1f / d : 0f, dq = f16(d);
for (int j = 0; j < 32; j++) out[b + j] = (Math.min(15, (int) (x[b + j] * id + 8.5f)) - 8) * dq;
}
return out;
}
static double rmse(float[] a, float[] b) {
double s = 0;
for (int i = 0; i < a.length; i++) s += (a[i] - b[i]) * (a[i] - b[i]);
return Math.sqrt(s / a.length);
}
public static void main(String[] args) {
Random rnd = new Random(42);
float[] w = new float[4096];
for (int i = 0; i < w.length; i++) w[i] = (float) (rnd.nextGaussian() * 0.02);
System.out.println(String.format(Locale.ROOT, "Q4_0 RMSE %.6f", rmse(w, q4_0(w))));
System.out.println(String.format(Locale.ROOT, "IQ4_NL RMSE %.6f", rmse(w, iq4nl(w))));
StringBuilder gaps = new StringBuilder("gaps between table values:");
for (int i = 1; i < 16; i++) gaps.append(' ').append(VALUES[i] - VALUES[i - 1]);
System.out.println(gaps);
}
}Q4_0 RMSE 0.001662
IQ4_NL RMSE 0.001487
gaps between table values: 23 21 18 16 14 13 12 11 12 12 13 15 16 20 24IQ4_NL stores a block exactly like Q4_0: an FP16 scale and 16 bytes of 4-bit indices, which is 18 bytes per 32 weights (4.5 bpw). The difference is the meaning of a code. In Q4_0, code q means d * (q - 8). In IQ4_NL, code q means d * kvalues_iq4nl[q], and the table is -127, -104, -83, -65, -49, -35, -22, -10, 1, 13, 25, 38, 53, 69, 89, 113.
The gaps line shows the shape: 11 to 13 between the values near zero, up to 23 and 24 at the ends. Weights in a trained model have a bell-shaped distribution, with many values near zero and few large ones. Placing levels closer together where values are dense lowers the average error, and the output confirms it: 0.001487 for IQ4_NL against 0.001662 for Q4_0, about 11 percent less, at the same size.
The quantizer also searches for a good scale. After a first guess d = max / 127, it tries 15 inverse scales (itry + values[0]) / max for itry from -7 to 7. For each one it maps every value to its nearest table entry with a binary search (best_index_int8), refits the scale by weighted least squares (d = sum(w*q*x) / sum(w*q*q)), and keeps the best. Without an imatrix the weights are x^2. The "NL" stands for non-linear: the mapping from code to value is a table instead of a straight line.
Example 11: IQ4_XS: IQ4_NL values in super-blocks
IQ4_NL pays 0.5 bpw for its FP16 scales, like Q4_0. Example 1 showed that super-blocks cut that cost. This example applies the same trick to the non-linear grid and measures what it saves.
The program implements the super-block path of quantize_row_iq4_nl_impl that quantize_iq4_xs uses. Each of the 8 sub-blocks gets a 6-bit signed scale, and one FP16 value d per 256 weights converts them. The program prints the error and the block size.
import java.util.Locale;
import java.util.Random;
public class Iq4Xs {
static final int[] VALUES = {-127, -104, -83, -65, -49, -35, -22, -10, 1, 13, 25, 38, 53, 69, 89, 113};
static float f16(float x) { return Float.float16ToFloat(Float.floatToFloat16(x)); }
static int bestIndex(float x) {
if (x <= VALUES[0]) return 0;
if (x >= VALUES[15]) return 15;
int lo = 0, hi = 15;
while (hi - lo > 1) { int mid = (lo + hi) / 2; if (x < VALUES[mid]) hi = mid; else lo = mid; }
return x - VALUES[hi - 1] < VALUES[hi] - x ? hi - 1 : hi;
}
static float blockScale(float[] x, int b) { // same search as IQ4_NL (Example 10)
float amax = 0, max = 0;
for (int j = 0; j < 32; j++) if (Math.abs(x[b + j]) > amax) { amax = Math.abs(x[b + j]); max = x[b + j]; }
float bestD = 0, best = 0;
for (int itry = -7; itry <= 7; itry++) {
float id = (itry + VALUES[0]) / max, sumqx = 0, sumq2 = 0;
for (int j = 0; j < 32; j++) {
float w = x[b + j] * x[b + j], q = VALUES[bestIndex(id * x[b + j])];
sumqx += w * q * x[b + j];
sumq2 += w * q * q;
}
if (sumq2 > 0 && sumqx * sumqx > best * sumq2) { bestD = sumqx / sumq2; best = bestD * sumqx; }
}
return bestD;
}
static float[] iq4xs(float[] x) {
float[] out = new float[x.length];
for (int sb = 0; sb < x.length; sb += 256) {
float[] scales = new float[8];
float maxScale = 0, amaxScale = 0;
for (int ib = 0; ib < 8; ib++) {
scales[ib] = blockScale(x, sb + 32 * ib);
if (Math.abs(scales[ib]) > amaxScale) { amaxScale = Math.abs(scales[ib]); maxScale = scales[ib]; }
}
float d = -maxScale / 32, dq = f16(d), id = d != 0 ? 1 / d : 0;
for (int ib = 0; ib < 8; ib++) {
int l = Math.max(-32, Math.min(31, (int) Math.rint(id * scales[ib]))); // 6-bit sub-block scale
float dl = dq * l, idl = dl != 0 ? 1 / dl : 0;
for (int j = 0; j < 32; j++) {
int i = sb + 32 * ib + j;
out[i] = dl * VALUES[bestIndex(idl * x[i])];
}
}
}
return out;
}
public static void main(String[] args) {
Random rnd = new Random(42);
float[] w = new float[4096];
for (int i = 0; i < w.length; i++) w[i] = (float) (rnd.nextGaussian() * 0.02);
double s = 0;
float[] q = iq4xs(w);
for (int i = 0; i < w.length; i++) s += (w[i] - q[i]) * (w[i] - q[i]);
System.out.println(String.format(Locale.ROOT, "IQ4_XS RMSE %.6f", Math.sqrt(s / w.length)));
int bytes = 2 + 2 + 256 / 64 + 256 / 2;
System.out.println(String.format(Locale.ROOT, "block: %d bytes per 256 weights = %.4f bpw", bytes, bytes * 8.0 / 256));
}
}IQ4_XS RMSE 0.001501
block: 136 bytes per 256 weights = 4.2500 bpwblock_iq4_xs holds an FP16 d, a 16-bit scales_h, 4 bytes of scales_l, and 128 bytes of 4-bit indices: 136 bytes per 256 weights, which is 4.25 bpw. Each of the 8 sub-blocks has a 6-bit signed scale l from -32 to 31, stored as l + 32: the low 4 bits in scales_l (two per byte) and the high 2 bits in scales_h. The super-block scale is d = -maxScale / 32, so the sub-block with the largest scale gets l = -32.
The error (0.001501) is almost the same as IQ4_NL (0.001487) at 0.25 bpw less, and lower than Q4_0 at 0.25 bpw less. IQ4_XS needs rows whose length is a multiple of 256. For other rows llama-quantize falls back to IQ4_NL. With these numbers it is the smallest 4-bit format in the table, and the llama-quantize help lists it as "4.25 bpw non-linear quantization".
Example 12: codebooks for IQ2 and IQ3: a toy vector quantizer
Below 3 bits, a scalar grid (one independent grid of levels per weight) has too few levels. A 2-bit code names only 4 values, so each weight can only be one of 4 numbers. The problem is how IQ2_XXS reaches 2.06 bpw with acceptable error.
The idea is to stop rounding weights one at a time. Instead, take a group of 8 weights, look for the most similar entry in a table of ready-made 8-value patterns, and store only the position of that entry. A table of 256 patterns needs an 8-bit position. This is called vector quantization, because a group of values is treated as one vector. The table is called a codebook.
The program builds a toy codebook: 256 vectors of 8 magnitudes, trained with k-means on |w|. K-means is a procedure that repeatedly assigns every group to its nearest table entry and then moves each entry to the average of its group. Each group of 8 weights stores an 8-bit index into the table and 7 sign bits. The program compares the error with a 2-bit scalar quantizer.
import java.util.Locale;
import java.util.Random;
public class ToyCodebook {
public static void main(String[] args) {
Random rnd = new Random(3);
int groups = 4096, dim = 8, k = 256;
float[][] w = new float[groups][dim];
for (float[] g : w) for (int j = 0; j < dim; j++) g[j] = (float) rnd.nextGaussian();
// Codebook of magnitude vectors, trained with a few rounds of k-means on |w|.
float[][] book = new float[k][];
for (int c = 0; c < k; c++) { book[c] = new float[dim]; for (int j = 0; j < dim; j++) book[c][j] = Math.abs(w[c * 7][j]); }
int[] assign = new int[groups];
for (int iter = 0; iter < 8; iter++) {
float[][] sum = new float[k][dim]; int[] count = new int[k];
for (int g = 0; g < groups; g++) {
assign[g] = nearest(book, w[g]);
count[assign[g]]++;
for (int j = 0; j < dim; j++) sum[assign[g]][j] += Math.abs(w[g][j]);
}
for (int c = 0; c < k; c++) if (count[c] > 0) for (int j = 0; j < dim; j++) book[c][j] = sum[c][j] / count[c];
}
double errVq = 0, err2bit = 0;
for (int g = 0; g < groups; g++) {
float[] code = book[nearest(book, w[g])];
int negatives = 0, smallest = 0;
for (int j = 0; j < dim; j++) {
if (w[g][j] < 0) negatives++;
if (Math.abs(w[g][j]) < Math.abs(w[g][smallest])) smallest = j;
}
for (int j = 0; j < dim; j++) {
boolean neg = w[g][j] < 0;
if (negatives % 2 == 1 && j == smallest) neg = !neg; // 7 sign bits: force even parity
double v = neg ? -code[j] : code[j];
errVq += (w[g][j] - v) * (w[g][j] - v);
double q = Math.max(-2, Math.min(1, Math.floor(w[g][j] / 0.996))); // 2-bit grid {-2,-1,0,1}+0.5
double v2 = (q + 0.5) * 0.996;
err2bit += (w[g][j] - v2) * (w[g][j] - v2);
}
}
int n = groups * dim;
System.out.println(String.format(Locale.ROOT, "codebook: 8-bit index + 7 sign bits = %.3f bits per weight", 15.0 / 8));
System.out.println(String.format(Locale.ROOT, "codebook RMSE %.4f", Math.sqrt(errVq / n)));
System.out.println(String.format(Locale.ROOT, "2-bit scalar RMSE %.4f (2 bits per weight)", Math.sqrt(err2bit / n)));
}
static int nearest(float[][] book, float[] g) {
int best = 0; double bestD = Double.MAX_VALUE;
for (int c = 0; c < book.length; c++) {
double dist = 0;
for (int j = 0; j < g.length; j++) { double diff = Math.abs(g[j]) - book[c][j]; dist += diff * diff; }
if (dist < bestD) { bestD = dist; best = c; }
}
return best;
}
}codebook: 8-bit index + 7 sign bits = 1.875 bits per weight
codebook RMSE 0.3106
2-bit scalar RMSE 0.3450 (2 bits per weight)A table of 256 entries needs an 8-bit index, so 8 weights cost 8 bits for the magnitudes, or 1 bit each.
The signs use a parity trick from ggml. Eight signs need 8 bits, but the quantizer forces the number of negative signs in a group to be even. If the count is odd, it flips the sign of the weight with the smallest magnitude, where the flip costs the least error. With even parity the eighth sign follows from the other seven, so 7 bits are enough. The toy then costs (8 + 7) / 8 = 1.875 bits per weight before scales.
The codebook error (0.3106) is lower than a 2-bit scalar quantizer with an optimal fixed step (0.3450), even though the codebook uses fewer bits (1.875 against 2). A scalar quantizer treats every weight alone. The codebook spends its 256 entries on the 8-value patterns that actually occur. The toy trains and tests on the same data, so the real gap is smaller, but the direction holds.
ggml's tables are fixed in the source, not trained per model. The table below compares the two codebook types in ggml.
| Type | Group | Table | Per 32 weights | bpw |
|---|---|---|---|---|
IQ2_XXS |
8 weights | iq2xxs_grid: 256 entries of 8 bytes, values 8, 25, 43 |
4 indices (32 bits), 4 x 7 sign bits, 4-bit scale | 2.0625 |
IQ3_XXS |
4 weights | iq3xxs_grid: 256 entries of 4 bytes |
8 indices (64 bits), 4 x 7 sign bits, 4-bit scale | 3.0625 |
In dequantize_row_iq2_xxs (the function that turns stored bits back into numbers) each 32-weight group reads two 32-bit words. The first holds four 8-bit grid indices. The second holds four 7-bit sign indices (looked up in ksigns_iq2xs, whose entries all have an even number of set bits) and a 4-bit scale in the top bits. One FP16 d per 256 weights completes the block: 8 groups of 8 bytes plus 2 bytes is 66 bytes, or 2.0625 bpw. IQ2_XS, IQ2_S, IQ3_S, IQ1_S, and IQ1_M use other tables and scale layouts.
These very low-bit formats have a rule. llama-quantize refuses to produce IQ1_S, IQ1_M, IQ2_XXS, IQ2_XS, IQ2_S, and IQ3_XXS tensors without an importance matrix (tensor_requires_imatrix in llama-quant.cpp). It stops with the message "this quantization requires an importance matrix!". The same rule covers Q2_K tensors in a Q2_K_S file. The token embedding and output tensors are exempt.
Example 13: error against bits per weight
Every format trades size for error. This example measures the curve. The program quantizes 32,768 Gaussian weights with symmetric absmax quantizers from 8 down to 2 bits (32-value blocks, FP16 scale), and with the IQ4_NL grid. It prints the RMSE divided by the weights' standard deviation, so the number is the error as a fraction of a typical weight. A value of 0.05 means a typical rounding error is 5 percent of a typical weight.
import java.util.Locale;
import java.util.Random;
public class ErrorVsBits {
static final int[] NL = {-127, -104, -83, -65, -49, -35, -22, -10, 1, 13, 25, 38, 53, 69, 89, 113};
static float f16(float x) { return Float.float16ToFloat(Float.floatToFloat16(x)); }
// Symmetric b-bit, 32-value blocks, FP16 scale from absmax: levels -(2^(b-1)-1) .. 2^(b-1)-1.
static double symmetric(float[] x, int bits) {
int qmax = (1 << (bits - 1)) - 1; double err = 0;
for (int b = 0; b < x.length; b += 32) {
float amax = 0;
for (int j = 0; j < 32; j++) amax = Math.max(amax, Math.abs(x[b + j]));
float d = f16(amax / qmax);
for (int j = 0; j < 32; j++) {
double v = d * Math.max(-qmax, Math.min(qmax, Math.rint(x[b + j] / d)));
err += (x[b + j] - v) * (x[b + j] - v);
}
}
return Math.sqrt(err / x.length);
}
// Non-linear 4-bit table with the plain absmax scale (no search), to isolate the effect of the grid.
static double nonLinear(float[] x) {
double err = 0;
for (int b = 0; b < x.length; b += 32) {
float amax = 0, max = 0;
for (int j = 0; j < 32; j++) if (Math.abs(x[b + j]) > amax) { amax = Math.abs(x[b + j]); max = x[b + j]; }
float d = f16(max / -127f);
for (int j = 0; j < 32; j++) {
double bestV = 0, bestE = Double.MAX_VALUE;
for (int v : NL) { double e = Math.abs(x[b + j] - d * v); if (e < bestE) { bestE = e; bestV = d * v; } }
err += (x[b + j] - bestV) * (x[b + j] - bestV);
}
}
return Math.sqrt(err / x.length);
}
public static void main(String[] args) {
Random rnd = new Random(42);
float[] w = new float[32768];
for (int i = 0; i < w.length; i++) w[i] = (float) (rnd.nextGaussian() * 0.02);
System.out.println("format bpw RMSE / sigma");
for (int bits = 8; bits >= 2; bits--)
System.out.println(String.format(Locale.ROOT, "symmetric %d-bit %5.2f %.4f", bits, bits + 0.5, symmetric(w, bits) / 0.02));
System.out.println(String.format(Locale.ROOT, "non-linear 4-bit %5.2f %.4f", 4.5, nonLinear(w) / 0.02));
}
}format bpw RMSE / sigma
symmetric 8-bit 8.50 0.0053
symmetric 7-bit 7.50 0.0107
symmetric 6-bit 6.50 0.0217
symmetric 5-bit 5.50 0.0447
symmetric 4-bit 4.50 0.0962
symmetric 3-bit 3.50 0.2256
symmetric 2-bit 2.50 0.6494
non-linear 4-bit 4.50 0.0793Each bit removed roughly doubles the error from 8 down to 4 bits (0.0053, 0.0107, 0.0217, 0.0447, 0.0962), because halving the number of levels doubles the step between them. Below 4 bits the error grows faster: 0.2256 at 3 bits and 0.6494 at 2 bits. The symmetric 2-bit quantizer here has only three levels (-1, 0, 1) so that zero is exact, and most weights are rounded to zero. That is why low-bit formats spend more effort on grids, minimums, codebooks, and importance weights than 8-bit formats do.
The non-linear 4-bit row uses the IQ4_NL table with a plain absmax scale and no search. It sits between 4 and 5 bits (0.0793, compared with 0.0962 at symmetric 4 bits and 0.0447 at 5 bits), which isolates the gain of the grid alone. The search in Example 10 adds the rest.
Combining Examples 3, 10, 11, and 13 for the same seed-42 weights gives one comparison of the 4-bit formats:
| Format | bpw | RMSE |
|---|---|---|
Q4_0 |
4.5 | 0.001662 |
Q4_K (simplified) |
4.5 | 0.001543 |
IQ4_NL |
4.5 | 0.001487 |
IQ4_XS |
4.25 | 0.001501 |
Example 14: choosing a GGUF quant by memory budget
All of this leads to a practical decision: which file to download for a given machine. The file must fit in memory together with two other things. The KV cache (key-value cache) is the memory where the model keeps data about every token it has already read, so it grows with the context length (the number of tokens in the conversation). The runtime also needs working buffers.
The program takes the 8B file sizes from Example 6, subtracts the KV cache and a 1 GiB runtime allowance from the memory budget, and picks the largest quant that fits. It tries budgets of 6, 8, and 12 GiB with contexts of 8,192 and 32,768 tokens.
import java.util.Locale;
public class PickQuant {
record Quant(String name, double gib) {}
public static void main(String[] args) {
// File sizes for an 8B Llama-shaped model, from Example 6.
Quant[] quants = {new Quant("Q8_0", 7.95), new Quant("Q6_K", 6.14), new Quant("Q5_K_M", 5.33),
new Quant("Q5_K_S", 5.21), new Quant("Q4_K_M", 4.58), new Quant("Q4_K_S", 4.36),
new Quant("IQ4_XS", 4.17), new Quant("Q3_K_M", 3.74), new Quant("Q3_K_S", 3.41)};
double kvGibPer1k = 0.125; // 8B with GQA at F16: 128 KiB per token (lesson 16)
double overheadGib = 1.0; // compute buffers and runtime
for (double budget : new double[] {6, 8, 12}) {
for (int ctx : new int[] {8192, 32768}) {
double free = budget - overheadGib - kvGibPer1k * ctx / 1024;
String pick = "none fits";
for (Quant q : quants) if (q.gib() <= free) { pick = q.name(); break; }
System.out.println(String.format(Locale.ROOT, "budget %4.1f GiB, context %5d: %.2f GiB for weights -> %s",
budget, ctx, free, pick));
}
}
}
}budget 6.0 GiB, context 8192: 4.00 GiB for weights -> Q3_K_M
budget 6.0 GiB, context 32768: 1.00 GiB for weights -> none fits
budget 8.0 GiB, context 8192: 6.00 GiB for weights -> Q5_K_M
budget 8.0 GiB, context 32768: 3.00 GiB for weights -> none fits
budget 12.0 GiB, context 8192: 10.00 GiB for weights -> Q8_0
budget 12.0 GiB, context 32768: 7.00 GiB for weights -> Q6_KThe KV cache size comes from Memory Math: Llama 3 8B stores 2 * 32 layers * 8 KV heads * 128 dims * 2 bytes = 128 KiB per token at F16, which is 0.125 GiB per 1,024 tokens. The IQ4_XS size (4.17 GiB) uses the same method as Example 6 with the IQ4_XS rules: attn_v at Q5_K because of grouped-query attention, output at Q6_K, and the first 4 ffn_down tensors at Q5_K when no imatrix is given.
Work through the first line by hand: 6 GiB budget, minus 1 GiB for the runtime, minus 8 * 0.125 = 1 GiB for an 8K cache, leaves 4 GiB for the weights. The largest file that is at most 4.00 GiB is Q3_K_M (3.74 GiB), because Q4_K_S (4.36) is too big.
The output shows that context length moves the answer as much as the budget does. At 8 GiB, an 8K context leaves room for Q5_K_M, while a 32K context leaves 3 GiB, and none of these files fits. The options there are a smaller model, a quantized KV cache (Memory Math), or offloading some layers to system RAM.
The criteria for choosing within what fits are in the table.
| Criterion | Consequence |
|---|---|
| Memory left after KV cache and buffers | Sets the largest file that loads without swapping |
| Decode speed | On a bandwidth-bound machine, tokens per second is roughly bandwidth divided by file size (the roofline model), so a smaller file decodes faster |
| Quality | KL divergence against the 16-bit model rises as bpw falls; below about 4 bpw it rises faster (Example 13) |
| Below 3 bpw | IQ1_*, IQ2_*, and IQ3_XXS files can only be made with an imatrix (Example 12) |
| Same size, different method | An IQ4_XS file with an imatrix has lower error than Q4_0 at a smaller size |
| Exact reproduction | A file quantized with an imatrix depends on the calibration text; the same command with a different text gives different bytes |
Interview points
- k-quants use 256-weight super-blocks. Each 16- or 32-weight sub-block has a small integer scale (and, for
Q2_K,Q4_K, andQ5_K, a minimum), and one or two FP16 values per super-block turn those integers into real scales. This saves space because the FP16 numbers are shared by many weights. Q4_Kis 144 bytes per 256 weights (4.5 bpw): FP16danddmin, 12 bytes of 6-bit scales and minimums, and 128 bytes of 4-bit codes. It has a per-32 minimum likeQ4_1at the cost ofQ4_0.- The bpw of the k-quants are
Q2_K2.625,Q3_K3.4375,Q4_K4.5,Q5_K5.5, andQ6_K6.5625.Q8_Kis used for activations in the CPU dot product, not for stored weights. _S,_M, and_Lare per-tensor mixes chosen inllama_tensor_get_type_impl.Q4_K_Mputsoutputand half of theattn_vandffn_downlayers (the layers selected byuse_more_bits) atQ6_K, and the rest atQ4_K.- An 8B Llama-shaped model at
Q4_K_Maverages about 4.89 bpw (4.58 GiB), not 4 bits, because scales and the higher-bit tensors add to the nominal width. attn_vis cheap to upgrade under grouped-query attention (1.7 percent of an 8B model), andoutputfeeds the logits directly. Both get more bits in most mixes.- The importance matrix is
mean(x_j^2)per input column, collected byllama-imatrixon calibration text. The quantizer minimizessum w_j * err_j^2with weights derived from it. The file format does not change. IQ4_NLuses a fixed 16-value non-linear table with 4.5 bpw.IQ4_XSputs the same table in 256-value super-blocks with 6-bit sub-block scales for 4.25 bpw.IQ2_XXSandIQ3_XXSare codebook formats: 8-bit indices into fixed tables of 8- or 4-value magnitude patterns, plus 7 sign bits per 8 weights with even parity.- Rows whose length is not a multiple of 256 cannot use 256-block types.
llama-quantizefalls back to a 32-block type (Q4_KtoQ5_0,IQ4_XStoIQ4_NL).
Summary
QK_K = 256is the super-block size of every k-quant and ofIQ2_*,IQ3_*,IQ1_*, andIQ4_XS.- Quantizing the scales themselves (a 6-bit scale and minimum per 32 weights) gives
Q4_Kthe flexibility ofQ4_1at 4.5 bpw. - The 12
Q4_Kscale bytes pack sixteen 6-bit values, andget_scale_min_k4unpacks them. - A per-sub-block minimum lets 16 levels cover an asymmetric range, which lowers error at the same size (0.001543 against 0.001662 in Example 3).
llama-quantizemixes types per tensor.Q4_K_Mfor an 8B model is 4.58 GiB andQ4_K_Sis 4.36 GiB.use_more_bitsselects the first and last eighth of the layers and every third layer between them.- Tensors with fewer than 2 dimensions are never quantized, so norm weights stay F32.
- The imatrix weights each column by its mean squared activation. In the toy it cut layer-output error by 36 percent at the same storage.
- ggml's weighted searches use
qw[j] * sqrt(sigma2 + x[j]^2)as the weight when an imatrix is present. - The
IQ4_NLtable is-127, -104, -83, -65, -49, -35, -22, -10, 1, 13, 25, 38, 53, 69, 89, 113, denser near zero. IQ4_XSis 136 bytes per 256 weights (4.25 bpw).IQ2_XXSis 66 bytes per 256 weights (2.0625 bpw), built from a 256-entry grid of 8-value patterns with magnitudes 8, 25, and 43.- From 8 to 4 bits, each removed bit roughly doubles the error, and below 4 bits it grows faster.
- The right quant is the largest one whose file, KV cache, and buffers fit in memory, adjusted for the decode speed the task needs.
