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

Eight Gigabytes

Whether it fits is a sum

Ask whether a model runs on an eight-gigabyte machine and the answer comes back with a shrug in it. Somebody ran one like it once. Somebody else says you need a graphics card. A forum post says it swaps. None of that is a measurement and none of it is about your machine, and the reason the question attracts opinions is that most people asking it have never had the two files in front of them.

What a loaded model costs to hold is three sums: the weights, the cache and the overhead. Two of them are arithmetic over numbers that are already in the file. The third is measured once. This world has had both files for five chapters. The header of the big one was walked field by field, its three hundred and thirty-nine tensors were sized from their shapes alone, and the sizes added up to the file on disk with nothing left over: 4,683,073,952 bytes at 4.9132 bits a weight once the block overhead is counted. The small one came to 397,807,936 bytes at 6.3455, and chapter 86 already made the point that two files carrying the same four-bit tag are not two files priced alike.

Everything below is built on those committed headers. The offline calculator also reads committed allocation fixtures, so its arithmetic needs no model server. The live weighing and pacing modes do. Hardware changes the rate: how fast the tokens come, and whether the thing sits in a graphics card's memory or spills into the processor's. That moves the rate by an order of magnitude and moves none of the arithmetic, so this page prints both kinds of number and keeps them apart.

A sum is not the same claim as a reading off a running process, and the page keeps that distinction too. A real allocation moves with the backend, the cache format, the batch size, the parallelism and whether the weights sit on a card or in main memory; file bytes need not be resident bytes, because mappings and conversions land somewhere. What the arithmetic settles is what the file and the settings demand, which is the part that is the same everywhere.

By the end of it there is a program that takes a header, a context length and a ceiling and says yes or no with the working shown; a table of what a server actually holds beside a model, weighed at nine context lengths; one configuration that fits and one that does not, with the knob between them named; and the daemon from the last volume with one job added to it, running between two ticks rather than inside one, with the bill for having a model beside this world measured in the world's own units.

▣ Build · stage 1 — three sums over the big file

The new code goes in internal/lang/room.go. It is bytes from end to end and it opens nothing:

// Cache is the four numbers one model's key-value cache is a product
// of, read out of its own header.
//
// The four fields are the whole of it. Layers is how many attention
// blocks the file stacks; Heads is how many key-and-value heads each
// block keeps, which on this architecture is fewer than it has query
// heads; Width is one head's numbers, which is the embedding width
// divided by the query head count; and Elem is how many bytes one
// number of cache costs, which is a setting of the server and not a
// fact about the file.
type Cache struct {
	Layers int64
	Heads  int64
	Width  int64
	Elem   int64
}

// ...

// Token is the raw KV storage per token slot, in bytes. A backend may
// reserve all context slots at load time, before tokens occupy them.
func (c Cache) Token() int64 {
	return KeysAndValues * c.Layers * c.Heads * c.Width * c.Elem
}

// At is the cache at a context length.
func (c Cache) At(ctx int64) int64 { return c.Token() * ctx }

and cmd/fits is the bench that drives it. Its first mode reads the committed header of one model and prints the three sums with everything they were made of:

$ go run ./cmd/fits -mode sums
fits: what one model costs to hold, in three sums

  the model          qwen2.5:7b-instruct-q4_K_M, in the hero role
  pinned at          2bada8a745067700
  the context        4096 tokens, and this model was trained at 32768
  a cache number     2 bytes, assumed by this calculator
  conversations      1 in flight

  the weights, which are the file
    the file on disk                        4,683,073,952
    tensors in it                                     339
    bits a weight, block counted                   4.9132

  the cache, which is arithmetic
    attention blocks                                   28
    key and value heads apiece                          4
    numbers in one head                               128
    bytes one number costs                              2
    keys, and values as well                            2
    so, one token of conversation                  57,344
    and 4096 tokens of it                     234,881,024
    for every conversation held               234,881,024

  the overhead, which is measured
    what the server holds besides             163,753,568
    weighed against ollama 0.12.3, at 4096 tokens of context

  all three                                 5,081,708,544
  the cache is 4.6% of this estimate; one extra cache alone adds 234,881,024
  fixture arithmetic, not measured process/host/GPU memory or concurrency

Five gigabytes and eighty-one megabytes, on a machine that has eight thousand million bytes to give. Every figure above the total came from the header, the kit or a number somebody weighed, and the banner says which each one is: the context is the operator's setting, printed off the kit and not typed into the program; the two bytes a cache number costs is this calculator's assumption, not a queried server setting; the 4,096 sits against the 32,768 the file itself was trained at, because those two numbers are different questions and the file only answers the second one.

▣ Build · stage 2 — the same three sums over the small file
$ go run ./cmd/fits -mode sums -role small
fits: what one model costs to hold, in three sums

  the model          qwen2.5:0.5b-instruct-q4_K_M, in the small role
  pinned at          c5396e06af294bd1
  the context        4096 tokens, and this model was trained at 32768
  a cache number     2 bytes, assumed by this calculator
  conversations      1 in flight

  the weights, which are the file
    the file on disk                          397,807,936
    tensors in it                                     290
    bits a weight, block counted                   6.3455

  the cache, which is arithmetic
    attention blocks                                   24
    key and value heads apiece                          2
    numbers in one head                                64
    bytes one number costs                              2
    keys, and values as well                            2
    so, one token of conversation                  12,288
    and 4096 tokens of it                      50,331,648
    for every conversation held                50,331,648

  the overhead, which is measured
    what the server holds besides             320,032,704
    weighed against ollama 0.12.3, at 4096 tokens of context

  all three                                   768,172,288
  the cache is 6.6% of this estimate; one extra cache alone adds 50,331,648
  fixture arithmetic, not measured process/host/GPU memory or concurrency

Two details in that run set the boundary. The small model's weights are a twelfth of the big one's and its overhead is nearly twice as large, which is already a warning that the third term is not a percentage of the first. And the model that is a twelfth of the size costs a sixth as much to hold, because two of its three terms have almost nothing to do with how many parameters it has.

The term that grows while you are talking

The weights are a file and the file does not move. Load the model, ask it nothing, ask it for a novel: 4,683,073,952 bytes either way. The second term behaves in the opposite way, and confusing the two is where nearly every wrong estimate of what a model costs comes from.

Chapter 82 built one attention head by hand on four tokens and left a fact behind that is about to become a line item. Every token in a row is turned into a query, a key and a value; every query is dotted against every key. A model reading a conversation does that again for every new token, and it would be doing the same arithmetic over the same earlier tokens each time, so it keeps the keys and the values it has already worked out and reuses them. That store is the cache, and it holds one key and one value for every token, in every block, for every head that has them.

So the size of it is a product of five numbers, four of which are in the header. The file says twenty-eight attention blocks, four key-and-value heads apiece, and an embedding width of 3,584 across twenty-eight query heads, which makes one head 128 numbers wide. The fifth depends on the server's cache format: this calculator assumes two bytes per number. Multiply those out and double the answer, because there are keys and there are values.

∑ Math Interlude — one token of conversation, in bytes

Numbers first. One block of the big model keeps, for one token, four keys of 128 numbers each: 4 × 128 = 512 numbers. It keeps four values the same size, another 512. At two bytes a number that block spends 2,048 bytes on one token. There are twenty-eight blocks, so one token of conversation costs 28 × 2,048 = 57,344 bytes.

Fifty-seven kilobytes for one token, and there is nothing exotic in the arithmetic: it is a count of the boxes multiplied by the size of a box. A context of 4,096 tokens is 4,096 × 57,344 = 234,881,024 bytes. The trained context of 32,768 is eight times that, 1,879,048,192 bytes, which is two fifths of the file the weights came in.

Written out, with K and V standing for the two arrays and nothing else standing for anything:

cache = 2 × L × H × W × n × b

Lattention blocks the file stacks: 28 here, 24 in the small model
Hkey-and-value heads in one block: 4 here, and fewer than its 28 query heads
Wnumbers in one head: the embedding width over the query head count, 3,584 ÷ 28 = 128
nreserved token slots at the chosen context length, not current live token occupancy
bassumed bytes per cached number: 2 for f16; verify the effective server cache format
2keys and values: two arrays, both this size, one of each for every token
L × H × Wnumbers one token costs across the whole stack: 14,336 for this file

Four of the six are read out of the header and never chosen. n is chosen, and it is the only one on the list that anybody turns a dial on, which is what makes the whole of this chapter's second half an argument about one number.

Two details in that product earn a sentence each. The head width is a division rather than a field: the format writes down the embedding width and the query head count and leaves the head width to whoever wants it, so a reader that guessed it would be guessing. And the key-and-value head count is four while the query head count is twenty-eight, which is a design decision made by the people who trained this model and is the single largest saving in the whole term. Twenty-eight key-and-value heads would have made a token cost 401,408 bytes instead of 57,344, and the same conversation seven times dearer.

The three sums at two context lengths, against one ceiling Two horizontal bars drawn to one scale under a ceiling mark at eight thousand million bytes. The upper bar is the two models at a context of 4,096 tokens: a long weights band of 5,080,881,888 bytes, then a narrow cache band of 285,212,672 and a narrow overhead band of 483,786,272, with a wide run of empty room before the ceiling. The lower bar is the same two models at 32,768 tokens: the same weights band, a cache band eight times longer at 2,281,701,376, and an overhead band of 1,195,771,424, ending past the ceiling by 558,354,688 bytes. The weights band is identical in the two rows and neither of the other two is. TWO MODELS HELD AT ONCE, AT TWO CONTEXT LENGTHS the ceiling, 8,000,000,000 4,096 weights room left over 5,080,881,888 + 285,212,672 + 483,786,272 = 5,849,880,832 32,768 weights cache 5,080,881,888 + 2,281,701,376 + 1,195,771,424 = 8,558,354,688 the middle band is the cache and the right-hand band is the overhead the lower row ends 558,354,688 bytes past the line
Figure 89.1 — both bars are the same two models and the same two files, drawn to one scale. The band on the left is the weights and it is the same band in both rows, because choosing a context length does not touch a file. Between the rows the cache grows by 1,996,488,704 bytes and the overhead by 711,985,152, and the lower bar ends past the ceiling.

The number the file does not have

Two sums down and the third has to be got a different way, because nothing in the header says it. A server holding a model holds working room as well: somewhere to put the arithmetic of one batch of tokens while it is being done. How much is a decision the server's own code makes out of the model's width and the batch it works in, and a program that guessed at it would be publishing a guess in the same column as two exact figures.

So it gets weighed. The server is asked to put a model in memory at a named context and then asked how much it needs to hold it; the two terms that can be computed are subtracted; what remains is the residual this page calls overhead. Ollama's /api/ps reports size and size_vram; subtracting file bytes and raw cache bytes from size defines this residual, without establishing a process-RSS or whole-machine measurement. A match in one row follows from the subtraction itself. Other contexts test how well that estimate carries.

▤ Reference capture conditions

The committed capture used Ollama 0.12.3 on an eight-core Ryzen 7 3700X with 30 GB of RAM, CPU-only. The digest-pinned image appears in the volume's run scripts. They explicitly set OLLAMA_MAX_LOADED_MODELS=1 and OLLAMA_HOST=0.0.0.0:11434. Requests name their context length; the calculator assumes two-byte, f16 KV elements and one cache slot by default. The capture does not record the effective cache type, num_batch, OLLAMA_NUM_PARALLEL or Flash Attention setting. Those remain unknown, not measured defaults. -elem and -held change arithmetic, not server configuration.

Record those effective settings, model digest, backend/device placement and memory counters before making a new capacity claim. Keep the old capture as evidence of that run; a dependency upgrade needs a new comparison, not replacement numbers guessed from the file.

▣ Build · stage 3 — a load is not a generation

The client needs one method it has not had. Asking a server for a completion loads the model as a side effect, and timing that request would be timing a disk read with a sentence on the end of it. A request with no prompt in it loads and answers nothing:

// Load puts a model in memory at a context and asks it for nothing.
//
// Loading and generating are two requests here rather than one, and
// keeping them apart is the difference between a rate about a model and
// a rate about a disk. The first answer out of a model that is not
// resident waits for several gigabytes to be read; the second does not.
// A page that timed one request and called the answer tokens a second
// would be quoting the disk.
//
// The server answers this with an empty response and its own word for
// why it stopped, which is the word load and not the word stop.
func (c *Client) Load(ctx context.Context, model string, want int) error {
	if model == "" {
		return fmt.Errorf("lang: a load with no model named")
	}
	body, err := json.Marshal(loadReq{Model: model, Options: ctxOnly{NumCtx: want}})
	if err != nil {
		return fmt.Errorf("lang: %s: %w", model, err)
	}
	var res generateRes
	if err := c.post(ctx, "/api/generate", body, &res); err != nil {
		return err
	}
	if !res.Done {
		return fmt.Errorf("lang: %s: the server sent a load it had not finished", model)
	}
	if res.Response != "" || res.Eval != 0 {
		return fmt.Errorf("lang: %s: a load answered with %d tokens, which makes it a generation",
			model, res.Eval)
	}
	return nil
}

The last check is the one that matters. If a server ever answers this with a token in it, the request stopped being a load and the run says so instead of quietly counting a completion nobody meant to spend. Beside it goes a reader for what the server is holding, which takes four fields off the answer and drops the rest: a name, the bytes, the context it was loaded at, and how many of those bytes are on a graphics card. There is no timestamp on it, and the relative-time column the server also offers is not read, for the reason every listing in this volume gives: a field never read is a field never printed by accident.

▣ Build · stage 4 — the small model, weighed nine times

This runs inside the toolchain container on the closed bridge, and it uses the small model because the small model is the one this world is allowed to load as often as it likes. Nine loads, no generations, and one question after each:

$ podman exec -w /bench world-go go run ./cmd/fits -mode weigh -role small -rungs 1024,2048,4096,6144,8192,12288,16384,24576,32768
fits: what the server holds beside the weights and the cache

  the server         world-lm:11434, waiting up to 10m0s for an answer
  the model          qwen2.5:0.5b-instruct-q4_K_M, in the small role
  pinned at          c5396e06af294bd1
  the file           397,807,936 bytes
  a cache number     2 bytes, so 12,288 bytes a token
  each rung is       one load and no generation at all

   context      server says        the cache      the residue    server's step     cache's step
      1024      728,850,688       12,582,912      318,459,840                -                -
      2048      741,957,888       25,165,824      318,984,128       13,107,200       12,582,912
      4096      768,172,288       50,331,648      320,032,704       26,214,400       25,165,824
      6144      794,386,688       75,497,472      321,081,280       26,214,400       25,165,824
      8192      820,601,088      100,663,296      322,129,856       26,214,400       25,165,824
     12288      941,189,376      150,994,944      392,386,496      120,588,288       50,331,648
     16384    1,119,447,296      201,326,592      520,312,768      178,257,920       50,331,648
     24576    1,475,963,136      301,989,888      776,165,312      356,515,840      100,663,296
     32768    1,832,478,976      402,653,184    1,032,017,856      356,515,840      100,663,296

  the residue column is what this world writes down as the overhead, and it is
  the only figure in this chapter's arithmetic that came off a socket

  what the committed table says, against what this run just measured
     context        committed    measured here   difference
        1024      318,459,840      318,459,840            0
        2048      318,984,128      318,984,128            0
        4096      320,032,704      320,032,704            0
        6144      321,081,280      321,081,280            0
        8192      322,129,856      322,129,856            0
       12288      392,386,496      392,386,496            0
       16384      520,312,768      520,312,768            0
       24576      776,165,312      776,165,312            0
       32768    1,032,017,856    1,032,017,856            0

Read the last two columns first, because between them they check that the cache arithmetic is right at all. From 4,096 tokens to 8,192, in two steps of 2,048, the server's own figure goes up by 26,214,400 bytes each time while the cache term goes up by 25,165,824. Near enough that the arithmetic is plainly what is moving; far enough apart to show that something else is moving with it, at 1,048,576 bytes a step, or half a kilobyte a token. The residual grows with it; these counters alone do not identify which internal buffer supplied that difference.

Then read the residue column top to bottom. It sits between 318 and 322 megabytes for the first five rows, over a stretch where the cache goes up eightfold, and then at 12,288 tokens it stops sitting still: 392 megabytes, then 520, then 776, then 1,032. By the last row the working room is more than three times what it was on the flat, and is bigger than the cache and bigger than the file. This capture reports 1,832,478,976 bytes for the model at that context, about four and a half times its file size.

That is the finding of the mode and it is why the overhead is written down as a table rather than as a constant. Each row carries the context it was taken at, and the type that holds one marks reuse at a longer context:

// Weighed is an overhead figure and where it came from.
//
// The two fields that are not the number are the point of the type. A
// figure measured against one server is not a figure about another,
// and a figure measured at one context is not a figure about another
// either, so neither can be dropped on the way to a page. Floor says
// the figure was taken at a shorter context than the one it is being
// used at. Treating that extrapolation as a lower bound requires an
// assumption of monotonic overhead on the same backend and settings.
type Weighed struct {
	Bytes  int64
	Ctx    int64
	Server string
	Floor  bool
}

The last table in the run is the fixture check. The nine numbers this run measured are the nine numbers committed in room.go, to the byte, which is what makes the offline budgets reproducible without a container. A fresh server's report can differ; reproducing arithmetic over a fixture does not reproduce the original hardware conditions.

▤ Note — what a floor is good for

The big model is weighed at one context and no others, because loading it costs most of the memory this world is allowed and this volume loads it once. Asking for its overhead at 32,768 tokens therefore gets the 4,096-token figure back with a flag on it saying so.

The Floor flag keeps its original API name, but represents an assumption: overhead stays constant or grows on the same backend and settings. The small model's measured rows support that pattern over those contexts; they do not prove it for the big model. Certain() checks only whether fixture contexts match. Neither method certifies physical capacity. A reused residual is an extrapolation that needs measurement at the intended configuration.

Two models against eight billion bytes

A budget wants a ceiling, and the ceiling here is a flag with eight thousand million bytes in it. That is smaller than the 8,589,934,592 bytes a machine sold with eight gigabytes of memory actually carries, and the gap is deliberate. Nothing gets a whole machine. An operating system wants some of it, this world's database wants some, and the daemon that runs The Hollow wants some, so a model server handed every byte the hardware has would have been priced against a machine nobody owns. This ceiling is smaller, but the gap does not establish enough headroom for those services. Measure and reserve their peak use separately; the ceiling is a planning input, adjustable by flag.

The sum below explores a hypothetical configuration: the hero and small model resident together, each with one cache slot. The reference server cannot hold this pair because OLLAMA_MAX_LOADED_MODELS=1 limits it to one resident model. Asking for the embedding model replaces the currently loaded model; it does not join the other two. The two-model sum neither describes that server's observed residency nor measures the unpriced embedding model. A real multi-model deployment needs its own allocation capture.

▣ Build · stage 5 — the pair at the context this world asks for
$ go run ./cmd/fits -mode room
fits: estimated two-model residency, against a ceiling

  the ceiling        8,000,000,000 bytes
  the context        4096 tokens for both
  conversations      1 of each in flight
  a cache number     2 bytes
  the cache term     counted

  model                 weights            cache         overhead        all three
  hero            4,683,073,952      234,881,024      163,753,568    5,081,708,544
  small             397,807,936       50,331,648      320,032,704      768,172,288

  hero         qwen2.5:7b-instruct-q4_K_M
  small        qwen2.5:0.5b-instruct-q4_K_M

  what the configuration costs          5,849,880,832
  the ceiling                           8,000,000,000
  left over                             2,150,119,168

  the estimate is under the ceiling; actual fit is unverified, and each overhead fixture matches this context, not necessarily this backend or concurrency
  hypothetical residency; the reference server loads at most one model
  reserve host/process memory separately; measure the actual backend and concurrency
  2,150,119,168 allows at most 9 extra hero cache(s) in this estimate, at 234,881,024 apiece; concurrency is unverified

The estimate leaves 2,150,119,168 bytes. Dividing by 234,881,024 gives nine extra cache slots if every other term stays fixed. That is a cache-only upper bound within this estimate, not nine proven conversations. Parallel requests share weights, but can change batching, work buffers, scheduling and latency. Set the desired server parallelism and measure the workload before admitting it.

▣ Build · stage 6 — the same pair, up the ladder

The ladder prices the same pair across context lengths. Most rows reuse a shorter-context hero residual, which the output marks explicitly:

$ go run ./cmd/fits -mode ladder
fits: estimated two-model residency across context lengths

  the ceiling        8,000,000,000 bytes
  conversations      1 of each in flight
  a cache number     2 bytes

   context      both caches   both overheads          all of it           left    under    cap
      4096      285,212,672      483,786,272      5,849,880,832  2,150,119,168      yes      9
      6144      427,819,008      484,834,848      5,993,535,744  2,006,464,256     yes*      5
      8192      570,425,344      485,883,424      6,137,190,656  1,862,809,344     yes*      3
     12288      855,638,016      556,140,064      6,492,659,968  1,507,340,032     yes*      2
     16384    1,140,850,688      684,066,336      6,905,798,912  1,094,201,088     yes*      1
     24576    1,711,276,032      939,918,880      7,732,076,800    267,923,200     yes*      0
     32768    2,281,701,376    1,195,771,424      8,558,354,688   -558,354,688       NO      0

  * a shorter-context overhead is reused; calling it a floor assumes monotonic growth.
  under compares only the estimate to the ceiling; no row proves actual fit.
  The reference server loads at most one model, not this hypothetical pair.

  cap is a cache-only upper bound within the estimate, not measured concurrency.
  Reserve host/process memory and remeasure overhead at the intended concurrency.

At 4,096 tokens the pair's estimate is 5,849,880,832 bytes; at 32,768 it is 8,558,354,688, exceeding the ceiling by 558,354,688. The raw cache reservation grows with the selected context, while the table supplies or extrapolates the residual.

Read the cap column as a cache-only limit. At 24,576 tokens the pair is inside the ceiling by 267,923,200 bytes, and one more conversation of the big model would want 1,407,647,744. Longer contexts leave less estimated room for parallel slots even before their other costs enter the budget.

Only the 4,096 row has matching-context fixtures for both models. A star marks an under-ceiling estimate that reuses the hero's 4,096-token residual. The last row also uses that extrapolation; its NO rejects the estimate, without proving how a different backend would allocate memory.

⚠ Worked failure — omitting the reserved cache

The mistake is easy to make and hard to see, so the bench can make it on purpose. The -nocache flag leaves the second term out and prices the weights and the overhead alone, which is how almost everybody estimates this:

$ go run ./cmd/fits -mode room -ctx 32768 -nocache
fits: estimated two-model residency, against a ceiling

  the ceiling        8,000,000,000 bytes
  the context        32768 tokens for both
  conversations      1 of each in flight
  a cache number     2 bytes
  the cache term     left out, which is what this run is for

  model                 weights            cache         overhead        all three
  hero            4,683,073,952                0      163,753,568    4,846,827,520
  small             397,807,936                0    1,032,017,856    1,429,825,792

  hero         qwen2.5:7b-instruct-q4_K_M
  small        qwen2.5:0.5b-instruct-q4_K_M

  what the configuration costs          6,276,653,312
  the ceiling                           8,000,000,000
  left over                             1,723,346,688

  the estimate is under the ceiling; actual fit is unverified, and a shorter-context overhead is reused; a floor requires a monotonic-overhead assumption
  hypothetical residency; the reference server loads at most one model
  reserve host/process memory separately; measure the actual backend and concurrency
  a budget with no cache term in it has no answer about conversations

Six gigabytes and a quarter against a ceiling of eight, and 1,723,346,688 bytes of room to spare. Both files are on the disk, both numbers came off them, and the answer is wrong. Here is the same configuration with the term put back:

$ go run ./cmd/fits -mode room -ctx 32768
fits: estimated two-model residency, against a ceiling

  the ceiling        8,000,000,000 bytes
  the context        32768 tokens for both
  conversations      1 of each in flight
  a cache number     2 bytes
  the cache term     counted

  model                 weights            cache         overhead        all three
  hero            4,683,073,952    1,879,048,192      163,753,568    6,725,875,712
  small             397,807,936      402,653,184    1,032,017,856    1,832,478,976

  hero         qwen2.5:7b-instruct-q4_K_M
  small        qwen2.5:0.5b-instruct-q4_K_M

  what the configuration costs          8,558,354,688
  the ceiling                           8,000,000,000
  over by                                 558,354,688

  the estimate exceeds the ceiling, and a shorter-context overhead is reused; a floor requires a monotonic-overhead assumption
  hypothetical residency; the reference server loads at most one model
  reserve host/process memory separately; measure the actual backend and concurrency
  no estimated room remains; one extra hero cache alone would require 1,879,048,192

The difference between the two runs is 2,281,701,376 bytes and it is entirely the cache. This is a worked arithmetic failure, not a captured out-of-memory event. The omitted bytes may matter at load time: this chapter's weighing mode loads with no prompt and already observes context-dependent allocation. A backend can reserve KV storage for the configured context before live tokens occupy it. Token occupancy then grows inside that reservation, without necessarily increasing process memory per token. A dynamically growing cache can behave differently. Either way, budget the intended capacity instead of assuming a short first answer proves the longest request will fit.

And the memory is only half of what a long conversation costs. Here is the same instruction sent to the small model with more and more of the chronicle in front of it, with the same context, the same token limit and the same seed every time, and with not a word of any answer read or printed:

$ podman exec -w /bench world-go go run ./cmd/fits -mode long -role small -limit 16
fits: the same question with more and more conversation in front of it

  the server         world-lm:11434
  the model          qwen2.5:0.5b-instruct-q4_K_M, in the small role
  pinned at          c5396e06af294bd1
  the context        4096 tokens
  at most            16 tokens back, every time
  temperature        0.00, seed 89
  nothing any of these answers said is read here or printed here

     lines     prompt   answer      seconds   tokens a sec against the first
         1        115       16        0.609         215.12          1.00x
         8        378       16        0.996         395.58          1.64x
        32       1051       16        2.271         469.83          3.73x
        64       2011       16        3.572         567.50          5.87x

  measured here on the processor, with no graphics card in it; yours will differ.
  What does not differ is which column grows and which one does not: the answer
  is the same length every time and the prompt is what got dearer

The answer is sixteen tokens in all four rows and the wall clock runs from 0.609 seconds to 3.572, on an eight-core Ryzen 7 3700X, on the processor, with no graphics card in it. Those four are the machine's and yours will be four other numbers; the two columns beside them count the prompt and the answer. The fixed prompt and tokenizer determine the input count; sixteen is the requested output limit, not a guarantee that another generation cannot stop earlier. Every token of prompt has to be read before a single token of answer can be written, and reading it is the work chapter 82 counted by hand: each token dotted against every other. At two thousand tokens that cost is still rising about in step with the prompt, which is what chapter 82's own crossover arithmetic predicted for a model of this width; the term that grows with the square of the row is waiting further up. A budget with no cache term in it has no way of seeing any of this coming, because the number it left out is the only number in the sum that knows how long the conversation is.

Loaded once and timed twice

Everything so far has been done to the big model without loading it. Its header was dumped to a file five chapters ago and every figure since has come off those bytes, which is why a chapter about seven billion parameters has been running on a workstation. That was the cheap half of the bargain. Now the expensive half: put the thing in memory once, weigh it where it stands, and ask it one question.

The load and the question are two requests, and separating them is the only way the rate at the end of this run means anything. A first answer out of a model that is not resident spends most of its time reading four and a half gigabytes off a disk. Time that and divide by the tokens and you have measured a disk.

Which is also why the server is emptied first, and why that is a command on this page instead of an assumption behind it. The run above left the small model in memory, and this server holds one model at a time, so the load below would be a load that evicted something — and the mode refuses to start at all if the file it means to time is already resident. Killing the container and starting it again is the shortest way to a server holding nothing; the weights are in a volume and are not touched by it.

▣ Build · stage 7 — the one load this volume spends
$ podman kill world-lm && podman start world-lm
world-lm
world-lm
$ podman exec -w /bench world-go go run ./cmd/fits -mode hero
fits: the model the budget is about, loaded once and asked one question

  the server         world-lm:11434, waiting up to 10m0s for an answer
  the model          qwen2.5:7b-instruct-q4_K_M
  pinned at          2bada8a745067700
  the context        4096 tokens
  at most            96 tokens back
  temperature        0.00, seed 89
  the prompt         261 bytes, 3 chronicle lines and one instruction

  the server is holding 0 model(s), and this is not one of them

  what it costs to hold, at this context
    the weights, which are the file           4,683,073,952
    the cache, 57,344 a token                   234,881,024
    the residue, which is the overhead          163,753,568
    the three of them                         5,081,708,544
    and what the server says it needs         5,081,708,544
    of which, on a graphics card                          0

  what the exchange cost
    tokens of prompt, its count                         120
    tokens of prompt, our own table                      69
    tokens of answer                                     27
    tokens in the whole decision                        147
    it stopped because                                 stop

  and what the clock said, which belongs to this machine and not to the file
    putting the weights in memory                    3.107s
    the whole exchange after that                    7.465s
    tokens a second over the decision                 19.69
    measured here on the processor, with no graphics card in it; yours will differ

  what the model wrote, and yours will differ:

These lines describe the number of hobbs present and the total born and gone over three consecutive years in a place called The Hollow.

Take the memory block first, because it is the last brick in the calculator. The server says it needs 5,081,708,544 bytes to hold this model at 4,096 tokens. The file is 4,683,073,952 of them and the cache arithmetic accounts for 234,881,024 more, so the residue is 163,753,568, and that is the figure the whole ladder above was built on. It is the one number in this chapter that came off a socket, and the run says so on the line that prints it.

Then the token block, which is chapter 81 arriving with a bill. The prompt is 261 bytes of plain text and it costs 120 tokens by the model's own table. The merge table this volume trained on the chronicle alone makes the same 261 bytes 69 tokens, because that table learned this exact corpus and nothing else, and 69 is the sort of number it can only produce about text that looks like The Hollow's own history. Two tables, one unit, and the unit is what everything in this volume has been priced in.

Last, the clock, and every figure on it belongs to one machine. Putting the weights in memory took 3.107 seconds and the exchange after that took 7.465, for 147 tokens, which is 19.69 tokens a second. Measured on an eight-core Ryzen 7 3700X, on the processor, with no graphics card involved anywhere. GPU performance and allocation depend on the device and backend; this run establishes neither a speedup factor nor GPU memory use.

The sentence at the bottom is an exemplar. A fresh run can change its words, token count and timings, as well as the server's allocation report.

▣ Build · stage 8 — from a rate to a plan

One decision took 7.465 seconds. That is an interesting number for about four seconds, and then the useful question arrives: what does a day of that buy? The arithmetic is a division and it takes the rate as a flag, because the rate is the reader's and not this book's:

$ go run ./cmd/fits -mode hour -in 120 -out 27 -rate 19.69
fits: what a measured rate buys, in decisions

  a decision is      120 tokens of prompt and 27 of answer
  which is           147 tokens
  measured at        19.69 tokens a second

    seconds a decision                         7.47
    decisions an hour                         482.2
    decisions a day                         11572.9

    a decision 2 times the size:    14.93 seconds,    241.1 an hour
    a decision 4 times the size:    29.86 seconds,    120.6 an hour
    a decision 8 times the size:    59.73 seconds,     60.3 an hour

  the rate came off a machine and the arithmetic did not. It is also a rate
  about one size of decision: a model reads a prompt faster than it writes an
  answer, so the four rows above are a plan and not a measurement

Four hundred and eighty-two decisions an hour, eleven and a half thousand a day, on a processor, from a seven-billion-parameter model at four bits a weight and a context of 4,096. Every one of those qualifiers has to travel with the number. A rate taken from the small model would be a different number about a different file, and setting the two side by side without naming the model, the quantization, the context and the machine for both is the one mistake this volume has been most careful not to make.

The three rows underneath are a plan and they say so. Doubling the size of a decision halves the number of them per hour if the rate holds, and the rate does not quite hold, because a model reads a prompt faster than it writes an answer. The run prints that caveat itself instead of leaving it to a footnote, and the honest way to get the number for a decision of a different size is to measure a decision of that size.

Between two ticks

There is one job in this world that wants a model, and the last chapter left it running by hand: read the chronicle lines that have no vector yet, ask for a row apiece, write them back. Twenty-six lines of loop. The question that chapter did not answer is where it goes, and the answer has been fixed since the last volume: not inside a tick.

The rule arrived as a rule about databases. A tick that can open a transaction is a tick that can wait on a disk, so nothing in the simulation reaches the store. Put a model server on the same bridge and the same rule comes back an order of magnitude harder. A tick is a hundred milliseconds at the rate this world was built to run at. The average embedding cost on this machine is 51 milliseconds; the decision the big model answered above took seven and a half seconds. Waiting for either call delays the next tick, whether the call sits inside a tick or immediately before it.

So the job goes where the save goes: on the boundary between two ticks, where no phase is part way through, no view is open and no creature is half stepped. That protects state consistency, not response time. This teaching example calls onePass synchronously; the world clock stops advancing until it returns. The call changes wall-clock pace without changing the ordered tick arithmetic.

▣ Build · stage 9 — the loop, lifted into the tick loop
	for w.tick() < ticks {
		// The boundary. Everything in this loop that is not a tick
		// happens here, because here is the one place where no phase is
		// part way through and nothing is holding anything.
		if w.tick()%terra.Year == 0 && w.tick() > 0 {
			text := fmt.Sprintf("year %d: %d %s standing in %s, %d born and %d gone",
				w.tick()/terra.Year, len(w.r.Live), doc.Many("living"), doc.One("ground"),
				w.p.Made, w.r.Gone)
			if err := db.Tell(ctx, w.tick(), "count", text); err != nil {
				return err
			}
		}
		if c.fill && w.tick()%c.every == 0 {
			at := time.Now()
			n, sent, err := onePass(ctx, db, cl, m, c.batch)
			inJob += time.Since(at)
			if err != nil {
				return err
			}
			if sent > 0 {
				passes++
			}
			asked += sent
			wrote += n
		}
		w.v.Tick()
		w.fold()
	}

One pass avoids an unbounded drain of the whole history. onePass takes up to a batch of waiting lines, embeds them, writes them and returns. A job that ran until there was nothing left would have the whole history as its worst case, and the history is the one thing about this world that only grows. Everything it holds it holds inside that call: by the time it returns, every socket it wanted is done with, and the next tick starts from where the last one ended. The batch bounds the number of rows, not an interactive latency guarantee. Each network request still has its own timeout; several slow requests can stall this serial loop for a long time.

The flag that turns it off is what makes the next two runs a measurement. Both found the same world from the same seed, tell the chronicle the same count once a year, and run for the same twenty valley-years. One of them talks to a model server and the other does not.

▣ Build · stage 10 — twenty valley-years, with the job off
$ podman exec -w /bench world-go go run ./cmd/fits -mode pace -years 20
fits: the valley, ticking, with the eighth table being filled between ticks

  the world          seed 5, 16x12 cells, 21 browsers and 8 hunters standing
  the run            20 valley-years, which is 72000 ticks
  the chronicle      one count a year, told between ticks
  the job            off

  what the world did, which the flag does not touch
    ticks run                                 72000
    animals walking at the end                  235
    born                                       1645
    gone                                       1439
    plants standing                             132
    the digest of every tick           a6b27c9079249f5c

  what the chronicle holds
    lines                                        30
    rows in the eighth table                      0
    lines with no row from this model            30

  what the job did between ticks
    passes that had work                          0
    lines sent to the model                       0
    rows the copies reported                      0
    spent inside the job                         0s

  72000 ticks in 31.169s, 2310 ticks a second (measured here; yours will differ)

Thirty lines in the chronicle: eleven written at the founding and one a year for the nineteen year-boundaries the run crossed. None of them has a vector, because nothing has asked for one.

▣ Build · stage 11 — and with the job on
$ podman exec -w /bench world-go go run ./cmd/fits -mode pace -years 20 -fill
fits: the valley, ticking, with the eighth table being filled between ticks

  the world          seed 5, 16x12 cells, 21 browsers and 8 hunters standing
  the run            20 valley-years, which is 72000 ticks
  the chronicle      one count a year, told between ticks
  the job            on, every 3600 ticks, 32 waiting lines a pass
  the model          nomic-embed-text:v1.5, at world-lm:11434
  pinned at          970aa74c0a90ef74

  what the world did, which the flag does not touch
    ticks run                                 72000
    animals walking at the end                  235
    born                                       1645
    gone                                       1439
    plants standing                             132
    the digest of every tick           a6b27c9079249f5c

  what the chronicle holds
    lines                                        30
    rows in the eighth table                     30
    lines with no row from this model             0

  what the job did between ticks
    passes that had work                         20
    lines sent to the model                      30
    rows the copies reported                     30
    spent inside the job                     1.524s
    for one line of chronicle                  51ms
    ticks this world runs in that               112

  72000 ticks in 32.703s, 2202 ticks a second (measured here; yours will differ)

Hold the two runs against each other a block at a time. Two hundred and thirty-five animals walking, 1,645 born, 1,439 gone, 132 plants standing, and the same sixteen characters folded over all seventy-two thousand ticks: a6b27c9079249f5c in both. Putting a language model beside this world changed not one number the world produced. That is the design working, and it is checkable rather than asserted, because the digest covers every plant, every creature, the ground, the litter and four counters, once a tick, for the whole run.

What did change is the clock. 2,310 ticks a second became 2,202: the world's own pace is down by just under five per cent, and 1.524 seconds of the 32.7 went into the job. Thirty lines of chronicle at 51 milliseconds apiece, and the run puts that in the world's own units: one round trip to the embedding model is 112 ticks of this valley. Both figures were measured on an eight-core Ryzen 7 3700X with the database and the model server in containers beside the daemon, on the processor, and yours will differ. The counts and the digest agree under the same reference simulation environment.

The 112-tick figure uses the accelerated bench rate, about 2,202 ticks per second. It does not mean 112 scheduled 100 ms intervals: 51 ms is roughly half of one such interval. An average also hides the slowest call and the initial backlog. This serial result establishes a throughput cost, not that interactive tick deadlines hold.

▤ Before interactive play

An interactive implementation needs a bounded queue and bounded workers, request deadlines and cancellation, plus a stated overload policy. Workers consume immutable requests and return results; only the simulation owner admits state changes at a tick boundary. Merely adding a goroutine lets completion timing choose which tick receives an answer, breaking replay if that answer changes the world.

Name each request and its intended admission rule. Record accepted outcomes and their admission ticks, including rejection, timeout and fallback decisions, so replay reads the recorded inputs instead of calling a live model again. Deterministic request order alone does not make wall-clock completion deterministic. Those are prerequisites for the planned village and multiplayer work, not features this serial bench implements.

▣ Build · stage 12 — nine things the arithmetic has to satisfy
$ go test ./internal/lang/ -run 'TestOneTokenOfCacheIsBothArraysAndNotOne|TestAHeadWidthThatIsNotWholeIsRefused|TestTheCacheGrowsWithTheContextAndTheWeightsDoNot|TestConversationsInFlightBuyCachesAndNotWeights|TestTheSamePairFitsAtOneContextAndNotAtAnother|TestAFloorIsTheLeastAnOverheadCanBe|TestABudgetWithNoCacheTermAnswersTheSameAtEveryContext|TestNobodyHasWeighedTheEmbeddingModel|TestADecisionIsPricedInTokensAndPaidForInSeconds' -v
=== RUN   TestOneTokenOfCacheIsBothArraysAndNotOne
    room_test.go:65: hero: 28 layers x 4 heads x 128 wide x 2 bytes, doubled, is 57344 bytes a token
    room_test.go:65: small: 24 layers x 2 heads x 64 wide x 2 bytes, doubled, is 12288 bytes a token
--- PASS: TestOneTokenOfCacheIsBothArraysAndNotOne (0.00s)
=== RUN   TestAHeadWidthThatIsNotWholeIsRefused
    room_test.go:80: lang: 3584 numbers a token over 27 heads is not a whole head width
--- PASS: TestAHeadWidthThatIsNotWholeIsRefused (0.00s)
=== RUN   TestTheCacheGrowsWithTheContextAndTheWeightsDoNot
    room_test.go:100: the weights are 4683073952 bytes at every context and the cache runs to 1879048192
--- PASS: TestTheCacheGrowsWithTheContextAndTheWeightsDoNot (0.00s)
=== RUN   TestConversationsInFlightBuyCachesAndNotWeights
    room_test.go:110: estimated one slot is 768172288 bytes and four are 919167232, holding the other terms fixed
--- PASS: TestConversationsInFlightBuyCachesAndNotWeights (0.00s)
=== RUN   TestTheSamePairFitsAtOneContextAndNotAtAnother
    room_test.go:136: the estimate is under at 4096 by 2150119168 bytes and over at 32768 by 558354688, with extrapolated overhead
--- PASS: TestTheSamePairFitsAtOneContextAndNotAtAnother (0.00s)
=== RUN   TestAFloorIsTheLeastAnOverheadCanBe
    room_test.go:158: lang: the hero model has been weighed at 4096 tokens and nothing shorter than 1024
    room_test.go:170: 9 weighings of the small model, none of them smaller than the one below it
--- PASS: TestAFloorIsTheLeastAnOverheadCanBe (0.00s)
=== RUN   TestABudgetWithNoCacheTermAnswersTheSameAtEveryContext
    room_test.go:191: with the cache left out the weights are 397807936 bytes whatever the context is set to
--- PASS: TestABudgetWithNoCacheTermAnswersTheSameAtEveryContext (0.00s)
=== RUN   TestNobodyHasWeighedTheEmbeddingModel
    room_test.go:198: lang: nobody has weighed the embed model
    room_test.go:210: small: 9 weighing(s), all of them against ollama 0.12.3
    room_test.go:210: hero: 1 weighing(s), all of them against ollama 0.12.3
--- PASS: TestNobodyHasWeighedTheEmbeddingModel (0.00s)
=== RUN   TestADecisionIsPricedInTokensAndPaidForInSeconds
    room_test.go:237: a decision of 240 tokens at 20 a second is 12 seconds and 300 an hour
--- PASS: TestADecisionIsPricedInTokensAndPaidForInSeconds (0.00s)
PASS
ok  	theworld/internal/lang	0.023s

Three of those are about arithmetic and six are about what the calculator refuses. A header whose embedding width is not a whole number of heads is turned down instead of rounded, because a rounded head width comes back as a cache figure wrong by a head and nothing would ever say so. An overhead is refused for a context nothing was weighed at or below. And a budget with the cache term left out declines to answer the question about conversations at all, which is correct: the number it was asked for is a division by the term it was told to ignore.

Why this works

The useful idea here is older than models and applies to anything that has to be held in a finite machine. Split a cost into the part that is fixed when you choose the artefact, the part that grows with the work, and the part nobody can derive; compute the first two; measure the third and write down the conditions you measured it under. What comes out is not a benchmark, it is a function, and a function can be asked about a configuration nobody has run.

KV storage is absent from the weights file, but the selected context can make it visible in allocation counters at load time. Reserved slots and occupied slots answer different questions. The calculator prices reserved capacity; it does not watch live token occupancy.

Keeping the budgets and the rates in separate columns costs one habit and buys the only thing a reader on a different machine actually needs: a way to check. Every byte figure on this page came out of a committed file or a committed weighing, so a reader who runs the same modes gets the same bytes and can tell immediately if their file is not the file this book priced. Every second and every tokens-a-second on this page names a processor. Neither kind of number is more honest than the other; mixing them is what makes both useless.

✓ Checkpoint — a model, priced

You should be able to do all of this without opening a browser.

  • Work out, from a header alone, what one token of conversation costs a given model: blocks times key-and-value heads times head width times bytes an element, doubled, and the head width is a division and not a field.
  • Say which of the three sums moves when you change the context length, which moves when you add a second conversation, and which moves when you change neither.
  • Explain why the small model on this page costs 320,032,704 bytes of overhead at 4,096 tokens and 1,032,017,856 at 32,768, and why writing either of those down without the context beside it makes it useless.
  • Turn estimated room into a cache-slot upper bound, then name the measurements needed before treating that result as concurrent capacity.
  • Point at any figure on this page and say whether it would be the same on a machine with a graphics card in it.
  • Explain why a synchronous call between ticks still stalls the clock, and what bounded workers and recorded admission would need to change before interactive play.
⚡ Exercises — try first, then reveal
Exercise 1 — price a model with the other head arrangement. Somebody offers you a seven-billion-parameter model with 28 key-and-value heads instead of 4, everything else the same. Work out its cache at 4,096 tokens and at 32,768, and say what happens to the ladder.

One token becomes 2 × 28 × 28 × 128 × 2 = 401,408 bytes, seven times the 57,344 on this page. At 4,096 tokens that is 1,644,167,168 bytes of cache for one conversation, against 234,881,024 here. This paper exercise holds file bytes fixed to isolate KV arithmetic; changing a real model's head arrangement can also change its projection weights and needs a new header and allocation measurement.

Reusing the old residual only as an assumption, the estimate stays below eight at 4,096: roughly 5,080,881,888 of weights, 483,786,272 of measured overhead and about 1,694,498,816 of cache for the two models comes to 7.26 gigabytes, inside eight with about three quarters of a gigabyte to spare. There is no estimated room for a second hero cache, where the original estimate allowed nine extra caches. Neither estimate measures concurrency. And at 32,768 tokens the big model's cache alone is 13,153,337,344 bytes, so the configuration is over the ceiling before the weights are counted.

That is the whole argument for the arrangement these files use, made in bytes: sharing one key-and-value head between seven query heads costs the model some of its ability to attend in different ways at once, and reduces the raw KV storage by a factor of seven.

Exercise 2 — find the last context with room for a second conversation. Read the cap column of the ladder and find the largest context at which the estimate leaves room for one extra cache of the big model. Predict whether it is above or below 8,192 tokens before you look, and say what you would have to do to answer for a context that is not on the ladder.

The column reads 9, 5, 3, 2, 1, 0, 0 down the seven rows, so the answer is 16,384 tokens. Below it the estimate leaves room for an extra hero cache; at 24,576 it leaves less than one cache's worth, without establishing physical fit.

Predicting it needs one observation: doubling the context makes each conversation twice as dear and eats the room that would have paid for it, so the count falls faster than the context rises. Nine at 4,096 becomes three at 8,192, roughly a quarter, so another doubling to 16,384 lands somewhere near one and a further one lands at none. The estimated ladder gives that cache-only limit.

A context that is not on the ladder can still be asked about, and the answer comes back marked. -mode ladder -rungs 5120,7168 prices both rows on the nearest available shorter-context residuals, says 7 and 4 extra caches, and puts a star on each: the totals use extrapolated overheads. Weighing the two rungs with -mode weigh -role small -rungs 5120,7168 against a running server improves the small-model estimate. The hero residual and concurrent workload still need measurement.

Exercise 3 — change how often the job looks. The between-ticks job cost this valley just under five per cent of its pace. Run the pace mode with the job every 360 ticks instead of every 3,600 and predict what happens to that percentage, and to the number of lines embedded, before you look.

Neither moves much, and the reason is the point. The cost of the job is the cost of the lines it embeds, and how often it looks does not change how many lines there are: thirty lines is thirty round trips whether they are collected in twenty passes or in two hundred. What ten times as many passes buys is ten times as many queries against an empty table, which are cheap, and a vector that exists sooner after its line is written.

The percentage is set by the ratio of two things, and neither is the cadence: seconds of embedding against seconds of ticking. This world writes one chronicle line a valley-year and runs a valley-year in about a second and a half, so it generates 51 milliseconds of work for every 1,500 milliseconds of ticking. Three per cent, in the steady state, plus whatever backlog the founding lines left.

Fewer lines reduce inference work. A separate machine can reduce local CPU contention, but this synchronous client would still wait on its replies. Bounded asynchronous workers can stop those waits from blocking ticks; they cannot make computation or network traffic free.

What the language machinery does not touch

This volume put a language beside The World and changed nothing inside it. There is a merge table trained on The Hollow's own history, one attention head worked out by hand on four tokens, a table of counts that writes fluent sentences about years that never happened, a model nobody here trained pinned by the hash of its own weights, and a sum that assigns every one of its 4,683,073,952 bytes to a tensor with nothing left over.

What it cannot do is a list that has not got shorter. Nobody in The Hollow says a word. There is no villager, no character, and nothing in the valley has a thought about anything: the hobbs still act on a row of twenty-four numbers through a wiring they were born with, exactly as they did before any of this arrived. No tick calls a model inside its simulation phase; the serial boundary job still delays subsequent ticks. Nothing that comes back from a search is fed to a model, nothing here is a memory, and the eighth table is thirty rows of numbers about thirty sentences a program wrote about a population count. The model on the other side of that socket does not know this world exists.

The sharpest limit is inside the result rather than beside it. Every honest thing on this page is honest because it was counted: the bytes came off files, the ticks came off a clock, the digest came off seventy-two thousand ticks of arithmetic that a second run reproduced exactly. The one thing on the page that none of that touches is the sentence the model wrote at the bottom of stage 7. Its bytes and token count can be checked, and its factual claims can be compared with the source rows. What these checks cannot promise is identical text from another live run. Keep that exemplar separate from the arithmetic fixtures and from any recorded decision the simulation must replay.