All writing
War story · Ops

The ceiling that was a flag

A 67.5 GB model loaded in 43 seconds. The next file up, 71.7 GB, took a whole server dark for fifteen minutes. We wrote a confident post-mortem saying we needed a bigger box. We were wrong, and the same box later loaded a file 1.41 times its total memory.

13 Sep 26 · 6 min · by Stephen Robinson

In early September 2026 we were doing dull, necessary work. We had published a quantization ladder, one 122B model cut thirteen ways, about 930 GB of files in total, and we had just made ourselves a rule: no rung of a ladder counts as validated until it has actually generated a token, and no rung ever inherits another rung’s numbers. So thirteen files had to be loaded and made to speak, one at a time.

On the fourth night of that work we took a server dark for fifteen minutes, wrote a confident post-mortem, and got the cause wrong. The correct answer arrived the following night, and it moved the ceiling by roughly a factor of two.

The problem that was real

The host was Deneb, a 94 GB unRAID box that also carries four VMs, a media server, an agent process and a background compute client. About 41 GB of its memory is spoken for before anything of ours starts.

Reading the model files straight off an array spinner, a load took longer than the generation it enabled. One 55.0 GB file took 649 seconds just to reach a healthy state. The job was load bound, not compute bound, so we staged each file onto a local SSD pool first, served it, verified it, then deleted the staged copy.

That worked well. A comparable 60.7 GB file loaded in 92 seconds instead of 649, and decoded about six times faster. One caveat worth stating plainly: every tokens-per-second figure here is a page-fault-bound floor on a contended CPU-only host. They prove a file loads and speaks. They are not serving numbers.

The night it went dark

A 67.5 GB file staged, loaded in 43 seconds, and generated. The next file in the ladder, 71.7 GB, staged fine and then never answered its health endpoint.

What the host did next was worse than failing. It threw away its entire page cache, 45 GB down to 2 GB, then started paging out everything else. Load average went from 1.0 to 111. SSH refused the banner exchange. The web UI returned nothing. All four VMs stopped answering ping, while the host itself still answered ICMP and still completed TCP handshakes on every listening port. The listeners were accepting. Userspace was simply never getting scheduled. It ended on its own at 02:02:18 when the kernel OOM killer took our process.

The kill line is the interesting part. Total virtual memory 137 GB, anonymous resident 55.4 GB, file resident 344 kB. The model was resident as anonymous memory, as a private allocation, even though nothing had passed a flag disabling memory mapping and mapping is on by default.

The post-mortem we got wrong

We read that as a host memory ceiling sitting somewhere between 67.5 GB, which had been clean, and 71.7 GB, which had been an outage. Gate every future job on free RAM. The remaining six files in the ladder all exceeded the ceiling, so they needed a machine with more memory, and we should not re-attempt them here.

That is a fair reading of the evidence, and it was wrong. A wrong mechanism does not just sit there being wrong. It blocks work that would have run. That post-mortem left a thirteen-file job waiting on a memory upgrade in a different machine.

The four gigabytes that did not add up

Two things nagged before the next attempt.

First, the gap. Four gigabytes between “loads in 43 seconds” and “takes the host down for fifteen minutes” is far too narrow to be a capacity wall.

Second, that 67.5 GB file had loaded in 43 seconds on a box with 52 GB available. You cannot allocate 67.5 GB of private memory out of 52 GB and be done inside a minute. That load must have been file backed. So the question was never “why is 71.7 GB too big.” It was “why did this one go anonymous when the last one did not.”

Then we listed the files that had worked, and the list stopped looking like a size list. The seven that loaded cleanly were the seven below a particular quantization type. The one that died was the ladder’s first file of that type.

Repack

The CPU backend in llama.cpp repacks eligible quantized tensors into a layout friendlier to its matrix kernels, and it does that at load time. Repacking is a copy into a freshly allocated buffer. The memory map of the file still happens, which is why total virtual memory read as roughly model plus copy, but the resident weights now live in a private allocation the kernel cannot reclaim. That behavior is on by default.

The eligible types happen to sit at the large end of a quantization ladder. So in a ladder ordered smallest first, the type boundary and the size boundary coincide exactly once, in a single incident, and are indistinguishable from each other. It was a type boundary wearing a size boundary’s clothes.

It also explains the shape of the outage rather than just its existence. A file that is merely too large runs slowly. Anonymous pages cannot be reclaimed by dropping cache, so the kernel dumped every page of cache on the box and then went after everyone else’s working set.

The test designed to be wrong safely

We turned repacking off, after checking the flag was actually present in the image we were running, and put the whole job inside a hard memory wall using cgroup v2. Hard limit, not the soft high-water variant, which against an anonymous heap becomes a throttle livelock that every liveness check cheerfully reports as healthy. We added a per-file memory pre-gate and a watchdog. If the hypothesis was wrong, the container would kill itself and the host would never notice.

Same file, same box, same staging path, the night after the outage: staged in 694 seconds, loaded in 44 seconds, 120 coherent tokens, not OOM killed, and host available memory flat at 52 GB for the entire run. Three more files followed that night, up to 88.9 GB, all green.

How far it actually goes

Two nights later we ran the last two files, which gave us a clean control. The 102.38 GB file is not repack eligible and loaded in 92 seconds. The 132.55 GB file is repack eligible, so it is precisely the case that caused the outage, at nearly double the size, and it is 1.41 times the host’s total memory. It loaded in 121 seconds and generated. Host available memory stayed between 53 and 55 GB and never moved. All four VMs stayed up. The ladder finished thirteen of thirteen.

So the corrected gate is conditional. With repacking on, the job is bounded by free RAM. With it off, the job is bounded by address space, and “far larger than total RAM” is a supported regime rather than a stretch.

One trap if you copy this. Our pre-gate read “available memory must exceed cap plus floor,” and when the host came up a few hundred megabytes short it silently skipped the file, and the chain still exited zero. A run that does nothing and reports success is worse than one that fails. We lowered the wall, which is strictly more protective, and recorded the cap alongside the numbers so runs under different walls never get silently compared.

The lessons

A gap too small to be a wall is a clue, not a coincidence. Four gigabytes between a clean load and a host outage was never a capacity story.

Sort your failures by what they are, not by the order you tried them in. Ordered smallest first, a type boundary is invisible. It only appeared when we listed which files had worked and noticed the list had nothing to do with size.

Design the retry so that being wrong is cheap. The hard memory wall meant the price of a bad hypothesis was a killed container instead of a second outage, and that is the only reason we were willing to test it on the machine it had already taken down.

Go back and amend the wrong answer where it lives. The original post-mortem still sits in our knowledge base with its conclusion struck out and replaced, because the next person to hit this searches the symptom, and the symptom leads straight to the paragraph we got wrong.

More from the journal

Everything here happened on the hardware this site is about. If this one was useful, the rest of the ledger is one click away.