Skip to content

Release 1.11.0 - #1467

Merged
JustVugg merged 170 commits into
mainfrom
dev
Sep 13, 2026
Merged

JustVugg merged 170 commits into
mainfrom
dev

Conversation

@JustVugg

Copy link
Copy Markdown
Owner

Release 1.11.0. Fifty-six pull requests since v1.10.2: a ninth model family, five real bugs closed across four engines, and the two platforms the C tests never built on (Windows UCRT64, musl) now building them in CI.

Merge with --admin (main is protected), then tag v1.11.0 and push the tag: release.yml builds the three archives on the tag and takes the notes from the ## [1.11.0] section of the CHANGELOG. This time the Windows archive carries deepseek_v41.exe.

The ninth engine: DeepSeek V4.1 Flash (#1453)

552B, 510 GB on disk, streamed from an SSD on a CPU box, no conversion: the released checkpoint is read natively. Engram, the DSA indexer, hyper-connections, the compressor, the 32-layer vision tower, DSpark speculative decoding and DSML tool calling are all in. Held token-exact in CI against a torch-only CPU reference at three cache capacities and under every speculative mode; vision matched to 5e-06.

Measured cold on the released checkpoint: a turn went from 78.7 s to 25.1 s during the work (0.305 to 0.957 tok/s), every step bit-exact against what it replaced. A five-turn chat runs at 1.14 to 1.58 tok/s. Verified end to end through coli serve (text and image_url) and coli chat on the real checkpoint before this PR was opened. Two ways of hiding the expert reads were built, measured worse, and removed; both are documented with the numbers.

If you hit one of these on 1.10.2, this is the fix

Added

Verified before opening

dev at the head of this PR: make test-c green, the python suite green (883 tests), every engine builds, and the CHANGELOG cites exactly the 56 merges after the v1.10.2 tag, none from before it. Full detail in CHANGELOG.md.

Unknown-Findout and others added 30 commits August 29, 2026 03:33
The auto tier decremented `remaining` by coli_cuda_tensor_bytes(), which is
the LOGICAL size of an expert. The allocator takes more than that, and
nothing charged the difference, so the budget drifted optimistic by a term
that grew with the tier. That is why auto could claim a card to within
4 MiB and then fail every lazy dense upload afterwards while reading as
healthy.

THE MECHANISM. It is not workspace overhead. cudaMalloc rounds a request
up, and a GLM-5.2 int4-g64 expert is six allocations of two sizes:

    3x weights  6,291,456 B = 6.00 MiB   already on a boundary, +0
    3x scales     786,432 B = 0.75 MiB   lands in 1.00 MiB, +0.25 each

0.75 MiB per expert uncounted. Measured here on sm_86; @terrizoaguimor
measured 0.741 MiB/expert (sigma 0.019, five clean configurations) on
H100/H200 in #687 from the other direction, and 0.750 sits inside that
interval. At the 6,235 experts auto selects on an H200 that is 4.6 GB
against a flat 2 GB reserve.

That reframes the fix. The conclusion in #687 was that 0.741 is
model-and-card specific and should be measured live from the first uploads
rather than frozen into the source. The distrust of the constant was right
and the reason was not: the term is roundup(scale_bytes) - scale_bytes,
which is a property of the allocator and the model geometry, so it is
knowable before a single expert is placed. No fixed-point iteration and no
measurement pass over real uploads.

coli_cuda_alloc_footprint() probes it, once per distinct size, cached. It
is PROBED rather than modelled on purpose: the rounding is a driver and
architecture property and a table fitted to one card would be silently
wrong on the next. coli_cuda_tensor_vram() applies it per ALLOCATION,
since the weights and the scales are separate cudaMallocs and each is
rounded on its own - summing first and rounding once would miss the scale
array's padding entirely, which is the whole term.

coli_cuda_tensor_bytes() is untouched and still logical: it mirrors upload
and free so the three cannot drift, and there is a test pinning that.
m->gpu_expert_bytes also stays logical, because it is reported to the user
as the tier's size and quoting allocator padding as model bytes would
trade one wrong number for another. Only `remaining` moves.

TWO IMPLEMENTATIONS THAT DID NOT SURVIVE MEASUREMENT, both recorded in the
source so nobody rebuilds them:

  cudaMemGetInfo per placed expert is the obvious exact answer and it is
  O(live allocations) - 0.52 us empty, 68.2 us with 12,000 live. A tier
  that places thousands would make that quadratic.

  A single-allocation probe over-reports by 3x. One cudaMalloc reserves a
  whole 2 MiB VMM page, so one allocation of anything smaller reads as a
  flat 2 MiB: 786,432 B measured 2.00 MiB at n=1 and 1.00 MiB at n=64.
  That version would have shrunk the tier worse than the bug it fixes. Its
  own test caught it.

Verified on Windows, RTX 3090 (sm_86), CUDA 13.1, MSVC 19.50.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Eight assertions against the LIVE allocator rather than against a rounding
table, because the rounding is a driver and architecture property and a
test that hardcoded one card's numbers would fail on the next.

The load-bearing one is "footprint never below the request". The whole bug
was a budget charged less than reality, so a footprint that under-reports
would reintroduce it somewhere new.

Two are negative controls, and they are the reason this suite means
anything:

  an aligned 6 MiB request must be charged EXACTLY 6 MiB. Without it the
  suite passes on a function that simply doubles everything, which is
  precisely the failure the first draft had.

  500 repeat calls must move free VRAM by 0 bytes. Checked by reading free
  VRAM rather than by timing, which would be flaky under load.

The probe is also cross-checked against an independent bulk measurement in
the same run, so the function and the ground truth cannot drift together.

Wired in at the END of cuda-test: it is the only test here that allocates
in bulk to measure an amortised cost, so it should not leave the card
fragmented underneath a kernel test that follows it.

Full run on RTX 3090 (sm_86), CUDA 13.1, MAKE_EXIT=0, all eight binaries:

  backend_cuda        q8/q4/q2/f32/e8 correctness ok
  ragged_attention    ok
  fp8_warp            shared-lut 0, hw-cvt 0, mutated entry 1 (fires)
  absorb_determinism  batch 0/145, ragged 0/29
  fp8_cuda            oracle 0 mismatches, API 0 mismatches
  weights_owned       8 fail cycles, 0 bytes cumulative
  mxfp4               ok
  alloc_footprint     ok, per-expert unaccounted 0.7500 MiB

Python suite unchanged at 696 tests, OK, skipped=52.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CI caught this: on Windows the host does not link backend_cuda.cu, it links
backend_loader.c and resolves coli_cuda_* out of coli_cuda.dll at runtime.
Every exported symbol is hand-wrapped there, and I added two without
wrapping them, so `make colibri CUDA_DLL=1` failed to link:

  undefined reference to `coli_cuda_tensor_vram'   (x6 call sites)

RESOLVE_OPT, not RESOLVE, and that distinction is the whole point. RESOLVE
prints "missing symbol" and calls FreeLibrary, so pairing a new host with a
coli_cuda.dll built before #687 would take the ENTIRE CUDA backend down over
one absent function - far worse than the over-commit this branch fixes. The
e8_set_grid and fp8_set_lut entries above it set that precedent for exactly
this reason and carry the same comment.

The wrappers degrade to the old behaviour rather than to zero:

  tensor_vram      -> tensor_bytes, the LOGICAL size, which is what the tier
                      charged before this branch. An under-count, and
                      identical to what shipped. Returning 0 would be much
                      worse: the caller would charge nothing at all for an
                      expert it just placed.
  alloc_footprint  -> the request unchanged, i.e. no padding, same as before.

Verified locally on the same two commands the CI job runs:

  make colibri CUDA_DLL=1                    exit 0   (was: undefined ref)
  make cuda-dll CUDA_ARCH=sm_80              exit 0

and both symbols are in the DLL's PE export table beside tensor_bytes:

  [ 0] coli_cuda_alloc_footprint
  [47] coli_cuda_tensor_bytes
  [53] coli_cuda_tensor_vram

Negative control run rather than assumed: reverting backend_loader.c alone
reproduces the six undefined references, restoring it links clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
test_abi_is_derived_from_the_loader_source pins the loader's export surface
by count, and it caught this branch: 5 optional against an asserted 3.

The test's own docstring says what to do - update the counts and name the
symbol you added, so the next person reading a failure gets a reason rather
than a different integer - so both, and the reason is the RESOLVE_OPT choice
rather than the symbols themselves.

  mandatory  47 -> 47   unchanged, neither symbol is required
  optional    3 ->  5
  exports    50 -> 52

Mandatory is the number that matters here and it deliberately did not move.
coli_cuda_tensor_vram and coli_cuda_alloc_footprint are resolved with
RESOLVE_OPT, so a coli_cuda.dll built before #687 leaves both pointers NULL
and the wrappers fall back to the logical byte count - exactly what the
expert tier charged before this branch. Adding them as mandatory would have
widened the contract every Windows DLL must satisfy and taken the entire
CUDA backend down on any older DLL, over a sizing refinement.

Python suite 707 tests, OK, skipped=50.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mw_ur3_raw_adapter.py parses a raw capture of the engine's batch wire
protocol and re-checks it against a frozen corpus: frame parsing for the
profile, done, echo and data kinds and for the group frames, with every
malformed field a named error carrying the frame's byte offset and every
unknown kind refused by name; identity proof, in which the request digest
is checked against the submitted bytes and the corpus, the token payload
digest is recomputed from the frozen tokenizer, and a capture that omits
an authority fails closed; and the longest-common-prefix and
log-likelihood-ratio helpers, pinned by hand-computed literals including
a direction-sensitive case, so a reversed comparison cannot pass.

Where a capture contains none of the frame kinds a check family needs --
which is every capture today's engine produces for some families -- the
adapter reports that family as UNSUPPORTED by name and never degrades it
to a partial pass. The process exit statuses are documented in the module
docstring exactly as main implements them.

The three real-authority digests the module ships are pinned against
literals stored in the test, loaded through an unpatched copy of the
module so the check is independent of the synthetic-authority patch the
rest of the suite runs under. The one test that needs the real frozen
corpus is opt-in through MW_UR3_CORPUS_DIR and names that variable in its
skip reason; every other test in the module is self-contained.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…latform

The raw adapter treats the corpus and comparisons authorities as byte-exact
JSONL and rejects a CRLF record as noncanonical. The test module wrote those
two files with write_text() and no newline="", so on Windows text mode
translated every "\n" to "\r\n" and 41 tests failed at corpus load with
`corpus:1: noncanonical JSONL record` (reproduced on macOS by forcing CRLF
into the same two writers: the identical 40 failures + 1 error). The other
byte-sensitive writers in the module already passed newline=""; these two
now do as well.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…n leak they found

Stacked on #1344. Its three regression tests each pin one scenario; this
adds the rules those scenarios are instances of, checked directly against
tests/qwen36_fake_cuda.h in the plain CPU build:

1. Budget accounting balances. On every device, bytes in use never exceed
   the budget and, once the queue is drained, equal exactly resident
   experts x bytes per expert: every reservation is consumed by an upload
   or handed back. Checked on one and two devices, across an LFRU swap
   (budget-neutral by construction, so `used` must not move), and across
   the path where a planned expert is reported without weights.

2. Shutdown wakes every waiter at once. All four cv_take sleepers -- the
   uploader's victim wait, qt_note_block, qt_note_planned, qt_fill_wait --
   are parked simultaneously behind a full queue and an open group;
   qt_shutdown has to bring every one of them home, under a watchdog
   thread rather than alarm() so it runs on MinGW too. Afterwards no slot
   may still read as queued and the abandoned swaps' victim keeps its
   tensor and its resident flag.

3. Issue geometry under random routing. Random resident sets, random K up
   to the row limit, one to three devices, 200 seeds each: every device
   block inside the replica buffer, pairwise disjoint, at its device's
   slot; the mask names exactly the routed experts that were resident on a
   device whose issue succeeded; hits + misses add up to everything
   routed. The row limit is read from the array the rows index
   (sizeof G.is_k[0] / sizeof G.is_k[0][0]), so the stride cannot drift
   from it without failing here. Under test-asan this is a fuzz for the
   #1339 class.

## What the first rule found

qt_plan_fill reserves budget and sets planned=1; qt_note_planned returned
early on NULL weights without undoing either, and the warmstart did not
call it at all when the loader came back empty. The bytes stayed out of
the budget for the life of the process and "if(resident||queued||planned)
continue" never reconsidered the expert. #1331 was this leak for every
expert of an int8 container; the class survived its fix.

Fix: qt_note_planned hands the reservation back when it receives no
weights, and tier_warmstart reports every planned expert, with or without
them. Without the fix the new test fails eight checks (all this leak) and
passes clean under ASan; with it, all green.

## Verified

- test_qwen36_tier_invariants: ok; under ASan+UBSan: 0 diagnostics
- without the fix: 8 FAIL, 0 sanitizer diagnostics (a red test, not a crash)
- the four #1344 tests, test_qwen36_ctx and the qwen36 build unchanged: ok
- one lesson kept in the file: the watchdog's first draft passed its timeout
  by pointer into the arming function's frame -- ASan flagged the
  stack-use-after-return (#1277's class) in the test itself before it could
  flag anything in the tier

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DLj9ctDNPGTsYxgBuDmy5a
qt_fill_wait() returns when the queue is empty, but the expert the uploader
dequeued last is still queued=1 until its upload returns. The residency
check right after it raced that upload and failed about one run in fifteen
locally ("expert did not become resident during warmstart") -- a red that
says nothing about #1339. Poll until no slot is queued before asserting.

The same two-line wait is what test_qwen36_tier_invariants uses
(WAIT_IDLE). Noted on #1344 as review feedback; carried here so the
stacked PRs stop rolling dice in CI. Drop this commit if #1344 takes the
fix first.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DLj9ctDNPGTsYxgBuDmy5a
The lm_head GEMV (248320x2048, one call per token at token END, outside
the per-layer latency chain) cost 13.8-14.8 ms/token on CPU — 18% of
decode. Park the dense-i8 rows + per-row scales on a CUDA device and run
them through coli_cuda_matmul (fmt=1, identical y[o]=acc*sc[o] semantics;
greedy output bit-identical across all runs).

qt_init removes the lm_head device from the expert PLACEMENT list (not
just budget=0: home() would still hash experts onto it and the hit rate
collapsed 89.9->49.5% in the first attempt) while keeping its CUDA
context; if it is the only device, experts stay and lm_head shares it.

Measured (gs64, heat-warm, 200 tokens):
  CPU lm_head          14.8 ms/tok   81.0 total   11.05 tok/s
  lm_head on Quadro     9.9 ms/tok   76.2 total   11.69 tok/s
  lm_head on 3070       3.9 ms/tok   72.7 total   12.21 tok/s  <- best,
  single GPU (experts + lm_head share the 3070; token end is idle time)

(cherry picked from commit 734e1e3d77344e3bc9613d6fbc6c752746b6a518)
(cherry picked from commit 4064493, ohne die versehentlich committeten
Binaries c/qwen36 und c/tools/bench_dnproj; qt_init-Signatur und
Device-Auswahl auf den #1344-Stand aufgeloest: die COLI_PLACE-Devices werden
nach der COLI_GPUS-/Auto-Auswahl und vor der Leer-Pruefung ergaenzt.
tests/qwen36_fake_cuda.h bekommt coli_cuda_matmul als aufzeichnenden Stub,
damit die Tier-Tests weiter im CPU-Build linken.)
The hand-written COLI_PLACE list from R4 is a measurement tool; nobody
running a 6 GB card should have to work out that 1.2 GB of dense trunk is
worth more than 800 experts (#1040). The engine knows every size involved,
so it decides.

Rule: bytes saved on the memory bus per token, per byte of VRAM spent.
Dense weights are read every token (value 1.0 per byte); an expert with the
probability p_e that a token routes to it -- its heat share when a
HEAT_FILE exists, topk/n_experts otherwise -- and the CPU fallback reads
the int8 slot, twice the VRAM bytes of an int4 expert (value 2*p_e; 1*p_e
on an int8 container). A trunk item goes to the device with the most room
if its value beats that of the coldest experts it would push out, i.e. the
tail of the heat order that still fits today. Without heat the tail is
worth 2*topk/n_experts per byte and the trunk always wins; with heat, a
card whose marginal expert is routed on more than every second token keeps
its experts -- the R4 measurement, where moving all projections onto one
near-full card cost 0.7 GB of hot experts and the 13 ms it had saved.

Mechanics:
- qt_trunk_offer(component, layer, bytes) before qt_init: the engine
  offers lmhead once and dnproj per DeltaNet layer, sizes from the same
  dense-i8 entries the uploads use. No entries (dense-i8 off) -> no offer.
- qt_init prices and places, writes the decision into the table
  qt_place_of() already reads, and subtracts placed bytes from that
  device's expert budget. The explicit list gets the same subtraction now;
  with COLI_PLACE="dnproj=0" the projections used to come out of the
  expert cache unannounced.
- COLI_PLACE unset or "auto" -> automatic; a list -> obeyed, auto off;
  "off" -> nothing placed. In auto mode the R4 role-split reservation is
  skipped on purpose: one card shares trunk and experts.
- Weight format, exp_bytes and the heat table move ahead of the role-split
  block in qt_init, and the parse state is re-derived per init.

tests/test_qwen36_tier_autoplace.c on the fake backend (free VRAM is what
the test says): fits -> placed and budget shrinks by exactly the trunk;
too big -> CPU; heat verdicts both ways at p_marginal 0.67 and 0.17, with
and without a heat file, and "room nobody would use goes to the trunk";
explicit list obeyed and charged; off; two devices greedy by room with
per-device budgets; nothing offered -> no-op. All six tier tests green,
ASan/UBSan clean on this and the invariants test, CPU and CUDA builds ok.

Not yet: dnout/attnproj (offered components other than lmhead/dnproj are
ignored), and the calibration on real cards that decides whether the
factor 2 and the greedy-by-room device choice survive contact with a
Quadro/3070 pair and a 6 GB card.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DLj9ctDNPGTsYxgBuDmy5a
…with the first calibration

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DLj9ctDNPGTsYxgBuDmy5a
cudaGetDeviceProperties says ordinal 0 is the RTX 3070 and ordinal 1 the
Quadro RTX 4000 on the calibration box (CUDA orders fastest-first, nvidia-smi
by PCI bus, and the two disagree there). The rows labelled "RTX 3070" were
measured on the Quadro and the row labelled "Quadro" on the 3070. Numbers
unchanged; names swapped; the sentence that explained the Quadro being the
faster card is gone, because it was the 3070.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DLj9ctDNPGTsYxgBuDmy5a
qwen36 keeps every expert in RAM and lets the tier retain raw pointers into
slots that are never recycled; `cap == n_experts` guards exactly that.
Qwen3.8 streams 24 576 experts of 4.7 MiB through a per-layer LRU whose
slots ARE recycled, so a retained pointer would dangle by the next token,
and 121 GB of experts do not fit beside the model anyway.

qt_init_fp8(nl, ne, D, Ih, cap, topk, e4m3_lut) starts the tier in a mode
that owns what it uploads:

- cap may be smaller than n_experts;
- weight format 8: e4m3 bytes as they came from the checkpoint, one f32
  scale per 128x128 block per matrix -- the layout #817's kernels and
  tensor_upload(fmt=8) already agree on, so no kernel changes; the decode
  table is published to the backend at init and the tier refuses to start
  without it;
- staging copies the bytes unchanged (no XOR) and the three block-scale
  tables, inside the qt_note call while the engine's slot is live, and the
  slot pointers are dropped before qt_note returns;
- therefore a promotion can only happen when the bytes pass by: the LFRU
  decision moves from the periodic tick into qt_note -- a non-resident expert
  noted on a full device evicts the coldest resident there when the shared
  admission rule says it is hotter, as a budget-neutral swap; the tick keeps
  only the decay.

The int4/int8 modes are untouched: same code paths, same `cap == n_experts`
refusal, the six existing tier tests unchanged and green.

tests/test_qwen36_tier_fp8.c on the fake backend (which now counts fmt 8 at
one byte per element and stubs the LUT publish): starts with cap 4 of 16,
exp_bytes = 3*D*Ih + three block tables + slack, three fmt=8 uploads per
expert with the bytes intact, no pointer survives a note, a cold newcomer is
refused on a full device, a hot one is swapped in budget-neutrally, hits and
misses count, shutdown clears the mode, a missing LUT refuses the mode, the
int4 mode still refuses cap < n_experts. Clean under ASan/UBSan, as are the
invariants and placement tests; CPU and CUDA builds of qwen36 ok.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DLj9ctDNPGTsYxgBuDmy5a
Budget accounting. The tier charged an expert by its payload. cudaMalloc
rounds an allocation above 1 MiB up to a multiple of 2 MiB and one at or
below 1 MiB up to 1 MiB (measured with cudaMemGetInfo: 1,638,400 B -> 2 MiB,
819,200 B -> 1 MiB, 400 B -> 10 KiB). In the fp8 streaming mode an expert of
three 1.56 MiB matrices was charged 4.69 MiB and took 6.03 MiB, so a 6.5 GB
budget filled an 8 GB card to the last megabyte: eight "tensor allocation:
out of memory" lines before the uploader's stop-trying fallback shrank the
budget to what had fit (1,279 experts, 7.77 GB on the card). exp_bytes is now
three weight footprints plus three scale footprints, so the planned count is
the resident count. Qwen3.6 is not affected: its int4 matrices are exactly
512 KiB and the allocator serves those exactly, so the payload accounting was
already right there. Recovering the fp8 loss needs an arena per device; not
done here.

COLI_GPU. resource_plan.py writes COLI_GPU for a one-device plan and
colibri.c reads it; the tier only read COLI_GPUS, so a `coli chat --gpu 1`
landed on every visible device. The tier now takes COLI_GPU when COLI_GPUS
is unset.

Tests: the two tier tests that compute exp_bytes use the tier's own
dev_alloc_footprint.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DLj9ctDNPGTsYxgBuDmy5a
…d CUDA threads

With OMP_PROC_BIND set, libgomp binds the initial thread to place 0 before
main() runs, and a pthread inherits the CPU mask of the thread that creates
it. The tier's uploader thread and the CUDA runtime's own threads were thus
jailed on the OpenMP master's core: every staging copy and driver call
competed with the master's share of each expert matmul, and the whole team
waited for it. On Qwen3.8 (cap 128, one 8 GB card, 12 threads) the CPU time
per remaining expert rose 64 % while the tier was on -- 3.7 ms against
2.25 ms -- which ate the whole gain of computing 45 % (one card) to 59 %
(two cards) of the routed experts on the GPU: 0.32/0.31 tok/s against
0.32 on the CPU alone. Fewer OpenMP threads did not help, because the
contention is on the master's core whatever the team size.

qt_init now widens the calling thread's mask to every online CPU while it
initializes CUDA and creates the uploader, and restores the caller's mask
afterwards. Raw sched_{get,set}affinity syscalls, Linux only, no _GNU_SOURCE
(the file is #included by tests after the engine's headers). The uploader
records how many CPUs it may run on; test_qwen36_tier_fp8 binds its own
thread to CPU 0 the way libgomp would, starts the tier, and checks that the
uploader is not jailed and that the caller's mask comes back.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DLj9ctDNPGTsYxgBuDmy5a
…urve

The first footprint rule charged every allocation at or below 1 MiB a full
1 MiB. That is what a 768 KiB request takes, but not what Qwen3.6's 512 KiB
int4 matrices or its 64 KiB gs64 scale tables take: measured over 256 allocations each, small requests round
to 8 KiB steps with roughly a sixteenth of overhead (96 KiB -> 104 KiB,
384 KiB -> 416 KiB), requests above 512 KiB take one 1 MiB page, and above
1 MiB multiples of 2 MiB. With the coarse rule a Qwen3.6 gs64 expert was
charged 6 MiB instead of its 1.7 MiB and a 4.9 GiB budget planned 836
experts where 2,800 fit (measured: 2,782 resident after the change). The fp8 Qwen3.8 expert is unchanged (three 2 MiB
matrices plus three 8 KiB scale tables).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DLj9ctDNPGTsYxgBuDmy5a
…tor footprint

test_qwen36_tier_int8 compared exp_bytes against the payload formulas and
test_qwen36_tier_shutdown sized its one-expert budget from them; both broke
when exp_bytes started charging what cudaMalloc takes. They now use the
tier's dev_alloc_footprint like the other tier tests. CI caught it; I had
run only the five tests I had touched.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DLj9ctDNPGTsYxgBuDmy5a
test(qwen36): tier invariants on the fake backend, and the reservation leak they found
feat(qwen36): the dense trunk places itself -- COLI_PLACE=auto, priced in bytes saved per byte of VRAM
qwen36 tier: fp8 streaming mode, VRAM accounting at allocator granularity, thread affinity, COLI_GPU
feat(tools): raw-evidence adapter for the scoring corpus
feat(experiments): validate reproducible performance records
…mmit limit (#1375)

glm53.c sized its expert cache from /proc/meminfo. That file exists on
Linux only: on Windows and macOS the function returned 0, the budget
clamped to 1 GB, and GLM-5.3-Flash ran with one expert slot per layer,
silently. colibri.c had the correct measurement (host_statistics64 on
macOS, GlobalMemoryStatusEx on Windows) as a function of its own. Two
copies, one wrong. The measurement now lives once, in compat.h, and both
engines call it.

On Windows the number that decides whether the next malloc succeeds is
not free physical memory but grantable commit (ullAvailPageFile), which
a small page file makes far lower. The shared function returns the
smaller of the two. The planner does the same: its MEMORYSTATUSEX
declaration skipped the two PageFile fields entirely, so ullAvailPhys
was read at the right offset by luck and the commit figure was never
looked at. It budgeted 88% of physical, handed the resulting cap to the
engine as an argument, and the engine filled it slot by slot until
Windows refused the next 14 MB on a 128 GB machine. That is #1375.

coli tune now prints the cap each run uses, and the engine's OOM line
says the slot size and how to shrink the cache.

Tests: test_mem_available.c runs in the Linux, macOS and Windows jobs
and fails wherever the measurement is 0 or disagrees with the system's
own figure; forcing the measurement to 0 makes it fail. Three planner
tests pin the min(physical, commit) rule and the struct layout.
fix(memory): measure available RAM on every platform, and mind the commit limit (#1375)
…and reported with the right number (#1376)

Reported: coli web --ctx 65536 on qwen36 accepted the flag, printed a
banner with every capacity knob except the context, and the first sign
that the ceiling was 8192 was a CONTEXT_EXCEEDED refusal. Reproduced the
two halves that are defects on dev; the third, the flag not reaching the
engine, does not reproduce: coli serve --ctx 20 on the qwen36 fixture
refuses a 30-token prompt and without the flag accepts it, so Q36_MAXT is
delivered. The report may come from a build before the family-owned
context mapping (2026-08-16); the banner now makes that self-diagnosing.

1. qwen36's startup banner prints ctx=<effective ceiling> next to cache,
   bits and the rest, so a clamp to the hard limit is visible at start
   rather than at the first failed request.

2. The server's CONTEXT_EXCEEDED message read the engine's key=value
   fields by position: fields[2] (requested=M, the completion budget) as
   the limit, printed raw. Users read "maximum context length is
   requested=4 tokens" and the real ceiling appeared nowhere. It now
   parses the fields and reports capacity as the limit and prompt_tokens
   as the usage.

3. coli refuses --ctx above the family's max_context with the number,
   instead of passing it on to be clamped in silence. A flag that appears
   to work and does not is worse than one refused.

Tests: the message parser with the real engine line; the refusal above
max_context and the pass-through below it. Reverting each fix fails its
test. Banner verified on the fixture at default (8192), Q36_MAXT=20 (20)
and an absurd value (262144, the clamp now visible).
JustVugg and others added 28 commits September 13, 2026 00:43
fix(coli): preserve explicit Windows expert budgets
fix(glm53): release dashboard hit tracking
…res-20260911

test(planner): avoid materializing zero-filled shard payloads
tests: include compat.h directly in the converted tests
…m-glm53

feat(web): drop redundant Medium from GLM 5.3 reasoning selector
dev went red on `Windows UCRT64` / `make check` right after #1390 landed:

    tests/test_qwen36_tier_fill_wait.c:42:5: error: implicit declaration of
    function 'setenv'; did you mean 'getenv'?

MinGW has no setenv. The tests that include an engine .c inherit compat.h
through it, but this one includes qwen36_tier.c, which does not pull it in,
so the declaration was never there. Linux never noticed because glibc has
setenv, and modern GCC turns an implicit declaration into an error rather
than a warning, so the Windows leg is where it surfaced. Same shape as
#1440, which added the include to the six tests that already had it.

Reproduced and fixed against mingw-w64 with the Makefile's own Windows flags
plus -Werror=implicit-function-declaration, then every other gated test was
cross-compiled the same way: this file is the only one affected. The three
that still fail that sweep (test_uring, test_deepseek_v4, test_v4_ownership)
are in TEST_EXCLUDE and are not built on the Windows leg at all.

compat.h joins the rule's prerequisites too, so editing the shim relinks the
test instead of leaving a stale one.
fix(tests): the qwen36 fill-wait test needs compat.h to build on Windows
metal: fix fmt=0 (raw f32) NULL-scale segfault in coli_metal_matmul
fix(mirror verify): support complete mirrors without a receipt
fix: enable qwen36 GPU tier when building with HIP=1
Serve mode emitted five literal zeros for the PROF phase timings, and the
gateway stores them verbatim into the rolling /profile window. A consumer
could not tell an unmeasured field from a measured zero, so `expert_disk_s`
read 0.0 on exactly the turns where the expert reads were the workload
(#1449, filed off the cap matrix in #1441).

The disk phase is now measured. `load_expert_merged` is the one place olmoe
touches the container for an expert, so timing it is enough; the reads run
unlocked and in parallel, so the accumulator is an atomic nanosecond counter
and the PROF line reports a per-turn delta of it.

Demonstrated on the tiny fixture, serve mode, cap 1 with EXPERT_DROP=1 so the
reads actually go to the device:

    PROF 0.026 83 12 0.020 0.0 0.0 0.0 0.0 13

0.020 s of a 0.026 s turn, which is what a cap-1 streaming turn spends on
disk. Before this the same run reported 0.0.

The other four fields stay zero because they are still unmeasured: olmoe does
not split the rest of its wall time the way glm.c and inkling.c do, and a
guessed number would be worse than a zero. That part of #1449 stays open.

Oracle still token-exact (8/8), make test-c and the python suite green.
…its cap

`coli cluster worker` without an explicit `--cap` could never start:

    cache/layer: expected a whole number, got "None"

`--cap` comes from the shared parser and defaults to `None`, meaning "auto".
Every other direct-engine launcher resolves that through `cap_for_launch`;
this one stringified it into argv, and `str(None)` is the literal "None",
which the engine's argument check refuses before anything else happens. The
documented default was therefore the one value that could not work (#1452),
on Linux and Windows alike.

Fallback 0 is what the other GLM launch sites pass: colibri.c treats a zero
cap as "not given" and resolves its own, after the Metal and SSD probes that
the platform default depends on.

tests/test_env_defaults.py gains the regression: the worker's argv must parse
as an integer, and an explicit --cap must still pass through untouched. Put
the old line back and the first of the two fails.
fix(olmoe): measure the expert disk phase instead of reporting a zero
fix(coli): the cluster worker handed the engine the string "None" as its cap
feat(glm53): add Metal routed MoE acceleration
fix(build): clean up Clang compiler warnings
…anup holds

#1458 silenced -Wmissing-field-initializers on the three expert_mats
initialisers by naming the `vk` field. #1462 landed alongside it and gave Mat
two more trailing fields, `resident` and `metal`, so the warning did not go
away, it moved:

    glm53.c:1411:11: warning: missing initializer for field 'resident' of 'Mat'

The zero the compiler was already supplying is the right value -- a streamed
expert slot is not resident and has no Metal tensor -- so this only says so
explicitly. glm53 now builds clean under -Wextra again.
fix(glm53): initialise the two Mat fields #1462 added, so #1458's cleanup holds
Version to 1.11.0 in the six places it lives (four READMEs, c/version.py,
site/index.html) and a CHANGELOG entry for the 54 pull requests merged since
v1.10.2, headed by the ninth engine, DeepSeek V4.1 Flash.

The family count in the READMEs said eight while the enumeration next to it
already named nine (V4.1 was added to the list by #1453 without the number
following), and the site's meta descriptions still said six. All say nine.

Every PR number cited in the entry was checked against
`git log --merges v1.10.2..dev`: all 54 post-tag merges are cited, and none
from before the tag. The heading keeps the file's `## [x.y.z] — date` form
because release.yml extracts the notes by matching it; the body carries no
em dashes or emoji, since it ships verbatim as the release notes.
feat(glm53): add routing usage telemetry and .coli_usage support
fix(glm53): correct RAM sizing and Apple unified-memory planning
Both rebased and merged after the entry was written: GLM-5.3 routing
telemetry and .coli_usage (#1457), and the planner's host-level unified
memory plus no generic tune knobs for glm53 (#1456). 56 pull requests.
@JustVugg
JustVugg merged commit 3a70acb into main Sep 13, 2026
56 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.