The World Vol 8 · Words
ch 86 / 105
Chapter 86

Four Bits and a Scale

Sixteen bytes a block

The header priced every tensor and left one constant unexplained. The type most of the model uses packs 256 weights into 144 bytes, but 256 weights at four bits each would take 128 bytes. The other sixteen bytes were taken on trust.

A quantized weight is a small whole number with no unit on it; a shared scale and offset turn it back into a weight, and those block fields are part of the price. This file holds 23,804,928 blocks of that type, and sixteen bytes each is 380,878,848 bytes. That is more than a third of a gigabyte, about eight percent of the model on disk, and the reason a reader who multiplies parameters by four bits gets a reproducibly wrong answer.

The rule here is the one the last chapter finished on. A tensor's size on disk is its weight count divided by the weights in one block of its element type, multiplied by what that block costs; add every tensor, the header and the padding, and the total is the file. Offsets are not consulted, which makes the sum a check instead of a restatement.

By the end of this page there is a second file in internal/lang that takes the size table apart field by field, so that every byte count in it is a thing the code can explain instead of assert; a run of the estimate everybody makes first, held against the file and missing by a number this page then accounts for in full; the bits a weight each of the three types in this model actually costs; the names of the tensors this file keeps at a wider type, which blocks of the stack got them and what fraction of the bytes they are; the same accounting run against the other model in the kit, which turns out not to be written the way its name suggests; and 256 real numbers put through a block of this format and read back out, with the error printed and the values the format has no room for named.

One practical note, because it changes how much of a day this costs. Nothing on this page opens a model. There is no container, no mount, no socket and no four-and-a-half gigabyte file anywhere in it. Every figure about somebody else's model is computed from the header this world dumped once and now carries in exemplars/, forty-five kilobytes standing in for the whole thing, and every figure about real numbers is computed from weights this world draws for itself. So the bench runs on a bare workstation in about the time it takes to compile, and the numbers it prints are the same on any machine, because there is nothing in them that a machine could change.

The flat four-bit estimate

Start with the estimate, because it is the one everybody reaches for and because being precise about why it fails is most of what this chapter has to teach. The model has 7,615,616,512 weights. It says four bits on the tin. Seven and a half billion times four bits is a little under 3.81 gigabytes, so a machine with four gigabytes free ought to hold it, and a page that stopped there would have said something false out of two true things.

The accounting from the last chapter is already written and it takes one flag to make it lie. -flat tells the sizer to ignore what each tensor is actually written in and charge every weight four bits, which is exactly the arithmetic the estimate does, only performed by machinery careful enough to show the damage.

⚠ Worked failure — the sum that is out by more than three quarters of a gigabyte

Here is the same accounting the file passed cleanly, with every tensor charged four bits a weight whatever the header says it holds.

$ go run ./cmd/bits -mode sum -flat
bits: every tensor sized at a flat four bits a weight
  the hero model, qwen2.5:7b-instruct-q4_K_M, 339 tensors, 4683073952 bytes on disk

  type    tensors          weights           bytes   bits a weight    share
  f32         141           333312          166656          4.0000    0.00%
  q4_K        169       6094061568      3047030784          4.0000   80.02%
  q6_K         29       1521221632       760610816          4.0000   19.98%
  all         339       7615616512      3807808256          4.0000  100.00%

  header and padding                  5953952
  every tensor's bytes             3807808256
  the two added together           3813762208
  the file on disk                 4683073952
  left over                         869311744

  4.0063 bits a weight once the header is counted too

The last chapter's version of that block ended with a zero, and a zero was the whole of its claim. This one ends with 869,311,744, which is not a small discrepancy or a rounding or an alignment gap. It is a fifth of the file unexplained, and the only thing the run got wrong was one assumption about what a weight costs.

Now the same mistake stated the way a person actually makes it, with the header left out of the reckoning as well, and then taken apart.

$ go run ./cmd/bits -mode miss
bits: parameters times four bits, and what it does not cover
  the hero model, qwen2.5:7b-instruct-q4_K_M, 339 tensors, 4683073952 bytes on disk

  the estimate
    weights                              7615616512
    at four bits a weight                3807808256
    the file on disk                     4683073952
    short by                              875265696

  where the bytes it does not cover are
    f32    333312 weights at 28.0000 bits a weight over four                 1166592
    q4_K   23804928 blocks of 256 at 0.5000 bits a weight over four        380878848
    q6_K   5942272 blocks of 256 at 2.5625 bits a weight over four         487266304
           the header and its padding                                        5953952
           in all                                                          875265696

  and the tensors cost 4677120000, which is 4.9132 bits a weight and not four

875,265,696 bytes short, and the reason the number looks arbitrary is that it is three unrelated mistakes added together with a fourth on top. The estimate charged four bits to 333,312 numbers the file keeps at full width, which is 1,166,592 bytes. It charged four bits to a billion and a half weights the file deliberately keeps at a wider type, which is 487,266,304. It charged four bits to the weights that really are four-bit coded, and forgot that every block of them carries sixteen bytes of its own, which is 380,878,848. And it forgot the header, which on this file is 5,953,952 and on a smaller file would matter a great deal more.

Follow that back from the symptom and the lesson is about where the number came from, not about arithmetic. Four is a name. It appears in the tag on the file, in the way people talk about a model, and in the type's own label, and in none of those places is it a measurement of anything. The bits a weight a file actually spends is a fact about its blocks, and a block is a thing with fields in it. So the fix is not a correction factor. It is to go and look at what a block holds.

The block fields

A four-bit code is a number from 0 to 15. On its own it is not a weight and it is not anything: there is no unit on it and no way to read it. To turn it back into a number somebody can multiply by, two more things are needed, and both of them have to be in the file. One is how far apart two neighbouring codes are. The other is where the run of codes starts. Call those the step and the offset, and the sixteen values a code can name are then the sixteen numbers you get by taking that many steps up from the offset.

If every weight carried its own step and offset the format would be pointless, because two full-width numbers to save half of one is a loss. So a step and an offset are shared, and how widely they are shared is the entire design question. Share them across too few weights and the bookkeeping costs more than the codes save. Share them across too many and a single unusual weight drags the step wide for everybody. The type this model is mostly written in answers that question twice over, at two different widths, and that is the thing the reader is about to see in code.

▣ Build · stage 1 — the size table, taken apart
// internal/lang/quant.go
// Part is one field of a block: what it holds, what it costs, and
// whether it is weight data or the bookkeeping that gives weight data
// a value.
//
// Kind is the column the whole chapter turns on. Add up the parts
// marked codes and divide by the weights in the block and you get the
// number the type's name says. Add up the rest and you get the
// difference between that number and what the file actually spends.
type Part struct {
	Name  string
	Bytes int64
	Kind  string // codes, scales, or raw for a type that is not quantized at all
}
// internal/lang/quant.go — one entry of the layout table.
	12: { // q4_K
		{"256 four-bit codes, two to a byte", 128, "codes"},
		{"8 sub-block scales and 8 offsets, six bits each", 12, "scales"},
		{"the block's scale, f16", 2, "scales"},
		{"the block's offset, f16", 2, "scales"},
	},
$ go run ./cmd/bits -mode ladder
bits: what one block holds, for every element type in the file
  the hero model, qwen2.5:7b-instruct-q4_K_M, 339 tensors, 4683073952 bytes on disk

  f32    1 weight in 4 bytes
        4  raw     the number itself
        4           32.0000 bits a weight: 32.0000 of codes and 0.0000 of scales

  q4_K   256 weights in 144 bytes
      128  codes   256 four-bit codes, two to a byte
       12  scales  8 sub-block scales and 8 offsets, six bits each
        2  scales  the block's scale, f16
        2  scales  the block's offset, f16
      144           4.5000 bits a weight: 4.0000 of codes and 0.5000 of scales

  q6_K   256 weights in 210 bytes
      128  codes   256 codes, the low four bits
       64  codes   the top two bits of all 256
       16  scales  16 sub-block scales, eight bits each
        2  scales  the block's scale, f16
      210           6.5625 bits a weight: 6.0000 of codes and 0.5625 of scales

  and every one of those fields adds up to the byte count the size table asserts

There are the sixteen bytes. Twelve of them hold eight steps and eight offsets, six bits each, one pair for every run of thirty-two weights inside the block. The other four are two numbers in sixteen-bit floating point that belong to the whole block: one that the eight six-bit steps are measured in, and one that the eight six-bit offsets are measured in. Two levels of scale, the cheap one repeated eight times and the expensive one written once, and that arrangement is what the K on the end of the type's name refers to.

The last line of the run is the check that makes the rest of it something other than a story. These field lists are written by hand, from the format, and a hand-written description of somebody else's binary layout is a hypothesis. So the code adds its own fields up and holds the total against the byte count the sizer has been using since the last chapter, for every type in the table and not only the ones this file happens to use. A layout that does not come to its own row is caught before it can be printed.

Read the q6_K rows beside the q4_K ones and the second design shows through. Its codes are six bits, and six bits do not divide a byte, so the file writes the low four bits of all 256 codes in one run of 128 bytes and the top two bits of all 256 in a second run of 64. Splitting a number across two arrays looks perverse until both halves are byte-aligned and a program can unpack the whole block with shifts and masks and no division at all.

Three types is not a table. The reader's own code can size fifteen of them, so print the lot and see what the range of the choice actually is.

▣ Build · stage 2 — fifteen types, and what a weight costs in each
$ go run ./cmd/bits -mode ladder -all
bits: every element type this reader can size, and what a weight costs in each

  type    weights    bytes  bits a weight    of codes   of scales
  f32           1        4        32.0000     32.0000      0.0000
  f16           1        2        16.0000     16.0000      0.0000
  q4_0         32       18         4.5000      4.0000      0.5000
  q4_1         32       20         5.0000      4.0000      1.0000
  q5_0         32       22         5.5000      5.0000      0.5000
  q5_1         32       24         6.0000      5.0000      1.0000
  q8_0         32       34         8.5000      8.0000      0.5000
  q8_1         32       36         9.0000      8.0000      1.0000
  q2_K        256       84         2.6250      2.0000      0.6250
  q3_K        256      110         3.4375      3.0000      0.4375
  q4_K        256      144         4.5000      4.0000      0.5000
  q5_K        256      176         5.5000      5.0000      0.5000
  q6_K        256      210         6.5625      6.0000      0.5625
  q8_K        256      292         9.1250      8.0000      1.1250
  bf16          1        2        16.0000     16.0000      0.0000

  not one of the quantized ones costs what its name says

Every quantized row costs more than its name. The cheapest overhead on the table is 0.4375 bits a weight and the dearest is 1.125, and there is no arrangement anywhere in it under which a four-bit type spends four bits. Notice also that the two families pay for their scales in different ways. The plain types at the top share one number across thirty-two weights and stop there. The k-quants at the bottom share across 256 and then spend part of what they saved on a second, finer level of scale inside the block, which is how q4_K arrives at the same 4.5 bits as q4_0 while dividing its block into eight separately scaled runs instead of one.

∑ Math Interlude — bits a weight, with the block counted

All of it is one division. A block of this type holds w = 256 weights in c = 144 bytes. A byte is 8 bits, so the block is 144 × 8 = 1,152 bits, and 1,152 ÷ 256 = 4.5. That is the number, and the only trick in it is remembering that the divisor is the weights and not the fields.

Split the same sum in two to see where the half comes from. The codes are k = 128 bytes: 128 × 8 = 1,024 bits, and 1,024 ÷ 256 = 4 exactly, which is the number on the tin and is true of the codes alone. Everything else is s = ck = 16 bytes: 16 × 8 = 128 bits, and 128 ÷ 256 = 0.5. So b = 8c ÷ w = 4.5, made of 4 that buys sixteen values a weight and 0.5 that buys the right to know what those values are.

Run it on the wider type and the split falls differently. q6_K is w = 256 in c = 210, so b = 210 × 8 ÷ 256 = 6.5625. Its codes are 128 + 64 = 192 bytes, giving 192 × 8 ÷ 256 = 6, and its scales are 18 bytes, giving 18 × 8 ÷ 256 = 0.5625. Two extra bits of code a weight, and a sixteenth of a bit more overhead.

One tensor's bytes then come out of the same three numbers. A tensor of n weights takes n ÷ w blocks, each costing c, so its size is n ÷ w × c bytes, which is the same thing as n × b ÷ 8. The output table of this model is 3,584 × 152,064 = 544,997,376 weights at q6_K: 544,997,376 ÷ 256 = 2,128,896 blocks, 2,128,896 × 210 = 447,068,160 bytes. The same figure the other way round is 544,997,376 × 6.5625 ÷ 8 = 447,068,160. Both routes have to agree or the layout is wrong, and one of the tests below is nothing but that comparison run over every tensor in both committed headers.

The one thing that expression will not do is take a fractional block. n ÷ w has to be a whole number, and a controller in The Hollow owns 378 weights, which is one block of 256 and 122 left over. A 378-number tensor cannot be written in this type at all without being padded first, and the reader from the last chapter refuses one instead of rounding, because a byte count derived from a part block is a plausible number and a wrong one.

wweights in one block of a type: 1 for a plain float, 32 for the older quantized types, 256 for a k-quant
cbytes that block costs on disk: 144 for q4_K, 210 for q6_K, 4 for a 32-bit float
khow many of those bytes are codes: 128 of q4_K's 144
sthe rest, the block's own scales and offsets: ck, which is 16 for q4_K
bbits a weight, overhead included: 8c ÷ w
nweights in one tensor, the product of its dimensions
a × b, a ÷ ba multiplied by b, a divided by b
One super-block of a four-bit k-quant, field by field Upper part: four stacked bands standing for the fields of one q4_K block, with the byte cost of each down the left. From the top: the block's scale, two bytes, one number for all 256 weights; the block's offset, two bytes, likewise; then twelve bytes drawn as eight small boxes numbered 0 to 7, a six-bit scale and a six-bit offset for each run of 32 weights; then 128 bytes drawn as eight boxes in the same positions, 32 four-bit codes in each. Short connectors join each small scale box to the code box below it. The four bands come to 144 bytes and 4.5 bits a weight. Middle: the same 144 bytes drawn as one strip to scale, showing 16 bytes of scales against 128 bytes of codes. Lower part: the expression that turns one code back into a weight. ONE SUPER-BLOCK OF q4_K: 256 WEIGHTS, 144 BYTES 2 the block's scale, f16 one number for all 256 2 the block's offset, f16 one number for all 256 12 0 1 2 3 4 5 6 7 a scale and an offset six bits each 128 32 32 32 32 32 32 32 32 four-bit codes in each run 144 bytes, and 4.5 bits a weight THE SAME 144 BYTES, TO SCALE 16 bytes that say what a code is worth 128 bytes of codes AND WHAT ONE CODE IS WORTH weight = D x sc[j] x q - Dmin x mn[j] q is the weight's own four bits, sc and mn its sub-block's six, and D and Dmin are the two f16 numbers the whole block shares so 32 weights share a ladder of 16 rungs, and 256 share two numbers
Figure 86.1 — the four bands at the top are drawn at readable heights and are nowhere near their real proportions; the strip in the middle is the same 144 bytes at true scale, and the four bytes at its left end are the two numbers the whole block shares. Eight boxes above eight boxes is the arrangement that makes this a k-quant: one cheap scale for every run of thirty-two, and one expensive scale that all eight of those are measured in.

Four and nine tenths bits a weight

With every type in the file explained, the accounting can be done honestly, and this is the sum the whole chapter exists to produce. Take each tensor's shape, look up what its element type costs a block, work out how many blocks it needs, multiply. Consult no offset, open no file, believe nothing the header says about where anything lives. Then add the header and its padding on and see whether the answer is the file.

▣ Build · stage 3 — the file added up at what its types actually cost
$ go run ./cmd/bits -mode sum
bits: every tensor sized from its shape and its element type alone
  the hero model, qwen2.5:7b-instruct-q4_K_M, 339 tensors, 4683073952 bytes on disk

  type    tensors          weights           bytes   bits a weight    share
  f32         141           333312         1333248         32.0000    0.03%
  q4_K        169       6094061568      3427909632          4.5000   73.29%
  q6_K         29       1521221632      1247877120          6.5625   26.68%
  all         339       7615616512      4677120000          4.9132  100.00%

  header and padding                  5953952
  every tensor's bytes             4677120000
  the two added together           4683073952
  the file on disk                 4683073952
  left over                                 0

  4.9194 bits a weight once the header is counted too

Zero left over, from arithmetic that never looked at an offset. That number was already true when the block sizes were taken from a table, and it is a different claim now that every one of those sizes has been broken into fields and each field named: the file adds up, and the reason it adds up is understood rather than observed.

The column to read is the fourth one. Nothing in this file costs four bits a weight. The bulk of it costs 4.5, a quarter of it costs 6.5625, a thin slice costs 32, and the weighted answer over all 7,615,616,512 weights is 4.9132. Counting the header as well takes it to 4.9194, which on a file this size is a difference in the fourth decimal place and on a small file is not.

4.9132 against 4 is twenty-three percent, and twenty-three percent of the thing that dominates the memory a model needs is the difference between a configuration that starts and one that does not. It is also, usefully, a number a reader can carry: for a file tagged this way, a weight costs about five bits, and parameters times five bits over eight is a first estimate that is wrong by less than two percent instead of by nearly a fifth.

Written at more than one width

The per-type table has an oddity in it that the last chapter noticed and did not explain. The file is tagged for a four-bit quantization, and 29 of its 339 tensors are written in a six-bit one. Not by accident, and not as a fallback: whoever produced this file chose, tensor by tensor, to spend more bytes on some of them than on the rest. The choice is recorded nowhere except in the element type of each info record, so the only way to find out which tensors got it is to go through them.

▣ Build · stage 4 — the tensors the file spends more on
$ go run ./cmd/bits -mode mixed
bits: which tensors are kept wider than the rest
  the hero model, qwen2.5:7b-instruct-q4_K_M, 339 tensors, 4683073952 bytes on disk

  3 element types in the file, the narrowest quantized one q4_K at 4.5000 bits a weight
  and the widest q6_K at 6.5625

  name                      at q6_K  at q4_K      wider bytes
  blk.N.ffn_down.weight          14       14        779735040
  blk.N.attn_v.weight            14       14         21073920
  output.weight                   1        0        447068160
  in all                         29                1247877120

  the weights in those tensors           1521221632, which is 19.98% of the file's weights
  what they cost at q6_K                 1247877120, which is 26.68% of the tensors' bytes
  what they would cost at q4_K            855687168
  so the decision costs                   392189952, which is 8.37% of the file

  and the blocks of the stack that got the wider type
    ffn_down.weight        0 1 2 5 7 12 14 17 20 23 24 25 26 27
    attn_v.weight          0 1 2 5 9 11 14 17 20 23 24 25 26 27

Three names, and one of them is not in the stack at all. output.weight is the table at the back of the model that turns a vector into a score for every token in the vocabulary, and every byte of it is at the wider type. The other two are inside the blocks: the value projection, which is the third of the three matrices an attention head uses, and the down projection, which is the wide-to-narrow half of the feed-forward layer. Exactly fourteen of the twenty-eight blocks got the wider version of each, and the other fourteen did not.

Read the two lists at the bottom side by side. Blocks 0, 1 and 2 are on both, and so are 24, 25, 26 and 27. The first three blocks of the stack and the last four are held wider in both tensors, and the remaining seven picks are scattered through the middle at roughly every third block. Whoever wrote this file spent its extra bytes at the two ends of the stack and then thinned out towards the centre, and while this page cannot say why, it can say that the pattern is deliberate: a rule that picked fourteen blocks at random would not put both ends of the stack on both lists.

Then the three figures underneath, because they answer three different questions and people mix them up. Those 29 tensors hold 19.98% of the model's weights. They take 26.68% of its bytes, which is higher for the obvious reason. And the decision to keep them wide, instead of writing them at the same 4.5 bits as everything else, cost 392,189,952 bytes, or 8.37% of the file. That last one is the number an operator actually cares about, and it is the only one of the three that is about the decision instead of about the outcome.

A tag on a file is a claim about the file, and a claim is a thing to check. The kit holds a second model with the same tag on it, four hundred megabytes against four and a half gigabytes, and the accounting takes one flag to point somewhere else.

▣ Build · stage 5 — the same tag, and not the same file
$ go run ./cmd/bits -mode sum -role small
bits: every tensor sized from its shape and its element type alone
  the small model, qwen2.5:0.5b-instruct-q4_K_M, 290 tensors, 397807936 bytes on disk

  type    tensors          weights           bytes   bits a weight    share
  f32         121            71552          286208         32.0000    0.07%
  q5_0        132        251854848       173150208          5.5000   44.19%
  q8_0         13        137510912       146105344          8.5000   37.29%
  q4_K         12         52297728        29417472          4.5000    7.51%
  q6_K         12         52297728        42900480          6.5625   10.95%
  all         290        494032768       391859712          6.3455  100.00%

  header and padding                  5948224
  every tensor's bytes              391859712
  the two added together            397807936
  the file on disk                  397807936
  left over                                 0

  6.4418 bits a weight once the header is counted too

Five element types, and the two that dominate it are not k-quants at all. Nearly half of this file is written at 5.5 bits a weight and better than a third at 8.5, and the type the tag names accounts for 7.51% of its bytes. The weighted answer is 6.3455 bits a weight against the larger file's 4.9132, which is twenty-nine percent more per weight for two files carrying the same three letters and the same trailing M.

There is a good reason and it is not the point. A model this small has tensors whose dimensions are 128 and 896, and a k-quant needs its weight count to divide by 256, so the writer falls back to the thirty-two weight types wherever it must; and a half billion parameter model has so little redundancy in it that four-bit codes hurt more. The point is that the name did not say any of that, and a reader who had compared these two files by their tags would have compared two things that are not the same. Every number on this page belongs to the file it was read from, and a bits-a-weight figure taken off one model is not a figure about another one even when the labels match.

The residue is zero here too, on a file with five types in it instead of three, and that is the accounting rule doing the only job it has: 391,859,712 bytes of tensors plus 5,948,224 of header and padding is 397,807,936, and the file is 397,807,936.

The 256-weight block round trip

Everything so far has been about what the format costs. What it takes is a separate question and it needs different numbers: not a header, but weights, put through the format and read out again with the difference printed. The weights this world has to hand are its own. A controller in The Hollow is 24 inputs, a middle row of 12 and 6 outputs, its 378 weights drawn in flat order from stream 14 on the world seed, every one of them between minus one and one and then divided by the square root of the fan its neuron adds up. Nobody chose them to make a point, which is the only property this page needs from them.

The quantizer is written the way the format writes one, in two levels. Every run of thirty-two weights gets sixteen rungs stretched between its own lowest and highest number. Those eight steps and eight offsets are then themselves rounded to six bits each, against two numbers held in sixteen-bit floating point for the whole block. Reading a weight back out is one line: the block's step times the sub-block's step code times the weight's own code, less the block's offset times the sub-block's offset code.

▣ Build · stage 6 — 256 weights into 144 bytes, and out
// internal/lang/quant.go
	// The eight ladders, in full precision, before anything is
	// rounded. step is the distance between two rungs and off is how
	// far below zero the bottom rung sits.
	var step, off [Subs]float64
	for j := range Subs {
		run := vals[j*Sub : (j+1)*Sub]
		lo, hi := run[0], run[0]
		for k, v := range run {
			if math.IsNaN(v) || math.IsInf(v, 0) {
				return Block{}, fmt.Errorf("lang: weight %d of the block is %v, which no ladder has a rung for", j*Sub+k, v)
			}
			lo = math.Min(lo, v)
			hi = math.Max(hi, v)
		}
		if lo > 0 {
			lo = 0
			b.Flat++
		}
		step[j] = (hi - lo) / 15
		off[j] = -lo
	}
// internal/lang/quant.go
// Back reads the block out again: 256 numbers, every one of them a
// rung on its own sub-block's ladder.
func (b Block) Back() []float64 {
	out := make([]float64, Super)
	for j := range Subs {
		s := b.D * float64(b.Sc[j])
		o := b.Dmin * float64(b.Mn[j])
		for i := range Sub {
			at := j*Sub + i
			out[at] = s*float64(b.Q[at]) - o
		}
	}
	return out
}
$ go run ./cmd/bits -mode round
bits: 256 weights of a 24-12-6 controller through one q4_K block and out again

  drawn on world seed 5, stream 14, 378 weights in the row, block starts at 0

  what the block holds
    the block's scale        0.0004220008850097656
    the block's offset       0.003238677978515625
    8 sub-block scales       [62 62 57 63 61 63 62 60]
    8 sub-block offsets      [58 60 50 61 60 61 63 55]
    256 codes, 0 to 15       [12 13 13 7 1 9 10 1] ... [1 5 7 4]
    written down             144 bytes
    ladders starting at 0    0

  the first sub-block's ladder: the only sixteen numbers its 32 weights can come back as
     -0.187843322754  -0.161679267883  -0.135515213013  -0.109351158142
     -0.083187103271  -0.057023048401  -0.030858993530  -0.004694938660
      0.021469116211   0.047633171082   0.073797225952   0.099961280823
      0.126125335693   0.152289390564   0.178453445435   0.204617500305

  the first 8 weights
      n              went in  code            came back               off by
      0       0.113737677987    12       0.126125335693       0.012387657706
      1       0.159530218718    13       0.152289390564      -0.007240828154
      2       0.165056277822    13       0.152289390564      -0.012766887258
      3      -0.001856715926     7      -0.004694938660      -0.002838222734
      4      -0.164724912571     1      -0.161679267883       0.003045644687
      5       0.054667885212     9       0.047633171082      -0.007034714130
      6       0.084729732706    10       0.073797225952      -0.010932506754
      7      -0.169342568901     1      -0.161679267883       0.007663301018

  over all 256
    distinct numbers in      256
    distinct numbers out     96, and 8 ladders of 16 rungs is 128 at the most
    largest error            0.012939653851, at weight 27 of sub-block 1
    mean error               0.006154076932

  how much of a weight survives
    within 1 percent of itself   30 of 256
    within 10 percent            175 of 256
    the lowest of the first 32   -0.187516522994, back as -0.187843322754
    the highest of the first 32  0.203254216671, back as 0.204617500305

  sub-block by sub-block
      n         rung to rung        largest error           mean error
      0       0.026164054871       0.012766887258       0.005920995071
      1       0.026164054871       0.012939653851       0.006586259048
      2       0.024054050446       0.011471153610       0.005253766755
      3       0.026586055756       0.012559609770       0.007199957263
      4       0.025742053986       0.012181908549       0.004864096139
      5       0.026586055756       0.012501260866       0.007070502301
      6       0.026164054871       0.012570653638       0.006202100168
      7       0.025320053101       0.012649403936       0.006134938710

  256 of these numbers in memory is 2048 bytes and this block is 144

The two lines in the middle are the plainest statement of what quantization is. Two hundred and fifty-six different numbers went in and ninety-six different numbers came out, and ninety-six was not the target of anything: it is what happens when eight runs of thirty-two are each squeezed onto sixteen rungs and some of the rungs go unused. The ceiling is 128 and it cannot be raised without spending more bits, because sixteen is all a four-bit code can name.

The sixteen rungs above them are the sixteen values the first thirty-two weights are allowed to come back as, and it repays a moment's staring, because it is the whole answer to which values the format cannot hold. It cannot hold 0.05. It cannot hold anything between 0.047633171082 and 0.073797225952, which is a whole rung wide and holds nothing at all. Every weight in that run lands on the nearest rung and the error is its distance to that rung, so the largest error possible is half a step, and the measured largest over all 256 is 0.012939653851 against a step of 0.026164054871. That bound is not a hope. One of the tests below checks it on every weight.

Which makes the loss easy to reason about and easy to get backwards. It is not the big weights that suffer. A sub-block's ladder is pinned at both ends by its own lowest and highest number, and the run shows both surviving to four decimal places. It is everything between the rungs, and the width of a rung is set by the two values furthest apart in the run. The 30 of 256 that came back within one percent of themselves are the ones that both landed near a rung and were big enough for half a step to be a small fraction of them. The ones that lost most of themselves in proportion are the small ones: weight 3 of the block went in at -0.001856715926 and came back as -0.004694938660, which is an absolute error smaller than most on the page and more than half again the number it was. The error is bounded uniformly and the harm it does is not.

One honesty about those errors before they get quoted anywhere. The quantizer above takes the obvious ladder for each run of thirty-two: bottom rung at the lowest number, top rung at the highest, fourteen evenly spaced rungs in between. A production writer does not stop there. It tries several ladders for each run and keeps whichever one leaves the smallest total error, which usually means giving up a little at the ends to gain in the crowded middle. So the errors on this page are the ones the simplest possible writer of this format would make, and a real file's are somewhat smaller. The byte counts are unaffected: a search changes which sixteen numbers a run comes back as, never how many bytes it takes to say so.

That is the well-behaved case, where all thirty-two numbers in a run sit in the same band because they were drawn from the same distribution. Real tensors are not always so obliging, and the format's exposure to one badly-placed number is best seen by putting one there. The flag replaces the first weight of the block with a number the rest of the row would never produce, and changes nothing else.

▣ Build · stage 7 — one weight out of place, and who pays for it
$ go run ./cmd/bits -mode round -spike 1 | tail -18
  how much of a weight survives
    within 1 percent of itself   25 of 256
    within 10 percent            164 of 256
    the lowest of the first 32   -0.187516522994, back as -0.187843322754
    the highest of the first 32  1.000000000000, back as 0.999967575073

  sub-block by sub-block
      n         rung to rung        largest error           mean error
      0       0.079187393188       0.038701550040       0.021983894822
      1       0.026395797729       0.013094026759       0.006621249165
      2       0.023881912231       0.011403626003       0.005022536288
      3       0.026395797729       0.013145881201       0.006948999730
      4       0.026395797729       0.012209682308       0.006000525761
      5       0.026395797729       0.013097403701       0.007445098863
      6       0.026395797729       0.011275972643       0.006094645115
      7       0.025138854980       0.011967908930       0.006009596647

  256 of these numbers in memory is 2048 bytes and this block is 144

Sub-block 0's rungs were 0.026164054871 apart and are now 0.079187393188 apart, three times as coarse, because its sixteen rungs have to span from its lowest number all the way up to a value the other thirty-one are nowhere near. Its largest error went from 0.012766887258 to 0.038701550040 and its mean from 0.005920995071 to 0.021983894822. The weight that caused all of it came back as 0.999967575073, off by three hundred-thousandths, because it is the top rung.

The other seven runs pay too, and the mechanism is the second level of scale. Their step codes were [62 62 57 63 61 63 62 60] and are now [63 21 19 21 21 21 21 20]: the block's shared step had to grow to describe sub-block 0's ladder, so every other ladder is now named with about a third as many of the sixty-four available codes, and their rung spacings come out slightly wrong in the fourth decimal place. Twenty-five of 256 within one percent instead of thirty, and 164 within ten percent instead of 175. One number in the wrong place taxed all 256.

What none of that says is how much any of it costs an answer. This page has measured the error a format introduces into a set of numbers, and the error is real and it is arithmetic. What it does to the quality of a model's output is a different quantity entirely, it is measured by running models against test sets on hardware this volume does not have, and no run on this page went anywhere near it. A number for that is not being withheld here. There is not one, and quoting one would mean inventing it.

Why this works

The move underneath the whole format is older than models and it turns up wherever a program has to store a lot of numbers that are similar to each other. Store each number independently and you pay full price for every one of them, and most of what you have paid for is the part they have in common. Store the common part once and give each number a short code that says where it sits relative to it, and the cost per number falls to whatever the code costs plus a share of the common part. The saving is entirely a function of how many numbers share, and the loss is entirely a function of how different they are.

Which is why the choice of how much to share is the whole design, and why q4_K makes it twice. One block of 256 amortises the two expensive numbers over enough weights that they disappear: four bytes over 256 weights is an eighth of a bit each. But 256 weights are too many to share one ladder without a straggler spoiling it, so the block subdivides into eight runs of thirty-two, each with its own cheap step and offset. Twelve bytes for eight pairs is three eighths of a bit a weight, which is what buys back the resolution the big block gave up. Half a bit a weight, spent in two places for two different reasons.

The second habit here is one this book has now used on the same file twice. When a program depends on a constant it did not derive, the constant is a liability until something checks it. The last chapter took 144 bytes a block from a table and produced a sum that came out exactly, which is evidence but not proof: a wrong block size would have produced a wrong total, and nothing in that run would have said which of the fifteen table entries was to blame. Writing the fields down and making the code add them up turns one global check into fifteen local ones, and the fifteen tell you where the mistake is. A number you can only test in aggregate is a number you cannot debug.

Both habits generalise past this format. Any time a size table, a wire layout or a struct alignment is written down from a specification, the specification's own arithmetic is sitting there unused: fields add up to records, records add up to files. Making the code perform that addition costs a dozen lines and catches the class of mistake that is otherwise invisible until something a long way downstream produces a plausible wrong answer.

Why block bookkeeping prices weights

The arithmetic on this page is now load-bearing for anything that wants to know what a model costs, so it gets tests, and none of them needs a model. Two of them run against the committed headers, which is the same substitution the last chapter made: forty-five kilobytes standing in for four and a half gigabytes, and the code under test cannot tell the difference.

▣ Build · stage 8 — eight things the sizes have to satisfy
$ go test ./internal/lang/ -run 'TestEveryBlockLayoutAddsUpToItsOwnSizeInTheTable|TestNoQuantizedTypeCostsWhatItsNameSays|TestABlockOfRealNumbersIsWrittenAsOneHundredAndFortyFourBytes|TestEveryWeightComesBackOnItsOwnSubBlocksLadder|TestNoWeightIsMoreThanHalfARungFromWhereItStarted|TestABlockThatIsNotTwoHundredAndFiftySixWeightsIsRefused|TestASixteenBitScaleIsWhatTheBlockActuallyHolds|TestTheCommittedHeadersCostWhatTheirTypesSayTheyDo' -v
=== RUN   TestEveryBlockLayoutAddsUpToItsOwnSizeInTheTable
    quant_test.go:39: all 15 element types taken apart, and every one of them adds up
--- PASS: TestEveryBlockLayoutAddsUpToItsOwnSizeInTheTable (0.00s)
=== RUN   TestNoQuantizedTypeCostsWhatItsNameSays
    quant_test.go:71: q4_K costs 4.5 bits a weight and q6_K 6.5625, block overhead included
--- PASS: TestNoQuantizedTypeCostsWhatItsNameSays (0.00s)
=== RUN   TestABlockOfRealNumbersIsWrittenAsOneHundredAndFortyFourBytes
    quant_test.go:86: 256 weights written down as 144 bytes, which is what the table asserts
--- PASS: TestABlockOfRealNumbersIsWrittenAsOneHundredAndFortyFourBytes (0.00s)
=== RUN   TestEveryWeightComesBackOnItsOwnSubBlocksLadder
    quant_test.go:111: all 256 weights landed on one of the 16 rungs their own sub-block has
--- PASS: TestEveryWeightComesBackOnItsOwnSubBlocksLadder (0.00s)
=== RUN   TestNoWeightIsMoreThanHalfARungFromWhereItStarted
    quant_test.go:131: every error inside half a rung, which is the whole of what four bits promises
--- PASS: TestNoWeightIsMoreThanHalfARungFromWhereItStarted (0.00s)
=== RUN   TestABlockThatIsNotTwoHundredAndFiftySixWeightsIsRefused
    quant_test.go:138: refused, and the message says both numbers: lang: a q4_K block holds 256 weights and 255 were handed over
    quant_test.go:145: refused: lang: weight 100 of the block is +Inf, which no ladder has a rung for
--- PASS: TestABlockThatIsNotTwoHundredAndFiftySixWeightsIsRefused (0.00s)
=== RUN   TestASixteenBitScaleIsWhatTheBlockActuallyHolds
    quant_test.go:159: 0.1 is held as 0.0999755859375, and a scale that has been through sixteen bits stays there
--- PASS: TestASixteenBitScaleIsWhatTheBlockActuallyHolds (0.00s)
=== RUN   TestTheCommittedHeadersCostWhatTheirTypesSayTheyDo
    quant_test.go:193: hero: 7615616512 weights, 4677120000 bytes of tensors, 4.913188569965641 bits a weight, 0 left over
    quant_test.go:193: small: 494032768 weights, 391859712 bytes of tensors, 6.345485358574433 bits a weight, 0 left over
--- PASS: TestTheCommittedHeadersCostWhatTheirTypesSayTheyDo (0.00s)

The first is the one that matters most and it is the cheapest: fifteen hand-written field lists, each added up and held against the byte count the sizer uses. The eighth is the accounting rule in its strongest form, run over both committed headers, checking not only that the residue is zero but that every single tensor's byte size equals its weight count times its type's bits a weight divided by eight. Two ways of computing the same thing, over 629 tensors, agreeing every time.

The fifth pins the promise the format makes. Sixteen rungs between a run's lowest and highest number means no weight can be more than half a step from where it started, and a quantizer with a rounding bug in it would break that quietly, producing numbers that are merely a bit worse rather than obviously wrong. The sixth pins the two refusals: a block that is not 256 weights, and a weight that is not a number at all. Neither is a case a real file would produce, and both are cases where a program that carried on would carry on with numbers nobody could account for.

✓ Checkpoint — a weight, priced
  • Name the four fields of a q4_K block and their byte costs, and produce 4.5 bits a weight from them without looking anything up.
  • Say which of those fields the 4 in the type's name accounts for, and what the other half a bit is buying.
  • Take 7,615,616,512 weights, charge them four bits each, and account for all 875,265,696 bytes by which that misses the file, in the four separate pieces it is made of.
  • Given a tensor of 3,584 by 512 at q4_K, produce its byte size two ways: through blocks of 256, and through bits a weight.
  • Say why 378 weights cannot be written as a q4_K tensor at all, and what a reader that rounded the block count instead of refusing would produce.
  • Explain why one weight far from the rest of its run of thirty-two makes the other thirty-one worse, and why the offending weight itself comes back nearly exact.
⚡ Exercises — try first, then reveal
Exercise 1 — price a model you have not got. Somebody tells you a 13-billion-parameter model is available at q4_K_M and asks whether it fits in eight gigabytes of memory. Answer with the arithmetic on this page, and say what you cannot answer.

The naive figure is 13,000,000,000 × 4 ÷ 8 = 6.5 gigabytes, which leaves a gigabyte and a half of room and is the wrong number. The hero model on this page came out at 4.9132 bits a weight over its tensors, so a first estimate on the same mix of types is 13,000,000,000 × 4.9132 ÷ 8, or about 7.98 gigabytes. The room is gone. The weights alone now fill the machine, and the weights are not the only thing a loaded model holds.

What you cannot answer is anything exact, and it is important to be clear about why. The fraction of tensors kept at the wider type is a decision the person who wrote that particular file made, and the two files on this page differ so much that one came out at 4.9132 bits a weight and the other at 6.3455 with the same tag. So the honest answer is an estimate with a stated basis and a note that the real figure needs the file, and the run that produces the real figure is -mode sum against that file's own header.

Exercise 2 — find the cheapest honest quantization. Using the fifteen-row table, work out which type gives the lowest bits a weight, what its overhead fraction is, and why that is the one nobody uses by default.

-mode ladder -all. q2_K is the cheapest at 2.625 bits a weight, and its split is the most lopsided on the table: 2 bits of codes and 0.625 of scales, so nearly a quarter of what it spends is bookkeeping. Compare that with q8_0, where 0.5 bits of overhead sits on 8 bits of code and is under six percent.

The reason overhead grows as a share is that it is nearly fixed. A block's scales cost about the same whatever the codes are, so the narrower the codes the larger the fraction they are. Two bits a weight also means four values a run of thirty-two, and the round-trip run on this page shows what sixteen values does to a set of weights that had 256 distinct values in it. Four would do considerably more. The arithmetic says a q2_K block is 42% smaller than a q4_K one; what that costs an answer is the question this page has already said it does not measure.

Exercise 3 — make the second level of scale earn its keep. The block spends 12 of its 16 overhead bytes on eight sub-block ladders instead of one ladder for all 256. Design a run of numbers where the extra seven ladders buy nothing, and one where they buy a great deal, and predict the errors before running either.

Buys nothing: 256 numbers drawn from one narrow band, where all eight runs of thirty-two have nearly the same lowest and highest values. Their step codes come out nearly equal, the eight ladders are nearly the same ladder, and a single ladder for the whole block would have given almost identical errors. That is close to what the plain run on this page shows, where the eight step codes are [62 62 57 63 61 63 62 60] and the eight rung spacings all fall between 0.0240 and 0.0266.

Buys a great deal: one run of thirty-two spread far wider than the other seven. Run -mode round -spike 1 and read the sub-block table: sub-block 0's rungs open to 0.079187393188 while the other seven stay near 0.026, and their errors stay near where they were. With one ladder for all 256, every weight in the block would have been on sub-block 0's coarse ladder and every error would have tripled. Twelve bytes bought the containment. The same experiment is also the argument for the third level nobody has built here: eight is not a magic number, it is a point on a curve between bookkeeping and resolution, and the format picked it.