Run GLM-5.2 — a 744-billion-parameter Mixture-of-Experts — on your own machine.
+ Pure C, zero dependencies, experts streamed from disk. From a 25 GB dev box to a
+ six-GPU workstation: same engine, same container, the hardware only changes where the experts live.
A replay of the engine at work, paced to measured decode speeds from real hardware.
+ Every token routes through 8 experts in each of 75 MoE layers — the grid on the right is all
+ 19,456 experts: colour is the storage tier, brightness is routing heat, and every expert
+ a token touches flashes white.
+
+
+
+
+ coli chat —
+
+
+
+
+ the brain — 75 MoE layers × 256 experts + MTP
+
+
+
+
0.0 tok/s
decode
+
— s
ttft (measured)
+
— %
resident hit
+
+
+ VRAM tier
+ RAM tier
+ disk tier
+ routed now
+
+
+
+
+
Simulation replays a fixed transcript at each profile's measured decode rate; TTFT shown is the measured value, not a live wait.
+
+
+
+
+
# the expert atlas
+
Routing affinity is measured, not assumed. Characterised experts cluster by what
+ they actually fire on — poetry, law, Chinese, SQL, code, mathematics… Position below is measured
+ routing affinity, not a learned embedding. Drag to spin.
+
+
+
+
+
drag to spin · scroll past to release
+
+
+
+
+
# how it fits
+
A 744B MoE activates only ~40B parameters per token — and only ~11 GB of those
+ change from token to token (the routed experts). colibrì keeps the dense weights resident and tiers the
+ 19,456 experts (~19 MB each, int4) across three levels by measured heat:
+
+
VRAM
+
hottest experts · grouped GPU matmuls
+
With CUDA or Metal, the hottest experts live on-device and are batched into grouped kernels.
+ Six RTX 5090s hold the entire routed set: disk reads drop to zero.
+
RAM
+
warm experts · pinned & wired
+
The warm set is pinned in RAM (PIN_GB), guided by
+ recorded usage stats, and served by AVX-512/NEON int4 kernels with an LRU cache behind it.
+
disk
+
cold tail · streamed on demand
+
Everything else streams from NVMe with io_uring, prefetched a layer ahead by the router.
+ This is how 744B fits a 25 GB machine at all — the proven floor.
+
+
token → router picks 8 of 256 experts per layer → engine gathers them from
+ VRAM / RAM / disk → int4 matmuls → next token. Output is validated token-exact against the
+ reference transformers implementation.
+
+
+
+
# the ladder
+
Same engine, same int4 container — measured decode on real machines,
+ from the proven floor to full residency.
+
+
+
+
+
+
+
+
From bca4e95d92615c94d9618a6e4d8811d2aea42bda Mon Sep 17 00:00:00 2001
From: ZacharyZcR
Date: Mon, 20 Jul 2026 00:08:28 +0800
Subject: [PATCH 23/71] =?UTF-8?q?site:=20redesign=20=E2=80=94=20measured?=
=?UTF-8?q?=20atlas=20everywhere,=20vision,=20algorithm,=20models?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- hero: the measured expert atlas as a full-screen slowly-turning backdrop
- atlas rebuilt on real data: canonical atlas v1 (721 canonical + 637
gate-sensitive specialists, 10 measured categories) embedded inline;
position IS the measured affinity vector, colour = top topic
- demo: third panel 'the atlas, live' — token routing flashes measured
specialists for the active topic in both the brain grid and the galaxy;
added SQL and Chinese-poetry turns so cluster shifts are visible
- profiles/ladder updated to current community numbers (#82 NUMA 9.0-9.2,
#389 Xeon 1TB 5.42, #387 M5 Max 2.0, #161 GB10 3.33, #120 1.23);
unpublished TTFT/hit values shown as em-dash, never invented
- new sections: vision manifesto (run/study/improve), 'A JIT, but for
weights' algorithm explainer with tier stack, models roadmap
(Kimi K2 / Qwen3 MoE / MiniMax planned) + open-weights acknowledgements,
contribute cards
- visual pass: numbered sections, gradient type, glass panels, fixed nav
---
site/index.html | 724 ++++++++++++++++++++++++++++++++----------------
1 file changed, 483 insertions(+), 241 deletions(-)
diff --git a/site/index.html b/site/index.html
index 3972690..dd6dac1 100644
--- a/site/index.html
+++ b/site/index.html
@@ -4,64 +4,124 @@
colibrì — tiny engine, immense model
-
+
-
+
-
-
+## The vision
+
+Frontier models should not be sealed inside datacenters. colibrì exists so that
+**anyone curious enough can open one up**: run a 744B-parameter mind on hardware
+you already own, watch every expert fire in real time, and change the code that
+does it. Not renting intelligence behind an API — *holding* it: probing it,
+measuring it, improving it. Every optimisation in this project started with
+someone measuring something on their own machine; the engine is deliberately
+small enough that the next one can come from you.
+
## The idea
A 744B Mixture-of-Experts model activates only ~40B parameters per token — and
@@ -61,6 +71,18 @@ So the model doesn't need to *fit* in fast memory — it needs to be **placed**:
at int4) live **on disk** (~370 GB) and are **streamed on demand**, with a
per-layer LRU cache, a learned pinned hot-store, and an optional VRAM tier.
+Think of the core algorithm as **a JIT, but for weights**. A compiler JIT never
+compiles the whole program — it watches what actually runs and compiles the hot
+paths, just in time. colibrì makes the same bet about a 744B parameter space:
+parameters are not resident state to be held, they are **data to be staged**
+across a heterogeneous storage hierarchy (VRAM / RAM / NVMe), exactly when the
+router proves they are needed. Measured routing heat decides which experts earn
+which tier, the router runs a layer ahead so prefetch hides the staging latency,
+and — like a JIT — the engine learns your workload: the more you run, the hotter
+the right experts get. It works because routing has measurable structure (see
+the [expert atlas](https://github.com/JustVugg/colibri/issues/175)) — and
+structure is cacheable.
+
The engine is a single C file (`c/glm.c`) plus small headers. No BLAS, no Python
at runtime, no GPU required.
@@ -204,6 +226,18 @@ install from the clone, not a standalone wheel).
| Grammar-forced drafts (structured output) | [docs/grammar-draft.md](docs/grammar-draft.md) |
| Environment variable inventory | [docs/ENVIRONMENT.md](docs/ENVIRONMENT.md) |
+## What's next
+
+- **Algorithmic research is active.** The current hierarchy is LRU + a learned
+ pin set; the next step is under way — smarter placement and scheduling,
+ overlap of CPU and GPU expert execution, and routing-aware speculation.
+ Everything lands the way this project always works: measured, reviewed, and
+ merged in the open.
+- **More open models.** The tiering algorithm is model-agnostic: any MoE with
+ routed experts can be staged the same way. GLM-5.2 and OLMoE run today;
+ support for more open-weight families — **Kimi K2** (Moonshot AI),
+ **Qwen3 MoE** (Alibaba), **MiniMax** — is on the roadmap.
+
## Supporting the project
colibrì started as a one-person project on a 12-core laptop with 25 GB of RAM;
@@ -244,6 +278,14 @@ The hummingbird weighs a few grams, hovers in place, and visits a thousand
flowers a day. This engine keeps a 744-billion-parameter giant alive on
hummingbird rations: 25 GB of RAM, twelve CPU cores, and a lot of disk patience.
+## Acknowledgements
+
+colibrì is an engine; the minds it runs are a gift. Thank you to the teams
+releasing frontier-class weights in the open — **Z.ai** (GLM), **Moonshot AI**
+(Kimi), **Alibaba Qwen**, **MiniMax**, and **Allen AI** (OLMoE) — and to every
+contributor who benchmarked, bisected, replicated an atlas run, or sent a patch.
+This project is proof of what open weights make possible.
+
## License
Apache 2.0. GLM-5.2 weights are released by Z.ai under MIT.
From 0049ea15ae59fd17eefe7e75f6d9f9b407fbc385 Mon Sep 17 00:00:00 2001
From: monotophic
Date: Sat, 18 Jul 2026 13:56:06 -0400
Subject: [PATCH 25/71] E5: MTLResidencySet lifecycle scaffolding
(init/register/unregister/shutdown)
Env-gated (COLI_METAL_RESSET=1) persistent MTLResidencySet attached to g_queue
(macOS 15+, @available-guarded with a one-line stderr fallback). Adds
resset_add/resset_remove/resset_flush helpers, wires them into the existing
coli_metal_register/coli_metal_unregister/coli_metal_shutdown bodies -- no new
functions in backend_metal.h, no glm.c changes, every existing call site keeps
its current signature and behavior.
register() defers the commit (resset_add just marks dirty, under the same
g_slab_mtx that already serializes parallel OMP loader threads) so a loader
burst doesn't pay a commit per slab; unregister() commits synchronously and
immediately, because the caller frees the underlying host memory right after
it returns and a deferred removal would leave the set referencing freed
memory. Nothing yet reads g_resset_enabled to change dispatch behavior --
this commit is bookkeeping only, gate on or off, so it does not change what
any command buffer does (verified by inspection: no useResource:/useHeap:
call site is touched here).
Gate off (default) is byte-for-byte the stock path: g_resset_enabled starts
false and nothing sets it outside the COLI_METAL_RESSET branch in
coli_metal_init, so every new helper is a no-op.
See SUMMARY.md (next commit) for the full design and UNCERTAINTIES.
---
c/backend_metal.mm | 81 ++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 78 insertions(+), 3 deletions(-)
diff --git a/c/backend_metal.mm b/c/backend_metal.mm
index 51ed8a0..b57fa66 100644
--- a/c/backend_metal.mm
+++ b/c/backend_metal.mm
@@ -258,6 +258,51 @@ extern "C" void coli_metal_attn_lat(double *ksched, double *gsched){
struct Slab { void *base; size_t len; id buf; };
static std::vector g_slabs;
static std::mutex g_slab_mtx; // expert_load registers slabs from parallel OpenMP threads
+
+// ---- E5 experiment: COLI_METAL_RESSET=1 -- one persistent MTLResidencySet attached to
+// g_queue (macOS 15+) replaces moe_submit's per-command-buffer useResource: loop over
+// resolved expert weight/scale slabs. Allocation is untouched (same newBufferWithBytesNoCopy
+// wrap as stock); only residency bookkeeping moves off the dispatch hot path -- see
+// SUMMARY.md for why skipping useResource: there is safe (read-only, indirectly-referenced
+// buffers only; residency sets don't do hazard tracking, but nothing here relied on it).
+// g_resset_obj is a bare `id` (holds id) so the global's declared type
+// carries no availability annotation -- the protocol name only appears inside
+// @available(macOS 15.0, *) guards below, keeping -Wunguarded-availability clean.
+static id g_resset_obj;
+static bool g_resset_enabled; // COLI_METAL_RESSET=1, macOS 15+, and creation succeeded
+static bool g_resset_dirty; // addAllocation: calls pending commit; g_slab_mtx-guarded
+
+// Add a just-wrapped buffer to the set. Deferred commit (batches an OMP loader burst into
+// one commit at the next moe_submit instead of one per slab): correct because every
+// register/unregister caller holds g_slab_mtx here, and resolve() (which every dispatch
+// calls before touching a slab) takes the same mutex -- a slab moe_submit's resolve() can
+// see was necessarily added, and marked dirty, before moe_submit's own resset_flush() ran.
+static void resset_add(id b) { // caller holds g_slab_mtx
+ if (!g_resset_enabled) return;
+ if (@available(macOS 15.0, *)) { [(id)g_resset_obj addAllocation:b]; g_resset_dirty = true; }
+}
+// Remove + commit immediately, NOT deferred: the caller frees the underlying host memory
+// right after coli_metal_unregister returns, so the removal must be applied before that --
+// an uncommitted-but-still-resident allocation pointing at freed memory is a use-after-free
+// risk the GPU could act on.
+static void resset_remove(id b) { // caller holds g_slab_mtx
+ if (!g_resset_enabled) return;
+ if (@available(macOS 15.0, *)) {
+ id rs = (id)g_resset_obj;
+ [rs removeAllocation:b]; [rs commit];
+ }
+ g_resset_dirty = false; // commit above also flushes any pending adds
+}
+// Flush pending adds before moe_submit relies on the set alone for residency -- the only
+// caller that skips per-buffer useResource: (see moe_submit below).
+static void resset_flush() {
+ if (!g_resset_enabled) return;
+ std::lock_guard lk(g_slab_mtx);
+ if (!g_resset_dirty) return;
+ if (@available(macOS 15.0, *)) { [(id)g_resset_obj commit]; }
+ g_resset_dirty = false;
+}
+
// Persistent scratch buffers (grow-only) for the MoE pipeline.
static id g_gg, g_uu, g_hh, g_xg; static size_t g_gg_cap, g_uu_cap, g_hh_cap, g_xg_cap;
static id ensure(id b, size_t *cap, size_t need) {
@@ -304,6 +349,25 @@ extern "C" int coli_metal_init(void) {
if (!g_gemv || !g_moe_gemv || !g_moe_silu || !g_a_rms || !g_a_rope || !g_a_copy ||
!g_a_qabs || !g_a_score || !g_a_smax || !g_a_clat || !g_a_ctx) {
fprintf(stderr, "[metal] pipeline failed\n"); g_dev = nil; return 0; }
+ // E5 experiment: COLI_METAL_RESSET=1 -- see g_resset_obj comment above.
+ if (getenv("COLI_METAL_RESSET") && atoi(getenv("COLI_METAL_RESSET"))) {
+ if (@available(macOS 15.0, *)) {
+ MTLResidencySetDescriptor *rd = [MTLResidencySetDescriptor new];
+ rd.initialCapacity = 4096; // hint only (internal array presize), not a hard limit
+ NSError *rerr = nil;
+ id rs = [g_dev newResidencySetWithDescriptor:rd error:&rerr];
+ if (rs) {
+ [g_queue addResidencySet:rs];
+ g_resset_obj = rs; g_resset_enabled = true;
+ fprintf(stderr, "[METAL] residency-set: on (macOS 15+, moe_submit skips per-buffer useResource:)\n");
+ } else {
+ fprintf(stderr, "[METAL] residency-set create failed: %s -- stock per-CB residency path\n",
+ rerr ? [[rerr localizedDescription] UTF8String] : "?");
+ }
+ } else {
+ fprintf(stderr, "[METAL] COLI_METAL_RESSET=1 requested but OS < macOS 15 -- stock per-CB residency path\n");
+ }
+ }
}
return 1;
}
@@ -314,12 +378,16 @@ extern "C" void coli_metal_register(void *base, size_t len) {
options:g_res_opts deallocator:nil];
if (!b) return;
std::lock_guard lk(g_slab_mtx); // called from parallel expert_load threads
- for (auto &s : g_slabs) if (s.base == base) { s.len = len; s.buf = b; return; }
+ for (auto &s : g_slabs) if (s.base == base) { s.len = len; s.buf = b; resset_add(b); return; }
g_slabs.push_back({base, len, b});
+ resset_add(b);
}
extern "C" void coli_metal_unregister(void *base) {
std::lock_guard lk(g_slab_mtx);
- for (size_t i=0;i resolve(const void *p, uint64_t *addr) {
@@ -357,7 +425,14 @@ extern "C" void coli_metal_spin_start(void) {
}
extern "C" void coli_metal_spin_stop(void) { g_spin_run.store(false); }
-extern "C" void coli_metal_shutdown(void) { coli_metal_spin_stop(); g_gemv=nil; g_queue=nil; g_dev=nil; g_tensor_count=g_tensor_bytes=0; }
+extern "C" void coli_metal_shutdown(void) {
+ coli_metal_spin_stop();
+ if (g_resset_enabled) {
+ if (@available(macOS 15.0, *)) { [g_queue removeResidencySet:(id)g_resset_obj]; }
+ }
+ g_resset_obj=nil; g_resset_enabled=false; g_resset_dirty=false;
+ g_gemv=nil; g_queue=nil; g_dev=nil; g_tensor_count=g_tensor_bytes=0;
+}
extern "C" int coli_metal_available(void) { return g_dev != nil; }
extern "C" void coli_metal_stats(size_t *c, size_t *b) { if(c)*c=g_tensor_count; if(b)*b=g_tensor_bytes; }
extern "C" int coli_metal_mem_info(size_t *used, size_t *total) {
From 309f20c9397fa7aae6a1f7b1a476cc3e2aa92d29 Mon Sep 17 00:00:00 2001
From: monotophic
Date: Sat, 18 Jul 2026 13:56:54 -0400
Subject: [PATCH 26/71] E5: moe_submit relies on the residency set instead of
per-buffer useResource:
The one seam the mechanism history actually implicates: moe_submit's `use`
vector (resolved expert weight/scale slabs) is the only useResource: loop
whose length scales with LRU cache size. With COLI_METAL_RESSET=1, skip that
loop entirely -- the queue-attached MTLResidencySet already guarantees those
buffers are resident -- after resset_flush() commits any pending adds from a
loader burst. Every other useResource: call site (bind_gemv's weight/scale
buffers, attn_decode/layer_decode's Lb/Rb/kvbW/kvbS/inB/pnB/rwB/rbB,
coli_metal_gemm's wb/sb) is left unconditionally unchanged: none of them
scale with cache size, and Lb/Rb carry real GPU-side write traffic ordered by
existing explicit memoryBarrierWithScope: calls, not by useResource:'s hazard
tracking -- narrowing the blast radius rather than removing useResource:
uniformly. Full reasoning, the hazard-tracking tradeoff (residency sets don't
support hazard tracking, per Apple's MTLResidencySet developer documentation
and residency-set adoption guide; the SDK header itself is silent on the
topic), and every judgment call in UNCERTAINTIES: see SUMMARY.md.
Gate off is unaffected: g_resset_enabled is false, so the useResource: loop
runs exactly as before.
---
SUMMARY.md | 275 +++++++++++++++++++++++++++++++++++++++++++++
c/backend_metal.mm | 14 ++-
2 files changed, 288 insertions(+), 1 deletion(-)
create mode 100644 SUMMARY.md
diff --git a/SUMMARY.md b/SUMMARY.md
new file mode 100644
index 0000000..a4b86e3
--- /dev/null
+++ b/SUMMARY.md
@@ -0,0 +1,275 @@
+# E5 — MTLResidencySet over the existing malloc'd slabs (experiment branch)
+
+Branch: `e5/metal-residency-set` (cut from `origin/dev` @ `caa49f7`, per spec — E4 was cut
+from `main` @ `72d3d37`; `backend_metal.mm`/`.h` are byte-identical between the two bases,
+confirmed via `git diff 72d3d37 origin/dev -- c/backend_metal.mm c/backend_metal.h`).
+
+## The hypothesis
+
+E4 (MTLHeap-backed slabs) proved that batching residency declaration kills the GPU stall
+(25.9s → 3.9s at cap16, −85%), but changing the *allocation* (heap sub-buffers instead of
+malloc'd host memory) brought a +12–13s expert-disk-load tax, suspected first-touch/lock
+contention on CPU-writes into GPU-owned heap pages. E5 decouples the two: keep the exact
+same malloc'd slabs and per-slab `newBufferWithBytesNoCopy`-wrapped `MTLBuffer`s, and change
+**only** residency bookkeeping — declare residency once, ahead of time, on a set attached to
+the command queue, instead of once per command buffer via `useResource:`. If the stall
+reduction survives without the load-path tax (malloc pages never change ownership), E5 wins.
+
+## What changed
+
+Everything is confined to `c/backend_metal.mm`. **No changes to `glm.c` or
+`backend_metal.h`** — `coli_metal_register`/`coli_metal_unregister`'s existing signatures and
+every call site in `glm.c` (expert_load, uring_load_add, qalloc, kv_alloc, map_of_fd) are
+untouched; the residency-set bookkeeping lives entirely inside those two functions' existing
+bodies. This is a smaller diff shape than E4, which needed new `backend_metal.h` declarations
+and new `glm.c` call sites because it changed the allocation function itself.
+
+Env-gated `COLI_METAL_RESSET=1`, default OFF, runtime `@available(macOS 15.0, *)` guard with
+a one-line stderr fallback when requested on an older OS or when residency-set creation
+fails. Gate off ⇒ every new branch is skipped and behavior is byte-for-byte the stock path
+(verified by inspection: `g_resset_enabled` starts `false` and nothing sets it except inside
+the `COLI_METAL_RESSET` `getenv` branch in `coli_metal_init`, so `resset_add`/`resset_remove`/
+`resset_flush` are no-ops and `moe_submit`'s `useResource:` loop runs unconditionally).
+
+### Lifecycle (`c/backend_metal.mm`)
+
+- **Init** (`coli_metal_init`, end of the existing pipeline-setup `@autoreleasepool`): if
+ `COLI_METAL_RESSET=1` and `@available(macOS 15.0, *)`, create one
+ `MTLResidencySetDescriptor` (`initialCapacity=4096`, a presize hint only), call
+ `[g_dev newResidencySetWithDescriptor:desc error:&err]`, and `[g_queue addResidencySet:rs]`
+ — one set, attached once, for the process lifetime. Failure (old OS or creation error)
+ prints one stderr line and leaves `g_resset_enabled=false` — stock path.
+- **`coli_metal_register`**: after wrapping the buffer exactly as today
+ (`newBufferWithBytesNoCopy`), calls `resset_add(b)` under the *same* `g_slab_mtx` that
+ already serializes `g_slabs` mutation from parallel OMP loader threads. `resset_add` calls
+ `[rs addAllocation:b]` and sets a `g_resset_dirty` flag — **it does not commit**.
+- **`coli_metal_unregister`**: calls `resset_remove(b)` (also under `g_slab_mtx`) *before*
+ clearing the `g_slabs` entry. `resset_remove` calls `[rs removeAllocation:b]` **and commits
+ immediately** — no batching. See UNCERTAINTIES for why this asymmetry is deliberate.
+- **`moe_submit`** (the one function whose `use` list — resolved expert weight/scale slabs —
+ scales with LRU cache size): calls `resset_flush()` at the top (commits any pending adds
+ from `resset_add`, under `g_slab_mtx`), then, if `g_resset_enabled`, **skips** the
+ `for(auto&b:use) [e useResource:b usage:MTLResourceUsageRead];` loop entirely — residency
+ is already guaranteed by the queue-attached set. Every other `useResource:` call site in the
+ file (`bind_gemv`'s weight/scale buffers, `coli_metal_attn_decode`/`coli_metal_layer_decode`'s
+ `Lb`/`Rb`/`kvbW`/`kvbS`/`inB`/`pnB`/`rwB`/`rbB`, `coli_metal_gemm`'s `wb`/`sb`) is
+ **left completely unchanged**, regardless of the flag — see "Why only `moe_submit`" below.
+- **Shutdown** (`coli_metal_shutdown`): `[g_queue removeResidencySet:rs]` then clears the
+ globals, ahead of the existing `g_queue=nil; g_dev=nil;`.
+
+### Why only `moe_submit` skips `useResource:`
+
+Apple's own `MTLResidencySet` docs are explicit: *"Residency sets don't support hazard
+tracking, so you need to account for hazards with fences and events."* (confirmed against
+the actual SDK header comment and Apple's "Simplifying GPU resource management with residency
+sets" guide, both read directly for this design — see UNCERTAINTIES for the exact quotes and
+how they were obtained). Dropping `useResource:` therefore risks losing whatever
+hazard-tracking value those calls provided. Rather than apply the residency set uniformly and
+argue *in general* that hazard tracking isn't load-bearing, this diff draws the line at the
+one call site the mechanism history actually implicates:
+
+`moe_submit`'s `use` vector holds only **read-only** (`MTLResourceUsageRead`), **indirectly
+referenced** slab buffers — the kernel (`moe_gemv`) never touches them via `setBuffer:`; it
+dereferences raw GPU addresses (`waddr[e]`/`saddr[e]`) baked into a separately-bound address
+array (`bag`/`bau`/`bad`/`bsg`/`bsu`/`bsd`), which is exactly the "indirect reference" case
+`useResource:` exists for. No GPU-side write ever touches these buffers, so there is no
+write-after-write/read-after-write hazard for Metal's tracking to have been serializing in
+the first place; the one real hazard — a slab unregistered+freed+reused by the CPU while an
+async in-flight `moe_block_begin` command buffer still references it via a baked-in GPU
+address — is a **CPU-write race that Metal's hazard tracking never protected against anyway**
+(hazard tracking only covers GPU-side command dependencies visible through the Metal API; a
+raw host-memory write via `pread`/`memcpy` is invisible to it regardless of `useResource:`).
+That race is, and always was, the engine's own responsibility (slot/generation lifecycle: a
+slab isn't freed while an outstanding async handle still owns it) — unrelated to E5.
+
+Every other call site (`bind_gemv`, attention K/V cache writes) either doesn't scale with
+cache size (fixed per-layer dense tensors — no perf benefit to touching) or has real
+GPU-side write traffic in the same encoder (`Lb`/`Rb` are written by `a_copy` and read by
+`a_score`/`a_clat` within one encoder — currently ordered by explicit
+`memoryBarrierWithScope:MTLBarrierScopeBuffers` calls already present in `encode_attention`,
+not by `useResource:`'s hazard tracking, but touching them wasn't needed for the hypothesis
+and was judged not worth the added surface area). Leaving them untouched keeps the diff's
+blast radius matched to the one seam the fix-plan's v5 finding actually names.
+
+### Deferred-commit design (`resset_add` batches; `resset_remove` doesn't)
+
+`coli_metal_register` is called from parallel OpenMP loader threads in tight bursts
+("warmup fan-out" — same phrase E4's audit used for the same threads). Committing on every
+single `addAllocation:` would reintroduce a per-slab cost on the load path, which is exactly
+what E4's own +12s regression looked like (mutex held across a live Metal call, serializing
+loader threads). So `resset_add` only marks `g_resset_dirty`; the commit is deferred to the
+next `moe_submit` call, which flushes once via `resset_flush()` before it relies on the set
+for residency.
+
+This is correct — not just fast — because of an existing invariant the codebase already
+depends on for `resolve()` to work at all: a slab's `coli_metal_register` call (mutex-guarded)
+always completes and releases `g_slab_mtx` before any dispatch that references that slab's
+pointer can call `resolve()` for it (the caller in `glm.c` cannot pass a freshly-loaded
+expert's pointer to a dispatch before the load — which registers it — returns). Since
+`resset_flush()` also takes `g_slab_mtx`, and runs immediately before `moe_submit`'s own
+`resolve()` calls in program order, any slab a given `moe_submit` invocation will resolve was
+already `addAllocation:`-ed (and marked dirty) strictly before that invocation's
+`resset_flush()` runs — so the flush is guaranteed to cover it, regardless of what other
+threads are concurrently registering unrelated slabs.
+
+`resset_remove`, by contrast, commits synchronously and immediately, with no batching,
+because the caller (`glm.c`, in every one of the four slab-realloc call sites, and in
+`kv_alloc`) frees the underlying host memory *right after* `coli_metal_unregister` returns.
+An uncommitted-but-still-set-member allocation pointing at memory the host has already freed
+is a potential use-after-free the GPU could act on — deferring that removal is not a
+performance-vs-safety tradeoff, it's just unsafe, so it isn't deferred. (The spec's own
+lifecycle wording backs this reading: "`coli_metal_register` → add allocation + commit
+**(batch commits where call pattern allows)**" carries a batching allowance that
+"`coli_metal_unregister` → remove + commit" does not.)
+
+## Instrumentation parity
+
+No existing counter's semantics changed. `coli_metal_moe_times`/`coli_metal_moe_counts`
+(`g_t_setup`, `g_t_gpu`, `g_t_kernel`, `g_t_scatter`, `g_moe_ok`/`g_moe_fb`/`g_moe_experts`)
+are computed exactly as before — `resset_flush()` runs *before* `ts_start = mnow()` in
+`moe_submit`, so its cost (whatever it is) is **outside** `g_t_setup` and therefore invisible
+to the existing setup/gpu/kernel breakdown. This is a deliberate choice: it keeps the
+orchestrator's A/B harness reading the same counters with the same meaning across stock/E4/E5,
+but it also means the harness's existing numbers will not show E5's flush cost if it turns
+out to be non-negligible — see UNCERTAINTIES. No new counters were added (no `glm.c`
+`profile_print` changes), unlike E4's `METAL-HEAP: alloc fallbacks` line, because there was no
+`glm.c` touch point to hang a print on without adding one — judged not worth the extra diff
+surface for an experiment branch; `[METAL] residency-set: on` / the two fallback stderr lines
+from `coli_metal_init` are the only new observability, sufficient to confirm which path a run
+took.
+
+## Per-seam differences vs E4
+
+| Seam | E4 (`e4/metal-heap`) | E5 (this branch) |
+|---|---|---|
+| Allocation | New: `MTLHeap` sub-buffers via `coli_metal_heap_alloc` | Unchanged: same `posix_memalign` + `newBufferWithBytesNoCopy` |
+| `glm.c` / `backend_metal.h` | Touched (new alloc/free API, 4 call sites + `expert_host_release`) | **Untouched** |
+| Residency scope | Declared once **per command buffer** (`useHeap:`, still inside `moe_submit`) | Declared once **for the process** (queue-attached set), refreshed incrementally at register/unregister |
+| Hazard tracking | Heap sub-buffers forced `MTLHazardTrackingModeUntracked` always (allocation-level) | Untouched at the resource level; `moe_submit` alone stops calling `useResource:` (encoder-level), independent of `COLI_METAL_UNTRACKED` |
+| Per-buffer vs per-set skip | `[b heap]` (Metal's own `MTLResource.heap` property) checked per buffer — heterogeneous mixes possible if a slab fell back to malloc | Blanket `if (!g_resset_enabled)` — homogeneous by construction, since every registered slab goes through the same `coli_metal_register` path when the gate is on |
+| Availability guard | None needed (`MTLHeap` is old API) | `@available(macOS 15.0, *)`, matching this box's macOS 26.5 but required for portability |
+| Known regression | +12–13s expert-disk load at cap16 (suspected first-touch/lock contention on heap pages) | None expected — malloc pages never change ownership; **unverified without a run** |
+
+## What to measure (orchestrator, cap1/cap16, stock vs E4 vs E5)
+
+1. **GPU stall** (`coli_metal_moe_times` gpu/kernel breakdown) — success: E5 ≈ E4's
+ −85%-class reduction vs stock at cap16.
+2. **Expert-disk load path** (existing load/service-time counters) — success: E5 ≈ stock,
+ i.e. **no** repeat of E4's +12–13s tax, since allocation is untouched.
+3. **tok/s** — should track (1) and (2) together.
+4. **md5 within a fixed dispatch composition** — flag on vs off must be byte-identical at a
+ given cap (the "Output-invariant by construction" hard constraint); flag-on vs flag-on
+ across cap1/cap16 may legitimately differ (different dispatch composition, per the
+ fix-plan's "Determinism side-finding").
+5. **`[METAL] residency-set: on` line present in stderr** at flag-on startup, and absent
+ (or the OS<15/create-failed fallback line) otherwise — cheap sanity check that a run
+ actually exercised the intended path before trusting its numbers.
+6. If the hypothesis holds (E5 stall ≈ E4, E5 load-path ≈ stock, identical output), E5 becomes
+ the upstream PR candidate and must include the cap-default recalibration flagged in PR
+ #386's CURRENT-STATE CALIBRATION markers, per the spec's validation plan.
+
+## Build
+
+`cd c && make glm METAL=1` and a separate explicit `-Wall -Wextra` compile of
+`backend_metal.mm` (the Makefile's `METALXX` line does not itself pass `-Wall -Wextra`, so
+"clean under `-Wall -Wextra`" was checked with those flags added explicitly), plus
+`cd c && make glm` (plain, non-Metal — unaffected, since this diff never touches `glm.c`), and
+`make metal-test` (existing synthetic kernel-correctness unit test — no model, no
+`glm52_i4/`, random weights — run once with `COLI_METAL_RESSET` unset and once with
+`COLI_METAL_RESSET=1` to numerically exercise `coli_metal_register`/`moe_submit`'s changed
+code path, since the task scope excludes running the real model). Exact results in the final
+report, not here (build results belong to the report per the task's deliverable split, and
+this file is written before the batched build run, per the scheduling constraint).
+
+## UNCERTAINTIES
+
+**Everything below is a judgment call, a seam where the residency-set lifecycle interacts
+with the existing queue/command-buffer structure, or something unverifiable without a real
+model run — flagged per the task's hard requirement.**
+
+1. **The central design risk: skipping `useResource:` in `moe_submit` gives up Metal's
+ automatic hazard tracking for that buffer set.** Apple's `MTLResidencySet.h` header
+ (read directly from this box's SDK at
+ `/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/.../Headers/MTLResidencySet.h`)
+ documents the protocol only in terms of residency; Apple's own "Simplifying GPU resource
+ management with residency sets" guide states plainly: *"You don't need to call
+ `useResource`/`useHeap`... for allocations in a residency set,"* and separately, the
+ `MTLResidencySet` class reference states *"Residency sets don't support hazard tracking,
+ so you need to account for hazards with fences and events."* I reasoned through every
+ code path that touches `moe_submit`'s `use` buffers (read-only, indirectly referenced,
+ never concurrently written, freed only after the engine's own slot lifecycle guarantees
+ no outstanding async reference) and concluded removing `useResource:` there specifically
+ is safe — but this reasoning is **not the same as having run the model**. If any code
+ path I didn't trace lets a slab get unregistered while an async `moe_block_begin` handle
+ is still in flight and reading it, this change removes a mitigation (weak as it may have
+ been) that existed before. **This is the #1 thing to watch for md5 divergence on**, and
+ the reason the scope was deliberately narrowed to `moe_submit` alone rather than applied
+ uniformly.
+2. **`resset_add`/`resset_remove`/`resset_flush` all run under `g_slab_mtx`, serializing
+ concurrent OMP loader threads' residency-set mutations against each other and against
+ dispatch's flush.** `MTLResidencySet`'s own header says *"all methods are non-threadsafe"*
+ (confirmed directly from the SDK header), so this serialization is required for
+ correctness, not optional — but it is structurally the same shape as the bug E4's
+ audit-round-2 found and fixed (mutex held across a live Metal call, serializing loader
+ threads during warmup fan-out, suspected root cause of E4's +12s regression). Apple's
+ guide frames `addAllocation:`/`commit` as lightweight bookkeeping relative to the actual
+ (deferred, async) page-in work ("Metal makes allocations resident when you call `commit()`
+ on the first command buffer using the set" — implying `commit()` itself doesn't
+ synchronously page anything in), which is why I judged holding the mutex across these
+ calls acceptable unlike E4's `newBufferWithLength:` (a real allocation call). **This is
+ unverified without profiling a loaded run** — if `commit()` or `addAllocation:` turns out
+ to be synchronously expensive on this hardware/OS build, this could reproduce E4's
+ expert-disk-load regression through a different code path, defeating E5's entire premise
+ (decoupling residency from the load path). Orchestrator: this is the single most important
+ number to check E5's load-path timing against stock, not just against E4.
+3. **`resset_flush()`'s cost sits outside `g_t_setup`/the `moe_times` breakdown** (it runs
+ before `ts_start = mnow()`), by design, to keep the harness's existing counters meaningful
+ — but this also means if `commit()` is expensive, it will show up as *general* wall-clock
+ slowdown / tok/s regression without a corresponding line in the existing instrumentation
+ pointing at it. No new counter was added for it (see "Instrumentation parity" above) to
+ avoid a `glm.c` touch; if E5 numbers look off without an obvious cause in the existing
+ breakdown, `resset_flush`/`commit()` cost is the first place to add a throwaway probe.
+4. **`initialCapacity = 4096` on the `MTLResidencySetDescriptor` is an unverified guess.**
+ It's documented as a presize hint only (no correctness effect either way), chosen to be
+ "clearly larger than the permanent-weight-tensor + KV-cache + plausible cap16 LRU-slab
+ count" without actually counting those registrations precisely. Too small just means
+ internal array growth; not a correctness concern, flagged only because it's a number I
+ picked without measuring.
+5. **Not calling `requestResidency()` proactively.** Apple's guide frames it as an optional
+ latency-hiding call ("call ahead of time during non-critical moments... to minimize [first
+ command buffer] latency"), and Blender's Cycles PR (the spec's cited reference
+ implementation) doesn't appear to use it either per its PR description. Omitted to keep
+ the lifecycle minimal and match the reference pattern; if profiling shows a
+ first-command-buffer-after-a-load-burst latency spike, this is the documented lever to try
+ next, not implemented here.
+6. **The deferred-commit correctness argument (item in "Deferred-commit design" above) rests
+ on a single-writer-before-single-reader program-order guarantee that is true today by
+ inspection but is not an invariant enforced anywhere in code** (no assertion, no type-level
+ guarantee) — it's the same kind of implicit ordering `resolve()` itself already depends on
+ for correctness (a slab must be registered before any dispatch can resolve its pointer),
+ so this diff doesn't introduce a new category of fragility, but it's worth naming
+ explicitly rather than leaving implicit.
+7. **Async `moe_block_begin`/`moe_block_end` overlap with concurrent `register()` calls**
+ (background loader threads registering new/different experts while an unrelated MoE block
+ is still in flight on the GPU) was reasoned through but never exercised in a real
+ concurrent stress scenario — the synthetic `metal-test` unit test's `run_moe` calls are
+ single-threaded and synchronous (`coli_metal_moe_block`, not the async `_begin`/`_end`
+ pair), so it does **not** cover this interleaving. The real engine's `PILOT`/prefetch and
+ `moe_block_begin`/`_end` overlap path is exactly the concurrency shape most likely to
+ expose a bug in this design if one exists, and is untested here by construction (out of
+ scope: no model runs).
+8. **`coli_metal_gemm` (prefill path) and `bind_gemv` (attention path) still call
+ `useResource:` unconditionally, so they get no CPU-overhead benefit from the residency set
+ even though their buffers are also set members.** This is deliberate (see "Why only
+ `moe_submit` skips" above) but means E5's win, if any, is scoped to the decode-path MoE
+ dispatch loop specifically — prefill and attention timing should be unaffected by the flag,
+ which is itself a testable prediction the orchestrator's harness can check.
+9. **API surface verified against this box's actual SDK headers**
+ (`MTLResidencySet.h`, `MTLDevice.h`, `MTLCommandQueue.h`, `MTLAllocation.h`,
+ `MTLResource.h` — all read directly, not from memory) and against Apple's own
+ "Simplifying GPU resource management with residency sets" guide, so the method names/
+ signatures (`newResidencySetWithDescriptor:error:`, `addResidencySet:`,
+ `removeResidencySet:`, `addAllocation:`, `removeAllocation:`, `commit`) are
+ high-confidence. What is **not** independently verified is runtime behavior beyond what
+ the docs state and what the synthetic unit test exercises — no substitute for the
+ orchestrator's real cap-sweep battery.
diff --git a/c/backend_metal.mm b/c/backend_metal.mm
index b57fa66..9c10533 100644
--- a/c/backend_metal.mm
+++ b/c/backend_metal.mm
@@ -743,6 +743,7 @@ static id moe_submit(int nb, int D, int Iinter, int fmt,
const float *xg, const int *xoff, const int *nr, int R,
id xg_buf, id gg_buf, id uu_buf, id hh_buf) {
if (!g_dev || (fmt != 1 && fmt != 2)) return nil;
+ resset_flush(); // E5: commit any pending slab adds before we may skip useResource: below
double ts_start = mnow();
std::vector ag(nb),au(nb),ad(nb),sgv(nb),suv(nb),sdv(nb);
std::vector> use; use.reserve(nb*2);
@@ -764,7 +765,18 @@ static id moe_submit(int nb, int D, int Iinter, int fmt,
memcpy([xg_buf contents], xg, (size_t)R*D*4);
id cb=[g_queue commandBuffer]; id e=[cb computeCommandEncoder];
- for(auto&b:use) [e useResource:b usage:MTLResourceUsageRead];
+ // E5 (COLI_METAL_RESSET=1): the queue-attached MTLResidencySet already guarantees these
+ // buffers are resident, so skip the per-buffer declaration whose count scales with LRU
+ // cache size (mechanism history v5). Residency sets don't do hazard tracking (Apple docs),
+ // but none was load-bearing here: every buffer in `use` is MTLResourceUsageRead-only and
+ // referenced only indirectly (moe_gemv dereferences waddr[]/saddr[] baked into bag/bsg's
+ // contents), so there's no GPU-side write to serialize against; the one real hazard -- a
+ // slab unregistered+freed+reused while an async in-flight CB still reads it -- is a
+ // CPU-write race outside Metal's hazard tracking either way, held by the engine's own slot
+ // lifecycle, not by useResource:. See SUMMARY.md UNCERTAINTIES.
+ if (!g_resset_enabled) {
+ for(auto&b:use) [e useResource:b usage:MTLResourceUsageRead];
+ }
auto gemv=[&](id wa,id sa,id xin,id y,int O,int K,int Kin){
int NT=R*O;
[e setComputePipelineState:g_moe_gemv];
From 82d06000b5bdfd62919fa17275bf1a323968083d Mon Sep 17 00:00:00 2001
From: monotophic
Date: Sat, 18 Jul 2026 14:36:00 -0400
Subject: [PATCH 27/71] E5: validator round-1 fixes (resset mutex split, flush
visibility, citations)
1. REQUIRED (backend_metal.mm): no Metal call runs under g_slab_mtx anymore.
The set mutations (addAllocation/removeAllocation/commit) and the dirty
flag moved to a dedicated g_resset_mtx; coli_metal_register/unregister do
their g_slabs bookkeeping under g_slab_mtx exactly as stock, then call
resset_add/resset_remove after dropping it (still before returning, so
unregister's remove+commit still lands before the caller frees the host
memory). The original shape -- commit under the slab lock the parallel OMP
loader threads contend on -- was structurally identical to the mutex-over-
live-Metal-call bug E4's audit round 2 fixed, the leading suspect for its
replicated +12s expert-disk regression. The register->flush->resolve
happens-before argument survives the split: resset_add completes inside
coli_metal_register before it returns, the engine cannot dispatch an
expert before its load returns, and add/flush are serialized by
g_resset_mtx (comment at resset_add). The two mutexes are never held
together, so no lock-order hazard exists.
2. REQUIRED (backend_metal.mm, backend_metal.h, glm.c): the resset_flush
cost in moe_submit -- deliberately outside the g_t_setup window so the
A/B harness counters keep their meaning -- was invisible. New
g_t_resset_flush accumulator timed around the flush, exported via
coli_metal_resset_stats(), printed by profile_print as a separate
gate-on-only "METAL-RESSET: flush" line (mirrors E4's METAL-HEAP line;
the METAL: line the harness parses keeps its exact format; stock output
byte-identical -- the stats call returns 0 with the gate off and the
whole block sits in the pre-existing #ifdef COLI_METAL arm). Register-
side add/remove costs land in the existing t_ewait window; comment at
resset_add says so instead of a second counter.
3. REQUIRED (SUMMARY.md; the moe_submit commit message was already reworded
in place, pre-push): corrected two false attributions -- the hazard-
tracking and thread-safety statements were credited to the SDK header,
which is silent on both; the actual source is Apple's online
MTLResidencySet class reference and residency-set adoption guide
(fetched 2026-07-18). Note: the scaffolding commit's message was checked
and contains no such claim, so it was left untouched.
4. DOC (SUMMARY.md): the pre-existing fslab OOM-unwind bug (glm.c ~1868-73,
frees s->slab without unregister) has a strictly longer-lived exposure
under E5 (permanent set member vs stock's transient per-CB declaration).
Out of scope here; the upstream PR built from E5 must carry the one-line
fix (reference: E4 branch commit 6753225).
---
SUMMARY.md | 208 ++++++++++++++++++++++++++++++---------------
c/backend_metal.h | 4 +
c/backend_metal.mm | 69 ++++++++++-----
c/colibri.c | 4 +-
4 files changed, 195 insertions(+), 90 deletions(-)
diff --git a/SUMMARY.md b/SUMMARY.md
index a4b86e3..3ccb3ae 100644
--- a/SUMMARY.md
+++ b/SUMMARY.md
@@ -17,12 +17,15 @@ reduction survives without the load-path tax (malloc pages never change ownershi
## What changed
-Everything is confined to `c/backend_metal.mm`. **No changes to `glm.c` or
-`backend_metal.h`** — `coli_metal_register`/`coli_metal_unregister`'s existing signatures and
-every call site in `glm.c` (expert_load, uring_load_add, qalloc, kv_alloc, map_of_fd) are
-untouched; the residency-set bookkeeping lives entirely inside those two functions' existing
-bodies. This is a smaller diff shape than E4, which needed new `backend_metal.h` declarations
-and new `glm.c` call sites because it changed the allocation function itself.
+All mechanism code is confined to `c/backend_metal.mm` —
+`coli_metal_register`/`coli_metal_unregister`'s existing signatures and every call site in
+`glm.c` (expert_load, uring_load_add, qalloc, kv_alloc, map_of_fd) are untouched; the
+residency-set bookkeeping lives entirely inside those two functions' existing bodies. The
+only `glm.c`/`backend_metal.h` touch is one instrumentation hook added in the validator
+round-1 fixes (`coli_metal_resset_stats` + the gate-on-only `METAL-RESSET:` stats line in
+`profile_print` — see "Validator round 1 fixes" below). Still a smaller diff shape than E4,
+which needed a new alloc/free API and four new `glm.c` call-site arms because it changed the
+allocation function itself.
Env-gated `COLI_METAL_RESSET=1`, default OFF, runtime `@available(macOS 15.0, *)` guard with
a one-line stderr fallback when requested on an older OS or when residency-set creation
@@ -40,12 +43,18 @@ the `COLI_METAL_RESSET` `getenv` branch in `coli_metal_init`, so `resset_add`/`r
— one set, attached once, for the process lifetime. Failure (old OS or creation error)
prints one stderr line and leaves `g_resset_enabled=false` — stock path.
- **`coli_metal_register`**: after wrapping the buffer exactly as today
- (`newBufferWithBytesNoCopy`), calls `resset_add(b)` under the *same* `g_slab_mtx` that
- already serializes `g_slabs` mutation from parallel OMP loader threads. `resset_add` calls
- `[rs addAllocation:b]` and sets a `g_resset_dirty` flag — **it does not commit**.
-- **`coli_metal_unregister`**: calls `resset_remove(b)` (also under `g_slab_mtx`) *before*
- clearing the `g_slabs` entry. `resset_remove` calls `[rs removeAllocation:b]` **and commits
- immediately** — no batching. See UNCERTAINTIES for why this asymmetry is deliberate.
+ (`newBufferWithBytesNoCopy`) and pushing the `g_slabs` entry under `g_slab_mtx` exactly as
+ today, calls `resset_add(b)` **after dropping `g_slab_mtx`** but before returning.
+ `resset_add` takes a dedicated `g_resset_mtx` (guarding only the set mutations and the
+ dirty flag), calls `[rs addAllocation:b]` and sets `g_resset_dirty` — **it does not
+ commit**. No Metal call ever runs under `g_slab_mtx` (validator round-1 fix; E4's audit
+ round 2 identified mutex-over-live-Metal-call as the leading suspect for its +12s
+ expert-disk regression).
+- **`coli_metal_unregister`**: erases the `g_slabs` entry under `g_slab_mtx` (stashing the
+ buffer), then calls `resset_remove(b)` **outside `g_slab_mtx`**, before returning.
+ `resset_remove` (under `g_resset_mtx`) calls `[rs removeAllocation:b]` **and commits
+ immediately** — no batching — because the caller frees the host memory right after the
+ function returns. See UNCERTAINTIES for why this asymmetry is deliberate.
- **`moe_submit`** (the one function whose `use` list — resolved expert weight/scale slabs —
scales with LRU cache size): calls `resset_flush()` at the top (commits any pending adds
from `resset_add`, under `g_slab_mtx`), then, if `g_resset_enabled`, **skips** the
@@ -59,12 +68,13 @@ the `COLI_METAL_RESSET` `getenv` branch in `coli_metal_init`, so `resset_add`/`r
### Why only `moe_submit` skips `useResource:`
-Apple's own `MTLResidencySet` docs are explicit: *"Residency sets don't support hazard
-tracking, so you need to account for hazards with fences and events."* (confirmed against
-the actual SDK header comment and Apple's "Simplifying GPU resource management with residency
-sets" guide, both read directly for this design — see UNCERTAINTIES for the exact quotes and
-how they were obtained). Dropping `useResource:` therefore risks losing whatever
-hazard-tracking value those calls provided. Rather than apply the residency set uniformly and
+Apple's `MTLResidencySet` class reference (developer.apple.com, fetched during design on
+2026-07-18) is explicit: *"Residency sets don't support hazard tracking, so you need to
+account for hazards with fences and events."* The SDK header on this box
+(`MTLResidencySet.h`, read directly) is **silent** on hazard tracking — the statement comes
+from Apple's online documentation and adoption guide ("Simplifying GPU resource management
+with residency sets"), not the header (see UNCERTAINTIES for sourcing). Dropping
+`useResource:` therefore risks losing whatever hazard-tracking value those calls provided. Rather than apply the residency set uniformly and
argue *in general* that hazard tracking isn't load-bearing, this diff draws the line at the
one call site the mechanism history actually implicates:
@@ -102,15 +112,20 @@ next `moe_submit` call, which flushes once via `resset_flush()` before it relies
for residency.
This is correct — not just fast — because of an existing invariant the codebase already
-depends on for `resolve()` to work at all: a slab's `coli_metal_register` call (mutex-guarded)
-always completes and releases `g_slab_mtx` before any dispatch that references that slab's
+depends on for `resolve()` to work at all: a slab's `coli_metal_register` call always
+completes — including its trailing `resset_add`, which runs after `g_slab_mtx` is dropped
+but **before the function returns** — before any dispatch that references that slab's
pointer can call `resolve()` for it (the caller in `glm.c` cannot pass a freshly-loaded
-expert's pointer to a dispatch before the load — which registers it — returns). Since
-`resset_flush()` also takes `g_slab_mtx`, and runs immediately before `moe_submit`'s own
-`resolve()` calls in program order, any slab a given `moe_submit` invocation will resolve was
-already `addAllocation:`-ed (and marked dirty) strictly before that invocation's
-`resset_flush()` runs — so the flush is guaranteed to cover it, regardless of what other
-threads are concurrently registering unrelated slabs.
+expert's pointer to a dispatch before the load — which registers it — returns). After the
+validator round-1 mutex split, the flush's synchronization runs through `g_resset_mtx`
+alone: `resset_add`'s set mutation + dirty write and `resset_flush`'s dirty read + commit
+are serialized by that one mutex, whose release/acquire pairs provide the memory ordering;
+`g_slab_mtx` still orders the slab-table bookkeeping (register-before-resolve) exactly as on
+stock. So any slab a given `moe_submit` invocation will resolve was `addAllocation:`-ed (and
+marked dirty) strictly before that invocation's `resset_flush()` acquired `g_resset_mtx` —
+the flush is guaranteed to cover it, regardless of what other threads are concurrently
+registering unrelated slabs. The two mutexes are never held simultaneously anywhere, so no
+deadlock ordering exists to maintain.
`resset_remove`, by contrast, commits synchronously and immediately, with no batching,
because the caller (`glm.c`, in every one of the four slab-realloc call sites, and in
@@ -127,16 +142,55 @@ lifecycle wording backs this reading: "`coli_metal_register` → add allocation
No existing counter's semantics changed. `coli_metal_moe_times`/`coli_metal_moe_counts`
(`g_t_setup`, `g_t_gpu`, `g_t_kernel`, `g_t_scatter`, `g_moe_ok`/`g_moe_fb`/`g_moe_experts`)
are computed exactly as before — `resset_flush()` runs *before* `ts_start = mnow()` in
-`moe_submit`, so its cost (whatever it is) is **outside** `g_t_setup` and therefore invisible
-to the existing setup/gpu/kernel breakdown. This is a deliberate choice: it keeps the
-orchestrator's A/B harness reading the same counters with the same meaning across stock/E4/E5,
-but it also means the harness's existing numbers will not show E5's flush cost if it turns
-out to be non-negligible — see UNCERTAINTIES. No new counters were added (no `glm.c`
-`profile_print` changes), unlike E4's `METAL-HEAP: alloc fallbacks` line, because there was no
-`glm.c` touch point to hang a print on without adding one — judged not worth the extra diff
-surface for an experiment branch; `[METAL] residency-set: on` / the two fallback stderr lines
-from `coli_metal_init` are the only new observability, sufficient to confirm which path a run
-took.
+`moe_submit`, so its cost is **outside** `g_t_setup`, keeping the orchestrator's A/B harness
+reading the same counters with the same meaning across stock/E4/E5. The flush cost is
+surfaced separately (validator round-1 fix — the original design left it invisible, a blind
+spot for the battery): a dedicated `g_t_resset_flush` accumulator timed around the flush in
+`moe_submit`, exported via `coli_metal_resset_stats()` (`backend_metal.h`) and printed by
+`profile_print` as its own `METAL-RESSET: flush N.NNs` line — a **separate line following
+the `METAL:` line, mirroring E4's `METAL-HEAP:` convention, so the existing `METAL:` line
+the harness parses keeps its exact format** — printed **only when the gate is on** (the
+function returns 0 when off), so stock output stays byte-identical. The register-side
+`resset_add`/`resset_remove` costs have no dedicated counter: they run inside the engine's
+existing expert-load wait accounting (the `t_ewait` window in `glm.c`), noted in a comment
+at `resset_add`, so a load-path regression from set bookkeeping would already show in the
+existing disk/wait numbers. `[METAL] residency-set: on` / the two fallback stderr lines from
+`coli_metal_init` confirm which path a run took.
+
+## Validator round 1 fixes
+
+1. **REQUIRED, Metal calls hoisted out of `g_slab_mtx`** (`backend_metal.mm`): the original
+ design ran `addAllocation:`/`removeAllocation:`/`commit` while holding `g_slab_mtx`, the
+ lock the parallel OMP loader threads contend on — structurally identical to the
+ mutex-over-live-Metal-call shape E4's audit round 2 identified as the leading suspect for
+ its replicated +12s expert-disk regression, and the SDK header notes commit on a resident
+ set tries to make resources resident "instantly" (real synchronous work; this set is
+ resident from startup since it is queue-attached for the process lifetime). Fixed by
+ introducing a dedicated `g_resset_mtx` guarding only the set mutations + dirty flag;
+ `g_slabs` push/erase stays under `g_slab_mtx` exactly as stock; the two mutexes are never
+ held together. The register→flush→resolve happens-before argument is preserved — see the
+ updated "Deferred-commit design" section and the comment at `resset_add`.
+2. **REQUIRED, false citations corrected** (this file + the `moe_submit` commit message,
+ rewritten pre-push): the original text attributed the hazard-tracking and thread-safety
+ statements to the SDK header (`MTLResidencySet.h`), which is in fact silent on both
+ topics. The statements come from Apple's **online** `MTLResidencySet` class reference and
+ the "Simplifying GPU resource management with residency sets" adoption guide (both
+ fetched 2026-07-18 during design). All attributions now name the actual source; where a
+ claim rests on design reasoning rather than documentation, it is labeled as such.
+3. **REQUIRED, flush cost made harness-visible**: `g_t_resset_flush` +
+ `coli_metal_resset_stats()` + the gate-on-only `METAL-RESSET:` line in `profile_print` —
+ see "Instrumentation parity" above.
+4. **NOTE — pre-existing fslab OOM-unwind bug becomes a longer-lived artifact under E5**
+ (no code change on this branch, per the fix round's scoping): `expert_load`'s fslab OOM
+ path (`c/glm.c` ~1868–1873 on this base) frees `s->slab` via `compat_aligned_free`
+ **without** `coli_metal_unregister` — on stock this leaves a stale `g_slabs` entry whose
+ GPU exposure ends with the last command buffer that declared it; under E5 the buffer is
+ additionally a **permanent residency-set member** referencing freed host memory until
+ some later realloc of the same slot unregisters by pointer, a strictly longer-lived
+ exposure than stock's transient per-CB one. When E5 graduates to the upstream PR, it must
+ carry the one-line unregister-before-free fix; E4's branch has the reference
+ implementation (`6753225`, "validator round-1 fixes (heap-create latch, unwind
+ unregister)").
## Per-seam differences vs E4
@@ -163,7 +217,9 @@ took.
fix-plan's "Determinism side-finding").
5. **`[METAL] residency-set: on` line present in stderr** at flag-on startup, and absent
(or the OS<15/create-failed fallback line) otherwise — cheap sanity check that a run
- actually exercised the intended path before trusting its numbers.
+ actually exercised the intended path before trusting its numbers. Also read the
+ **`METAL-RESSET: flush` line** (gate-on only): if that number is large, the deferred
+ set-commit cost is eating the stall win from the dispatch side.
6. If the hypothesis holds (E5 stall ≈ E4, E5 load-path ≈ stock, identical output), E5 becomes
the upstream PR candidate and must include the cap-default recalibration flagged in PR
#386's CURRENT-STATE CALIBRATION markers, per the spec's validation plan.
@@ -173,7 +229,9 @@ took.
`cd c && make glm METAL=1` and a separate explicit `-Wall -Wextra` compile of
`backend_metal.mm` (the Makefile's `METALXX` line does not itself pass `-Wall -Wextra`, so
"clean under `-Wall -Wextra`" was checked with those flags added explicitly), plus
-`cd c && make glm` (plain, non-Metal — unaffected, since this diff never touches `glm.c`), and
+`cd c && make glm` (plain, non-Metal — the one `glm.c` touch, the `METAL-RESSET` stats line,
+is inside the pre-existing `#ifdef COLI_METAL` arm of `profile_print`, so the plain build
+compiles none of it), and
`make metal-test` (existing synthetic kernel-correctness unit test — no model, no
`glm52_i4/`, random weights — run once with `COLI_METAL_RESSET` unset and once with
`COLI_METAL_RESSET=1` to numerically exercise `coli_metal_register`/`moe_submit`'s changed
@@ -188,14 +246,16 @@ with the existing queue/command-buffer structure, or something unverifiable with
model run — flagged per the task's hard requirement.**
1. **The central design risk: skipping `useResource:` in `moe_submit` gives up Metal's
- automatic hazard tracking for that buffer set.** Apple's `MTLResidencySet.h` header
- (read directly from this box's SDK at
- `/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/.../Headers/MTLResidencySet.h`)
- documents the protocol only in terms of residency; Apple's own "Simplifying GPU resource
- management with residency sets" guide states plainly: *"You don't need to call
- `useResource`/`useHeap`... for allocations in a residency set,"* and separately, the
- `MTLResidencySet` class reference states *"Residency sets don't support hazard tracking,
- so you need to account for hazards with fences and events."* I reasoned through every
+ automatic hazard tracking for that buffer set.** Sourcing (corrected in validator round
+ 1): the SDK header on this box
+ (`/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/.../Headers/MTLResidencySet.h`,
+ read directly) documents the protocol only in terms of residency and says nothing about
+ hazard tracking either way; the two operative statements are from Apple's **online**
+ documentation (fetched 2026-07-18): the "Simplifying GPU resource management with
+ residency sets" adoption guide — *"You don't need to call `useResource`/`useHeap`... for
+ allocations in a residency set"* — and the `MTLResidencySet` class reference —
+ *"Residency sets don't support hazard tracking, so you need to account for hazards with
+ fences and events."* I reasoned through every
code path that touches `moe_submit`'s `use` buffers (read-only, indirectly referenced,
never concurrently written, freed only after the engine's own slot lifecycle guarantees
no outstanding async reference) and concluded removing `useResource:` there specifically
@@ -205,30 +265,33 @@ model run — flagged per the task's hard requirement.**
been) that existed before. **This is the #1 thing to watch for md5 divergence on**, and
the reason the scope was deliberately narrowed to `moe_submit` alone rather than applied
uniformly.
-2. **`resset_add`/`resset_remove`/`resset_flush` all run under `g_slab_mtx`, serializing
- concurrent OMP loader threads' residency-set mutations against each other and against
- dispatch's flush.** `MTLResidencySet`'s own header says *"all methods are non-threadsafe"*
- (confirmed directly from the SDK header), so this serialization is required for
- correctness, not optional — but it is structurally the same shape as the bug E4's
- audit-round-2 found and fixed (mutex held across a live Metal call, serializing loader
- threads during warmup fan-out, suspected root cause of E4's +12s regression). Apple's
- guide frames `addAllocation:`/`commit` as lightweight bookkeeping relative to the actual
- (deferred, async) page-in work ("Metal makes allocations resident when you call `commit()`
- on the first command buffer using the set" — implying `commit()` itself doesn't
- synchronously page anything in), which is why I judged holding the mutex across these
- calls acceptable unlike E4's `newBufferWithLength:` (a real allocation call). **This is
- unverified without profiling a loaded run** — if `commit()` or `addAllocation:` turns out
- to be synchronously expensive on this hardware/OS build, this could reproduce E4's
- expert-disk-load regression through a different code path, defeating E5's entire premise
- (decoupling residency from the load path). Orchestrator: this is the single most important
- number to check E5's load-path timing against stock, not just against E4.
+2. **Residency-set mutations are serialized under a dedicated `g_resset_mtx` (validator
+ round-1 fix — originally they ran under `g_slab_mtx`, the E4-regression shape; no Metal
+ call runs under the slab lock anymore).** The serialization itself is kept as required
+ for correctness: Apple's online `MTLResidencySet` class reference states the set's
+ *"methods aren't thread-safe"* (the SDK header contains no thread-safety statement either
+ way — citation corrected in round 1; the online doc is the source). What remains
+ **unverified without profiling a loaded run** is the *cost* of the calls themselves:
+ `resset_remove`'s synchronous `commit` runs inside `coli_metal_unregister` on the
+ loader path (its cost lands in the existing `t_ewait` accounting), and the SDK header
+ says commit on a resident set tries to make added/removed resources resident/non-resident
+ *"instantly"* — real synchronous work, since this set is resident from startup
+ (queue-attached for the process lifetime). If `commit()`/`addAllocation:` turn out
+ expensive on this hardware/OS build, the load path degrades through set bookkeeping
+ rather than mutex contention — a different, now-decoupled failure mode, but the same
+ symptom as E4's regression. Orchestrator: check E5's load-path timing against stock, not
+ just against E4, and read the new `METAL-RESSET: flush` line for the dispatch-side share.
3. **`resset_flush()`'s cost sits outside `g_t_setup`/the `moe_times` breakdown** (it runs
- before `ts_start = mnow()`), by design, to keep the harness's existing counters meaningful
- — but this also means if `commit()` is expensive, it will show up as *general* wall-clock
- slowdown / tok/s regression without a corresponding line in the existing instrumentation
- pointing at it. No new counter was added for it (see "Instrumentation parity" above) to
- avoid a `glm.c` touch; if E5 numbers look off without an obvious cause in the existing
- breakdown, `resset_flush`/`commit()` cost is the first place to add a throwaway probe.
+ before `ts_start = mnow()`), by design, to keep the harness's existing counters
+ meaningful — and, since validator round 1, it is **no longer invisible**: the
+ `g_t_resset_flush` accumulator surfaces it as the gate-on-only `METAL-RESSET: flush`
+ line (see "Instrumentation parity"). Residual blind spots: (a) the accumulator is a
+ plain double written from `moe_submit` on the engine thread, matching the existing
+ `g_t_setup` convention — if `moe_submit` were ever called from multiple threads
+ concurrently, both counters would be equally wrong; (b) the register-side
+ `resset_add`/`resset_remove` costs have no dedicated counter and are only visible
+ blended into the existing `t_ewait`/disk-wait numbers (comment at `resset_add` says so)
+ — a fine-grained attribution would need a throwaway probe.
4. **`initialCapacity = 4096` on the `MTLResidencySetDescriptor` is an unverified guess.**
It's documented as a presize hint only (no correctness effect either way), chosen to be
"clearly larger than the permanent-weight-tensor + KV-cache + plausible cap16 LRU-slab
@@ -273,3 +336,10 @@ model run — flagged per the task's hard requirement.**
high-confidence. What is **not** independently verified is runtime behavior beyond what
the docs state and what the synthetic unit test exercises — no substitute for the
orchestrator's real cap-sweep battery.
+10. **Pre-existing fslab OOM-unwind bug has a strictly longer-lived exposure window under
+ E5** — see "Validator round 1 fixes" item 4. Not fixed on this branch (out of the
+ experiment's scope per the fix round), but the upstream PR built from E5 **must** carry
+ the one-line unregister-before-free fix (reference implementation on E4's branch,
+ commit `6753225`). The bug is only reachable through the fslab-OOM path (allocation
+ failure mid-load), so it cannot affect the orchestrator's controlled A/B runs at sane
+ RAM headroom — flagged for the PR, not for the battery.
diff --git a/c/backend_metal.h b/c/backend_metal.h
index 4383b3b..e6e24dd 100644
--- a/c/backend_metal.h
+++ b/c/backend_metal.h
@@ -98,6 +98,10 @@ int coli_metal_attn_decode(const float *x,
void coli_metal_moe_counts(uint64_t *ok, uint64_t *fb, uint64_t *experts);
void coli_metal_moe_times(double *setup, double *gpu, double *scatter);
double coli_metal_moe_kernel_time(void);
+/* E5 (COLI_METAL_RESSET=1): returns 1 when the queue-attached residency set is active and
+ * writes the cumulative seconds moe_submit spent committing pending set adds -- a cost that
+ * sits OUTSIDE the setup/gpu/scatter breakdown above. Returns 0 (and writes 0) when off. */
+int coli_metal_resset_stats(double *flush_s);
/*
* Batched routed-expert SwiGLU for one MoE block, in ONE command buffer.
diff --git a/c/backend_metal.mm b/c/backend_metal.mm
index 9c10533..be7dae8 100644
--- a/c/backend_metal.mm
+++ b/c/backend_metal.mm
@@ -270,23 +270,36 @@ static std::mutex g_slab_mtx; // expert_load registers slabs from parallel Ope
// @available(macOS 15.0, *) guards below, keeping -Wunguarded-availability clean.
static id g_resset_obj;
static bool g_resset_enabled; // COLI_METAL_RESSET=1, macOS 15+, and creation succeeded
-static bool g_resset_dirty; // addAllocation: calls pending commit; g_slab_mtx-guarded
+static bool g_resset_dirty; // addAllocation: calls pending commit; g_resset_mtx-guarded
+// Set mutations + dirty flag get their OWN mutex, never held together with g_slab_mtx: no
+// live Metal call may run under the slab lock the parallel OMP loader threads contend on
+// (E4's audit round 2 found exactly that shape -- mutex over a live Metal call -- as the
+// leading suspect for its +12s expert-disk regression). g_slab_mtx keeps guarding g_slabs
+// bookkeeping only, exactly as on stock.
+static std::mutex g_resset_mtx;
+static double g_t_resset_flush; // sec committing pending adds in moe_submit (gate on only)
-// Add a just-wrapped buffer to the set. Deferred commit (batches an OMP loader burst into
-// one commit at the next moe_submit instead of one per slab): correct because every
-// register/unregister caller holds g_slab_mtx here, and resolve() (which every dispatch
-// calls before touching a slab) takes the same mutex -- a slab moe_submit's resolve() can
-// see was necessarily added, and marked dirty, before moe_submit's own resset_flush() ran.
-static void resset_add(id b) { // caller holds g_slab_mtx
+// Add a just-wrapped buffer to the set; commit deferred (an OMP loader burst batches into
+// one commit at the next moe_submit instead of one per slab). Called by coli_metal_register
+// after it drops g_slab_mtx but before it returns -- and the engine cannot dispatch an
+// expert before the load that registers its slab returns, so any slab a given moe_submit
+// can resolve() was added (and marked dirty) under g_resset_mtx strictly before that
+// moe_submit's resset_flush() acquired the same mutex: the flush covers it. The slab-table
+// ordering itself (register-before-resolve) is unchanged and stays under g_slab_mtx.
+// Cost lands in the caller's existing expert-load accounting (t_ewait window in glm.c);
+// no separate counter for the add/remove side.
+static void resset_add(id b) {
if (!g_resset_enabled) return;
+ std::lock_guard lk(g_resset_mtx);
if (@available(macOS 15.0, *)) { [(id)g_resset_obj addAllocation:b]; g_resset_dirty = true; }
}
// Remove + commit immediately, NOT deferred: the caller frees the underlying host memory
// right after coli_metal_unregister returns, so the removal must be applied before that --
// an uncommitted-but-still-resident allocation pointing at freed memory is a use-after-free
-// risk the GPU could act on.
-static void resset_remove(id b) { // caller holds g_slab_mtx
+// risk the GPU could act on. Also runs outside g_slab_mtx (see g_resset_mtx above).
+static void resset_remove(id b) {
if (!g_resset_enabled) return;
+ std::lock_guard lk(g_resset_mtx);
if (@available(macOS 15.0, *)) {
id rs = (id)g_resset_obj;
[rs removeAllocation:b]; [rs commit];
@@ -294,14 +307,23 @@ static void resset_remove(id b) { // caller holds g_slab_mtx
g_resset_dirty = false; // commit above also flushes any pending adds
}
// Flush pending adds before moe_submit relies on the set alone for residency -- the only
-// caller that skips per-buffer useResource: (see moe_submit below).
+// caller that skips per-buffer useResource: (see moe_submit below). Takes g_resset_mtx
+// only, never g_slab_mtx; the happens-before argument lives at resset_add above.
static void resset_flush() {
if (!g_resset_enabled) return;
- std::lock_guard lk(g_slab_mtx);
+ std::lock_guard lk(g_resset_mtx);
if (!g_resset_dirty) return;
if (@available(macOS 15.0, *)) { [(id)g_resset_obj commit]; }
g_resset_dirty = false;
}
+// Harness visibility for the flush cost, which sits OUTSIDE the moe_times setup/gpu
+// breakdown (timed around resset_flush in moe_submit, before ts_start). Returns whether
+// the set is active so glm.c prints the METAL-RESSET line only when the gate is on --
+// stock output stays byte-identical.
+extern "C" int coli_metal_resset_stats(double *flush_s) {
+ if (flush_s) *flush_s = g_t_resset_flush;
+ return g_resset_enabled ? 1 : 0;
+}
// Persistent scratch buffers (grow-only) for the MoE pipeline.
static id g_gg, g_uu, g_hh, g_xg; static size_t g_gg_cap, g_uu_cap, g_hh_cap, g_xg_cap;
@@ -377,17 +399,22 @@ extern "C" void coli_metal_register(void *base, size_t len) {
id b = [g_dev newBufferWithBytesNoCopy:base length:len
options:g_res_opts deallocator:nil];
if (!b) return;
- std::lock_guard lk(g_slab_mtx); // called from parallel expert_load threads
- for (auto &s : g_slabs) if (s.base == base) { s.len = len; s.buf = b; resset_add(b); return; }
- g_slabs.push_back({base, len, b});
- resset_add(b);
+ {
+ std::lock_guard lk(g_slab_mtx); // called from parallel expert_load threads
+ bool found = false;
+ for (auto &s : g_slabs) if (s.base == base) { s.len = len; s.buf = b; found = true; break; }
+ if (!found) g_slabs.push_back({base, len, b});
+ }
+ resset_add(b); // E5: outside g_slab_mtx (no Metal call under the slab lock); still before return
}
extern "C" void coli_metal_unregister(void *base) {
- std::lock_guard lk(g_slab_mtx);
- for (size_t i=0;i b = nil;
+ {
+ std::lock_guard lk(g_slab_mtx);
+ for (size_t i=0;i resolve(const void *p, uint64_t *addr) {
@@ -743,7 +770,9 @@ static id moe_submit(int nb, int D, int Iinter, int fmt,
const float *xg, const int *xoff, const int *nr, int R,
id xg_buf, id gg_buf, id uu_buf, id hh_buf) {
if (!g_dev || (fmt != 1 && fmt != 2)) return nil;
- resset_flush(); // E5: commit any pending slab adds before we may skip useResource: below
+ if (g_resset_enabled) { // E5: commit any pending slab adds before we may skip useResource:
+ double t0 = mnow(); resset_flush(); g_t_resset_flush += mnow() - t0; // METAL-RESSET line
+ }
double ts_start = mnow();
std::vector ag(nb),au(nb),ad(nb),sgv(nb),suv(nb),sdv(nb);
std::vector> use; use.reserve(nb*2);
diff --git a/c/colibri.c b/c/colibri.c
index 03eb878..d08615a 100644
--- a/c/colibri.c
+++ b/c/colibri.c
@@ -3801,7 +3801,9 @@ static void profile_print(Model *m, double elapsed){
if(aok){ double ks=0,gs=0; coli_metal_attn_lat(&ks,&gs);
printf("METAL-ATTN: layer GPU %llu | gpu-wall %.2fs (kernel %.2fs | cpu-sched %.2fs gpu-sched %.2fs)\n",(unsigned long long)aok,aw,ak,ks,gs); } }
printf("METAL: blocchi GPU %llu | fallback CPU %llu | expert su GPU %llu | setup %.2fs gpu-wall %.2fs (kernel %.2fs) scatter %.2fs\n",
- (unsigned long long)ok,(unsigned long long)fb,(unsigned long long)ex,su,gp,coli_metal_moe_kernel_time(),sc); }
+ (unsigned long long)ok,(unsigned long long)fb,(unsigned long long)ex,su,gp,coli_metal_moe_kernel_time(),sc);
+ { double rsf=0; if(coli_metal_resset_stats(&rsf)) /* E5: printed only when the gate is on */
+ printf("METAL-RESSET: flush %.2fs (residency-set commit in moe_submit, outside setup/gpu-wall)\n",rsf); } }
#endif
}
From e011092ce164c1001220c5c211b4523fa07b54db Mon Sep 17 00:00:00 2001
From: monotophic
Date: Sat, 18 Jul 2026 15:35:12 -0400
Subject: [PATCH 28/71] E5: carry the fslab-OOM unwind fix (unregister before
free)
expert_load's fslab-OOM unwind freed s->slab via compat_aligned_free without
coli_metal_unregister -- a pre-existing gap on main/dev (validator-confirmed
during E4) that leaves a stale g_slabs entry over freed memory, letting
resolve() hand the GPU a dangling pointer. Under COLI_METAL_RESSET=1 the
exposure is strictly longer-lived: the wrapped buffer would stay a permanent
residency-set member over the freed pages instead of stock's transient
last-command-buffer window. Ported from e4/metal-heap validator fix 6753225,
adapted to dev's non-heap code shape (no coli_metal_heap_free wrapper --
plain unregister-before-free).
The uring_load_add analog (E4 audit round-2 insurance) is deliberately not
carried: that arm is #ifdef __linux__-gated while COLI_METAL is macOS-only,
so it is dead code on every real build target, and unlike E4 this branch has
no allocation-path reason to touch the function.
Reachable only through allocation failure mid-load; verified by inspection
and clean builds (no OOM-injection harness in tree). SUMMARY.md item 10
updated to "carried on this branch".
---
SUMMARY.md | 46 ++++++++++++++++++++++++++--------------------
c/colibri.c | 5 +++++
2 files changed, 31 insertions(+), 20 deletions(-)
diff --git a/SUMMARY.md b/SUMMARY.md
index 3ccb3ae..599b8ca 100644
--- a/SUMMARY.md
+++ b/SUMMARY.md
@@ -21,9 +21,10 @@ All mechanism code is confined to `c/backend_metal.mm` —
`coli_metal_register`/`coli_metal_unregister`'s existing signatures and every call site in
`glm.c` (expert_load, uring_load_add, qalloc, kv_alloc, map_of_fd) are untouched; the
residency-set bookkeeping lives entirely inside those two functions' existing bodies. The
-only `glm.c`/`backend_metal.h` touch is one instrumentation hook added in the validator
-round-1 fixes (`coli_metal_resset_stats` + the gate-on-only `METAL-RESSET:` stats line in
-`profile_print` — see "Validator round 1 fixes" below). Still a smaller diff shape than E4,
+`glm.c`/`backend_metal.h` touches are two, both coordinator-sanctioned: the validator
+round-1 instrumentation hook (`coli_metal_resset_stats` + the gate-on-only `METAL-RESSET:`
+stats line in `profile_print`) and the ported fslab-OOM unwind fix (see "Validator round 1
+fixes" item 4). Still a smaller diff shape than E4,
which needed a new alloc/free API and four new `glm.c` call-site arms because it changed the
allocation function itself.
@@ -180,17 +181,19 @@ existing disk/wait numbers. `[METAL] residency-set: on` / the two fallback stder
3. **REQUIRED, flush cost made harness-visible**: `g_t_resset_flush` +
`coli_metal_resset_stats()` + the gate-on-only `METAL-RESSET:` line in `profile_print` —
see "Instrumentation parity" above.
-4. **NOTE — pre-existing fslab OOM-unwind bug becomes a longer-lived artifact under E5**
- (no code change on this branch, per the fix round's scoping): `expert_load`'s fslab OOM
- path (`c/glm.c` ~1868–1873 on this base) frees `s->slab` via `compat_aligned_free`
- **without** `coli_metal_unregister` — on stock this leaves a stale `g_slabs` entry whose
- GPU exposure ends with the last command buffer that declared it; under E5 the buffer is
- additionally a **permanent residency-set member** referencing freed host memory until
+4. **Pre-existing fslab OOM-unwind bug — now CARRIED ON THIS BRANCH** (follow-up commit,
+ coordinator-sanctioned second `glm.c` change): `expert_load`'s fslab OOM path
+ (`c/glm.c` ~1870 on this base) freed `s->slab` via `compat_aligned_free` **without**
+ `coli_metal_unregister` — on stock that leaves a stale `g_slabs` entry whose GPU
+ exposure ends with the last command buffer that declared it; under E5 the buffer would
+ additionally be a **permanent residency-set member** referencing freed host memory until
some later realloc of the same slot unregisters by pointer, a strictly longer-lived
- exposure than stock's transient per-CB one. When E5 graduates to the upstream PR, it must
- carry the one-line unregister-before-free fix; E4's branch has the reference
- implementation (`6753225`, "validator round-1 fixes (heap-create latch, unwind
- unregister)").
+ exposure than stock's transient per-CB one. Fixed by porting E4's reference
+ implementation (`6753225`) to dev's non-heap code shape: `coli_metal_unregister(s->slab)`
+ before the free. The `uring_load_add` analog (E4's audit round-2 "cheap insurance") is
+ deliberately NOT carried: that arm is `#ifdef __linux__`-gated and `COLI_METAL` is
+ macOS-only, so it is dead code on every real build target, and unlike E4 this branch has
+ no allocation-path reason to touch the function at all.
## Per-seam differences vs E4
@@ -336,10 +339,13 @@ model run — flagged per the task's hard requirement.**
high-confidence. What is **not** independently verified is runtime behavior beyond what
the docs state and what the synthetic unit test exercises — no substitute for the
orchestrator's real cap-sweep battery.
-10. **Pre-existing fslab OOM-unwind bug has a strictly longer-lived exposure window under
- E5** — see "Validator round 1 fixes" item 4. Not fixed on this branch (out of the
- experiment's scope per the fix round), but the upstream PR built from E5 **must** carry
- the one-line unregister-before-free fix (reference implementation on E4's branch,
- commit `6753225`). The bug is only reachable through the fslab-OOM path (allocation
- failure mid-load), so it cannot affect the orchestrator's controlled A/B runs at sane
- RAM headroom — flagged for the PR, not for the battery.
+10. **Pre-existing fslab OOM-unwind bug — carried on this branch** (follow-up commit; see
+ "Validator round 1 fixes" item 4 for the full mechanism). The one-line
+ unregister-before-free fix from E4's `6753225` is ported to dev's non-heap code shape,
+ so the upstream PR built from E5 inherits it automatically. Residual notes: (a) the fix
+ is only reachable through the fslab-OOM path (allocation failure mid-load), so it is
+ untestable without an OOM-injection harness and cannot affect the orchestrator's
+ controlled A/B runs at sane RAM headroom — carried as correctness insurance, verified by
+ inspection + clean builds only; (b) the `__linux__`-gated `uring_load_add` analog is
+ deliberately not carried (dead code on every real build target — rationale in the fixes
+ section).
diff --git a/c/colibri.c b/c/colibri.c
index d08615a..05c9174 100644
--- a/c/colibri.c
+++ b/c/colibri.c
@@ -1134,6 +1134,11 @@ static int expert_load_impl(Model *m, int layer, int eid, ESlot *s, int fatal){
if(ftot<0 || (uint64_t)ftot > SIZE_MAX/sizeof(float) ||
posix_memalign((void**)&s->fslab,16384,fb)){
fprintf(stderr,"OOM fslab\n"); if(fatal) exit(1);
+ /* unregister BEFORE freeing -- a stale g_slabs entry would let resolve() hand
+ * the GPU a pointer into freed memory (and under COLI_METAL_RESSET=1, leave the
+ * buffer a permanent residency-set member over it). Ported from e4/metal-heap
+ * validator fix 6753225; pre-existing gap on main/dev. */
+ if(s->slab && g_metal_enabled) coli_metal_unregister(s->slab);
compat_aligned_free(s->slab); s->slab=NULL; s->slab_cap=0; /* clean, hidden slot (eid stays -1) */
s->fslab=NULL; s->fslab_cap=0; return -1;
}
From 8efa9ec6c3d123ea9af4f8deb54191d03ab3d01d Mon Sep 17 00:00:00 2001
From: monotophic
Date: Sat, 18 Jul 2026 15:57:12 -0400
Subject: [PATCH 29/71] E5: hazard-audit round-2 fixes (re-register set
hygiene, SUMMARY mutex ref)
1. DEFENSIVE (backend_metal.mm, coli_metal_register): re-registering a live
base overwrote s.buf and resset_add'ed the new wrapper without removing
the OLD one from the residency set -- ARC drops our reference but the set
retains the object and keeps its pages resident: a leak and a
set/g_slabs divergence. The found branch now stashes the replaced
wrapper under g_slab_mtx and, after dropping the lock (round-1 invariant:
no Metal call under the slab lock), resset_remove(old)s it before
resset_add(b); identical old==b early-outs both set operations.
Invariant defended, stated in the comment: residency-set membership
mirrors g_slabs exactly. No in-tree caller re-registers a live base
today (audited) -- closed defensively.
2. DOC (SUMMARY.md): the moe_submit lifecycle bullet still said
resset_flush commits "under g_slab_mtx" -- stale text from before the
round-1 mutex split; the code takes g_resset_mtx and never touches the
slab lock there. Parenthetical corrected; register bullet updated to
describe the re-register hygiene from item 1.
---
SUMMARY.md | 8 ++++++--
c/backend_metal.mm | 11 +++++++++--
2 files changed, 15 insertions(+), 4 deletions(-)
diff --git a/SUMMARY.md b/SUMMARY.md
index 599b8ca..d196eb1 100644
--- a/SUMMARY.md
+++ b/SUMMARY.md
@@ -50,7 +50,10 @@ the `COLI_METAL_RESSET` `getenv` branch in `coli_metal_init`, so `resset_add`/`r
dirty flag), calls `[rs addAllocation:b]` and sets `g_resset_dirty` — **it does not
commit**. No Metal call ever runs under `g_slab_mtx` (validator round-1 fix; E4's audit
round 2 identified mutex-over-live-Metal-call as the leading suspect for its +12s
- expert-disk regression).
+ expert-disk regression). Re-registering a live base (no in-tree caller does today) drops
+ the replaced wrapper from the set via `resset_remove(old)` before adding the new one
+ (hazard-audit defensive fix — the set would otherwise retain the old buffer, and its
+ pages' residency, forever), keeping set membership an exact mirror of `g_slabs`.
- **`coli_metal_unregister`**: erases the `g_slabs` entry under `g_slab_mtx` (stashing the
buffer), then calls `resset_remove(b)` **outside `g_slab_mtx`**, before returning.
`resset_remove` (under `g_resset_mtx`) calls `[rs removeAllocation:b]` **and commits
@@ -58,7 +61,8 @@ the `COLI_METAL_RESSET` `getenv` branch in `coli_metal_init`, so `resset_add`/`r
function returns. See UNCERTAINTIES for why this asymmetry is deliberate.
- **`moe_submit`** (the one function whose `use` list — resolved expert weight/scale slabs —
scales with LRU cache size): calls `resset_flush()` at the top (commits any pending adds
- from `resset_add`, under `g_slab_mtx`), then, if `g_resset_enabled`, **skips** the
+ from `resset_add`, under `g_resset_mtx` — it never touches the slab lock), then, if
+ `g_resset_enabled`, **skips** the
`for(auto&b:use) [e useResource:b usage:MTLResourceUsageRead];` loop entirely — residency
is already guaranteed by the queue-attached set. Every other `useResource:` call site in the
file (`bind_gemv`'s weight/scale buffers, `coli_metal_attn_decode`/`coli_metal_layer_decode`'s
diff --git a/c/backend_metal.mm b/c/backend_metal.mm
index be7dae8..6a8e3a0 100644
--- a/c/backend_metal.mm
+++ b/c/backend_metal.mm
@@ -399,13 +399,20 @@ extern "C" void coli_metal_register(void *base, size_t len) {
id b = [g_dev newBufferWithBytesNoCopy:base length:len
options:g_res_opts deallocator:nil];
if (!b) return;
+ id old = nil; // E5: replaced wrapper on re-register of a live base (defensive)
{
std::lock_guard lk(g_slab_mtx); // called from parallel expert_load threads
bool found = false;
- for (auto &s : g_slabs) if (s.base == base) { s.len = len; s.buf = b; found = true; break; }
+ for (auto &s : g_slabs) if (s.base == base) { old = s.buf; s.len = len; s.buf = b; found = true; break; }
if (!found) g_slabs.push_back({base, len, b});
}
- resset_add(b); // E5: outside g_slab_mtx (no Metal call under the slab lock); still before return
+ // E5, outside g_slab_mtx (no Metal call under the slab lock), before returning. Invariant
+ // defended: set membership mirrors g_slabs exactly -- a re-register of a live base must
+ // drop the replaced wrapper from the set (ARC releases our reference, but the set retains
+ // it and keeps its pages resident forever) before adding the new one. No in-tree caller
+ // re-registers a live base today; defensive.
+ if (old && old != b) resset_remove(old);
+ if (old != b) resset_add(b);
}
extern "C" void coli_metal_unregister(void *base) {
id b = nil;
From 042fd64a7c4d7854946a797716ff70486324e5e2 Mon Sep 17 00:00:00 2001
From: Kyle Kelley
Date: Sun, 19 Jul 2026 09:19:22 -0700
Subject: [PATCH 30/71] Align residency-set docs with split runtime
Update the rebased documentation and comments to name colibri.c after the #391 coordinator split, and correct the E5 comparison table to reflect its instrumentation and OOM-unwind touches.
Co-authored-by: Christopher Brand
---
SUMMARY.md | 18 +++++++++---------
c/backend_metal.mm | 4 ++--
2 files changed, 11 insertions(+), 11 deletions(-)
diff --git a/SUMMARY.md b/SUMMARY.md
index d196eb1..2947578 100644
--- a/SUMMARY.md
+++ b/SUMMARY.md
@@ -19,9 +19,9 @@ reduction survives without the load-path tax (malloc pages never change ownershi
All mechanism code is confined to `c/backend_metal.mm` —
`coli_metal_register`/`coli_metal_unregister`'s existing signatures and every call site in
-`glm.c` (expert_load, uring_load_add, qalloc, kv_alloc, map_of_fd) are untouched; the
+`colibri.c` (expert_load, uring_load_add, qalloc, kv_alloc, map_of_fd) are untouched; the
residency-set bookkeeping lives entirely inside those two functions' existing bodies. The
-`glm.c`/`backend_metal.h` touches are two, both coordinator-sanctioned: the validator
+`colibri.c`/`backend_metal.h` touches are two, both coordinator-sanctioned: the validator
round-1 instrumentation hook (`coli_metal_resset_stats` + the gate-on-only `METAL-RESSET:`
stats line in `profile_print`) and the ported fslab-OOM unwind fix (see "Validator round 1
fixes" item 4). Still a smaller diff shape than E4,
@@ -120,7 +120,7 @@ This is correct — not just fast — because of an existing invariant the codeb
depends on for `resolve()` to work at all: a slab's `coli_metal_register` call always
completes — including its trailing `resset_add`, which runs after `g_slab_mtx` is dropped
but **before the function returns** — before any dispatch that references that slab's
-pointer can call `resolve()` for it (the caller in `glm.c` cannot pass a freshly-loaded
+pointer can call `resolve()` for it (the caller in `colibri.c` cannot pass a freshly-loaded
expert's pointer to a dispatch before the load — which registers it — returns). After the
validator round-1 mutex split, the flush's synchronization runs through `g_resset_mtx`
alone: `resset_add`'s set mutation + dirty write and `resset_flush`'s dirty read + commit
@@ -133,7 +133,7 @@ registering unrelated slabs. The two mutexes are never held simultaneously anywh
deadlock ordering exists to maintain.
`resset_remove`, by contrast, commits synchronously and immediately, with no batching,
-because the caller (`glm.c`, in every one of the four slab-realloc call sites, and in
+because the caller (`colibri.c`, in every one of the four slab-realloc call sites, and in
`kv_alloc`) frees the underlying host memory *right after* `coli_metal_unregister` returns.
An uncommitted-but-still-set-member allocation pointing at memory the host has already freed
is a potential use-after-free the GPU could act on — deferring that removal is not a
@@ -157,7 +157,7 @@ the `METAL:` line, mirroring E4's `METAL-HEAP:` convention, so the existing `MET
the harness parses keeps its exact format** — printed **only when the gate is on** (the
function returns 0 when off), so stock output stays byte-identical. The register-side
`resset_add`/`resset_remove` costs have no dedicated counter: they run inside the engine's
-existing expert-load wait accounting (the `t_ewait` window in `glm.c`), noted in a comment
+existing expert-load wait accounting (the `t_ewait` window in `colibri.c`), noted in a comment
at `resset_add`, so a load-path regression from set bookkeeping would already show in the
existing disk/wait numbers. `[METAL] residency-set: on` / the two fallback stderr lines from
`coli_metal_init` confirm which path a run took.
@@ -186,8 +186,8 @@ existing disk/wait numbers. `[METAL] residency-set: on` / the two fallback stder
`coli_metal_resset_stats()` + the gate-on-only `METAL-RESSET:` line in `profile_print` —
see "Instrumentation parity" above.
4. **Pre-existing fslab OOM-unwind bug — now CARRIED ON THIS BRANCH** (follow-up commit,
- coordinator-sanctioned second `glm.c` change): `expert_load`'s fslab OOM path
- (`c/glm.c` ~1870 on this base) freed `s->slab` via `compat_aligned_free` **without**
+ coordinator-sanctioned second `colibri.c` change): `expert_load`'s fslab OOM path
+ (`c/colibri.c`, in `expert_load_impl`) freed `s->slab` via `compat_aligned_free` **without**
`coli_metal_unregister` — on stock that leaves a stale `g_slabs` entry whose GPU
exposure ends with the last command buffer that declared it; under E5 the buffer would
additionally be a **permanent residency-set member** referencing freed host memory until
@@ -204,7 +204,7 @@ existing disk/wait numbers. `[METAL] residency-set: on` / the two fallback stder
| Seam | E4 (`e4/metal-heap`) | E5 (this branch) |
|---|---|---|
| Allocation | New: `MTLHeap` sub-buffers via `coli_metal_heap_alloc` | Unchanged: same `posix_memalign` + `newBufferWithBytesNoCopy` |
-| `glm.c` / `backend_metal.h` | Touched (new alloc/free API, 4 call sites + `expert_host_release`) | **Untouched** |
+| Coordinator C source / `backend_metal.h` | `glm.c` touched (new alloc/free API, 4 call sites + `expert_host_release`) | `colibri.c` + header touched only for instrumentation and the OOM-unwind fix |
| Residency scope | Declared once **per command buffer** (`useHeap:`, still inside `moe_submit`) | Declared once **for the process** (queue-attached set), refreshed incrementally at register/unregister |
| Hazard tracking | Heap sub-buffers forced `MTLHazardTrackingModeUntracked` always (allocation-level) | Untouched at the resource level; `moe_submit` alone stops calling `useResource:` (encoder-level), independent of `COLI_METAL_UNTRACKED` |
| Per-buffer vs per-set skip | `[b heap]` (Metal's own `MTLResource.heap` property) checked per buffer — heterogeneous mixes possible if a slab fell back to malloc | Blanket `if (!g_resset_enabled)` — homogeneous by construction, since every registered slab goes through the same `coli_metal_register` path when the gate is on |
@@ -236,7 +236,7 @@ existing disk/wait numbers. `[METAL] residency-set: on` / the two fallback stder
`cd c && make glm METAL=1` and a separate explicit `-Wall -Wextra` compile of
`backend_metal.mm` (the Makefile's `METALXX` line does not itself pass `-Wall -Wextra`, so
"clean under `-Wall -Wextra`" was checked with those flags added explicitly), plus
-`cd c && make glm` (plain, non-Metal — the one `glm.c` touch, the `METAL-RESSET` stats line,
+`cd c && make glm` (plain, non-Metal — the one `colibri.c` instrumentation touch, the `METAL-RESSET` stats line,
is inside the pre-existing `#ifdef COLI_METAL` arm of `profile_print`, so the plain build
compiles none of it), and
`make metal-test` (existing synthetic kernel-correctness unit test — no model, no
diff --git a/c/backend_metal.mm b/c/backend_metal.mm
index 6a8e3a0..2dee846 100644
--- a/c/backend_metal.mm
+++ b/c/backend_metal.mm
@@ -286,7 +286,7 @@ static double g_t_resset_flush; // sec committing pending adds in moe_submit (
// can resolve() was added (and marked dirty) under g_resset_mtx strictly before that
// moe_submit's resset_flush() acquired the same mutex: the flush covers it. The slab-table
// ordering itself (register-before-resolve) is unchanged and stays under g_slab_mtx.
-// Cost lands in the caller's existing expert-load accounting (t_ewait window in glm.c);
+// Cost lands in the caller's existing expert-load accounting (t_ewait window in colibri.c);
// no separate counter for the add/remove side.
static void resset_add(id b) {
if (!g_resset_enabled) return;
@@ -318,7 +318,7 @@ static void resset_flush() {
}
// Harness visibility for the flush cost, which sits OUTSIDE the moe_times setup/gpu
// breakdown (timed around resset_flush in moe_submit, before ts_start). Returns whether
-// the set is active so glm.c prints the METAL-RESSET line only when the gate is on --
+// the set is active so colibri.c prints the METAL-RESSET line only when the gate is on --
// stock output stays byte-identical.
extern "C" int coli_metal_resset_stats(double *flush_s) {
if (flush_s) *flush_s = g_t_resset_flush;
From e135119f1951c03b2ce262060e9ac930de479ed8 Mon Sep 17 00:00:00 2001
From: Kyle Kelley
Date: Sun, 19 Jul 2026 09:41:50 -0700
Subject: [PATCH 31/71] Document current Metal warning baseline
Record that the explicit Objective-C++ warning build reports only the pre-existing unused TG constant inherited from current dev.
Co-authored-by: Christopher Brand
---
SUMMARY.md | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/SUMMARY.md b/SUMMARY.md
index 2947578..f4de311 100644
--- a/SUMMARY.md
+++ b/SUMMARY.md
@@ -235,7 +235,8 @@ existing disk/wait numbers. `[METAL] residency-set: on` / the two fallback stder
`cd c && make glm METAL=1` and a separate explicit `-Wall -Wextra` compile of
`backend_metal.mm` (the Makefile's `METALXX` line does not itself pass `-Wall -Wextra`, so
-"clean under `-Wall -Wextra`" was checked with those flags added explicitly), plus
+the warning surface was checked with those flags added explicitly; current `dev` contributes
+one pre-existing `unused variable 'TG'` warning), plus
`cd c && make glm` (plain, non-Metal — the one `colibri.c` instrumentation touch, the `METAL-RESSET` stats line,
is inside the pre-existing `#ifdef COLI_METAL` arm of `profile_print`, so the plain build
compiles none of it), and
From 113ece3bc792c556ae6126295ff7556bb73a6d06 Mon Sep 17 00:00:00 2001
From: ZacharyZcR
Date: Mon, 20 Jul 2026 02:53:43 +0800
Subject: [PATCH 32/71] =?UTF-8?q?cuda:=20COLI=5FCUDA=5FROUTER=3D1=20?=
=?UTF-8?q?=E2=80=94=20route=20the=20decode=20row=20on=20the=20layer's=20h?=
=?UTF-8?q?ome=20device=20(#431=20PR-A)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
First increment of the #431 plan (device router -> indirect kernels ->
one-graph decode). At decode (S=1) on the pipe2 path, the router runs on
the layer's home device: a tiny E x D logits GEMV + sigmoid, then a
single-thread selection kernel that clones moe()'s plain routing path
verbatim — bias-augmented top-K by choice with strict-> tie-breaking,
weights from the raw logit, route-level TOPP truncation, norm_topk,
routed_scale. Results pack into one scratch buffer and come back in a
single ~68-byte D2H; moe() consumes them through the same pre-routed
shortcut the Metal layer-CB uses (g_pre_idx, #417 bookkeeping included),
so usage/heat/recency accounting is identical to the CPU router.
Structural value: routing becomes available ON the device timeline,
which is what PR-B (indirect expert kernels, static topology) and PR-C
(whole-decode CUDA Graph) build on.
Opt-in, default off. Gated to the plain routing path — CACHE_ROUTE,
ROUTE_P and ROUTE_TRACE keep the CPU ranking they need; any upload or
launch failure falls back to the CPU router silently. Router weights
(E x D f32, ~6.3 MB/layer) upload lazily to the layer's home device.
tests/test_router_cuda.cu: kernel-vs-CPU-reference oracle over 200
random trials (mixed TOPP/norm_topk/scale): 200/200 exact selections,
zero near-tie flips, zero weight mismatches on a 5090.
---
c/backend_cuda.cu | 82 +++++++++++++++++++++++++++++++++++--
c/backend_cuda.h | 4 ++
c/backend_loader.c | 8 ++++
c/colibri.c | 50 +++++++++++++++++++---
c/tests/test_router_cuda.cu | 79 +++++++++++++++++++++++++++++++++++
5 files changed, 215 insertions(+), 8 deletions(-)
create mode 100644 c/tests/test_router_cuda.cu
diff --git a/c/backend_cuda.cu b/c/backend_cuda.cu
index 413e53b..a52aa99 100644
--- a/c/backend_cuda.cu
+++ b/c/backend_cuda.cu
@@ -34,7 +34,7 @@ typedef struct {
size_t qx_cap, qscale_cap;
float *host_x,*host_y,*host_kv; size_t host_x_cap,host_y_cap,host_kv_cap;
float *aq,*al,*ar,*ac; size_t aq_cap,al_cap,ar_cap,ac_cap;
- float *pipe_buf[24]; size_t pipe_cap[24]; /* scratch persistenti del resident pipeline */
+ float *pipe_buf[27]; size_t pipe_cap[27]; /* scratch persistenti del resident pipeline */
cudaStream_t stream;
void *group_desc; size_t group_desc_cap;
size_t tensor_count, tensor_bytes;
@@ -455,7 +455,7 @@ extern "C" void coli_cuda_shutdown(void) {
if (ctx->qx) cudaFree(ctx->qx);
if (ctx->qscale) cudaFree(ctx->qscale);
if(ctx->aq)cudaFree(ctx->aq);if(ctx->al)cudaFree(ctx->al);if(ctx->ar)cudaFree(ctx->ar);if(ctx->ac)cudaFree(ctx->ac);
- for(int b=0;b<24;b++) if(ctx->pipe_buf[b]) cudaFree(ctx->pipe_buf[b]);
+ for(int b=0;b<27;b++) if(ctx->pipe_buf[b]) cudaFree(ctx->pipe_buf[b]);
if (ctx->host_x) cudaFreeHost(ctx->host_x);
if (ctx->host_y) cudaFreeHost(ctx->host_y);
if (ctx->host_kv) cudaFreeHost(ctx->host_kv);
@@ -986,7 +986,7 @@ __global__ static void pipe_rows_add(float *x,const float *partial,const int *ro
* per layer (78 x ~10 alloc/richiesta erano puro churn). */
extern "C" float *coli_cuda_pipe_scratch(int device,int slot,size_t bytes){
DeviceContext *ctx=find_ctx(device);
- if(slot<0||slot>=24||!select_ctx(ctx)) return NULL;
+ if(slot<0||slot>=27||!select_ctx(ctx)) return NULL;
if(!reserve(&ctx->pipe_buf[slot],&ctx->pipe_cap[slot],bytes)) return NULL;
return ctx->pipe_buf[slot];
}
@@ -1038,6 +1038,82 @@ extern "C" int coli_cuda_pipe_rope_base(int device,float *v_dev,int pos_base,int
pipe_rope_rows<<>>(v_dev,NULL,pos_base,stride,offset,R,heads,theta);
return cuda_ok(cudaGetLastError(),"pipe rope base");
}
+/* ---- device router (#431 PR-A) -------------------------------------------
+ * Router for one decode row, entirely on the layer's home device: logits GEMV
+ * (E x D, tiny) + sigmoid, bias-augmented top-K selection, route-level TOPP
+ * truncation, norm_topk and routed_scale — a float-faithful clone of moe()'s
+ * plain routing path (colibri.c FASE A). Selection runs single-thread so the
+ * argmax order, tie-breaking (strict >, lowest index wins) and weight math
+ * match the CPU reference exactly; only the dot/expf rounding can differ,
+ * which is the documented kernel-family divergence class (#100/#163).
+ * Results are packed [idx[K] | w[K] | keff] in one scratch buffer and read
+ * back with a single tiny D2H. */
+__global__ void pipe_router_logits(const float *__restrict__ x,
+ const float *__restrict__ W,
+ const float *__restrict__ bias,
+ int D, float *logit, float *choice){
+ int e = blockIdx.x;
+ const float *w = W + (size_t)e*D;
+ float acc = 0.f;
+ for(int i=threadIdx.x; i>1; s>0; s>>=1){
+ if(threadIdx.xbv){bv=choice[e];best=e;} }
+ idx[kk]=best; w[kk]=logit[best];
+ }
+ int Ke=Ksel;
+ if(topp>0.f && topp<1.f){
+ for(int a=1;a=0 && w[b]=topp*tot){ Ke=kk+1; break; } }
+ }
+ if(norm_topk){ float sm=0.f; for(int kk=0;kk4096||Ksel<1||Ksel>64||!select_ctx(ctx)) return 0;
+ size_t pack=(size_t)Ksel*(sizeof(int)+sizeof(float))+sizeof(int);
+ float *logit=coli_cuda_pipe_scratch(device,22,(size_t)E*sizeof(float));
+ float *chc =coli_cuda_pipe_scratch(device,23,(size_t)E*sizeof(float));
+ char *out =(char*)coli_cuda_pipe_scratch(device,24,pack);
+ if(!logit||!chc||!out) return 0;
+ pipe_router_logits<<>>(x_dev,(const float*)rw_dev,(const float*)rb_dev,D,logit,chc);
+ pipe_router_select<<<1,1>>>(logit,chc,E,Ksel,topp,norm_topk,routed_scale,out);
+ if(!cuda_ok(cudaGetLastError(),"pipe router launch")) return 0;
+ char buf[64*(sizeof(int)+sizeof(float))+sizeof(int)];
+ if(!cuda_ok(cudaMemcpy(buf,out,pack,cudaMemcpyDeviceToHost),"pipe router readback")) return 0;
+ memcpy(idx_host,buf,(size_t)Ksel*sizeof(int));
+ memcpy(w_host,buf+Ksel*sizeof(int),(size_t)Ksel*sizeof(float));
+ memcpy(keff_host,buf+Ksel*(sizeof(int)+sizeof(float)),sizeof(int));
+ return 1;
+}
extern "C" int coli_cuda_pipe_copy2d(int device,float *dst,int dpitch,const float *src,
int spitch,int width,int height){
DeviceContext *ctx=find_ctx(device); if(!select_ctx(ctx)) return 0;
diff --git a/c/backend_cuda.h b/c/backend_cuda.h
index 63c1f11..eb959c2 100644
--- a/c/backend_cuda.h
+++ b/c/backend_cuda.h
@@ -126,6 +126,10 @@ COLI_CUDA_DLLEXPORT int coli_cuda_pipe_rmsnorm_s(int device,float *y_dev,const f
int xstride,int ystride);
COLI_CUDA_DLLEXPORT int coli_cuda_pipe_rope_base(int device,float *v_dev,int pos_base,int rows,
int stride,int offset,int R,int heads,float theta);
+COLI_CUDA_DLLEXPORT int coli_cuda_pipe_router(int device,const float *x_dev,
+ const void *rw_dev,const void *rb_dev,int D,int E,int Ksel,
+ float topp,int norm_topk,float routed_scale,
+ int *idx_host,float *w_host,int *keff_host);
COLI_CUDA_DLLEXPORT int coli_cuda_pipe_copy2d(int device,float *dst,int dpitch,const float *src,
int spitch,int width,int height);
COLI_CUDA_DLLEXPORT int coli_cuda_attention_project_batch_dev(ColiCudaTensor *kv_b,ColiCudaTensor *o_proj,
diff --git a/c/backend_loader.c b/c/backend_loader.c
index eedbd50..c87fbcd 100644
--- a/c/backend_loader.c
+++ b/c/backend_loader.c
@@ -76,6 +76,7 @@ typedef int (*fn_pipe_gemm)(ColiCudaTensor *t,float *y_dev,const float *x_dev,in
typedef int (*fn_pipe_peer_copy)(int dst_dev,float *dst,int src_dev, const float *src,size_t bytes);
typedef int (*fn_pipe_rmsnorm)(int device,float *y_dev,const float *x_dev, const float *w_dev,int S,int D,float eps);
typedef int (*fn_pipe_rmsnorm_s)(int device,float *y_dev,const float *x_dev, const float *w_dev,int S,int D,float eps, int xstride,int ystride);
+typedef int (*fn_pipe_router)(int device,const float *x_dev,const void *rw_dev,const void *rb_dev,int D,int E,int Ksel,float topp,int norm_topk,float routed_scale,int *idx_host,float *w_host,int *keff_host);
typedef int (*fn_pipe_rope)(int device,float *v_dev,const int *pos_dev,int rows, int stride,int offset,int R,int heads,float theta);
typedef int (*fn_pipe_rope_base)(int device,float *v_dev,int pos_base,int rows, int stride,int offset,int R,int heads,float theta);
typedef int (*fn_pipe_rows_add)(int device,float *x_dev,const float *partial_dev, const int *rows_dev,int nrows,int D);
@@ -123,6 +124,7 @@ static struct {
fn_pipe_peer_copy pipe_peer_copy;
fn_pipe_rmsnorm pipe_rmsnorm;
fn_pipe_rmsnorm_s pipe_rmsnorm_s;
+ fn_pipe_router pipe_router;
fn_pipe_rope pipe_rope;
fn_pipe_rope_base pipe_rope_base;
fn_pipe_rows_add pipe_rows_add;
@@ -217,6 +219,7 @@ static int coli_cuda_load(void){
RESOLVE(pipe_peer_copy, fn_pipe_peer_copy)
RESOLVE(pipe_rmsnorm, fn_pipe_rmsnorm)
RESOLVE(pipe_rmsnorm_s, fn_pipe_rmsnorm_s)
+ RESOLVE(pipe_router, fn_pipe_router)
RESOLVE(pipe_rope, fn_pipe_rope)
RESOLVE(pipe_rope_base, fn_pipe_rope_base)
RESOLVE(pipe_rows_add, fn_pipe_rows_add)
@@ -407,6 +410,11 @@ int coli_cuda_pipe_rmsnorm(int device,float *y_dev,const float *x_dev, const flo
return g_cuda.pipe_rmsnorm(device, y_dev, x_dev, w_dev, S, D, eps);
}
+int coli_cuda_pipe_router(int device,const float *x_dev,const void *rw_dev,const void *rb_dev,int D,int E,int Ksel,float topp,int norm_topk,float routed_scale,int *idx_host,float *w_host,int *keff_host){
+ if(!g_cuda.available || !g_cuda.pipe_router){ return 0; }
+ return g_cuda.pipe_router(device, x_dev, rw_dev, rb_dev, D, E, Ksel, topp, norm_topk, routed_scale, idx_host, w_host, keff_host);
+}
+
int coli_cuda_pipe_rmsnorm_s(int device,float *y_dev,const float *x_dev, const float *w_dev,int S,int D,float eps, int xstride,int ystride){
if(!g_cuda.available){ return 0; }
return g_cuda.pipe_rmsnorm_s(device, y_dev, x_dev, w_dev, S, D, eps, xstride, ystride);
diff --git a/c/colibri.c b/c/colibri.c
index 03eb878..1414296 100644
--- a/c/colibri.c
+++ b/c/colibri.c
@@ -69,10 +69,12 @@ static inline int omp_get_thread_num(void){ return 0; }
#include
static int g_metal_enabled;
static int g_metal_gemm_min=16; /* COLI_METAL_GEMM_MIN: min rows to send a matmul_qt GEMM to GPU */
-/* routing precalcolata dalla GPU (layer CB): moe() la usa e salta la FASE A */
-static const int *g_pre_idx; static const float *g_pre_w; static const int *g_pre_keff;
-static const float *g_pre_sh; /* output dello shared expert gia' calcolato su GPU */
+/* output dello shared expert gia' calcolato su GPU (solo Metal layer-CB) */
+static const float *g_pre_sh;
#endif
+/* routing precalcolata dalla GPU (Metal layer CB o device router CUDA, #431):
+ * moe() la usa e salta la FASE A. NULL = router su CPU. */
+static const int *g_pre_idx; static const float *g_pre_w; static const int *g_pre_keff;
#ifdef __APPLE__
#include /* host_statistics64: MemAvailable di macOS */
#endif
@@ -125,6 +127,10 @@ typedef struct {
QT gate_proj, up_proj, down_proj;
/* moe (sparse==1) */
float *router, *router_bias; /* router f32 (sensibile) */
+#ifdef COLI_CUDA
+ void *router_cuda, *router_bias_cuda; /* device router (#431 PR-A), lazy-uploaded */
+ int router_cuda_bad; /* upload failed once: stay on the CPU router */
+#endif
QT sh_gate, sh_up, sh_down; /* shared expert */
} Layer;
@@ -1601,6 +1607,7 @@ static void qt_matvec_rows(const QT *t, int r0, int n, const float *x, float *y)
static int g_absorb=-1;
#ifdef COLI_CUDA
static int g_cuda_pipe=0; /* COLI_CUDA_PIPE=1: prefill attention chain resident on the layer home device */
+static int g_cuda_router=0; /* COLI_CUDA_ROUTER=1 (#431 PR-A): router on the layer home device at decode */
#endif /* ABSORB: -1 auto (decode S<=4), 0 mai, 1 sempre (test) */
static int g_dsa_force=0; /* DSA_FORCE=1: selezione sempre attiva (test: top-min(k,T)=denso) */
static int cmp_fdesc(const void *a,const void *b){
@@ -2163,7 +2170,8 @@ static void moe(Model *m, Layer *l, int layer, float *x, int S, float *out, int
/* router in UN matmul batch: stessa matematica, via le S chiamate S=1 */
float *logits_all=falloc((int64_t)S*E);
int pre_routed=0; (void)pre_routed;
-#ifdef COLI_METAL
+ /* pre-routed shortcut: Metal layer-CB o device router CUDA (#431) — stessa
+ * contabilita' (#417: recency clock incluso), la selezione arriva dalla GPU */
if(g_pre_idx){ /* routing gia' calcolata dal layer CB (GPU) */
memcpy(idxs,g_pre_idx,(size_t)S*K*sizeof(int));
memcpy(ws,g_pre_w,(size_t)S*K*sizeof(float));
@@ -2184,7 +2192,6 @@ static void moe(Model *m, Layer *l, int layer, float *x, int S, float *out, int
}
pre_routed=1;
}
-#endif
if(!pre_routed) matmul(logits_all, x, l->router, S, D, E);
if(!pre_routed)
for(int s=0;seps)) return 0;
+ /* device router (#431 PR-A): route THIS row on the home device while the
+ * stream is still hot, then hand the selection to moe() through the same
+ * pre-routed shortcut the Metal layer-CB uses. Any failure (upload, launch,
+ * feature gate) falls back to the CPU router inside moe() — byte-identical
+ * behaviour, just slower. Gated to the plain routing path: CACHE_ROUTE /
+ * ROUTE_P / ROUTE_TRACE keep the CPU ranking they need. */
+ static int lr_idx[64]; static float lr_w[64]; static int lr_keff[1];
+ int dev_routed=0;
+ if(g_cuda_router && S==1 && !g_cache_route && g_route_p<=0.f && !g_route_fp
+ && c->n_experts<=4096 && c->topk<=64 && !l->router_cuda_bad){
+ int E=c->n_experts, K=c->topk;
+ int Ksel = g_topk>0 ? (g_topk0 && g_topp<1.f) ? g_topp : 0.f;
+ if(!l->router_cuda){
+ void *rw=coli_cuda_pipe_alloc(dev,(size_t)E*D*4);
+ void *rb=coli_cuda_pipe_alloc(dev,(size_t)E*4);
+ if(rw&&rb&&coli_cuda_pipe_upload(dev,rw,l->router,(size_t)E*D*4)
+ &&coli_cuda_pipe_upload(dev,rb,l->router_bias,(size_t)E*4)){
+ l->router_cuda=rw; l->router_bias_cuda=rb;
+ } else {
+ if(rw)coli_cuda_pipe_free(dev,rw); if(rb)coli_cuda_pipe_free(dev,rb);
+ l->router_cuda_bad=1;
+ }
+ }
+ if(l->router_cuda &&
+ coli_cuda_pipe_router(dev,nrm_d,l->router_cuda,l->router_bias_cuda,
+ D,E,Ksel,tp,c->norm_topk,c->routed_scale,
+ lr_idx,lr_w,lr_keff))
+ dev_routed=1;
+ }
if(!coli_cuda_pipe_download(dev,nrm_d,nrm_host,xb)) return 0;
m->t_attn+=now_s()-ta;
/* OVERLAP: issue the shared expert on the GPU BEFORE moe() runs on the CPU.
@@ -3118,7 +3155,9 @@ static int pipe_layer_sparse(Model *m, Layer *l, int li, float *x_dev, int S, in
if(!coli_cuda_pipe_add(dev,x_dev,y_d,(size_t)S*D)) return 0; /* shared residual (async) */
m->t_emm += now_s()-te; /* shared-expert GPU dispatch only */
/* expert routed su CPU/gruppi GPU come oggi (shared saltata: la fa il device) */
+ if(dev_routed){ g_pre_idx=lr_idx; g_pre_w=lr_w; g_pre_keff=lr_keff; }
moe(m,l,li,nrm_host,S,out_host,0); /* self-times its own t_emm */
+ if(dev_routed){ g_pre_idx=NULL; g_pre_w=NULL; g_pre_keff=NULL; }
te=now_s();
if(!coli_cuda_pipe_upload(dev,y_d,out_host,xb)) return 0; /* sync: waits for moe */
if(!coli_cuda_pipe_add(dev,x_dev,y_d,(size_t)S*D)) return 0; /* routed residual (async) */
@@ -5245,6 +5284,7 @@ int main(int argc, char **argv){
}
g_cuda_dense=getenv("CUDA_DENSE")?atoi(getenv("CUDA_DENSE")):0;
g_cuda_pipe=getenv("COLI_CUDA_PIPE")?atoi(getenv("COLI_CUDA_PIPE")):0;
+ g_cuda_router=getenv("COLI_CUDA_ROUTER")?atoi(getenv("COLI_CUDA_ROUTER")):0;
const char *cuda_expert=getenv("CUDA_EXPERT_GB");
g_cuda_expert_auto=cuda_expert&&!strcmp(cuda_expert,"auto");
g_cuda_expert_gb=cuda_expert&&!g_cuda_expert_auto?atof(cuda_expert):0;
diff --git a/c/tests/test_router_cuda.cu b/c/tests/test_router_cuda.cu
new file mode 100644
index 0000000..4e714a6
--- /dev/null
+++ b/c/tests/test_router_cuda.cu
@@ -0,0 +1,79 @@
+/* Device-router kernel oracle (#431 PR-A).
+ *
+ * Feeds random activations/router weights through pipe_router_logits +
+ * pipe_router_select and checks against a CPU reference that replicates
+ * moe()'s plain routing path verbatim (sigmoid -> bias-augmented top-K by
+ * `choice`, weights from raw `logit`, route-level TOPP truncation, norm_topk,
+ * routed_scale). The dot/expf rounding may differ from libm at ~1e-6 rel, so
+ * a handful of near-tie index flips across trials is tolerated; the weight
+ * math itself must agree to 1e-4 rel on matching selections.
+ *
+ * Build: nvcc -O2 -std=c++17 -arch=native tests/test_router_cuda.cu -o tests/test_router_cuda
+ */
+#include
+#include
+#include
+#include
+#include
+
+/* pull in the kernel definitions (same idiom as the CPU tests' #include "../colibri.c") */
+#include "../backend_cuda.cu"
+
+static void cpu_ref(const float *x,const float *W,const float *bias,int D,int E,
+ int Ksel,float topp,int norm_topk,float rscale,
+ int *idx,float *w,int *keff){
+ float *logit=(float*)malloc(E*sizeof(float)),*choice=(float*)malloc(E*sizeof(float));
+ for(int e=0;ebv){bv=choice[e];best=e;} }
+ idx[kk]=best; w[kk]=logit[best]; }
+ int Ke=Ksel;
+ if(topp>0.f && topp<1.f){
+ for(int a=1;a=0 && w[b]=topp*tot){ Ke=kk+1; break; } } }
+ if(norm_topk){ float sm=0; for(int kk=0;kk>>(x,W,b,D,lg,ch);
+ pipe_router_select<<<1,1>>>(lg,ch,E,K,topp,nt,rs,out);
+ char pack[K*8+4];
+ if(cudaMemcpy(pack,out,sizeof(pack),cudaMemcpyDeviceToHost)!=cudaSuccess){
+ printf("FAIL cuda\n"); return 1; }
+ int gidx[K],gkeff; float gw[K];
+ memcpy(gidx,pack,K*4); memcpy(gw,pack+K*4,K*4); memcpy(&gkeff,pack+K*8,4);
+ int ridx[K],rkeff; float rw[K];
+ cpu_ref(x,W,b,D,E,K,topp,nt,rs,ridx,rw,&rkeff);
+ int mism=0; for(int k2=0;k21e-4f*(fabsf(ref)+1e-6f)+1e-6f){ bad++; break; }
+ }
+ }
+ printf("router oracle: %d trials, %d near-tie flips, %d weight mismatches\n",TRIALS,flips,bad);
+ if(flips>4||bad){ printf("FAIL\n"); return 1; }
+ printf("OK\n"); return 0;
+}
From 34f6e500914aa7d096d8a1c894c6c4aee442ec13 Mon Sep 17 00:00:00 2001
From: ZacharyZcR
Date: Mon, 20 Jul 2026 03:35:46 +0800
Subject: [PATCH 33/71] cuda: decode expert groups take the grouped kernels
even under TC_W4A16 (#431)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The TC_W4A16 branch of coli_cuda_expert_group handled every expert in a
per-expert loop: rows >= threshold got the Tensor Core path, everything
below fell back to 4 naive launches per expert. At decode every expert
has 1 row, so the whole group rode the fallback — ~981 quant_matmul
micro-launches per token (#431's measured flood) — while the grouped
3-launch path (grouped_hidden_w4_dual + silu + grouped_down_w4) sat one
else-if below, unreachable whenever the PREFILL tuning flag was set.
Gate the branch on 'at least one expert reaches the TC row threshold':
all-small groups (decode) now fall through to the grouped kernels.
Measured on 6x RTX 5090 (full residency, 39 forwards under nsys):
expert-side quant_matmul instances drop 981 -> 337 per forward, total
launches ~1,490 -> ~850 per token (-43%). Wall-clock is parity at the
A/B operating point — the win is structural (PR-C graph node count,
launch-tax share at champion speed).
Behavioural fix folded in: before this change, toggling TC_W4A16 — a
prefill-only optimization — changed DECODE output text (kernel-family
divergence, #100 class). After it, decode always uses the grouped
family: TC_W4A16=1 and =0 now produce byte-identical decode text
(verified, 96-token greedy A/B), and the flag affects only the prefill
it was built for.
---
c/backend_cuda.cu | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/c/backend_cuda.cu b/c/backend_cuda.cu
index 413e53b..236992b 100644
--- a/c/backend_cuda.cu
+++ b/c/backend_cuda.cu
@@ -689,7 +689,15 @@ extern "C" int coli_cuda_expert_group(ColiCudaTensor *const *gates,
quantize_s4_rows<<stream>>>(ctx->qx,ctx->qscale,ctx->gate,total,I);
grouped_s4_wmma<<stream>>>(ctx->y,ctx->qx,ctx->qscale,dev,I,D,2);
}else if(all_s4&&ctx->compute_major>=7&&getenv("COLI_CUDA_TC_W4A16")&&
- atoi(getenv("COLI_CUDA_TC_W4A16"))){
+ atoi(getenv("COLI_CUDA_TC_W4A16"))&&
+ [&]{ int tc16_min=getenv("COLI_CUDA_TC_W4A16_MIN")?atoi(getenv("COLI_CUDA_TC_W4A16_MIN")):16;
+ for(int c=0;c=tc16_min) return 1;
+ return 0; }()){
+ /* At least one expert has enough rows for a Tensor Core tile. Groups
+ * where EVERY expert is below the threshold (decode: r=1) fall through
+ * to the grouped-W4 path below — 3 launches for the whole group instead
+ * of 4 per expert (#431: the launch flood measured at ~981 micro-kernels
+ * per token came from decode riding this branch's per-expert fallback). */
/* W4A16 Tensor Core per gruppo: attivazioni fp16 per tile (lossless al
* contrario del path W4A4), un lancio per expert dentro lo stream —
* l'overhead di lancio e' trascurabile rispetto ai GEMM. */
From 70fb5b00f388685a015d5e94a3c75916f40cf3a3 Mon Sep 17 00:00:00 2001
From: Colibri Developer
Date: Sun, 19 Jul 2026 13:38:57 -0600
Subject: [PATCH 34/71] fix(api): pass tools and tool_choice as parameters to
generation() to prevent duplicate extraction
The generation() method was re-extracting tools from the raw request body,
bypassing the filtering that render_chat() applies when tool_choice is a
function object (forced function call). This caused parse_tool_calls() to
receive the unfiltered tool list, producing incorrect or missing tool_calls
in the response.
- Add tools and tool_choice parameters to generation() signature
- Pass them from chat_completion() after render_chat() processing
- Add early structural validation of tools in generation_options()
for clear HTTP 400 errors on malformed input
---
c/openai_server.py | 32 +++++++++++++++++++++++++++-----
1 file changed, 27 insertions(+), 5 deletions(-)
diff --git a/c/openai_server.py b/c/openai_server.py
index ba19e43..d8703a8 100644
--- a/c/openai_server.py
+++ b/c/openai_server.py
@@ -365,6 +365,27 @@ def generation_options(body, limit):
if body.get("n", 1) != 1:
raise APIError(400, "Colibri currently supports `n=1` only.", "n", "unsupported_value")
# `tools`/`functions` are handled by render_chat (declaration) + parse_tool_calls (output).
+ # Validate tools/functions structure early so malformed input fails with a clear error.
+ tools_raw = body.get("tools") or body.get("functions")
+ if tools_raw is not None:
+ if not isinstance(tools_raw, list):
+ raise APIError(400, "`tools` must be a non-empty array.", "tools", "invalid_value")
+ if not tools_raw:
+ raise APIError(400, "`tools` must be a non-empty array.", "tools", "invalid_value")
+ for idx, tool in enumerate(tools_raw):
+ if not isinstance(tool, dict):
+ raise APIError(400, f"Each tool must be an object, got {type(tool).__name__} at index {idx}.",
+ f"tools.{idx}", "invalid_value")
+ fn = tool.get("function", tool) if isinstance(tool, dict) else {}
+ if not isinstance(fn, dict):
+ raise APIError(400, f"Tool function must be an object at index {idx}.",
+ f"tools.{idx}.function", "invalid_value")
+ if not fn.get("name"):
+ raise APIError(400, f"Each tool must have a `name` at index {idx}.",
+ f"tools.{idx}.function.name", "invalid_value")
+ if not isinstance(fn["name"], str):
+ raise APIError(400, f"Tool `name` must be a string at index {idx}.",
+ f"tools.{idx}.function.name", "invalid_value")
choice = body.get("tool_choice")
if choice is not None:
if isinstance(choice, str):
@@ -829,7 +850,7 @@ class APIHandler(BaseHTTPRequestHandler):
except OSError:
pass
- def generation(self, body, prompt, request_id, chat):
+ def generation(self, body, prompt, request_id, chat, tools=None, tool_choice=None):
# COLI_DEBUG tees the engine transaction to stderr: 1 = decoded output stream only,
# 2 = both sides (rendered prompt + output). render_chat already folds prior turns and
# tool results into `prompt`, so level 2 is the full conversation the engine saw.
@@ -841,8 +862,8 @@ class APIHandler(BaseHTTPRequestHandler):
sys.stderr.write(f"\n===== PROMPT [{request_id}] =====\n{prompt}\n===== OUTPUT [{request_id}] =====\n")
sys.stderr.flush()
maximum, temperature, top_p = generation_options(body, self.server.max_tokens)
- tools = (body.get("tools") or body.get("functions") or None) if chat else None
- if body.get("tool_choice") == "none":
+ # tools and tool_choice come from chat_completion() already processed/filtered
+ if chat and tool_choice == "none":
tools = None # client forbade tools: never surface tool_calls
cache_slot = body.get("cache_slot")
if (cache_slot is not None and
@@ -1044,9 +1065,10 @@ class APIHandler(BaseHTTPRequestHandler):
if not isinstance(enable_thinking, bool):
raise APIError(400, "`enable_thinking` must be a boolean.", "enable_thinking")
tools = body.get("tools") or body.get("functions") or None
+ tool_choice = body.get("tool_choice")
prompt = render_chat(body.get("messages"), enable_thinking, reasoning_effort, tools,
- body.get("tool_choice"))
- self.generation(body, prompt, request_id, True)
+ tool_choice)
+ self.generation(body, prompt, request_id, True, tools, tool_choice)
def completion(self, body, request_id):
prompt = body.get("prompt")
From 26bd8b403a8db04e684e4006523885385a2305a2 Mon Sep 17 00:00:00 2001
From: Colibri Developer
Date: Sun, 19 Jul 2026 13:39:11 -0600
Subject: [PATCH 35/71] fix(engine): filter non-EOS stop tokens in serve mode
to prevent premature halt on tool calls
GLM-5.2's config defines three stop tokens: <|endoftext|>, <|user|>, and
<|observation|>. In serve mode, when the model generates blocks,
int4-quantized logit noise can cause argmax to pick a <|user|> or
<|observation|> token ID, immediately stopping generation.
The <|user|> and <|observation|> tokens are role markers handled by the
Python API server, not the C engine. Filter them out in stops_arm() when
SERVE=1, keeping only <|endoftext|> as the stop token.
- Add SERVE-mode guard in stops_arm() that retains only the EOS token
- Log the number of filtered tokens for diagnostics
---
c/glm.c | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/c/glm.c b/c/glm.c
index 00c7076..3237c0a 100644
--- a/c/glm.c
+++ b/c/glm.c
@@ -4115,6 +4115,19 @@ static void stops_arm(const Cfg *c, int tok_eos){
g_nstop=0;
for(int i=0;in_stop;i++) g_stop[g_nstop++]=c->stop_ids[i];
if(tok_eos>=0 && !is_stop(tok_eos)) g_stop[g_nstop++]=tok_eos;
+ /* In serve mode (API), keep only <|endoftext|> as a stop token. The tokens
+ * <|user|> and <|observation|> are role markers that the Python server
+ * handles — keeping them as stop tokens causes the engine to halt
+ * prematurely when the model tries to emit blocks, because
+ * int4-quantized logits can be noisy enough that argmax picks a stop-token
+ * ID instead of the correct tool-call token. (#401) */
+ if(getenv("SERVE")){
+ int kept=0;
+ for(int i=0;i
Date: Sun, 19 Jul 2026 15:42:57 -0400
Subject: [PATCH 36/71] =?UTF-8?q?PROF:=20DISK-CLASS=20=E2=80=94=20per-load?=
=?UTF-8?q?=20cold/warm=20disk=20instrumentation=20(re-derived=20onto=20#3?=
=?UTF-8?q?91=20split)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Re-derivation of e7/disk-class-instr @ de6dd6d (base caa49f7) onto
origin/dev @ 61004dc, after PR #391 split c/glm.c into c/colibri.c plus
quant.h/sample.h/kv_persist.h/telemetry.h/grammar.h. Semantic equivalence,
not a copy: same events, same accounting, re-sited onto the new tree.
Placement: colibri.c, unchanged from before the split. telemetry.h (#391)
is the dashboard/stats module (HWINFO/TIERS/EMAP/HITS protocol lines +
usage persistence) — prof_report(), expert_load_impl(), moe(), g_prof_io
and g_edisk_ns all stayed in colibri.c, so DISK-CLASS's aggregation and
printing follow them there. No relocation needed, no DEVIATION.
Read path: #362 (prefetcher-v3, 22509fc) turned out to be testbed-only
scope per its own merge message ("glm.c untouched") — confirmed zero
c/glm.c changes in that merge's diff. The seven expert_load() call sites
this patch touches (pipe_worker, expert_host_ensure, moe()'s OMP miss
loop, pilot_realload, repin_pass_limit, pin_load x2) are structurally
identical to the pre-split tree; the demand flag re-attaches at the same
sites with the same semantics (1 only at moe()'s own PIPE/OMP miss path,
0 everywhere else), so DISK-CLASS still counts demand loads only.
One real drift, unrelated to #362: #417 (cfcc742) fixed the exact "Metal
pre-routed FASE A never bumps the real elast/eaccess_clock" defect this
feature's comments described as a documented, deliberately-unfixed
upstream issue — the real clock now ticks in FASE A too. The private
elast_dc/eaccess_clock_dc clock is kept anyway: its job was never only
to route around that freeze, it also snapshots pre-bump state so a
call's own routing bump can't contaminate its own classification, and
keeping DISK-CLASS's bookkeeping fully separate from stock elast state
is what makes "byte-identical with PROF=0" provable by construction
instead of by argument. Code comments referencing the old defect are
updated to reflect the fix (historical note + #417/cfcc742 pointer)
rather than describing a bug that no longer exists.
Gates: make glm METAL=0 and METAL=1 both clean, zero warnings (matches
stock 61004dc, also built clean with zero warnings for comparison).
make test-c: 0 failures. make test-python: 77 tests, OK.
Authored by Fable 5 in Claude Code, analysis in partnership with
@Monotophic.
Co-Authored-By: Claude Fable 5
---
c/colibri.c | 259 +++++++++++++++++++++++++++++++++++++++++++++++++---
1 file changed, 244 insertions(+), 15 deletions(-)
diff --git a/c/colibri.c b/c/colibri.c
index 03eb878..2b618dd 100644
--- a/c/colibri.c
+++ b/c/colibri.c
@@ -168,6 +168,28 @@ typedef struct {
uint32_t **eusage; /* contatori persistenti (per STATS/PIN) */
uint32_t **eheat; /* calore recente per promotion/demotion live */
uint32_t **elast, eaccess_clock; /* recency per LFRU session-local */
+ /* DISK-CLASS: PRIVATE recency state, read only by expert_classify(). Private --
+ * not the real elast/eaccess_clock -- kept fully separate so DISK-CLASS's bookkeeping
+ * can never read from or write into stock eviction state: every DISK-CLASS write lives
+ * inside its own need_classify/dc_on gate, so "byte-identical with PROF=0" is provable
+ * by construction instead of by argument. (Historical note: when this was first written,
+ * the Metal pre-routed FASE A path (g_pre_idx) never bumped the real elast/eaccess_clock
+ * -- on Metal decode the real clock froze at end of prefill, so REPIN's LRU tie-breaker
+ * ran on stale recency for the rest of the run. That was an upstream defect; it has since
+ * been reported and fixed (#417, cfcc742) -- FASE A now bumps the real clock too. The
+ * private clock is retained anyway: separation from stock state is the stronger property,
+ * independent of whether the real clock is correct.) elast_dc/eaccess_clock_dc tick in
+ * BOTH FASE A paths, under the same need_classify gate, at the same rate the real clock
+ * ticks on the CPU path (one per selected (position,expert)) -- so the
+ * COLI_DISKCLASS_WINDOW window keeps its meaning in every mode. elast_pre snapshots
+ * elast_dc just BEFORE this call's own bump (see the touched[] guard in FASE A) --
+ * classifying against the live array would read the bump routing just made a few lines
+ * above the load that needed it, so a giant cold prefill burst would score every expert
+ * "just accessed" and get called warm. Recency alone (not eheat's access COUNT): a count
+ * never decays, so an expert hot early in a long session would keep reading "warm" long
+ * after it dropped out of the working set. Same shape/allocation as elast; NULL for dense
+ * layers. */
+ uint32_t **elast_dc, **elast_pre, eaccess_clock_dc;
/* DSA lightning indexer (attivo solo se i pesi out-idx-* sono presenti) */
int has_dsa;
QT *ix_wq, *ix_wk, *ix_wp; /* per layer FULL: wq_b, wk, weights_proj */
@@ -298,6 +320,65 @@ static _Atomic int64_t g_prof_io; /* bytes pread()/faulted from e
* wait ~ service means the loads block the compute thread. */
static _Atomic int64_t g_edisk_ns;
static double edisk_s(void){ return atomic_load_explicit(&g_edisk_ns,memory_order_relaxed)*1e-9; }
+/* DISK-CLASS (PROF=1): per-load cold/warm classification against the engine's own
+ * recency state. Instrumentation only -- it never changes which fd serves a read (see
+ * expert_classify() and its call site in expert_load_impl; the fd choice expression is
+ * untouched by this feature). COLI_DISKCLASS_WINDOW is the recency window in ticks of the
+ * PRIVATE clock (m->eaccess_clock_dc -- NOT the real eaccess_clock; see elast_dc in Model
+ * for why DISK-CLASS keeps its own clock instead of reading the real one): one tick per
+ * selected (position,expert) in FASE A while classification is active, the same per-token
+ * rate the real clock has on the CPU path, so the window's meaning is unchanged. At or
+ * under the window = warm; 0 (default, unset) derives it from topk*n_layers*8 once the
+ * model config is known (main(), right after model_init) -- roughly "seen in the last ~8
+ * tokens", generous on purpose (conservative-toward-warm: a load the page cache could have
+ * served that gets labeled cold overstates the cold class, the bucket this line exists to
+ * size). */
+static uint32_t g_direct_heat_ticks=0;
+static int g_direct_heat_explicit=0; /* 1 if COLI_DISKCLASS_WINDOW was set (skip the auto-derive) */
+#define DC_COLD 0
+#define DC_WARM 1
+static _Atomic uint64_t g_dc_n[2]; /* [DC_COLD]/[DC_WARM]: loads classified */
+static _Atomic int64_t g_dc_bytes[2]; /* bytes read (weights + scales, matches g_prof_io) */
+static _Atomic int64_t g_dc_ns[2]; /* wall ns spent reading (thread-seconds, like g_edisk_ns) */
+static _Atomic uint64_t g_dc_direct_n[2]; /* subset of the above ACTUALLY served by the uncached fd */
+/* Busy-wall per class + combined: how much WALL time had >=1 classified load of the
+ * class in flight (thread-seconds / busy-wall = average concurrency; bytes / busy-wall
+ * = aggregate GB/s the disk actually delivered for that class -- the quantity the
+ * thread-second numbers alone can't answer: N slow overlapped reads can beat N fast
+ * serial ones in aggregate, and only wall-denominated rates see it). Transition scheme:
+ * 0->1 records a start, 1->0 accumulates (now - start). One dedicated mutex serializes
+ * the transition bookkeeping -- two short lock/unlock pairs per load against ms-scale
+ * reads; a CAS scheme would save nothing measurable and be harder to audit
+ * (correctness over cleverness). Only COMPLETED intervals are in the accumulators: an
+ * interval still open at report time is not counted (bounded by one read's duration --
+ * noise at report granularity). */
+static pthread_mutex_t g_dc_wall_mx=PTHREAD_MUTEX_INITIALIZER;
+static int g_dc_inflight[2], g_dc_inflight_all; /* guarded by g_dc_wall_mx */
+static double g_dc_wall_open[2], g_dc_wall_open_all; /* start of the open interval */
+static int64_t g_dc_wall_ns[2], g_dc_wall_all_ns; /* completed busy-wall ns */
+static void dc_wall_enter(int cls, double now){
+ pthread_mutex_lock(&g_dc_wall_mx);
+ if(g_dc_inflight[cls]++==0) g_dc_wall_open[cls]=now;
+ if(g_dc_inflight_all++==0) g_dc_wall_open_all=now;
+ pthread_mutex_unlock(&g_dc_wall_mx);
+}
+static void dc_wall_exit(int cls, double now){
+ pthread_mutex_lock(&g_dc_wall_mx);
+ if(--g_dc_inflight[cls]==0) g_dc_wall_ns[cls]+=(int64_t)((now-g_dc_wall_open[cls])*1e9);
+ if(--g_dc_inflight_all==0) g_dc_wall_all_ns +=(int64_t)((now-g_dc_wall_open_all)*1e9);
+ pthread_mutex_unlock(&g_dc_wall_mx);
+}
+static int dc_needed(void); /* fwd: defined with the classifier (needs g_prof) */
+static void dc_wall_read(int64_t out[2], int64_t *all){ /* mutex-consistent snapshot for the report */
+ if(!dc_needed()){ out[0]=out[1]=0; *all=0; return; } /* off for the whole process => accumulators are
+ * provably zero (only dc_wall_exit writes them,
+ * only under dc_on): skip the lock, zero work.
+ * Needed because prof_base runs unconditionally
+ * at some call sites ("cheap enough to always"). */
+ pthread_mutex_lock(&g_dc_wall_mx);
+ out[0]=g_dc_wall_ns[0]; out[1]=g_dc_wall_ns[1]; *all=g_dc_wall_all_ns;
+ pthread_mutex_unlock(&g_dc_wall_mx);
+}
#define PROF_LAT_CAP 32768
static double g_prof_lat[PROF_LAT_CAP]; /* per-forward decode wall clock (ring) */
static uint64_t g_prof_nlat; /* forwards recorded (monotonic) */
@@ -307,6 +388,8 @@ typedef struct {
double edisk,ewait,emm,ecpu,egpu,route,p2p,attn,head;
int64_t io,cpu_bytes; uint64_t hits,miss,ereq,n_fw,n_emit,nlat,n_p2p,cpu_rows;
uint64_t hit_pin,hit_ecache;
+ uint64_t dc_n[2], dc_direct_n[2]; int64_t dc_bytes[2], dc_ns[2]; /* DISK-CLASS */
+ int64_t dc_wall_ns[2], dc_wall_all_ns; /* busy-wall (per class + combined) */
} ProfBase;
static void prof_base(Model *m, ProfBase *b){
b->edisk=edisk_s(); b->ewait=m->t_ewait; b->emm=m->t_emm;
@@ -317,6 +400,13 @@ static void prof_base(Model *m, ProfBase *b){
b->hit_pin=m->hit_pin; b->hit_ecache=m->hit_ecache;
b->n_fw=m->n_fw; b->n_emit=m->n_emit; b->nlat=g_prof_nlat; b->n_p2p=m->n_p2p;
b->cpu_bytes=m->cpu_expert_bytes;b->cpu_rows=m->cpu_expert_rows;
+ for(int i=0;i<2;i++){
+ b->dc_n[i]=atomic_load_explicit(&g_dc_n[i],memory_order_relaxed);
+ b->dc_bytes[i]=atomic_load_explicit(&g_dc_bytes[i],memory_order_relaxed);
+ b->dc_ns[i]=atomic_load_explicit(&g_dc_ns[i],memory_order_relaxed);
+ b->dc_direct_n[i]=atomic_load_explicit(&g_dc_direct_n[i],memory_order_relaxed);
+ }
+ dc_wall_read(b->dc_wall_ns,&b->dc_wall_all_ns);
}
static float *falloc(int64_t n){
@@ -804,6 +894,7 @@ static void model_init(Model *m, const char *snap, int cap, int ebits, int dbits
m->pin=calloc(NR,sizeof(ESlot*)); m->npin=calloc(NR,sizeof(int));
m->eusage=calloc(NR,sizeof(uint32_t*)); m->eheat=calloc(NR,sizeof(uint32_t*));
m->elast=calloc(NR,sizeof(uint32_t*));
+ m->elast_dc=calloc(NR,sizeof(uint32_t*)); m->elast_pre=calloc(NR,sizeof(uint32_t*));
m->kv=calloc(1,sizeof(KVState));
m->kv_start=m->kv->kv_start=calloc(NR,sizeof(int));
for(int i=0;in_layers;i++){
@@ -848,6 +939,8 @@ static void model_init(Model *m, const char *snap, int cap, int ebits, int dbits
m->eusage[i]=calloc(c->n_experts,sizeof(uint32_t));
m->eheat[i]=calloc(c->n_experts,sizeof(uint32_t));
m->elast[i]=calloc(c->n_experts,sizeof(uint32_t));
+ m->elast_dc[i]=calloc(c->n_experts,sizeof(uint32_t));
+ m->elast_pre[i]=calloc(c->n_experts,sizeof(uint32_t));
}
#undef P
}
@@ -894,6 +987,8 @@ static void model_init(Model *m, const char *snap, int cap, int ebits, int dbits
m->eusage[i]=calloc(c->n_experts,sizeof(uint32_t));
m->eheat[i]=calloc(c->n_experts,sizeof(uint32_t));
m->elast[i]=calloc(c->n_experts,sizeof(uint32_t));
+ m->elast_dc[i]=calloc(c->n_experts,sizeof(uint32_t));
+ m->elast_pre[i]=calloc(c->n_experts,sizeof(uint32_t));
m->kv_start[i]=-1; /* KV MTP: parte dalla prima posizione di decode */
#undef PM
}
@@ -1028,7 +1123,35 @@ static int pread_full(int fd, void *buf, int64_t n, int64_t off, const char *tag
}
return 0;
}
-static int expert_load_impl(Model *m, int layer, int eid, ESlot *s, int fatal){
+/* DISK-CLASS: is classification active right now? Single point of truth shared by
+ * moe()'s pre-bump snapshot (FASE A, below) and expert_load_impl's classify-and-read --
+ * they must agree, or expert_load_impl reads a snapshot moe() never bothered to write.
+ * PROF=1 is the only trigger: this feature is measurement-only, the verdict never picks
+ * an fd (kept as one function anyway so a future policy consumer cannot drift out of
+ * sync with the snapshot writer by construction). */
+static int dc_needed(void){ return g_prof; }
+/* DISK-CLASS: cold/warm verdict for ONE demand load, read from the pre-bump snapshot
+ * (Model's elast_pre) so routing's OWN bump for THIS call can't contaminate the read --
+ * prefill is one giant moe() call where every newly-seen expert gets its `last` bumped
+ * a few lines above the load that made it "new"; classifying off the live, post-bump
+ * array would call the whole cold burst warm. Ages against the PRIVATE clock
+ * (eaccess_clock_dc, see its declaration in Model), NEVER the real eaccess_clock: kept
+ * separate by design (see elast_dc in Model for why DISK-CLASS keeps its own clock
+ * instead of reading the real one -- before #417/cfcc742 the real one also froze on the
+ * Metal pre-routed decode path, which would have made every prefill-touched expert warm
+ * forever and everything else cold forever; that's fixed upstream now, but DISK-CLASS
+ * still doesn't read the real clock, for the isolation property, not the freeze).
+ * Conservative-toward-warm on the one genuinely ambiguous input (missing snapshot --
+ * defensive only, elast_pre is allocated everywhere elast is); a demonstrable first-ever
+ * access (last_pre==0) is not a judgment call, it stays cold regardless of that bias. */
+static int expert_classify(Model *m, int layer, int eid){
+ if(!m->elast_pre || !m->elast_pre[layer]) return DC_WARM; /* no snapshot: label as the safe class */
+ uint32_t last_pre=m->elast_pre[layer][eid];
+ if(last_pre==0) return DC_COLD; /* never touched before this call: certain cold */
+ uint32_t age=m->eaccess_clock_dc-last_pre; /* ticks since last access, PRE this call's bump */
+ return age>g_direct_heat_ticks ? DC_COLD : DC_WARM; /* '>' not '>=': ties lean warm */
+}
+static int expert_load_impl(Model *m, int layer, int eid, ESlot *s, int fatal, int demand){
#ifdef COLI_CUDA
/* A live REPIN may reuse a GPU-enabled pinned slot for a different expert.
* Keep its tier assignment, but invalidate the old device weights. */
@@ -1155,12 +1278,22 @@ static int expert_load_impl(Model *m, int layer, int eid, ESlot *s, int fatal){
numa_slab_bind(s->fslab,(size_t)ftot*sizeof(float));
#endif
}
+ /* DISK-CLASS: classify before the reads; computed unconditionally at dc_on sites so
+ * the timer (dc_t0) brackets exactly the read work, matching what the GB/s in the
+ * DISK-CLASS line describes. dc_on gates ALL of it off demand=0 call sites
+ * (pilot/repin/pin -- never classified, see the call sites) and off PROF=0 runs
+ * (dc_needed()) -- zero cost, zero behavior change there. The fd choice below is
+ * NOT influenced by the verdict: this is measurement only. */
+ int dc_on = demand && dc_needed();
+ int dc_cls = dc_on ? expert_classify(m,layer,eid) : DC_WARM;
+ double dc_t0 = dc_on ? now_s() : 0;
+ if(dc_on) dc_wall_enter(dc_cls,dc_t0); /* busy-wall open; EVERY exit path below must pair it */
int ord[3]={0,1,2}; /* ordina per offset nel file */
for(int a=0;a<3;a++) for(int bb=a+1;bb<3;bb++) if(tw[ord[bb]]->offoff){ int t=ord[a]; ord[a]=ord[bb]; ord[bb]=t; }
int contig = tw[ord[0]]->fd==tw[ord[1]]->fd && tw[ord[1]]->fd==tw[ord[2]]->fd
&& tw[ord[0]]->off+tw[ord[0]]->nbytes==tw[ord[1]]->off
&& tw[ord[1]]->off+tw[ord[1]]->nbytes==tw[ord[2]]->off;
- int64_t pos[3]; int done=0;
+ int64_t pos[3]; int done=0, dc_direct=0;
if(contig){
int64_t off0=tw[ord[0]]->off;
int dfd = g_direct ? st_direct_fd(&m->S, tw[ord[0]]->fd) : -1;
@@ -1170,25 +1303,41 @@ static int expert_load_impl(Model *m, int layer, int eid, ESlot *s, int fatal){
ssize_t r=pread(dfd, s->slab, len, base);
if(r>=need){
pos[ord[0]]=off0-base; pos[ord[1]]=pos[ord[0]]+tw[ord[0]]->nbytes;
- pos[ord[2]]=pos[ord[1]]+tw[ord[1]]->nbytes; done=1;
+ pos[ord[2]]=pos[ord[1]]+tw[ord[1]]->nbytes; done=1; dc_direct=1;
}
}
if(!done){ /* fallback bufferizzato */
- if(pread_full(tw[ord[0]]->fd, s->slab, wtot, off0, "pread expert")){ if(fatal) exit(1); return -1; }
+ if(pread_full(tw[ord[0]]->fd, s->slab, wtot, off0, "pread expert")){ if(fatal) exit(1);
+ if(dc_on) dc_wall_exit(dc_cls,now_s()); /* pair the enter on the non-fatal unwind */
+ return -1; }
pos[ord[0]]=0; pos[ord[1]]=tw[ord[0]]->nbytes; pos[ord[2]]=tw[ord[0]]->nbytes+tw[ord[1]]->nbytes; done=1;
}
}
if(!done){ /* non contigui: 3 pread bufferizzate */
int64_t o=0;
for(int a=0;a<3;a++){ int k=ord[a];
- if(pread_full(tw[k]->fd, s->slab+o, tw[k]->nbytes, tw[k]->off, "pread expert")){ if(fatal) exit(1); return -1; }
+ if(pread_full(tw[k]->fd, s->slab+o, tw[k]->nbytes, tw[k]->off, "pread expert")){ if(fatal) exit(1);
+ if(dc_on) dc_wall_exit(dc_cls,now_s()); /* pair the enter on the non-fatal unwind */
+ return -1; }
pos[k]=o; o+=tw[k]->nbytes; }
}
float *fp[3]; int64_t fo=0; /* scale (piccole) */
for(int k=0;k<3;k++){
- if(pread_full(tq[k]->fd, (char*)(s->fslab+fo), tq[k]->nbytes, tq[k]->off, "pread qs")){ if(fatal) exit(1); return -1; }
+ if(pread_full(tq[k]->fd, (char*)(s->fslab+fo), tq[k]->nbytes, tq[k]->off, "pread qs")){ if(fatal) exit(1);
+ if(dc_on) dc_wall_exit(dc_cls,now_s()); /* pair the enter on the non-fatal unwind */
+ return -1; }
fp[k]=s->fslab+fo; fo+=tq[k]->nbytes/4; }
atomic_fetch_add_explicit(&g_prof_io,wtot+fo*4,memory_order_relaxed);
+ if(dc_on){ /* DISK-CLASS accounting, see dc_needed() */
+ double dc_t1=now_s(); /* one clock read for thread-ns AND the wall exit */
+ int64_t bytes=wtot+fo*4;
+ atomic_fetch_add_explicit(&g_dc_n[dc_cls],1,memory_order_relaxed);
+ atomic_fetch_add_explicit(&g_dc_bytes[dc_cls],bytes,memory_order_relaxed);
+ atomic_fetch_add_explicit(&g_dc_ns[dc_cls],(int64_t)((dc_t1-dc_t0)*1e9),memory_order_relaxed);
+ dc_wall_exit(dc_cls,dc_t1);
+ if(dc_direct) /* which fd ACTUALLY served this class */
+ atomic_fetch_add_explicit(&g_dc_direct_n[dc_cls],1,memory_order_relaxed);
+ }
if(g_drop){ /* scarta subito le pagine: evita che la page
* cache in pressione strangoli il throughput */
posix_fadvise(tw[ord[0]]->fd, tw[ord[0]]->off, wtot, POSIX_FADV_DONTNEED);
@@ -1206,9 +1355,15 @@ static int expert_load_impl(Model *m, int layer, int eid, ESlot *s, int fatal){
}
/* Every expert read goes through here: time the whole load (pread/fault +
* bookkeeping) on the thread that runs it, into the disk-service counter. */
-static int expert_load(Model *m, int layer, int eid, ESlot *s, int fatal){
+static int expert_load(Model *m, int layer, int eid, ESlot *s, int fatal, int demand){
+ /* `demand` marks a routing-driven demand-load (moe()'s PIPE/OMP miss path, where the
+ * pre-bump elast_pre snapshot moe() just wrote is valid) -- pass 0 from anywhere else
+ * (pilot speculative loads, repin, startup PIN loading): those never run through THIS
+ * call's own FASE A, so the snapshot either doesn't apply or was never written for
+ * them, and DISK-CLASS deliberately leaves them unclassified -- see expert_classify()'s
+ * call site. */
double t0=now_s();
- int rc=expert_load_impl(m,layer,eid,s,fatal);
+ int rc=expert_load_impl(m,layer,eid,s,fatal,demand);
atomic_fetch_add_explicit(&g_edisk_ns,(int64_t)((now_s()-t0)*1e9),memory_order_relaxed);
return rc;
}
@@ -1458,7 +1613,7 @@ static void *pipe_worker(void *arg){
memory_order_acq_rel,memory_order_relaxed)){
int L =atomic_load_explicit(&p->layer,memory_order_relaxed);
int eid=atomic_load_explicit(&p->eids[i],memory_order_relaxed); /* AFTER winning CAS */
- expert_load(p->m,L,eid,&p->m->ws[i],1); /* needed-now load: fatal on I/O error (matches serial path) */
+ expert_load(p->m,L,eid,&p->m->ws[i],1,1); /* needed-now load: fatal on I/O error (matches serial path); demand=1: this IS moe()'s own miss path */
atomic_store_explicit(&p->ready[i],1,memory_order_release);
}
/* CAS failed → another worker advanced index (or gen advanced): re-loop */
@@ -1536,7 +1691,7 @@ static void expert_host_release(Model *m, ESlot *s){
m->resident_bytes-=bytes; if(m->resident_bytes<0) m->resident_bytes=0;
}
static void expert_host_ensure(Model *m, int layer, ESlot *s){
- if(!s->slab) expert_load(m,layer,s->eid,s,1);
+ if(!s->slab) expert_load(m,layer,s->eid,s,1,0); /* re-materializing a GPU-resident expert's host copy, not a routing miss: demand=0 */
}
#endif
@@ -2144,6 +2299,15 @@ static void moe(Model *m, Layer *l, int layer, float *x, int S, float *out, int
pthread_mutex_unlock(&g_pilot_mx);
}
Cfg *c=&m->c; int D=c->hidden, E=c->n_experts, K=c->topk, I=c->moe_inter;
+ /* DISK-CLASS: does THIS call need the pre-bump recency snapshot? Must agree with
+ * dc_needed() in expert_load_impl -- that's what reads what this writes. touched[]
+ * makes the write once-per-call: an expert routed by more than one position in a big
+ * batch (prefill's S) must snapshot the state from BEFORE this call started, not from
+ * an earlier position's bump within the SAME call (which would reintroduce the
+ * same-call contamination one position later). Unconditional VLA like the FASE B
+ * `seen[E]` below -- E is small, cost is noise. */
+ int need_classify = dc_needed();
+ unsigned char touched[E]; if(need_classify) memset(touched,0,(size_t)E);
float *choice=falloc(E);
int sI=c->moe_inter*c->n_shared;
/* Rank buffer for CACHE_ROUTE max-rank selection (up to all E experts). */
@@ -2173,6 +2337,15 @@ static void moe(Model *m, Layer *l, int layer, float *x, int S, float *out, int
for(int kk=0;kkeusage[layer][idxs[(int64_t)s*K+kk]]++;
ehit_mark(m,layer,idxs[(int64_t)s*K+kk]);
+ if(need_classify){ /* DISK-CLASS private recency -- snapshot BEFORE this call's own bump,
+ * then tick. This path also bumps the REAL elast/eaccess_clock a few
+ * lines below (#417/cfcc742 fixed the once-missing bump on Metal
+ * decode) -- DISK-CLASS's own clock stays independent regardless of
+ * that fix, see elast_dc in Model. */
+ int e=idxs[(int64_t)s*K+kk];
+ if(!touched[e]){ m->elast_pre[layer][e]=m->elast_dc[layer][e]; touched[e]=1; }
+ m->elast_dc[layer][e]=++m->eaccess_clock_dc;
+ }
if(m->eheat[layer][idxs[(int64_t)s*K+kk]]eheat[layer][idxs[(int64_t)s*K+kk]]++;
/* #417: la scorciatoia GPU-prerouted deve far avanzare l'orologio di recency
* come il percorso router completo (riga ~3055), altrimenti elast/eaccess_clock
@@ -2299,6 +2472,11 @@ static void moe(Model *m, Layer *l, int layer, float *x, int S, float *out, int
for(int kk=0;kkeusage[layer][idx[kk]]++;
ehit_mark(m,layer,idx[kk]);
+ if(need_classify){ /* DISK-CLASS private recency -- snapshot BEFORE this call's own bump,
+ * then tick (same rate as the real clock below: one per (s,kk)) */
+ if(!touched[idx[kk]]){ m->elast_pre[layer][idx[kk]]=m->elast_dc[layer][idx[kk]]; touched[idx[kk]]=1; }
+ m->elast_dc[layer][idx[kk]]=++m->eaccess_clock_dc;
+ }
if(m->eheat[layer][idx[kk]]eheat[layer][idx[kk]]++;
m->elast[layer][idx[kk]]=++m->eaccess_clock;
}
@@ -2499,7 +2677,7 @@ static void moe(Model *m, Layer *l, int layer, float *x, int S, float *out, int
* are timed as service inside expert_load */
} else { double t0=now_s(); /* ORIGINALE: blocking parallel load */
#pragma omp parallel for schedule(dynamic,1)
- for(int q=0;qws[q],1);
+ for(int q=0;qws[q],1,1); /* demand=1: this IS the miss path */
m->t_ewait += now_s()-t0; } /* compute thread blocked for the whole load */
}
/* I/O ASINCRONO: readahead (WILLNEED) del blocco SUCCESSIVO mentre calcoliamo
@@ -2805,7 +2983,7 @@ static void pilot_realload(Model *m, int layer, int eid){
g_pilot_inflight[layer]++;
pthread_mutex_unlock(&g_pilot_mx);
- int rc=expert_load(m,layer,eid,dst,0); /* pread VERO — fuori dal lock, sovrapposto al compute; fatal=0: un errore su una speculazione NON deve uccidere il server */
+ int rc=expert_load(m,layer,eid,dst,0,0); /* pread VERO — fuori dal lock, sovrapposto al compute; fatal=0: un errore su una speculazione NON deve uccidere il server; demand=0: speculative, never classified */
pthread_mutex_lock(&g_pilot_mx);
if(rc==0){
@@ -3852,6 +4030,42 @@ static void prof_report(Model *m, const ProfBase *b, double elapsed, int tokens,
g_mmap?"; COLI_MMAP=1: page cache may serve part":"",
hitp,(unsigned long long)dhp,(unsigned long long)dhe,(unsigned long long)dm, tokens>0?(double)dq/tokens:0.0,
io_svc,io_w);
+ /* DISK-CLASS: per-load cold/warm classification vs. which fd ACTUALLY served it.
+ * Three per-class rates, labeled to keep the units unambiguous (ambiguous units
+ * mislead -- measured lesson): GB/s-thread = bytes / thread-seconds (per-read
+ * service rate, same convention as the read-service line above); GB/s-wall =
+ * bytes / busy-wall (aggregate rate the disk actually delivered while >=1 load of
+ * the class was in flight); avg-conc = thread-seconds / busy-wall (mean overlap
+ * depth). disk-busy = combined busy-wall (either class in flight) as a share of
+ * the profile window. */
+ {
+ uint64_t dcn[2], dcdn[2]; int64_t dcbytes[2], dcns[2], dcwall[2], wnow[2], dcwall_all, wall_now;
+ dc_wall_read(wnow,&wall_now);
+ for(int i=0;i<2;i++){
+ dcn[i]=atomic_load_explicit(&g_dc_n[i],memory_order_relaxed)-b->dc_n[i];
+ dcbytes[i]=atomic_load_explicit(&g_dc_bytes[i],memory_order_relaxed)-b->dc_bytes[i];
+ dcns[i]=atomic_load_explicit(&g_dc_ns[i],memory_order_relaxed)-b->dc_ns[i];
+ dcdn[i]=atomic_load_explicit(&g_dc_direct_n[i],memory_order_relaxed)-b->dc_direct_n[i];
+ dcwall[i]=wnow[i]-b->dc_wall_ns[i];
+ }
+ dcwall_all=wall_now-b->dc_wall_all_ns;
+ if(dcn[DC_COLD]+dcn[DC_WARM]){
+ double ct=dcns[DC_COLD]>0?(double)dcbytes[DC_COLD]/dcns[DC_COLD]:0.0; /* bytes/ns = GB/s */
+ double wt=dcns[DC_WARM]>0?(double)dcbytes[DC_WARM]/dcns[DC_WARM]:0.0;
+ double cw=dcwall[DC_COLD]>0?(double)dcbytes[DC_COLD]/dcwall[DC_COLD]:0.0;
+ double ww=dcwall[DC_WARM]>0?(double)dcbytes[DC_WARM]/dcwall[DC_WARM]:0.0;
+ double cc=dcwall[DC_COLD]>0?(double)dcns[DC_COLD]/dcwall[DC_COLD]:0.0;
+ double wc=dcwall[DC_WARM]>0?(double)dcns[DC_WARM]/dcwall[DC_WARM]:0.0;
+ fprintf(f,"[PROF] DISK-CLASS (recency split): cold %llu x %.2f GB @ %.2f GB/s-thread, wall %.1fs @ %.2f GB/s-wall, avg-conc %.1f (direct %llu/%llu) | "
+ "warm %llu x %.2f GB @ %.2f GB/s-thread, wall %.1fs @ %.2f GB/s-wall, avg-conc %.1f (direct %llu/%llu) | "
+ "disk-busy %.1fs (%.0f%% of window)\n",
+ (unsigned long long)dcn[DC_COLD],dcbytes[DC_COLD]/1e9,ct,dcwall[DC_COLD]/1e9,cw,cc,
+ (unsigned long long)dcdn[DC_COLD],(unsigned long long)dcn[DC_COLD],
+ (unsigned long long)dcn[DC_WARM],dcbytes[DC_WARM]/1e9,wt,dcwall[DC_WARM]/1e9,ww,wc,
+ (unsigned long long)dcdn[DC_WARM],(unsigned long long)dcn[DC_WARM],
+ dcwall_all/1e9,100.0*(dcwall_all/1e9)/elapsed);
+ }
+ }
fprintf(f,"[PROF] resident experts: %d pinned (%.1f GB) + %d in LRU (%.1f GB, cap %d/layer)\n",
pinned,pinned*eb/1e9,lru,lru*eb/1e9,m->ecap);
double emm=m->t_emm-b->emm, ecpu=m->t_ecpu-b->ecpu, egpu=m->t_egpu-b->egpu;
@@ -4189,7 +4403,7 @@ static void repin_pass_limit(Model *m,int limit){
+(int64_t)coli_cuda_tensor_bytes(s->d.cuda) : 0;
#endif
double t0=now_s();
- expert_load(m,cd[b].l,cd[b].eid,s,1); /* disk -> RAM, same resident slot */
+ expert_load(m,cd[b].l,cd[b].eid,s,1,0); /* disk -> RAM, same resident slot; demand=0: repin, never classified */
const char *tier="RAM";
#ifdef COLI_CUDA
if(gpu){ /* refresh the same VRAM slot now, not lazily */
@@ -4803,7 +5017,7 @@ static void pin_load(Model *m, const char *statspath, double gb){
* released before the disjoint RAM-ranked suffix is allocated. */
#pragma omp parallel for schedule(dynamic,1)
for(int a=0;a<(gpu_prefix?gpu_prefix:npin);a++)
- expert_load(m,r[a].l,r[a].e,&m->pin[r[a].l][slot_of[a]],1);
+ expert_load(m,r[a].l,r[a].e,&m->pin[r[a].l][slot_of[a]],1,0); /* startup pin load; demand=0, never classified */
m->resident_bytes+=(int64_t)(gpu_prefix?gpu_prefix:npin)*eb;
#ifdef COLI_CUDA
if(g_cuda_enabled && budget>0){
@@ -4847,7 +5061,7 @@ static void pin_load(Model *m, const char *statspath, double gb){
if(gpu_prefix>0&&gpu_prefixpin[r[a].l][slot_of[a]],1);
+ expert_load(m,r[a].l,r[a].e,&m->pin[r[a].l][slot_of[a]],1,0); /* startup pin load; demand=0, never classified */
m->resident_bytes+=(int64_t)(npin-gpu_prefix)*eb;
}
fprintf(stderr,"[PIN] placement: %d VRAM + %d RAM expert (%.1f GB warm) in %.0fs da %s\n",
@@ -5186,6 +5400,8 @@ int main(int argc, char **argv){
g_pipe_nw = getenv("PIPE_WORKERS")?atoi(getenv("PIPE_WORKERS")):8; /* I/O worker threads */
if(g_pipe_nw<1) g_pipe_nw=1;
g_direct = getenv("DIRECT")?atoi(getenv("DIRECT")):0;
+ { const char *dh=getenv("COLI_DISKCLASS_WINDOW"); /* DISK-CLASS recency window, see its declaration */
+ if(dh){ g_direct_heat_ticks=(uint32_t)strtoul(dh,NULL,10); g_direct_heat_explicit=1; } }
g_uring = getenv("URING")?atoi(getenv("URING")):0;
if(g_uring){
#ifdef __linux__
@@ -5284,6 +5500,19 @@ int main(int argc, char **argv){
printf("== GLM C engine (glm_moe_dsa), cache=%d experts/layer | experts@%d-bit dense@%d-bit | idot: " IDOT_KERNEL " ==\n", cap, ebits, dbits);
g_mem_avail_boot = mem_available_gb();
Model m; double t0=now_s(); model_init(&m,snap,cap,ebits,dbits);
+ if(!g_direct_heat_explicit){ /* COLI_DISKCLASS_WINDOW default, needs m.c (topk/n_layers) */
+ /* CURRENT-STATE CALIBRATION: the "8" multiplier (recency window ~= the last 8
+ * tokens' worth of routing) is a measured-config constant, not a derived truth.
+ * Coordinates: measured 2026-07, macOS 26.5, M5 Max 128 GB, base caa49f7,
+ * GLM-5.2 int4 (topk=8, n_layers=78). On that setup it splits the classes
+ * cleanly (decode cold share ~71% of classified bytes, 1061.85/1491.49 GB);
+ * other models, page-cache pressures, or disks may want a different window --
+ * COLI_DISKCLASS_WINDOW overrides, and the DISK-CLASS line itself is the
+ * tuning instrument. */
+ uint32_t nl=(uint32_t)(m.c.n_layers>0?m.c.n_layers:1), k=(uint32_t)(m.c.topk>0?m.c.topk:1);
+ g_direct_heat_ticks = k*nl*8u; /* ~last 8 tokens' worth of routing, see the declaration */
+ if(!g_direct_heat_ticks) g_direct_heat_ticks=1;
+ }
if(g_draft<0){
#ifdef COLI_CUDA
/* MTP is disabled under CUDA by default: cold (streaming) experts still
From 5c84b256451e9873e6fac50fd18572217590e315 Mon Sep 17 00:00:00 2001
From: Oleksandr Kuvshynov <661042+okuvshynov@users.noreply.github.com>
Date: Sun, 19 Jul 2026 16:06:49 -0400
Subject: [PATCH 37/71] fix: old x86 Mac scalar -> vnni
Older macs still need -march, not -mcpu.
Before this fix, no ymm (or zmm, vnni, etc.).
Standalone repro:
```
% clang -O3 -mcpu=native glm.c -o /tmp/glm
clang: error: unsupported option '-mcpu=' for target 'x86_64-apple-darwin25.4.0'
% clang -O3 -mcpu=native glm.c -o /tmp/glm -lm
% otool -tv /tmp/glm | grep -c ymm
0
```
I'm not entirely sure why `-lm` would ignore `-mcpu=` rather than error, but either way we don't get vector instructions:
colibri itself:
```
% ulimit -l unlimited; OMP_NUM_THREADS=16 RAM_GB=700 PIN=auto PIN_GB=all PIN_FILL=1 ./coli run --ram 700 --model ~/projects/llms/glm5p2-i4 --ngen 256 "Explain the difference between concurrency and parallelism"
...
== GLM C engine (glm_moe_dsa), cache=8 experts/layer | experts@8-bit dense@8-bit | idot: scalar ==
```
After the fix, we'll get it:
```
% clang -O3 -march=native glm.c -o /tmp/glm -lm
% otool -tv /tmp/glm | grep -c ymm
2621
```
colibri run:
```
% ulimit -l unlimited; OMP_NUM_THREADS=16 RAM_GB=700 PIN=auto PIN_GB=all PIN_FILL=1 ./coli run --ram 700 --model ~/projects/llms/glm5p2-i4 --ngen 256 "Explain the difference between concurrency and parallelism"
...
== GLM C engine (glm_moe_dsa), cache=8 experts/layer | experts@8-bit dense@8-bit | idot: avx512-vnni ==
```
---
c/Makefile | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/c/Makefile b/c/Makefile
index af17357..0341c0a 100644
--- a/c/Makefile
+++ b/c/Makefile
@@ -58,9 +58,14 @@ CFLAGS = -O3 $(OMPC) -Wall -Wextra -Wno-unused-parameter -Wno-misleading-indent
# Opt-in: ARCH=native appends -mcpu=native (arm64 clang uses -mcpu, not -march),
# which unlocks the i8mm SMMLA int8/int4 dot kernels in colibri.c. ARCH unset ->
# no -mcpu, default build byte-identical. Apple clang knows apple-m4 / native.
+# For older X86_64 Macs (for example, Mac Pro 2019) we need to use -march
ifneq ($(ARCH),)
+ifneq (,$(X86_64))
+CFLAGS += -march=$(ARCH)
+else
CFLAGS += -mcpu=$(ARCH)
endif
+endif
LDFLAGS = -lm $(OMPL)
EXE =
else ifneq ($(IS_WIN),)
@@ -273,8 +278,9 @@ olmoe$(EXE): olmoe.c st.h json.h compat.h
# Use a baseline that matches the compiler target. macOS already targets a
# portable baseline when ARCH is empty; forcing the x86 value there breaks
# Apple Silicon. Unknown targets use native rather than an invalid x86 flag.
+# Intel Macs need -march for vector instructions
ifneq (,$(DARWIN))
-PORTABLE_ARCH =
+PORTABLE_ARCH = $(if $(X86_64),x86-64-v3,)
else ifneq (,$(AARCH64))
PORTABLE_ARCH = armv8-a
else ifneq (,$(PPC64))
From 9c698b29a684c75a15831f0bf6dbecc099e1a73c Mon Sep 17 00:00:00 2001
From: cdhdt
Date: Sun, 19 Jul 2026 23:09:51 +0200
Subject: [PATCH 38/71] bench: microbench for AVX-VNNI idot accumulator A/B
(not a gate)
---
c/Makefile | 5 +++
c/tests/bench_idot.c | 92 ++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 97 insertions(+)
create mode 100644 c/tests/bench_idot.c
diff --git a/c/Makefile b/c/Makefile
index af17357..b9e462d 100644
--- a/c/Makefile
+++ b/c/Makefile
@@ -352,6 +352,11 @@ tests/test_dsa_select$(EXE): tests/test_dsa_select.c colibri.c st.h uring.h json
tests/bench_dsa_select$(EXE): tests/bench_dsa_select.c colibri.c st.h uring.h json.h tok.h tok_unicode.h compat.h grammar.h tier.h quant.h sample.h kv_persist.h telemetry.h
$(CC) $(CFLAGS) $< -o $@ $(LDFLAGS)
+# bench_idot: microbenchmark (single-acc vs independent-acc AVX-VNNI idot), NOT a test gate.
+# Build on demand on an AVX-VNNI CPU: make tests/bench_idot ARCH=native
+tests/bench_idot$(EXE): tests/bench_idot.c colibri.c st.h uring.h json.h tok.h tok_unicode.h compat.h grammar.h tier.h quant.h sample.h kv_persist.h telemetry.h
+ $(CC) $(CFLAGS) $< -o $@ $(LDFLAGS)
+
tests/test_uring$(EXE): tests/test_uring.c colibri.c st.h uring.h json.h tok.h tok_unicode.h compat.h grammar.h tier.h quant.h sample.h kv_persist.h telemetry.h
$(CC) $(CFLAGS) $< -o $@ $(LDFLAGS)
diff --git a/c/tests/bench_idot.c b/c/tests/bench_idot.c
new file mode 100644
index 0000000..37b0b30
--- /dev/null
+++ b/c/tests/bench_idot.c
@@ -0,0 +1,92 @@
+/* Microbenchmark: old (single-accumulator) vs new (independent-accumulator) AVX-VNNI
+ * int8/int4 dot kernels (quant.h). NOT a unit test -- test_idot.c proves correctness.
+ * This measures the headline claim: breaking the serial vpdpbusd->acc chain lifts
+ * per-core kernel throughput, the same win the NEON path already took ("26->63 GB/s, 2.4x").
+ *
+ * It re-implements the OLD single-acc AVX-VNNI kernels inline and calls the REAL (new)
+ * ones via the include-colibri.c pattern -- one process, identical frozen inputs, warm
+ * caches. Reports median ns/call + GB/s-of-weights + new/old ratio. Measures the kernel's
+ * compute ceiling (warm caches), NOT end-to-end tok/s.
+ *
+ * Run: make tests/bench_idot ARCH=native && ./tests/bench_idot (not in TEST_BINS -- not a gate)
+ */
+#define main coli_glm_main_unused
+#include "../colibri.c"
+#undef main
+#include
+#include
+
+static uint32_t rs=0x2545F491u;
+static uint32_t xr(void){ rs^=rs<<13; rs^=rs>>17; rs^=rs<<5; return rs; }
+
+/* ---- OLD kernels: verbatim copies of the pre-change __AVXVNNI__ branches (single acc) ---- */
+#if defined(__AVXVNNI__) && defined(__AVX2__)
+static int32_t dot_i8i8_old(const int8_t *w, const int8_t *x, int I){
+ int32_t sum=0; int i=0;
+ __m128i acc=_mm_setzero_si128();
+ for(;i+16<=I;i+=16){
+ __m128i wv=_mm_loadu_si128((const __m128i*)(w+i));
+ __m128i xv=_mm_loadu_si128((const __m128i*)(x+i));
+ __m128i xs=_mm_sign_epi8(xv,wv);
+ acc=_mm_dpbusd_epi32(acc,_mm_abs_epi8(wv),xs);
+ }
+ sum=hsum128_i32(acc);
+ for(;i>1)));
+ __m128i lo=_mm_and_si128(by,m4), hi=_mm_and_si128(_mm_srli_epi16(by,4),m4);
+ __m128i n0=_mm_unpacklo_epi8(lo,hi), n1=_mm_unpackhi_epi8(lo,hi);
+ __m128i w0=_mm_sub_epi8(n0,b8), w1=_mm_sub_epi8(n1,b8);
+ __m128i x0=_mm_loadu_si128((const __m128i*)(x+i));
+ __m128i x1=_mm_loadu_si128((const __m128i*)(x+i+16));
+ acc=_mm_dpbusd_epi32(acc,_mm_abs_epi8(w0),_mm_sign_epi8(x0,w0));
+ acc=_mm_dpbusd_epi32(acc,_mm_abs_epi8(w1),_mm_sign_epi8(x1,w1));
+ }
+ sum=hsum128_i32(acc);
+ for(;i>1]; int v=(i&1)?((int)(b>>4)-8):((int)(b&0xF)-8); sum+=v*x[i]; }
+ return sum;
+}
+#else
+#error "bench_idot requires an AVX-VNNI build: make tests/bench_idot ARCH=native on an AVX-VNNI CPU"
+#endif
+
+#define I_DIM 6144
+#define N_REPEAT 20000
+static int cmp_d(const void*a,const void*b){ double x=*(const double*)a,y=*(const double*)b; return xy?1:0; }
+
+int main(void){
+ static int8_t w8[I_DIM], x8[I_DIM]; static uint8_t w4[I_DIM/2];
+ for(int i=0;i
Date: Sun, 19 Jul 2026 23:09:51 +0200
Subject: [PATCH 39/71] idot: independent accumulators on the AVX-VNNI
int8/int4 dot kernels
The __AVXVNNI__ branches of dot_i8i8/dot_i4i8 (quant.h) accumulated every vpdpbusd
into a single register, a serial dependency chain (vpdpbusd is latency-bound ~5c).
Use 4 independent accumulators, mirroring the NEON 4-accumulator path in this file.
On x86 the tiled SMMLA _mm path is ARM-only, so these two dots are THE int8/int4
IDOT matmul kernels on x86 (g_idot=1 default), prefill and decode.
Integer accumulation is associative -> bit-identical (test_idot passes bit-for-bit).
Microbench (tests/bench_idot, i7-12700H P-core, I=6144):
dot_i8i8 6.85 -> 19.26 GB/s (2.81x)
dot_i4i8 3.41 -> 7.25 GB/s (2.13x)
---
c/quant.h | 48 +++++++++++++++++++++++++++++++++++++++++-------
1 file changed, 41 insertions(+), 7 deletions(-)
diff --git a/c/quant.h b/c/quant.h
index 928de96..eb7dba2 100644
--- a/c/quant.h
+++ b/c/quant.h
@@ -314,12 +314,27 @@ static inline int32_t dot_i8i8(const int8_t *w, const int8_t *x, int I){
}
sum=_mm512_reduce_add_epi32(acc);
#elif defined(__AVXVNNI__) && defined(__AVX2__)
- __m128i acc=_mm_setzero_si128();
+ /* 4 accumulatori indipendenti (64 byte/iter): un solo acc incatena i vpdpbusd
+ * (latenza-bound ~5c). Somme intere associative -> bit-identico. Stessa struttura
+ * dei 4 accumulatori del ramo NEON piu' sotto.
+ * EN: four independent accumulators break the serial vpdpbusd->acc chain; integer
+ * adds are associative, so the result is bit-identical (mirrors the NEON path). */
+ __m128i a0=_mm_setzero_si128(),a1=_mm_setzero_si128(),a2=_mm_setzero_si128(),a3=_mm_setzero_si128();
+ for(;i+64<=I;i+=64){
+ __m128i w0=_mm_loadu_si128((const __m128i*)(w+i)), x0=_mm_loadu_si128((const __m128i*)(x+i));
+ __m128i w1=_mm_loadu_si128((const __m128i*)(w+i+16)), x1=_mm_loadu_si128((const __m128i*)(x+i+16));
+ __m128i w2=_mm_loadu_si128((const __m128i*)(w+i+32)), x2=_mm_loadu_si128((const __m128i*)(x+i+32));
+ __m128i w3=_mm_loadu_si128((const __m128i*)(w+i+48)), x3=_mm_loadu_si128((const __m128i*)(x+i+48));
+ a0=_mm_dpbusd_epi32(a0,_mm_abs_epi8(w0),_mm_sign_epi8(x0,w0));
+ a1=_mm_dpbusd_epi32(a1,_mm_abs_epi8(w1),_mm_sign_epi8(x1,w1));
+ a2=_mm_dpbusd_epi32(a2,_mm_abs_epi8(w2),_mm_sign_epi8(x2,w2));
+ a3=_mm_dpbusd_epi32(a3,_mm_abs_epi8(w3),_mm_sign_epi8(x3,w3));
+ }
+ __m128i acc=_mm_add_epi32(_mm_add_epi32(a0,a1),_mm_add_epi32(a2,a3));
for(;i+16<=I;i+=16){
__m128i wv=_mm_loadu_si128((const __m128i*)(w+i));
__m128i xv=_mm_loadu_si128((const __m128i*)(x+i));
- __m128i xs=_mm_sign_epi8(xv,wv);
- acc=_mm_dpbusd_epi32(acc,_mm_abs_epi8(wv),xs);
+ acc=_mm_dpbusd_epi32(acc,_mm_abs_epi8(wv),_mm_sign_epi8(xv,wv));
}
sum=hsum128_i32(acc);
#elif defined(__AVX2__)
@@ -390,13 +405,32 @@ static inline int32_t dot_i4i8(const uint8_t *w4, const int8_t *x, int I){
}
sum=_mm512_reduce_add_epi32(acc);
#elif defined(__AVXVNNI__) && defined(__AVX2__)
+ /* 4 accumulatori indipendenti (64 elementi = 32 byte packed/iter): un solo acc
+ * incatena i vpdpbusd (latenza-bound ~5c). Somme intere associative -> bit-identico.
+ * Stessa struttura dei 4 accumulatori del ramo NEON piu' sotto.
+ * EN: four independent accumulators break the serial vpdpbusd->acc chain; integer
+ * adds are associative, so the result is bit-identical (mirrors the NEON path). */
const __m128i m4=_mm_set1_epi8(0x0F); const __m128i b8=_mm_set1_epi8(8);
- __m128i acc=_mm_setzero_si128();
- for(;i+32<=I;i+=32){
+ __m128i a0=_mm_setzero_si128(),a1=_mm_setzero_si128(),a2=_mm_setzero_si128(),a3=_mm_setzero_si128();
+ for(;i+64<=I;i+=64){
+ __m128i by0=_mm_loadu_si128((const __m128i*)(w4+(i>>1))); /* elem i..i+31 */
+ __m128i by1=_mm_loadu_si128((const __m128i*)(w4+(i>>1)+16)); /* elem i+32..i+63 */
+ __m128i lo0=_mm_and_si128(by0,m4), hi0=_mm_and_si128(_mm_srli_epi16(by0,4),m4);
+ __m128i lo1=_mm_and_si128(by1,m4), hi1=_mm_and_si128(_mm_srli_epi16(by1,4),m4);
+ __m128i w0=_mm_sub_epi8(_mm_unpacklo_epi8(lo0,hi0),b8), w1=_mm_sub_epi8(_mm_unpackhi_epi8(lo0,hi0),b8);
+ __m128i w2=_mm_sub_epi8(_mm_unpacklo_epi8(lo1,hi1),b8), w3=_mm_sub_epi8(_mm_unpackhi_epi8(lo1,hi1),b8);
+ __m128i x0=_mm_loadu_si128((const __m128i*)(x+i)), x1=_mm_loadu_si128((const __m128i*)(x+i+16));
+ __m128i x2=_mm_loadu_si128((const __m128i*)(x+i+32)), x3=_mm_loadu_si128((const __m128i*)(x+i+48));
+ a0=_mm_dpbusd_epi32(a0,_mm_abs_epi8(w0),_mm_sign_epi8(x0,w0));
+ a1=_mm_dpbusd_epi32(a1,_mm_abs_epi8(w1),_mm_sign_epi8(x1,w1));
+ a2=_mm_dpbusd_epi32(a2,_mm_abs_epi8(w2),_mm_sign_epi8(x2,w2));
+ a3=_mm_dpbusd_epi32(a3,_mm_abs_epi8(w3),_mm_sign_epi8(x3,w3));
+ }
+ __m128i acc=_mm_add_epi32(_mm_add_epi32(a0,a1),_mm_add_epi32(a2,a3));
+ for(;i+32<=I;i+=32){ /* 32-nibble remainder: 2 dpbusd, same unpack */
__m128i by=_mm_loadu_si128((const __m128i*)(w4+(i>>1)));
__m128i lo=_mm_and_si128(by,m4), hi=_mm_and_si128(_mm_srli_epi16(by,4),m4);
- __m128i n0=_mm_unpacklo_epi8(lo,hi), n1=_mm_unpackhi_epi8(lo,hi);
- __m128i w0=_mm_sub_epi8(n0,b8), w1=_mm_sub_epi8(n1,b8);
+ __m128i w0=_mm_sub_epi8(_mm_unpacklo_epi8(lo,hi),b8), w1=_mm_sub_epi8(_mm_unpackhi_epi8(lo,hi),b8);
__m128i x0=_mm_loadu_si128((const __m128i*)(x+i));
__m128i x1=_mm_loadu_si128((const __m128i*)(x+i+16));
acc=_mm_dpbusd_epi32(acc,_mm_abs_epi8(w0),_mm_sign_epi8(x0,w0));
From 453d1401bae95c1b522ad255853f2d9fb9eea873 Mon Sep 17 00:00:00 2001
From: ZacharyZcR
Date: Mon, 20 Jul 2026 05:15:45 +0800
Subject: [PATCH 40/71] pin: with CUDA_RELEASE_HOST the VRAM prefix must not
consume the RAM pin budget
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
pin_load computed npin from the RAM budget FIRST, then carved the
VRAM-ranked prefix out of those npin slots. With CUDA_RELEASE_HOST the
prefix's host slabs are freed right after upload — so on a multi-GPU
host the top-ranked experts consumed the RAM budget without occupying
RAM, and the CPU tier pinned only the leftovers. Measured on 6x RTX 5090
(251 GB): 9,280 VRAM + only 1,721 RAM pins (32.5 GB warm) on a box whose
RAM fits ~10k more — the cold tail then paid disk on every token and the
hit rate ceilinged at 99.0% forever.
Move the VRAM budget estimate above the npin finalization and make the
release-destined prefix ADDITIVE to the RAM-derived count. Same box,
same env plus the fix:
[PIN] placement: 9,280 VRAM + 10,176 RAM (192.5 GB warm)
expert hit rate 100.0% (pin 100.0% + lru 0.0%) — disk 0
1,024-token greedy decode: 3.62 -> 6.21 tok/s (+72%)
warm late segment (t=768-1024): 5.11 -> 5.73 tok/s (+12%)
prefill: 10.4 -> 8.8 s
The whole-run gain is the LRU warmup penalty disappearing (full
residency from the first token); the late-segment gain is the steady
~0.9%-miss disk residue recovered. Non-release configs are untouched:
prefix_est stays 0 and the arithmetic reduces to today's exactly.
---
c/colibri.c | 36 ++++++++++++++++++++++++------------
1 file changed, 24 insertions(+), 12 deletions(-)
diff --git a/c/colibri.c b/c/colibri.c
index 03eb878..f2e24b4 100644
--- a/c/colibri.c
+++ b/c/colibri.c
@@ -4774,6 +4774,29 @@ static void pin_load(Model *m, const char *statspath, double gb){
double avail=expert_avail(m,ram_env,m->ebits,est_ctx);
npin=avail>0?(int)(avail/eb):0;
} else npin=(int)(gb*1e9/eb);
+#ifdef COLI_CUDA
+ /* The VRAM budget must be known BEFORE npin is finalized: with
+ * CUDA_RELEASE_HOST the VRAM-ranked prefix's host slabs are freed right
+ * after upload, so those slots must NOT consume the RAM pin budget.
+ * Before this, a 6-GPU host lost its top ~9k ranked experts from the RAM
+ * count and pinned only the leftovers (measured: 9,280 VRAM + 1,721 RAM
+ * on a box whose RAM fits ~11k — the cold tail then paid disk forever). */
+ double remaining[COLI_CUDA_MAX_DEVICES]={0}, placed_b[COLI_CUDA_MAX_DEVICES]={0};
+ int placed_n[COLI_CUDA_MAX_DEVICES]={0}, gpu_prefix=0, prefix_est=0;
+ double budget=g_cuda_expert_gb*1e9, safe_total=0;
+ if(g_cuda_enabled&&(g_cuda_expert_gb>0||g_cuda_expert_auto)) for(int i=0;isafe_total) budget=safe_total;
+ if(g_cuda_enabled&&g_cuda_release_host&&budget>0){
+ prefix_est=(int)(budget/eb)+g_cuda_ndev;
+ npin+=prefix_est; /* additive: prefix RAM is returned after upload */
+ }
+#endif
if(npin>n) npin=n;
if(npin<1){ free(r); return; }
int *cnt_l=calloc(c->n_layers+1,sizeof(int)); /* +1: riga MTP */
@@ -4784,18 +4807,7 @@ static void pin_load(Model *m, const char *statspath, double gb){
for(int i=0;i<=c->n_layers;i++) m->npin[i]=cnt_l[i];
double t0=now_s();
#ifdef COLI_CUDA
- double remaining[COLI_CUDA_MAX_DEVICES]={0}, placed_b[COLI_CUDA_MAX_DEVICES]={0};
- int placed_n[COLI_CUDA_MAX_DEVICES]={0}, gpu_prefix=0;
- double budget=g_cuda_expert_gb*1e9, safe_total=0;
- if(g_cuda_enabled&&(g_cuda_expert_gb>0||g_cuda_expert_auto)) for(int i=0;isafe_total) budget=safe_total;
- if(g_cuda_enabled&&g_cuda_release_host&&budget>0){ gpu_prefix=(int)(budget/eb)+g_cuda_ndev; if(gpu_prefix>npin)gpu_prefix=npin; }
+ if(prefix_est>0){ gpu_prefix=prefix_est; if(gpu_prefix>npin) gpu_prefix=npin; }
#else
int gpu_prefix=0;
#endif
From 85107f8ce602a62caa93a728f7ec504abf70c698 Mon Sep 17 00:00:00 2001
From: cdhdt
Date: Sun, 19 Jul 2026 23:21:10 +0200
Subject: [PATCH 41/71] io: skip the wasted WILLNEED on expert weights under
DIRECT=1
Under O_DIRECT the expert *weights* are read with the direct fd (bypassing the
page cache), so a POSIX_FADV_WILLNEED on them warms pages the demand read never
consumes -- pure wasted readahead on the disk, the scarcest resource when
streaming. The .qs scales are ALWAYS read buffered (pread on the normal fd), so
keep their WILLNEED. This only changes hint-only PILOT (PILOT=1, PILOT_REAL=0)
combined with DIRECT=1; fadvise is advisory, so output is bit-identical.
Prepared, not yet measured: needs a real NVMe + full model to A/B cold-stream
tok/s (PILOT=1 DIRECT=1, with/without this patch, TEMP=0). See #441.
---
c/colibri.c | 13 +++++++++++--
1 file changed, 11 insertions(+), 2 deletions(-)
diff --git a/c/colibri.c b/c/colibri.c
index 4b37fa8..0daf928 100644
--- a/c/colibri.c
+++ b/c/colibri.c
@@ -1701,12 +1701,21 @@ static void expert_host_ensure(Model *m, int layer, ESlot *s){
#endif
/* prefetch asincrono dei pesi di un expert (e delle sue scale .qs): avvia il readahead
- * cosi' le letture sincrone successive trovano la page-cache calda. */
+ * cosi' le letture sincrone successive trovano la page-cache calda.
+ * Sotto g_direct i PESI vengono letti con O_DIRECT (bypassa la page-cache, vedi
+ * expert_load): il WILLNEED su di essi scalda pagine che la lettura di domanda non
+ * consuma -> readahead sprecato sul disco, la risorsa piu' scarsa nello streaming.
+ * Le scale .qs restano SEMPRE bufferizzate (pread sul fd normale), quindi il loro
+ * WILLNEED resta utile anche con DIRECT=1. fadvise e' solo consultivo: saltarlo non
+ * cambia mai l'output (bit-identico), riduce solo I/O sprecato.
+ * EN: under O_DIRECT the weights bypass the page cache, so their WILLNEED is wasted;
+ * the .qs scales are always buffered, so keep theirs. Advisory hint -> output-preserving. */
static void expert_prefetch(Model *m, int layer, int eid){
char nm[300];
const char *suf[3]={"gate_proj.weight","up_proj.weight","down_proj.weight"};
for(int k=0;k<3;k++){
- snprintf(nm,sizeof(nm),"model.layers.%d.mlp.experts.%d.%s",layer,eid,suf[k]); st_prefetch(&m->S,nm);
+ snprintf(nm,sizeof(nm),"model.layers.%d.mlp.experts.%d.%s",layer,eid,suf[k]);
+ if(!g_direct) st_prefetch(&m->S,nm);
char qs[320]; snprintf(qs,sizeof(qs),"%s.qs",nm); st_prefetch(&m->S,qs);
}
}
From 9f3091600303ec7348f98d6ea95e3d9c25f18752 Mon Sep 17 00:00:00 2001
From: monotophic
Date: Sun, 19 Jul 2026 16:56:14 -0400
Subject: [PATCH 42/71] metal: parallel top-8 expert selection (r_top8_par),
default ON
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Re-derives the campaign's rtop8 breakthrough (fuse/rtop8-par @ b32439b,
commits 48b3a98 + b32439b) cleanly onto origin/dev @ 61004dc (post-#391
split), dropping the cb-chain commit (1130ac9) that branch was stacked
on. cb-chain touched only c/glm.c (now split into c/colibri.c); rtop8
touches only c/backend_metal.{h,mm} and c/tests/test_backend_metal.mm —
disjoint files, verified zero cb-chain remnants in this diff.
- r_top8_par: one SIMDGROUP per row (blocked lane ownership, taken
bitmask, shuffle-down argmax with ties->lower-index) replicating the
serial r_top8's first-max-wins ascending order exactly; topp/normk/
rscale tail verbatim on lane 0. Bench: serial 0.465 ms/layer (~55% of
the layer CB), parallel ~93x faster on the kernel, bit-exact.
- COLI_RTOP8 gate, default ON (renamed and inverted from the campaign's
COLI_RTOP8_PAR=0-default gate; COLI_RTOP8=0 is the opt-out escape).
- Expert-count generality (new hard requirement, REAP E=168 packages):
the parallel kernel's E<=256 contract is now enforced at EVERY call
site in host code (g_rtop8_width_ok / E<=256 checked before selecting
the pipeline), not just as an in-kernel defensive no-op -- closes a
latent gap where the campaign's SIMD-width guard only protected the
engine's automatic dispatch, not the standalone coli_metal_rtop8(par=1,
...) probe function metal-test itself uses. Out-of-contract requests
(E>256, or non-32-wide SIMD) now transparently run the serial kernel
instead of silently no-op'ing.
- metal-test: 16 new cases -- the campaign's 10-case exact-match fuzz
(now E-parametric) plus 6 new E-generality cases: E=168 (REAP) generic
+ massed-dup-ties, E=24 (<32 lane width) generic + ALL-EQUAL ties,
E=200 (a lane straddles the E boundary -- per=ceil(200/32)=7 doesn't
divide 200, so lane 28's ch[] block mixes 4 real indices with 3
sentinel ones; input is constructed to force those 4 into the top-8
deterministically, and the test asserts they were actually selected,
not just that serial==parallel), and E=257 (out-of-contract, proves
the auto-serial-fallback engages: this case is confirmed load-bearing
-- it fails without the new host-side guard). 15 stock + 10 ported +
6 new = 31 total metal-test cases.
Builds (METAL=0/1) and full C/python suites verified unchanged vs stock
61004dc (0 new warnings, identical pass counts). On-box A/B on this
branch is pending (see PR_BODY.md) -- no model runs performed here.
Co-Authored-By: Claude Fable 5
---
c/backend_metal.h | 17 +++++
c/backend_metal.mm | 140 ++++++++++++++++++++++++++++++++--
c/tests/test_backend_metal.mm | 88 +++++++++++++++++++++
3 files changed, 240 insertions(+), 5 deletions(-)
diff --git a/c/backend_metal.h b/c/backend_metal.h
index 4383b3b..0d72b2e 100644
--- a/c/backend_metal.h
+++ b/c/backend_metal.h
@@ -84,6 +84,23 @@ int coli_metal_layer_decode(float *x,
int coli_metal_gemm(float *y, const float *x, const void *weights, const float *scales,
int fmt, int S, int I, int O); /* large-batch sync GEMM; 0 -> CPU */
+/* Parallel top-8 expert selection (r_top8_par): run ONE top-8 selection kernel standalone
+ * on host arrays — par=0 the serial r_top8, par=1 the parallel exact-match replica gated
+ * in the engine by COLI_RTOP8 (default ON; COLI_RTOP8=0 opts out to the serial kernel).
+ * Exists so the metal-test suite (and any battery probe) can prove serial/parallel
+ * equivalence on the ENGINE build's own compiled shaders, not just in the bench tool.
+ * sig[S*E], bias[E], idx[S*K], w[S*K], keff[S].
+ * Expert-count generality: the parallel kernel's blocked-lane design (ch[8]/32-lane
+ * threadgroup) is validated correct for arbitrary E<=256, including non-multiples of the
+ * 32-lane width and small E (see metal-test's E=24/E=168/E=256 cases — 168 is the REAP
+ * expert-pruned package width from #428/#426). For E>256 (out of contract) this function
+ * transparently falls back to the serial kernel even when par=1 is requested, and the
+ * same automatic fallback is wired into the engine dispatch site — "par" is a request,
+ * never a guarantee, so no caller can reach the unguarded parallel path out of contract.
+ * Returns 1 on success, 0 if Metal is unavailable. */
+int coli_metal_rtop8(int par, const float *sig, const float *bias, int S, int E, int K,
+ int Ksel, float topp, int normk, float rscale,
+ int *idx, float *w, int *keff);
void coli_metal_attn_counts(uint64_t *ok, double *wall, double *kernel);
void coli_metal_attn_lat(double *ksched, double *gsched);
int coli_metal_attn_decode(const float *x,
diff --git a/c/backend_metal.mm b/c/backend_metal.mm
index 51ed8a0..b19d8a5 100644
--- a/c/backend_metal.mm
+++ b/c/backend_metal.mm
@@ -219,6 +219,63 @@ kernel void r_top8(device const float* sig [[buffer(0)]], device const float* bi
if(normk){ float sm=0; for(int kk=0;kk' ascending max (lowest index wins
+// within a lane, matching the serial ascending scan), then a shuffle-down argmax
+// reduction where ties prefer the LOWER index — together exactly the serial kernel's
+// first-max-wins order. The topp/normk/rscale tail is the serial code verbatim on lane 0
+// (same ops, same order => bitwise-identical results; metal-test enforces this with
+// memcmp). Contract: E<=256 (ch[8]/taken mask sizing: ceil(E/32)<=8) — the defensive
+// return below makes an out-of-contract dispatch a visible no-op (idx/w/keff untouched),
+// never an OOB write; both call sites (coli_metal_layer_decode's dispatch and the
+// standalone coli_metal_rtop8 runner) additionally gate on E<=256 in host code before
+// selecting this pipeline at all, so the return here is defense-in-depth, not the only
+// guard. Sentinel-per-lane design (ch[j]=-1e30f for e>=E) makes non-multiple-of-32 E
+// and small E correct without special-casing — validated for E=24, E=168 (REAP
+// expert-pruned packages, see the upstream feature-request thread) and E=256 by metal-test.
+// ASSUMES SIMD width 32 (shuffle offsets 16..1, 32-thread threadgroup per row): enforced
+// at init — coli_metal_init clears g_rtop8_width_ok (and therefore both call sites' use
+// of this pipeline) if threadExecutionWidth != 32.
+kernel void r_top8_par(device const float* sig [[buffer(0)]], device const float* bias [[buffer(1)]],
+ device int* idx [[buffer(2)]], device float* w [[buffer(3)]],
+ device int* keff [[buffer(4)]], constant int& E [[buffer(5)]],
+ constant int& K [[buffer(6)]], constant int& Ksel [[buffer(7)]],
+ constant float& topp [[buffer(8)]], constant int& normk [[buffer(9)]],
+ constant float& rscale [[buffer(10)]],
+ uint s [[threadgroup_position_in_grid]],
+ uint slane [[thread_index_in_simdgroup]]) {
+ if(E>256) return;
+ device const float* sg=sig+(long)s*E;
+ device int* id_=idx+(long)s*K; device float* ww=w+(long)s*K;
+ int per=(E+31)/32, base=(int)slane*per;
+ float ch[8]; uint taken=0u;
+ for(int j=0;jbv){ bv=ch[j]; bi=base+j; }
+ for(uint off=16;off>0;off>>=1){
+ float ov=simd_shuffle_down(bv,off); int oi=simd_shuffle_down(bi,off);
+ if(ov>bv || (ov==bv && oi=base && bi0.0f && topp<1.0f){
+ for(int a=1;a=0 && ww[b]=topp*tot){ Ke=kk+1; break; } }
+ }
+ keff[s]=Ke;
+ if(normk){ float sm=0; for(int kk=0;kk g_dev;
static id g_queue;
static id g_gemv, g_moe_gemv, g_moe_silu;
static id g_a_rms, g_a_rope, g_a_copy, g_a_qabs, g_a_score, g_a_smax, g_a_clat, g_a_ctx;
-static id g_a_add, g_r_router, g_r_top8;
+static id g_a_add, g_r_router, g_r_top8, g_r_top8p;
+static int g_rtop8_par = 1; // COLI_RTOP8 (default ON); COLI_RTOP8=0 opts out to the
+ // serial kernel — see coli_metal_init.
+static int g_rtop8_width_ok = 1; // hardware fact, independent of the policy gate above:
+ // false if this device's threadExecutionWidth != 32.
+ // Consulted by BOTH the engine dispatch site and the
+ // standalone coli_metal_rtop8 runner, so no caller can
+ // reach r_top8_par's 32-lane reduction on an unsafe
+ // device even by explicitly requesting par=1.
static size_t g_tensor_count, g_tensor_bytes;
static uint64_t g_moe_ok, g_moe_fb, g_moe_experts; // GPU blocks / CPU-fallback blocks / experts on GPU
static double g_t_setup, g_t_gpu, g_t_scatter, g_t_kernel; // per-block time breakdown (seconds)
@@ -284,6 +349,8 @@ extern "C" int coli_metal_init(void) {
if (g_dev) return 1;
if (getenv("COLI_METAL_UNTRACKED") && atoi(getenv("COLI_METAL_UNTRACKED")))
g_res_opts = MTLResourceStorageModeShared | MTLResourceHazardTrackingModeUntracked;
+ { const char *e = getenv("COLI_RTOP8"); // default ON; COLI_RTOP8=0 opts out
+ if (e && atoi(e) == 0) g_rtop8_par = 0; }
@autoreleasepool {
g_dev = MTLCreateSystemDefaultDevice();
if (!g_dev) return 0;
@@ -299,8 +366,23 @@ extern "C" int coli_metal_init(void) {
auto P=[&](const char*n){ return [g_dev newComputePipelineStateWithFunction:[lib newFunctionWithName:@(n)] error:&err]; };
g_a_rms=P("a_rmsnorm"); g_a_rope=P("a_rope"); g_a_copy=P("a_copy");
g_a_qabs=P("a_qabs"); g_a_score=P("a_score"); g_a_smax=P("a_smax"); g_a_clat=P("a_clat"); g_a_ctx=P("a_ctx");
- g_a_add=P("a_add"); g_r_router=P("r_router"); g_r_top8=P("r_top8");
- if(!g_a_add||!g_r_router||!g_r_top8){ fprintf(stderr,"[metal] tail pipelines failed\n"); g_dev=nil; return 0; }
+ g_a_add=P("a_add"); g_r_router=P("r_router"); g_r_top8=P("r_top8"); g_r_top8p=P("r_top8_par");
+ if(!g_a_add||!g_r_router||!g_r_top8||!g_r_top8p){ fprintf(stderr,"[metal] tail pipelines failed\n"); g_dev=nil; return 0; }
+ // r_top8_par's reduction hardcodes SIMD width 32 (shuffle-down offsets 16..1, one
+ // 32-thread threadgroup per row). True on all Apple Silicon shipped to date, but a
+ // non-32-width device would reduce wrongly AND race multiple lane-0 writers, so this
+ // is a hard safety fact (g_rtop8_width_ok), not just a policy default: it gates BOTH
+ // the engine dispatch site and the standalone coli_metal_rtop8 runner (degrade-to-safe,
+ // same pattern as the pool/ring fallbacks elsewhere) — no caller can opt back into an
+ // unsafe reduction on such a device, even by explicitly requesting par=1.
+ if ([g_r_top8p threadExecutionWidth] != 32) {
+ g_rtop8_width_ok = 0;
+ if (g_rtop8_par)
+ fprintf(stderr, "[metal] COLI_RTOP8 parallel top-8 disabled: threadExecutionWidth=%lu "
+ "!= 32 (r_top8_par's reduction assumes 32-lane simdgroups) — serial "
+ "r_top8 in use\n", (unsigned long)[g_r_top8p threadExecutionWidth]);
+ g_rtop8_par = 0;
+ }
if (!g_gemv || !g_moe_gemv || !g_moe_silu || !g_a_rms || !g_a_rope || !g_a_copy ||
!g_a_qabs || !g_a_score || !g_a_smax || !g_a_clat || !g_a_ctx) {
fprintf(stderr, "[metal] pipeline failed\n"); g_dev = nil; return 0; }
@@ -597,12 +679,22 @@ extern "C" int coli_metal_layer_decode(float *x,
// 5) silu(gate)*up + exact top-K select
[e setComputePipelineState:g_moe_silu]; [e setBuffer:ash1_ offset:0 atIndex:0]; [e setBuffer:ash2_ offset:0 atIndex:1];
[e dispatchThreads:MTLSizeMake((size_t)S*SI,1,1) threadsPerThreadgroup:MTLSizeMake(256,1,1)];
- { [e setComputePipelineState:g_r_top8];
+ { // COLI_RTOP8 (default ON) swaps the serial 1-thread-per-row select for the exact-
+ // match 1-simdgroup-per-row replica (same buffers/args; only pipeline+grid change).
+ // E<=256 is required by r_top8_par's ch[8]/32-lane blocking contract; this call
+ // site's E is always 256 today (layer_forward_rows' own architecture-shape gate in
+ // colibri.c requires c->n_experts==256 to reach coli_metal_layer_decode at all —
+ // see PR body "Scope statement") but the check is kept here too, defense-in-depth,
+ // so a future relaxation of that gate (e.g. to admit REAP-pruned E=168 models into
+ // the fused path) degrades safely to the serial kernel instead of mis-dispatching.
+ int use_par = g_rtop8_par && g_rtop8_width_ok && E<=256;
+ [e setComputePipelineState:use_par?g_r_top8p:g_r_top8];
[e setBuffer:asig_ offset:0 atIndex:0]; [e setBuffer:rbB offset:rboff atIndex:1];
[e setBuffer:aidx_ offset:0 atIndex:2]; [e setBuffer:aw_ offset:0 atIndex:3]; [e setBuffer:akeff_ offset:0 atIndex:4];
[e setBytes:&E length:4 atIndex:5]; [e setBytes:&K length:4 atIndex:6]; [e setBytes:&Ksel length:4 atIndex:7];
[e setBytes:&topp length:4 atIndex:8]; [e setBytes:&normk length:4 atIndex:9]; [e setBytes:&rscale length:4 atIndex:10];
- [e dispatchThreads:MTLSizeMake(S,1,1) threadsPerThreadgroup:MTLSizeMake(S,1,1)]; }
+ if(use_par) [e dispatchThreadgroups:MTLSizeMake(S,1,1) threadsPerThreadgroup:MTLSizeMake(32,1,1)];
+ else [e dispatchThreads:MTLSizeMake(S,1,1) threadsPerThreadgroup:MTLSizeMake(S,1,1)]; }
BAR();
// 6) shared down
bind_gemv(e,shd_w,shd_s,shd_fmt,SI,AH,ash1_,ashout_,S);
@@ -651,6 +743,44 @@ extern "C" int coli_metal_gemm(float *y, const float *x, const void *wp, const f
return 1;
}
+// Standalone single-kernel runner for the top-8 select (see backend_metal.h). Fresh
+// shared buffers per call (a test/probe path, not a hot path); grids exactly as the
+// engine dispatch site: serial = S threads of one S-wide threadgroup, parallel = S
+// threadgroups x 32 (one simdgroup per row). "par" is a REQUEST, not a guarantee: same
+// E<=256 and SIMD-width-32 host-side checks as the engine dispatch site gate the actual
+// pipeline choice, so a caller (including metal-test itself) can never reach the parallel
+// kernel out of contract by asking for it — par=1 with E>256, or on a non-32-wide device,
+// transparently runs the serial kernel instead and still returns 1 (success).
+extern "C" int coli_metal_rtop8(int par, const float *sig, const float *bias, int S, int E, int K,
+ int Ksel, float topp, int normk, float rscale,
+ int *idx, float *w, int *keff) {
+ if (!g_dev || S < 1 || E < 1 || K < 1 || Ksel < 1 || Ksel > K) return 0;
+ int use_par = par && g_r_top8p && g_rtop8_width_ok && E<=256;
+ @autoreleasepool {
+ id bs=[g_dev newBufferWithBytes:sig length:(size_t)S*E*4 options:MTLResourceStorageModeShared];
+ id bb=[g_dev newBufferWithBytes:bias length:(size_t)E*4 options:MTLResourceStorageModeShared];
+ id bi=[g_dev newBufferWithLength:(size_t)S*K*4 options:MTLResourceStorageModeShared];
+ id bw=[g_dev newBufferWithLength:(size_t)S*K*4 options:MTLResourceStorageModeShared];
+ id bk=[g_dev newBufferWithLength:(size_t)S*4 options:MTLResourceStorageModeShared];
+ if(!bs||!bb||!bi||!bw||!bk) return 0;
+ memset(bi.contents,0xFF,(size_t)S*K*4); // poison: untouched slots stay visible
+ id cb=[g_queue commandBuffer]; id e=[cb computeCommandEncoder];
+ [e setComputePipelineState:use_par?g_r_top8p:g_r_top8];
+ [e setBuffer:bs offset:0 atIndex:0]; [e setBuffer:bb offset:0 atIndex:1];
+ [e setBuffer:bi offset:0 atIndex:2]; [e setBuffer:bw offset:0 atIndex:3]; [e setBuffer:bk offset:0 atIndex:4];
+ [e setBytes:&E length:4 atIndex:5]; [e setBytes:&K length:4 atIndex:6]; [e setBytes:&Ksel length:4 atIndex:7];
+ [e setBytes:&topp length:4 atIndex:8]; [e setBytes:&normk length:4 atIndex:9]; [e setBytes:&rscale length:4 atIndex:10];
+ if(use_par) [e dispatchThreadgroups:MTLSizeMake((NSUInteger)S,1,1) threadsPerThreadgroup:MTLSizeMake(32,1,1)];
+ else [e dispatchThreads:MTLSizeMake((NSUInteger)S,1,1) threadsPerThreadgroup:MTLSizeMake((NSUInteger)S,1,1)];
+ [e endEncoding]; [cb commit]; [cb waitUntilCompleted];
+ if(cb.status==MTLCommandBufferStatusError){ fprintf(stderr,"[metal] rtop8 cmdbuf error\n"); return 0; }
+ memcpy(idx,bi.contents,(size_t)S*K*4);
+ memcpy(w,bw.contents,(size_t)S*K*4);
+ memcpy(keff,bk.contents,(size_t)S*4);
+ }
+ return 1;
+}
+
extern "C" void coli_metal_tensor_free(ColiMetalTensor *t) {
if (!t) return;
g_tensor_count--; g_tensor_bytes -= t->wbytes;
diff --git a/c/tests/test_backend_metal.mm b/c/tests/test_backend_metal.mm
index ffdd39b..bff0444 100644
--- a/c/tests/test_backend_metal.mm
+++ b/c/tests/test_backend_metal.mm
@@ -177,6 +177,68 @@ static int run_attn(int S, int pos_base, const char* name){
return pass?0:1;
}
+// serial r_top8 vs parallel r_top8_par on the ENGINE build's own compiled shaders — the
+// exact-match contract (same indices, same order, same weights bitwise, same keff)
+// enforced with memcmp, per adversarial input family. `mode` selects the input
+// construction; see the inventory at the call sites in main(). E is a parameter (not
+// hardcoded 256) so the same helper drives both the original E=256 fuzz and the
+// expert-count-generality cases (E=24 <32-lane-width, E=168 REAP-pruned, E=200
+// lane-straddling boundary, E=257 out-of-contract auto-serial-fallback proof).
+static int run_rtop8(int mode, int S, int E, float topp, int normk, float rscale, const char *name) {
+ const int K=8, Ksel=8;
+ std::vector sig((size_t)S*E), bias(E);
+ srand(4242+mode*17+S+E);
+ for (int e=0;e=E-4)?1.0f:(float)(rand()%10000)/10000.f; break;
+ default: *v=(float)(rand()%10000)/10000.f; break;
+ }
+ }
+ if (mode==1) for (int e=0;e is((size_t)S*K), ip((size_t)S*K); std::vector ws((size_t)S*K), wp((size_t)S*K);
+ std::vector ks(S), kp(S);
+ if (!coli_metal_rtop8(0,sig.data(),bias.data(),S,E,K,Ksel,topp,normk,rscale,is.data(),ws.data(),ks.data()) ||
+ !coli_metal_rtop8(1,sig.data(),bias.data(),S,E,K,Ksel,topp,normk,rscale,ip.data(),wp.data(),kp.data())) {
+ printf(" %-34s FAIL (rtop8 runner returned 0)\n", name); return 1; }
+ int ok = memcmp(is.data(),ip.data(),(size_t)S*K*4)==0 &&
+ memcmp(ws.data(),wp.data(),(size_t)S*K*4)==0 && // bitwise: same ops, same order
+ memcmp(ks.data(),kp.data(),(size_t)S*4)==0;
+ if (mode==5 && ok) {
+ // Don't just trust the input design -- confirm the straddling lane's valid segment
+ // (E-4..E-1) was actually selected, in EVERY row, so this case can't silently
+ // degrade into an unrelated pass if the input construction above ever changes.
+ for (int s=0;s=E-4 && ip[(size_t)s*K+k]256, auto-serial-fallback)");
printf(fail? "metal backend tests: FAILED\n" : "metal backend tests: ok\n");
coli_metal_shutdown();
return fail;
From 9c5ab39b62b733c8746fced311ff64e9bbbba757 Mon Sep 17 00:00:00 2001
From: cdhdt
Date: Mon, 20 Jul 2026 01:27:28 +0200
Subject: [PATCH 43/71] rss_guard: free the evicted expert slab under
g_pilot_mx (use-after-free)
rss_guard marked the victim slot eid=-1 under the lock, unlocked, and only then
freed s->slab. In that window the slot reads as {eid=-1, slab still valid} --
exactly the state pilot_realload's victim scan reuses first -- so a pilot worker
could claim it and pread into the slab while it was being freed: use-after-free,
or a double-free if the loader took its own realloc path.
Keep the free and the pointer/capacity NULLing inside the critical section, so
'slab valid' and 'slot reusable' are never simultaneously observable. The lock is
held across a free() (microseconds); workers only take it briefly for scan+reserve.
Reproduces on dev with a SINGLE pilot worker (rss_guard runs on the main thread
while the pilot worker runs in the background), whenever the RAM guard is active
(RSS_GUARD_GB, or any resolved g_ram_budget_gb).
make check 83/83, native + portable builds 0 warnings.
---
c/colibri.c | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/c/colibri.c b/c/colibri.c
index 4b37fa8..8735a2e 100644
--- a/c/colibri.c
+++ b/c/colibri.c
@@ -4331,16 +4331,21 @@ static void rss_guard(Model *m){
if(lru<0){ pthread_mutex_unlock(&g_pilot_mx); continue; }
ESlot *s=&m->ecache[l][lru];
s->eid=-1; /* nascosto: nessun hit/evict altrui */
- pthread_mutex_unlock(&g_pilot_mx);
int64_t sb=s->slab_cap + s->fslab_cap*4;
#ifdef COLI_METAL
if(s->slab && g_metal_enabled) coli_metal_unregister(s->slab);
#endif
+ /* La free resta SOTTO g_pilot_mx. Sbloccando prima, lo slot e' visibile come
+ * {eid=-1, slab ancora valido}: il pilota (pilot_realload) riusa per primo gli
+ * slot eid==-1, quindi puo' prenderlo e fare pread dentro lo slab MENTRE lo
+ * liberiamo -> use-after-free / double-free. Slab valido e slot riusabile
+ * devono restare mutuamente esclusivi finche' il puntatore non e' NULL. */
compat_aligned_free(s->slab); free(s->fslab);
s->slab=NULL; s->fslab=NULL; s->slab_cap=s->fslab_cap=0;
QT *q[3]={&s->g,&s->u,&s->d};
for(int k=0;k<3;k++){ q[k]->qf=NULL; q[k]->q8=NULL; q[k]->q4=NULL; q[k]->s=NULL; }
s->used=0; /* primo candidato al riuso */
+ pthread_mutex_unlock(&g_pilot_mx);
freed += sb; dropped++;
}
if(m->ecap>2) m->ecap--; /* il tetto scende: niente ricrescita */
From a74e3e0c3a1375e602d5a61fff52e497a992d7e0 Mon Sep 17 00:00:00 2001
From: ZacharyZcR
Date: Mon, 20 Jul 2026 08:56:49 +0800
Subject: [PATCH 44/71] cuda: grouped-int4 (fmt=4) support in the expert-group
kernels (#334)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The grouped MoE kernels were per-row-only: GroupDesc had no group-size
fields, row_bytes() returned 0 for fmt=4, the scale buffer was hardcoded
to O floats, and a fmt=4 group that reached the generic path would have
been silently decoded as int2. This closed the GPU expert tier to every
grouped container — including the g64 quality line (#225) and the E8
lattice route (#347) whose whole point is fitting more experts in VRAM.
- ColiCudaTensor gains gs/scale_count; upload allocates O*ceil(I/gs)
scales for fmt=4 and applies the same offset->signed nibble conversion
as fmt=2 (identical packing). New ABI entry coli_cuda_tensor_upload_g
carries gs without touching the existing symbol — an old Windows DLL
missing it returns 0 and the tensor simply stays CPU-side.
- GroupDesc gains per-tensor group sizes; new grouped_hidden_g4_dual /
grouped_down_g4 apply the per-group scale inside the accumulation
(gs is required even, so a packed byte never straddles groups; gs=0
degrades to per-row, letting fmt=2 members ride the same launch).
- coli_cuda_expert_group routes any group containing fmt=4 through the
g4 kernels; pure-fmt=2 groups keep the existing paths byte-identical.
The generic fallback now explicitly rejects fmt=4 instead of decoding
garbage (#334's prevention note, made real).
tests/test_grouped_g4_cuda.cu: kernel-vs-CPU oracle over 50 trials x 3
experts — gs=64, a non-divisible tail group (200 % 64), and a per-row
member in the same launch: zero mismatches on a 5090.
make check 77/77; CPU, CUDA and MinGW builds clean.
---
c/backend_cuda.cu | 90 +++++++++++++++++++++++---
c/backend_cuda.h | 3 +
c/backend_loader.c | 8 +++
c/colibri.c | 5 ++
c/tests/test_grouped_g4_cuda.cu | 108 ++++++++++++++++++++++++++++++++
5 files changed, 206 insertions(+), 8 deletions(-)
create mode 100644 c/tests/test_grouped_g4_cuda.cu
diff --git a/c/backend_cuda.cu b/c/backend_cuda.cu
index 413e53b..47a8eab 100644
--- a/c/backend_cuda.cu
+++ b/c/backend_cuda.cu
@@ -20,6 +20,8 @@ struct ColiCudaTensor {
float *scales;
size_t weight_bytes;
int fmt, I, O, device;
+ int gs; /* quant group size; 0 = per-row scales (#334) */
+ size_t scale_count; /* floats in `scales`: O per-row, O*ng grouped */
int tracked;
RaggedKVEntry ragged[512];
int ragged_count;
@@ -43,6 +45,7 @@ typedef struct {
typedef struct {
const void *g,*u,*d; const float *gs,*us,*ds;
int gf,uf,df,rows,offset;
+ int ggs,ugs,dgs; /* per-tensor quant group size; 0 = per-row scales (#334 fmt=4) */
} GroupDesc;
static DeviceContext g_ctx[COLI_CUDA_MAX_DEVICES];
@@ -81,6 +84,7 @@ __host__ __device__ static size_t row_bytes(int fmt, int I) {
if (fmt == 1) return (size_t)I;
if (fmt == 2) return (size_t)(I + 1) / 2;
if (fmt == 3) return (size_t)(I + 3) / 4;
+ if (fmt == 4) return (size_t)(I + 1) / 2; /* grouped int4: nibbles like fmt 2 */
return 0;
}
@@ -296,6 +300,42 @@ __global__ static void grouped_down_w4(float *y,const float *x,const GroupDesc *
if(!threadIdx.x)y[(size_t)(d.offset+s)*D+o]=p[0]*d.ds[o];
}
+/* fmt=4 grouped-int4 variants (#334): identical structure to the w4 kernels,
+ * but the scale varies along the input dimension — sc[o*ng + i/gs], applied
+ * per element inside the accumulation (gs is even, so a packed byte never
+ * straddles a group). gs<=0 degrades to per-row (ng=1), so mixed fmt2/fmt4
+ * groups run correctly through this one kernel family. */
+__global__ static void grouped_hidden_g4_dual(float *gate,float *up,const float *x,
+ const GroupDesc *desc,int I,int D){
+ int o=blockIdx.x,s=blockIdx.y,c=blockIdx.z;GroupDesc d=desc[c];if(s>=d.rows)return;
+ const uint8_t *gr=(const uint8_t*)d.g+(size_t)o*((D+1)/2);
+ const uint8_t *ur=(const uint8_t*)d.u+(size_t)o*((D+1)/2);
+ int ggs=d.ggs>0?d.ggs:D, ugs=d.ugs>0?d.ugs:D;
+ const float *gsc=d.gs+(size_t)o*(size_t)((D+ggs-1)/ggs);
+ const float *usc=d.us+(size_t)o*(size_t)((D+ugs-1)/ugs);
+ const float *xs=x+(size_t)(d.offset+s)*D;float ga=0,ua=0;
+ for(int b=threadIdx.x;b<(D+1)/2;b+=blockDim.x){float g0,g1,u0,u1;unpack_s4(gr[b],&g0,&g1);unpack_s4(ur[b],&u0,&u1);
+ int i=b*2;float gv=gsc[i/ggs],uv=usc[i/ugs];
+ ga+=xs[i]*g0*gv;ua+=xs[i]*u0*uv;
+ if(i+1>=1){if(threadIdx.x=d.rows)return;
+ const uint8_t *row=(const uint8_t*)d.d+(size_t)o*((I+1)/2);
+ int dgs=d.dgs>0?d.dgs:I;
+ const float *dsc=d.ds+(size_t)o*(size_t)((I+dgs-1)/dgs);
+ const float *xs=x+(size_t)(d.offset+s)*I;float sum=0;
+ for(int b=threadIdx.x;b<(I+1)/2;b+=blockDim.x){float a,z;unpack_s4(row[b],&a,&z);
+ int i=b*2;float sv=dsc[i/dgs];
+ sum+=xs[i]*a*sv;if(i+1>=1){if(threadIdx.x(std::calloc(1, sizeof(*t)));
if (!t) return 0;
t->fmt = fmt; t->I = I; t->O = O; t->device = device; t->weight_bytes = rb * (size_t)O;
+ t->gs = (fmt==4 && g_upload_gs>0) ? g_upload_gs : 0;
+ t->scale_count = t->gs ? (size_t)O * (size_t)((I + t->gs - 1) / t->gs) : (size_t)O;
if (!cuda_ok(cudaMalloc(&t->weights, t->weight_bytes), "tensor allocation") ||
!cuda_ok(cudaMemcpy(t->weights, weights, t->weight_bytes, cudaMemcpyHostToDevice), "tensor upload")) {
coli_cuda_tensor_free(t);
return 0;
}
- if(fmt==2){offset_to_signed_s4<<<(unsigned)((t->weight_bytes+255)/256),256>>>((uint8_t*)t->weights,t->weight_bytes);
+ if(fmt==2||fmt==4){ /* same nibble layout: offset-binary -> signed in place */
+ offset_to_signed_s4<<<(unsigned)((t->weight_bytes+255)/256),256>>>((uint8_t*)t->weights,t->weight_bytes);
if(!cuda_ok(cudaGetLastError(),"int4 weight conversion")){coli_cuda_tensor_free(t);return 0;}}
if (fmt) {
- if (!cuda_ok(cudaMalloc(&t->scales, (size_t)O * sizeof(float)), "scale allocation") ||
- !cuda_ok(cudaMemcpy(t->scales, scales, (size_t)O * sizeof(float), cudaMemcpyHostToDevice), "scale upload")) {
+ if (!cuda_ok(cudaMalloc(&t->scales, t->scale_count * sizeof(float)), "scale allocation") ||
+ !cuda_ok(cudaMemcpy(t->scales, scales, t->scale_count * sizeof(float), cudaMemcpyHostToDevice), "scale upload")) {
coli_cuda_tensor_free(t);
return 0;
}
}
t->tracked = 1;
ctx->tensor_count++;
- ctx->tensor_bytes += t->weight_bytes + (fmt ? (size_t)O * sizeof(float) : 0);
+ ctx->tensor_bytes += t->weight_bytes + (fmt ? t->scale_count * sizeof(float) : 0);
*tensor = t;
return 1;
}
+extern "C" int coli_cuda_tensor_upload_g(ColiCudaTensor **tensor,
+ const void *weights, const float *scales,
+ int fmt, int I, int O, int device, int gs){
+ g_upload_gs = gs>0 ? gs : 0;
+ int r = coli_cuda_tensor_upload(tensor, weights, scales, fmt, I, O, device);
+ g_upload_gs = 0;
+ return r;
+}
extern "C" int coli_cuda_tensor_update(ColiCudaTensor *tensor,
const void *weights,
@@ -546,13 +604,14 @@ extern "C" int coli_cuda_tensor_update(ColiCudaTensor *tensor,
if (!select_ctx(ctx)) return 0;
if (!cuda_ok(cudaMemcpy(tensor->weights,weights,tensor->weight_bytes,
cudaMemcpyHostToDevice),"tensor refresh")) return 0;
- if(tensor->fmt==2){
+ if(tensor->fmt==2||tensor->fmt==4){
offset_to_signed_s4<<<(unsigned)((tensor->weight_bytes+255)/256),256>>>(
(uint8_t*)tensor->weights,tensor->weight_bytes);
if(!cuda_ok(cudaGetLastError(),"int4 weight refresh")) return 0;
}
return !tensor->fmt || cuda_ok(cudaMemcpy(tensor->scales,scales,
- (size_t)tensor->O*sizeof(float),cudaMemcpyHostToDevice),"scale refresh");
+ (tensor->scale_count?tensor->scale_count:(size_t)tensor->O)*sizeof(float),
+ cudaMemcpyHostToDevice),"scale refresh");
}
extern "C" int coli_cuda_matmul(ColiCudaTensor **tensor,
@@ -641,14 +700,18 @@ extern "C" int coli_cuda_expert_group(ColiCudaTensor *const *gates,
if (!first) return 0;
int device=first->device,D=first->I,I=first->O,total=0,max_rows=0;
GroupDesc host[64]; if(count>64) return 0;
- int all_s4=1;
+ int all_s4=1,all_q4=1,any_g4=0;
for(int c=0;cdevice!=device||u->device!=device||d->device!=device||
g->I!=D||u->I!=D||g->O!=I||u->O!=I||d->I!=I||d->O!=D) return 0;
host[c]={g->weights,u->weights,d->weights,g->scales,u->scales,d->scales,
- g->fmt,u->fmt,d->fmt,rows[c],total};
+ g->fmt,u->fmt,d->fmt,rows[c],total,
+ g->gs,u->gs,d->gs};
all_s4&=g->fmt==2&&u->fmt==2&&d->fmt==2;
+ all_q4&=(g->fmt==2||g->fmt==4)&&(u->fmt==2||u->fmt==4)&&(d->fmt==2||d->fmt==4)&&
+ !(g->gs&1)&&!(u->gs&1)&&!(d->gs&1); /* even gs: a packed byte never straddles groups */
+ any_g4|=g->fmt==4||u->fmt==4||d->fmt==4;
total+=rows[c]; if(rows[c]>max_rows) max_rows=rows[c];
}
DeviceContext *ctx=find_ctx(device); if(!select_ctx(ctx)) return 0;
@@ -730,7 +793,18 @@ extern "C" int coli_cuda_expert_group(ColiCudaTensor *const *gates,
}
silu_mul<<<(unsigned)(((size_t)total*I+255)/256),256,0,ctx->stream>>>(ctx->gate,ctx->up,(size_t)total*I);
grouped_down_w4<<stream>>>(ctx->y,ctx->gate,dev,D,I);
+ }else if(all_q4&&any_g4){
+ /* grouped-int4 (fmt=4) present: per-group scales (#334). fmt=2 members
+ * ride along as the ng=1 special case. */
+ dim3 hg((unsigned)I,(unsigned)max_rows,(unsigned)count),og((unsigned)D,(unsigned)max_rows,(unsigned)count);
+ grouped_hidden_g4_dual<<stream>>>(ctx->gate,ctx->up,ctx->x,dev,I,D);
+ silu_mul<<<(unsigned)(((size_t)total*I+255)/256),256,0,ctx->stream>>>(ctx->gate,ctx->up,(size_t)total*I);
+ grouped_down_g4<<stream>>>(ctx->y,ctx->gate,dev,D,I);
}else{
+ /* generic path decodes fmt 0/1/2/3 only — a fmt=4 group that slipped the
+ * gates above (odd gs) must NOT be silently decoded as int2 (#334). */
+ for(int c=0;cstream>>>(ctx->gate,ctx->x,dev,I,D,0);
grouped_hidden<<stream>>>(ctx->up,ctx->x,dev,I,D,1);
diff --git a/c/backend_cuda.h b/c/backend_cuda.h
index 63c1f11..c311b9c 100644
--- a/c/backend_cuda.h
+++ b/c/backend_cuda.h
@@ -36,6 +36,9 @@ COLI_CUDA_DLLEXPORT void coli_cuda_group_stats(uint64_t *calls, uint64_t *expert
double *h2d_ms, double *kernel_ms, double *d2h_ms);
/* Upload without executing, so capacity failures happen during model startup. */
+COLI_CUDA_DLLEXPORT int coli_cuda_tensor_upload_g(ColiCudaTensor **tensor,
+ const void *weights, const float *scales,
+ int fmt, int I, int O, int device, int gs);
COLI_CUDA_DLLEXPORT int coli_cuda_tensor_upload(ColiCudaTensor **tensor,
const void *weights, const float *scales,
int fmt, int I, int O, int device);
diff --git a/c/backend_loader.c b/c/backend_loader.c
index eedbd50..1002d3e 100644
--- a/c/backend_loader.c
+++ b/c/backend_loader.c
@@ -46,6 +46,7 @@ typedef int (*fn_attention_absorb)(ColiCudaTensor *kv_b, float *ctx,
int R, int V, int K, int T, float attention_scale);
typedef int (*fn_tensor_upload)(ColiCudaTensor **tensor, const void *weights,
const float *scales, int fmt, int I, int O, int device);
+typedef int (*fn_tensor_upload_g)(ColiCudaTensor **tensor, const void *weights, const float *scales, int fmt, int I, int O, int device, int gs);
typedef int (*fn_matmul)(ColiCudaTensor **tensor, float *y, const float *x,
const void *weights, const float *scales,
int fmt, int S, int I, int O, int device);
@@ -102,6 +103,7 @@ static struct {
fn_expert_group expert_group;
fn_attention_absorb attention_absorb;
fn_tensor_upload tensor_upload;
+ fn_tensor_upload_g tensor_upload_g;
fn_matmul matmul;
fn_tensor_free tensor_free;
fn_tensor_bytes tensor_bytes;
@@ -196,6 +198,7 @@ static int coli_cuda_load(void){
RESOLVE(expert_group, fn_expert_group)
RESOLVE(attention_absorb, fn_attention_absorb)
RESOLVE(tensor_upload, fn_tensor_upload)
+ RESOLVE(tensor_upload_g, fn_tensor_upload_g)
RESOLVE(matmul, fn_matmul)
RESOLVE(tensor_free, fn_tensor_free)
RESOLVE(tensor_bytes, fn_tensor_bytes)
@@ -302,6 +305,11 @@ int coli_cuda_tensor_upload(ColiCudaTensor **tensor, const void *weights,
return g_cuda.tensor_upload(tensor, weights, scales, fmt, I, O, device);
}
+int coli_cuda_tensor_upload_g(ColiCudaTensor **tensor, const void *weights, const float *scales, int fmt, int I, int O, int device, int gs){
+ if(!g_cuda.available || !g_cuda.tensor_upload_g){ return 0; }
+ return g_cuda.tensor_upload_g(tensor, weights, scales, fmt, I, O, device, gs);
+}
+
int coli_cuda_matmul(ColiCudaTensor **tensor, float *y, const float *x,
const void *weights, const float *scales,
int fmt, int S, int I, int O, int device){
diff --git a/c/colibri.c b/c/colibri.c
index 03eb878..30a3f7b 100644
--- a/c/colibri.c
+++ b/c/colibri.c
@@ -235,6 +235,11 @@ static void qt_cuda_reset(QT *t){
static int qt_cuda_upload(QT *t){
const void *weights = t->fmt==0 ? (const void*)t->qf
: t->fmt==1 ? (const void*)t->q8 : (const void*)t->q4;
+ if(t->fmt==4) /* grouped int4 (#334): scales are [O, ceil(I/gs)] — the plain
+ * upload would truncate them to O floats and the group kernels
+ * would read garbage. An old DLL without the _g symbol returns 0
+ * and the tensor simply stays CPU-side. */
+ return coli_cuda_tensor_upload_g(&t->cuda,weights,t->s,t->fmt,t->I,t->O,t->cuda_device,t->gs);
return coli_cuda_tensor_upload(&t->cuda,weights,t->s,t->fmt,t->I,t->O,t->cuda_device);
}
static int qt_cuda_update(QT *t){
diff --git a/c/tests/test_grouped_g4_cuda.cu b/c/tests/test_grouped_g4_cuda.cu
new file mode 100644
index 0000000..b370cdd
--- /dev/null
+++ b/c/tests/test_grouped_g4_cuda.cu
@@ -0,0 +1,108 @@
+/* Grouped-int4 (fmt=4) CUDA kernel oracle (#334).
+ *
+ * Feeds random offset-binary nibble weights + [O, ng] group scales through
+ * grouped_hidden_g4_dual / grouped_down_g4 and checks against a CPU reference
+ * that replicates matmul_i4_grouped's semantics (value = nibble - 8, per-group
+ * partial dot x scale). Covers gs=64, a non-divisible tail group, and a
+ * per-row (gs=0) member riding in the same launch — the fmt=2-compat case.
+ *
+ * The device buffers get the same XOR 0x88 offset->signed conversion the
+ * upload path applies, so the kernels are exercised exactly as deployed.
+ *
+ * Build: nvcc -O2 -std=c++17 -arch=native tests/test_grouped_g4_cuda.cu -o tests/test_grouped_g4
+ */
+#include
+#include
+#include
+#include
+#include
+
+#include "../backend_cuda.cu"
+
+static void cpu_gemv_g4(const uint8_t *q,const float *sc,int K,int O,int gs,
+ const float *x,float *y){
+ int rb=(K+1)/2, ng=gs>0?(K+gs-1)/gs:1, egs=gs>0?gs:K;
+ for(int o=0;oK) glen=K-base;
+ double p=0;
+ for(int i=base;i>1]; int n=(i&1)?(v>>4):(v&15);
+ p+=(double)x[i]*(n-8);
+ }
+ a+=p*scl[g];
+ }
+ y[o]=(float)a;
+ }
+}
+
+int main(void){
+ srand(7);
+ const int D=200, I=96, gs=64; /* tail group: 200 % 64 = 8 */
+ const int COUNT=3; /* expert 0,1: fmt4 gs=64; expert 2: per-row (gs=0) */
+ const int rbD=(D+1)/2, rbI=(I+1)/2;
+ const int ngD=(D+gs-1)/gs, ngI=(I+gs-1)/gs;
+ int trials=50, bad=0;
+ for(int t=0;t>>(qg[c],(size_t)I*rbD);
+ offset_to_signed_s4<<<64,256>>>(qu[c],(size_t)I*rbD);
+ offset_to_signed_s4<<<64,256>>>(qd[c],(size_t)D*rbI);
+ cudaMemcpy(sg[c],hgs[c],(size_t)I*cngD*4,cudaMemcpyHostToDevice);
+ cudaMemcpy(su[c],hus[c],(size_t)I*cngD*4,cudaMemcpyHostToDevice);
+ cudaMemcpy(sd[c],hds[c],(size_t)D*cngI*4,cudaMemcpyHostToDevice);
+ host[c]={qg[c],qu[c],qd[c],sg[c],su[c],sd[c],4,4,4,1,c,cgs,cgs,cgs};
+ }
+ for(size_t i=0;i<(size_t)COUNT*D;i++) xs[i]=(rand()/(float)RAND_MAX-.5f)*2.f;
+ GroupDesc *ddesc; cudaMalloc(&ddesc,sizeof(host));
+ cudaMemcpy(ddesc,host,sizeof(host),cudaMemcpyHostToDevice);
+ dim3 hgd((unsigned)I,1,(unsigned)COUNT),ogd((unsigned)D,1,(unsigned)COUNT);
+ grouped_hidden_g4_dual<<>>(gate,up,xs,ddesc,I,D);
+ grouped_down_g4<<>>(y,gate,ddesc,D,I);
+ if(cudaDeviceSynchronize()!=cudaSuccess){ printf("FAIL cuda\n"); return 1; }
+ for(int c=0;c1e-3f*(fabsf(rg[o])+1e-3f)||
+ fabsf(up[(size_t)c*I+o]-ru[o])>1e-3f*(fabsf(ru[o])+1e-3f)) bad++;
+ }
+ cpu_gemv_g4(hd[c],hds[c],I,D,cgs,(float*)gate+(size_t)c*I,ry);
+ for(int o=0;o1e-3f*(fabsf(ry[o])+1e-3f)) bad++;
+ }
+ for(int c=0;c
Date: Mon, 20 Jul 2026 00:47:57 +0800
Subject: [PATCH 45/71] =?UTF-8?q?numa:=20per-layer=20pin=20arenas=20?=
=?UTF-8?q?=E2=80=94=20fix=20the=20PIN=5FGB=3Dall=20VMA=20explosion=20(#41?=
=?UTF-8?q?9)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Root cause of #419's 'OOM slab': every per-slab mbind carries its own
memory policy, so bound regions cannot merge — measured ~2 VMAs per slab,
with or without MPOL_MF_MOVE. A PIN_GB=all load (19,456 experts x
slab+fslab) creates ~78k VMAs and crosses the default
vm.max_map_count=65530: posix_memalign dies with terabytes free.
The fix binds the pinned hot-store as ONE arena per layer. Experts of a
layer share a tensor shape, so a layer's pins pack at a fixed stride into
two arenas (weights + scales): 2 mbinds and a handful of VMAs per layer
instead of ~500. Slices are pre-attached to the slots before the load
loop — slab_cap covers expert_load's realloc check, so its alloc branch
never fires and expert_load itself is untouched. aslab marks arena
ownership: expert_host_release detaches instead of freeing (a REPIN
gpu-swap promotion must not free() an interior pointer), and
expert_host_ensure re-attaches the slice before reloading.
Per-slab mbind remains for the bounded allocations (dense qalloc, LRU
ecache, GPU-tier staging), now without MPOL_MF_MOVE: every bind lands
before the pread that first-touches the pages, so there is nothing to
migrate.
numa_init also gains a capability probe done right: one page-aligned
page (mbind rejects unaligned addresses with EINVAL), disabling only on
errno==EPERM — so a constrained container degrades with a message
instead of crashing later, and an EINVAL can never masquerade as a
missing capability.
The arena path activates only when interleave is actually on
(g_numa_nodes>=2, Linux, non-mmap): default builds stay byte-identical.
---
c/colibri.c | 106 ++++++++++++++++++++++++++++++++++++++++++++++++----
1 file changed, 98 insertions(+), 8 deletions(-)
diff --git a/c/colibri.c b/c/colibri.c
index 9528ee9..20086b0 100644
--- a/c/colibri.c
+++ b/c/colibri.c
@@ -133,7 +133,12 @@ typedef struct {
* slab_cap/fslab_cap: capienza allocata — gli slot ws[] sono riusati TRA layer e gli
* expert non hanno tutti la stessa taglia (layer MTP int8 = 2x i layer int4). */
typedef struct { int eid; QT g,u,d; uint8_t *slab; float *fslab;
- int64_t slab_cap, fslab_cap; uint64_t used; } ESlot;
+ int64_t slab_cap, fslab_cap; uint64_t used;
+ /* pin-arena backing (#419): when set, slab/fslab are interior
+ * slices of a per-layer arena and must never be free()d —
+ * expert_host_release detaches them, expert_host_ensure
+ * re-attaches. NULL for every individually-allocated slot. */
+ uint8_t *aslab; float *afslab; } ESlot;
typedef struct {
float **Lc, **Rc, **Ic;
@@ -549,8 +554,17 @@ static int g_disk_split=0; /* DISK_SPLIT=1: contatori che spezzano i DISK LOAD (
* +40% on a 4-socket (#82). Blanket `numactl --interleave=all` is NOT equivalent:
* it also interleaves the CUDA pinned staging buffers and cost a 4-socket GPU host
* 10x (#82) — hence per-region mbind here and nothing else. Raw syscall, no libnuma
- * dependency; MPOL_MF_MOVE migrates pages of reused heap chunks too. Linux-only,
- * silent no-op elsewhere or on single-node hosts. */
+ * dependency. Linux-only, silent no-op elsewhere or on single-node hosts.
+ *
+ * VMA discipline (#419): every mbind carries its own memory policy, so a bound
+ * region cannot merge with its neighbours — measured ~2 VMAs per slab, with or
+ * without MPOL_MF_MOVE. Per-slab binds on a PIN_GB=all load (19,456 experts x
+ * slab+fslab) cross the default vm.max_map_count=65530 and posix_memalign dies
+ * with terabytes free. So the bulk (the pinned hot-store) is bound as ONE arena
+ * per layer (see pin_load), and per-slab mbind remains only for the bounded
+ * allocations: dense qalloc, the LRU ecache, and GPU-tier staging. No flag:
+ * every bind here lands before the pread that first-touches the pages, so
+ * there is nothing to migrate. */
#ifdef __linux__
static int g_numa_nodes=0; /* only touched under __linux__; off-Linux NUMA is a no-op */
#endif
@@ -561,7 +575,7 @@ static void numa_slab_bind(void *p, size_t n){
uintptr_t a=(uintptr_t)p & ~(uintptr_t)4095;
size_t len=(((uintptr_t)p+n+4095) & ~(uintptr_t)4095) - a;
syscall(SYS_mbind,a,len,3/*MPOL_INTERLEAVE*/,&mask,
- (unsigned long)(g_numa_nodes+1),(unsigned)2/*MPOL_MF_MOVE*/);
+ (unsigned long)(g_numa_nodes+1),0);
#else
(void)p;(void)n;
#endif
@@ -571,8 +585,25 @@ static void numa_init(void){
if(!getenv("COLI_NUMA")||!atoi(getenv("COLI_NUMA"))) return;
for(int i=0;i<64;i++){ char pth[64]; snprintf(pth,sizeof(pth),"/sys/devices/system/node/node%d",i);
struct stat st; if(stat(pth,&st)) break; g_numa_nodes=i+1; }
- if(g_numa_nodes>=2) fprintf(stderr,"[NUMA] expert slabs interleaved across %d nodes\n",g_numa_nodes);
- else fprintf(stderr,"[NUMA] single node: COLI_NUMA ignored\n");
+ if(g_numa_nodes<2){ fprintf(stderr,"[NUMA] single node: COLI_NUMA ignored\n"); return; }
+ /* Probe mbind once so a constrained container degrades with a message
+ * instead of silently losing the interleave. The probe page must be
+ * page-aligned (mbind rejects unaligned addresses with EINVAL) and only
+ * errno==EPERM disables — any other failure keeps NUMA on. */
+ { void *pg=NULL;
+ if(!posix_memalign(&pg,4096,4096)){
+ unsigned long mask=(1UL<slab); free(s->fslab); s->slab=NULL; s->fslab=NULL; s->slab_cap=s->fslab_cap=0;
+ if(s->aslab){ s->slab=NULL; s->fslab=NULL; } /* arena slice (#419): detach, keep caps, never free */
+ else { compat_aligned_free(s->slab); free(s->fslab); s->slab=NULL; s->fslab=NULL; s->slab_cap=s->fslab_cap=0; }
QT *q[3]={&s->g,&s->u,&s->d};
for(int k=0;k<3;k++){ q[k]->qf=NULL; q[k]->q8=NULL; q[k]->q4=NULL; q[k]->s=NULL; }
m->resident_bytes-=bytes; if(m->resident_bytes<0) m->resident_bytes=0;
}
static void expert_host_ensure(Model *m, int layer, ESlot *s){
- if(!s->slab) expert_load(m,layer,s->eid,s,1,0); /* re-materializing a GPU-resident expert's host copy, not a routing miss: demand=0 */
+ if(s->slab) return;
+ if(s->aslab){ s->slab=s->aslab; s->fslab=s->afslab; } /* re-attach the arena slice; caps survived release */
+ /* re-materializing a GPU-resident expert's host copy, not a routing miss: demand=0 */
+ expert_load(m,layer,s->eid,s,1,0); /* rebuild the QT views (release NULLed them) + reload */
}
#endif
@@ -4960,6 +4995,56 @@ typedef struct { int l,e; uint32_t c; } PinRec;
static int pin_rec_cmp(const void *a,const void *b){
const PinRec *x=a,*y=b; return x->cc?1:x->c>y->c?-1:0;
}
+
+#ifdef __linux__
+/* #419: bind the pinned hot-store as ONE arena per layer instead of one mbind
+ * per slab. Per-slab policies cost ~2 unmergeable VMAs each; a PIN_GB=all load
+ * (19,456 experts x slab+fslab) crosses the default vm.max_map_count=65530 and
+ * posix_memalign dies with terabytes free. Experts of one layer share a tensor
+ * shape, so a layer's pins pack into two arenas (weights + scales) at a fixed
+ * stride: 2 binds and a handful of VMAs per layer instead of ~500. Slices are
+ * pre-attached to the slots (slab_cap covers expert_load's realloc check, so
+ * its alloc branch never fires); aslab marks arena ownership for the
+ * release/ensure paths. Arena-OOM just leaves the slots on the individual path. */
+static void pin_arena_bind(Model *m, PinRec *r, int *slot_of, int from, int to){
+ if(g_numa_nodes<2 || g_mmap || from>=to) return;
+ Cfg *c=&m->c; int NR=c->n_layers+1;
+ int *cnt=calloc((size_t)NR,sizeof(int)); int *first=malloc((size_t)NR*sizeof(int));
+ if(!cnt||!first){ free(cnt); free(first); return; }
+ for(int i=0;iS,nm), *tq=st_find(&m->S,qn);
+ if(!tw||!tq) ok=0; else { wtot+=tw->nbytes; qtot+=tq->nbytes; }
+ }
+ if(!ok) continue; /* unquantized fallback: individual allocs */
+ size_t ws=((size_t)wtot+8192+4095)&~(size_t)4095;
+ size_t fs=((size_t)(qtot/4)*sizeof(float)+4095)&~(size_t)4095;
+ uint8_t *aw=NULL; float *af=NULL;
+ if(posix_memalign((void**)&aw,4096,(size_t)cnt[l]*ws)) continue;
+ if(posix_memalign((void**)&af,4096,(size_t)cnt[l]*fs)){ free(aw); continue; }
+ numa_slab_bind(aw,(size_t)cnt[l]*ws);
+ numa_slab_bind(af,(size_t)cnt[l]*fs);
+ int i=0;
+ for(int a=from;apin[l][slot_of[a]];
+ s->slab=aw+(size_t)i*ws; s->slab_cap=(int64_t)ws; s->aslab=s->slab;
+ s->fslab=(float*)((uint8_t*)af+(size_t)i*fs);
+ s->fslab_cap=(int64_t)(fs/sizeof(float)); s->afslab=s->fslab;
+ i++;
+ }
+ }
+ free(cnt); free(first);
+}
+#endif
static double expert_avail(Model *m, double ram_gb, int ebits, int max_ctx); /* def. sotto */
static void pin_load(Model *m, const char *statspath, double gb){
FILE *f=fopen(statspath,"r"); if(!f){ perror(statspath); return; }
@@ -5034,6 +5119,11 @@ static void pin_load(Model *m, const char *statspath, double gb){
if(prefix_est>0){ gpu_prefix=prefix_est; if(gpu_prefix>npin) gpu_prefix=npin; }
#else
int gpu_prefix=0;
+#endif
+#ifdef __linux__
+ /* CPU-resident pins only: the GPU prefix stays on individual allocs (its
+ * host backing is released after upload; arena slices are never freed). */
+ pin_arena_bind(m,r,slot_of,gpu_prefix,npin);
#endif
/* Load the VRAM-ranked prefix first. Once uploaded its host backing is
* released before the disjoint RAM-ranked suffix is allocated. */
From 57e67d6c2fa914c2da176ba4f0857b086794e306 Mon Sep 17 00:00:00 2001
From: ZacharyZcR
Date: Mon, 20 Jul 2026 01:21:21 +0800
Subject: [PATCH 46/71] numa: skip binding the GPU-prefix staging slabs
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The VRAM-ranked prefix's host slabs are upload staging — read once and
freed right after — so binding them buys nothing and cost ~2 transient
VMAs each: measured PEAK maps 28,958 on the six-GPU host even with the
pin arenas in place. With the skip: 11,771, all of it the bounded LRU
ecache, which serves decode reads and stays correctly bound.
---
c/colibri.c | 11 ++++++++++-
1 file changed, 10 insertions(+), 1 deletion(-)
diff --git a/c/colibri.c b/c/colibri.c
index 20086b0..20c65da 100644
--- a/c/colibri.c
+++ b/c/colibri.c
@@ -567,10 +567,13 @@ static int g_disk_split=0; /* DISK_SPLIT=1: contatori che spezzano i DISK LOAD (
* there is nothing to migrate. */
#ifdef __linux__
static int g_numa_nodes=0; /* only touched under __linux__; off-Linux NUMA is a no-op */
+static int g_numa_skip_bind=0; /* raised around the GPU-prefix pin load: those slabs are
+ * upload staging, freed right after — binding them buys
+ * nothing and costs ~2 transient VMAs each (#419) */
#endif
static void numa_slab_bind(void *p, size_t n){
#ifdef __linux__
- if(g_numa_nodes<2 || !p || !n) return;
+ if(g_numa_nodes<2 || g_numa_skip_bind || !p || !n) return;
unsigned long mask=(1UL<0) g_numa_skip_bind=1; /* prefix slabs = transient upload staging: don't bind (#419) */
+#endif
#pragma omp parallel for schedule(dynamic,1)
for(int a=0;a<(gpu_prefix?gpu_prefix:npin);a++)
expert_load(m,r[a].l,r[a].e,&m->pin[r[a].l][slot_of[a]],1,0); /* startup pin load; demand=0, never classified */
+#ifdef __linux__
+ g_numa_skip_bind=0;
+#endif
m->resident_bytes+=(int64_t)(gpu_prefix?gpu_prefix:npin)*eb;
#ifdef COLI_CUDA
if(g_cuda_enabled && budget>0){
From ae4e31a15af1e19cec6fc169d0917c5a2130bd77 Mon Sep 17 00:00:00 2001
From: ZacharyZcR
Date: Mon, 20 Jul 2026 12:55:44 +0800
Subject: [PATCH 47/71] tools: IQ3_XXS-codebook scheme in the quant ablation
(#452 step 1)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Adds `-iq3` to the ablation harness: a faithful torch model of llama.cpp's
deployed 3.06-bpw IQ3_XXS format — 256-entry 4-dim magnitude grid
(extracted from ggml-common.h, MIT), signs factored per 8 weights with
the odd-parity constraint priced in (a violating block flips its
smallest-magnitude sign), fp16 super-scale per 256 + 4-bit sub-scale per
32 searched over all 16 codes. Nearest-grid search runs as chunked
matmul-argmin (|g|^2 - 2 q.g) — a full cdist materializes tens of GB on
a 100M-param tensor and OOMed the first run.
Measured (OLMoE-1B-7B, n=200 x hellaswag/arc/mmlu; the first four rows
reproduce the published ablations exactly):
fp16 58.0%
int4 per-row 48.7% (-9.3pp, the shipped container's scheme)
int3-g64 50.5% (-7.5pp)
int3-g64-e8-rot 51.5% (-6.5pp, simulated rate-scaled ball)
int3-iq3 49.3% (-8.7pp)
int3-iq3-rot 51.5% (-6.5pp)
The deployable IQ3 codebook plus rotation exactly ties the simulated E8
ball — that settles #452's codebook decision toward the IQ3-style block
structure, with rotation mandatory (worth 2.2pp on this codebook).
---
c/tools/iq3xxs_grid.json | 1 +
c/tools/quant_ablation.py | 70 ++++++++++++++++++++++++++++++++++++++-
2 files changed, 70 insertions(+), 1 deletion(-)
create mode 100644 c/tools/iq3xxs_grid.json
diff --git a/c/tools/iq3xxs_grid.json b/c/tools/iq3xxs_grid.json
new file mode 100644
index 0000000..3cde1a2
--- /dev/null
+++ b/c/tools/iq3xxs_grid.json
@@ -0,0 +1 @@
+[[4, 4, 4, 4], [20, 4, 4, 4], [36, 4, 4, 4], [12, 12, 4, 4], [28, 12, 4, 4], [62, 12, 4, 4], [4, 20, 4, 4], [20, 20, 4, 4], [12, 28, 4, 4], [20, 36, 4, 4], [28, 62, 4, 4], [44, 62, 4, 4], [12, 4, 12, 4], [28, 4, 12, 4], [4, 12, 12, 4], [20, 12, 12, 4], [12, 20, 12, 4], [44, 20, 12, 4], [4, 28, 12, 4], [20, 28, 12, 4], [12, 36, 12, 4], [36, 44, 12, 4], [4, 62, 12, 4], [4, 4, 20, 4], [20, 4, 20, 4], [36, 4, 20, 4], [12, 12, 20, 4], [4, 20, 20, 4], [20, 20, 20, 4], [12, 28, 20, 4], [28, 28, 20, 4], [62, 28, 20, 4], [12, 44, 20, 4], [62, 44, 20, 4], [44, 62, 20, 4], [12, 4, 28, 4], [62, 4, 28, 4], [4, 12, 28, 4], [20, 12, 28, 4], [44, 20, 28, 4], [4, 62, 28, 4], [28, 12, 36, 4], [62, 28, 36, 4], [36, 36, 36, 4], [62, 44, 36, 4], [28, 62, 36, 4], [44, 62, 36, 4], [12, 4, 44, 4], [62, 4, 44, 4], [20, 28, 44, 4], [20, 44, 44, 4], [44, 28, 52, 4], [36, 52, 52, 4], [4, 12, 62, 4], [36, 12, 62, 4], [52, 12, 62, 4], [28, 36, 62, 4], [12, 52, 62, 4], [12, 4, 4, 12], [28, 4, 4, 12], [4, 12, 4, 12], [20, 12, 4, 12], [12, 20, 4, 12], [28, 20, 4, 12], [4, 28, 4, 12], [20, 28, 4, 12], [36, 28, 4, 12], [62, 36, 4, 12], [4, 44, 4, 12], [4, 4, 12, 12], [20, 4, 12, 12], [12, 12, 12, 12], [4, 20, 12, 12], [20, 20, 12, 12], [12, 4, 20, 12], [28, 4, 20, 12], [4, 12, 20, 12], [20, 12, 20, 12], [12, 20, 20, 12], [4, 28, 20, 12], [20, 62, 20, 12], [4, 4, 28, 12], [20, 4, 28, 12], [4, 20, 28, 12], [12, 28, 28, 12], [52, 36, 28, 12], [52, 52, 28, 12], [12, 4, 36, 12], [44, 4, 36, 12], [4, 44, 36, 12], [4, 20, 44, 12], [36, 20, 44, 12], [52, 36, 44, 12], [12, 62, 44, 12], [44, 4, 52, 12], [20, 20, 62, 12], [4, 36, 62, 12], [4, 4, 4, 20], [20, 4, 4, 20], [12, 12, 4, 20], [28, 12, 4, 20], [4, 20, 4, 20], [20, 20, 4, 20], [52, 20, 4, 20], [12, 28, 4, 20], [20, 36, 4, 20], [12, 4, 12, 20], [28, 4, 12, 20], [44, 4, 12, 20], [4, 12, 12, 20], [20, 12, 12, 20], [12, 20, 12, 20], [4, 28, 12, 20], [28, 52, 12, 20], [62, 52, 12, 20], [4, 62, 12, 20], [4, 4, 20, 20], [20, 4, 20, 20], [12, 12, 20, 20], [62, 12, 20, 20], [4, 20, 20, 20], [20, 20, 20, 20], [62, 28, 20, 20], [4, 36, 20, 20], [44, 44, 20, 20], [12, 4, 28, 20], [4, 12, 28, 20], [36, 12, 28, 20], [4, 62, 28, 20], [36, 62, 28, 20], [44, 28, 36, 20], [28, 44, 36, 20], [28, 4, 44, 20], [62, 20, 44, 20], [12, 36, 44, 20], [36, 62, 44, 20], [12, 4, 62, 20], [28, 4, 62, 20], [52, 12, 62, 20], [44, 36, 62, 20], [12, 4, 4, 28], [4, 12, 4, 28], [20, 12, 4, 28], [12, 20, 4, 28], [28, 20, 4, 28], [4, 44, 4, 28], [44, 52, 4, 28], [20, 62, 4, 28], [4, 4, 12, 28], [20, 4, 12, 28], [4, 20, 12, 28], [12, 28, 12, 28], [36, 36, 12, 28], [52, 36, 12, 28], [12, 4, 20, 28], [28, 4, 20, 28], [4, 12, 20, 28], [44, 20, 20, 28], [20, 44, 20, 28], [20, 62, 20, 28], [12, 12, 28, 28], [28, 28, 28, 28], [4, 28, 36, 28], [62, 36, 36, 28], [20, 62, 36, 28], [4, 4, 44, 28], [52, 4, 44, 28], [20, 20, 44, 28], [44, 44, 44, 28], [36, 12, 52, 28], [52, 28, 52, 28], [28, 52, 52, 28], [28, 28, 62, 28], [4, 52, 62, 28], [36, 4, 4, 36], [62, 12, 4, 36], [44, 28, 4, 36], [62, 28, 4, 36], [28, 44, 4, 36], [62, 44, 4, 36], [36, 62, 12, 36], [4, 20, 20, 36], [62, 28, 20, 36], [4, 36, 20, 36], [4, 52, 20, 36], [52, 52, 20, 36], [62, 4, 28, 36], [44, 36, 28, 36], [36, 4, 36, 36], [12, 44, 36, 36], [36, 52, 36, 36], [44, 20, 44, 36], [28, 36, 44, 36], [4, 62, 44, 36], [44, 4, 62, 36], [4, 12, 62, 36], [20, 12, 62, 36], [4, 28, 62, 36], [20, 12, 4, 44], [12, 36, 4, 44], [4, 62, 4, 44], [4, 4, 12, 44], [52, 4, 12, 44], [52, 20, 12, 44], [44, 44, 12, 44], [36, 12, 20, 44], [20, 28, 20, 44], [20, 62, 20, 44], [20, 4, 28, 44], [28, 44, 28, 44], [4, 12, 36, 44], [28, 20, 36, 44], [62, 20, 36, 44], [20, 62, 36, 44], [20, 4, 44, 44], [12, 28, 44, 44], [4, 44, 52, 44], [36, 20, 62, 44], [20, 36, 62, 44], [36, 20, 4, 52], [36, 36, 4, 52], [52, 36, 4, 52], [36, 52, 4, 52], [12, 20, 12, 52], [12, 52, 12, 52], [62, 12, 20, 52], [36, 52, 20, 52], [4, 28, 28, 52], [52, 28, 28, 52], [36, 36, 36, 52], [44, 4, 44, 52], [20, 44, 44, 52], [28, 28, 52, 52], [28, 4, 62, 52], [12, 20, 62, 52], [28, 4, 4, 62], [44, 4, 4, 62], [62, 4, 4, 62], [4, 12, 4, 62], [20, 28, 4, 62], [20, 44, 4, 62], [52, 20, 12, 62], [4, 36, 12, 62], [20, 12, 20, 62], [44, 36, 20, 62], [20, 44, 20, 62], [4, 4, 28, 62], [44, 12, 28, 62], [28, 28, 28, 62], [4, 52, 28, 62], [12, 20, 36, 62], [12, 36, 36, 62], [4, 4, 44, 62], [20, 4, 44, 62], [36, 20, 44, 62], [4, 28, 52, 62]]
\ No newline at end of file
diff --git a/c/tools/quant_ablation.py b/c/tools/quant_ablation.py
index dd08715..a602ae8 100644
--- a/c/tools/quant_ablation.py
+++ b/c/tools/quant_ablation.py
@@ -117,11 +117,79 @@ def quantize_param(w, bits, group, rot=False, e8=""):
def _grid_or_e8(x, bits, group, e8):
+ if e8 == "-iq3":
+ return _quant_iq3(x.float())
if e8:
return _quant_e8(x.float(), group, bits, ball=(e8 == "-e8"))
return _quant_last_dim(x, bits, group)
+# --------------------------------------------------------------------------------------
+# IQ3_XXS-style codebook (#452 candidate (a)): llama.cpp's deployed 3.06-bpw scheme.
+# 4-dim magnitude blocks quantized to a 256-entry lattice-subset grid (magnitudes on the
+# odd ladder 4,12,..,62 in half-units), signs factored out per 8 weights with an odd-parity
+# constraint (7 stored + 1 derived: a block whose true signs violate parity gets its
+# smallest-magnitude sign flipped — modelled here so the ablation pays the real cost).
+# Scales: fp16 super-scale per 256 + 4-bit sub-scale per 32, db = d*(0.5+s)*0.5.
+# Grid extracted from ggml-common.h (MIT).
+# --------------------------------------------------------------------------------------
+_IQ3_GRID = None
+def _iq3_grid(device):
+ global _IQ3_GRID
+ if _IQ3_GRID is None or _IQ3_GRID.device != device:
+ import json, os
+ path = os.path.join(os.path.dirname(__file__), "iq3xxs_grid.json")
+ _IQ3_GRID = torch.tensor(json.load(open(path)), dtype=torch.float32, device=device)
+ return _IQ3_GRID # [256,4], half-unit magnitudes (value/2 = weight units)
+
+def _quant_iq3(x):
+ orig = x.shape
+ K = orig[-1]
+ assert K % 256 == 0, "iq3 needs multiples of 256 along the input dim"
+ xb = x.reshape(-1, 256) # super-blocks
+ grid = _iq3_grid(x.device) * 0.5 # weight units
+ out = torch.empty_like(xb)
+ signs = torch.sign(xb); signs[signs == 0] = 1.0
+ mags = xb.abs()
+ for sb in range(8): # 8 sub-blocks of 32
+ m = mags[:, sb*32:(sb+1)*32] # [N,32]
+ s = signs[:, sb*32:(sb+1)*32]
+ # per-8 sign parity: flip the smallest-|w| sign where the product is negative
+ s8 = s.reshape(-1, 4, 8)
+ m8 = m.reshape(-1, 4, 8)
+ viol = (s8.prod(-1) < 0) # odd number of minus signs
+ idxmin = m8.argmin(-1)
+ flip = torch.zeros_like(s8)
+ flip.scatter_(-1, idxmin[..., None], 1.0)
+ s8 = torch.where(viol[..., None].expand_as(s8) & (flip > 0), -s8, s8)
+ s = s8.reshape(-1, 32)
+ # sub-scale search: db candidates from the 4-bit code, super d from block RMS
+ d = m.pow(2).mean(-1, keepdim=True).sqrt() / 20.0 + 1e-12 # rough anchor
+ best = None
+ for code in range(16):
+ db = d * (0.5 + code) * 0.5
+ q = m / db # [N,32] target magnitudes
+ q4 = q.reshape(-1, 4) # 4-dim grid blocks
+ # chunked argmin ||q-g||^2 = argmin(|g|^2 - 2 q.g): a full cdist on a
+ # 100M-param tensor materializes tens of GB — this stays at ~256 MB.
+ g2 = grid.pow(2).sum(-1)
+ idx = torch.empty(q4.shape[0], dtype=torch.long, device=q4.device)
+ CH = 1 << 18
+ for i0 in range(0, q4.shape[0], CH):
+ cc = q4[i0:i0+CH]
+ idx[i0:i0+CH] = (g2 - 2.0 * (cc @ grid.T)).argmin(-1)
+ hit = grid[idx].reshape(-1, 8, 4)
+ rec = (hit.reshape(-1, 32) * db)
+ err = (rec - m).pow(2).sum(-1, keepdim=True)
+ if best is None:
+ best = (err, rec)
+ else:
+ take = err < best[0]
+ best = (torch.where(take, err, best[0]), torch.where(take, rec, best[1]))
+ out[:, sb*32:(sb+1)*32] = best[1] * s
+ return out.reshape(orig)
+
+
def _rot_quant(x, bits, group, e8=""):
"""W -> Qn(W@Q) @ Q^T along the last (input) dim — see rotation() above."""
q = rotation(x.shape[-1], x.device)
@@ -206,7 +274,7 @@ def _quant_e8(x, group, bits, ball):
return best_out.reshape(shp)
-SCHEME_RE = re.compile(r"^int(2|3|4|8)(?:-g(\d+))?(-e8u?)?(-rot)?(-nohead)?$")
+SCHEME_RE = re.compile(r"^int(2|3|4|8)(?:-g(\d+))?(-e8u?|-iq3)?(-rot)?(-nohead)?$")
def parse_scheme(name):
From 42a5417c137a96dc5dabd664bfa6031518f496fd Mon Sep 17 00:00:00 2001
From: monotophic
Date: Mon, 20 Jul 2026 00:19:41 -0400
Subject: [PATCH 48/71] convert: mirror the --indir resume params guard onto
--repo download loops
Upstream courtesy fix, found while auditing the conversion recipe for this
branch -- pre-existing in dev, not introduced by int3-g64, but directly
relevant to anyone converting for real (a #383-class gap: same failure
family as the resume/manifest work already done for --indir, and the
silent-mixing mechanism issue #355 fixed for a narrower case).
The --indir path already refuses to resume with different conversion
parameters on the same --outdir (a manifest records ebits/xbits/io_bits/
group_size/n_layers/bits_map and compares on every resume). The --repo
streaming download loops (main model, --mtp, --indexer) never got the
same guard: each shard's resume check is just `if os.path.exists(outp):
continue` -- true whether or not THIS run's flags match the flags that
produced that shard. A --repo conversion resumed with changed bits
(--xbits 3 -> 4 mid-run, say, after an interruption) would silently mix
bit-widths across shards in the same container, with no error and no log
line distinguishing it from a normal resume.
Fix: check_or_record_params(), a small shared helper mirroring the
--indir manifest's refuse-on-mismatch logic but without needing its
per-shard bookkeeping (the --repo loops already track shard completion
correctly via out-NNNNN.safetensors existence, since shard index maps
directly to output filename there -- only whether the params used SO FAR
still match needed adding). Applied to all three --repo loops with
per-mode sidecar files (.out-mtp-params.json / .out-idx-params.json /
.out-params.json) so a --mtp and a main-model conversion into the same
--outdir don't cross-check each other's parameters.
Also includes PROJ_BITS (the per-projection expert bit overrides) in the
tracked params dict on BOTH paths -- it was missing from --indir's
existing manifest too, so a resume with a changed --up-bits/--gate-bits/
--down-bits would have passed the existing guard silently.
Verified directly (no real HF downloads; --repo network paths can't be
exercised under this task's constraints): unit-tested
check_or_record_params() standalone -- fresh outdir accepts and records,
a same-params resume accepts, a changed --xbits is refused, and a
proj_bits-only change (nothing else different) is refused. Re-ran the
existing --indir dry-run end to end (convert, resume, resume-with-changed-
xbits) to confirm the manifest-based path still works correctly with
proj_bits added to its params dict.
Gates: make test-c (20/20) and make test-python (85/85) both pass.
---
c/tools/convert_fp8_to_int4.py | 41 +++++++++++++++++++++++++++++++++-
1 file changed, 40 insertions(+), 1 deletion(-)
diff --git a/c/tools/convert_fp8_to_int4.py b/c/tools/convert_fp8_to_int4.py
index eac51f9..75f2164 100644
--- a/c/tools/convert_fp8_to_int4.py
+++ b/c/tools/convert_fp8_to_int4.py
@@ -247,6 +247,32 @@ def convert_shard(path, out_dict, n_layers, ebits, io_bits, xbits,
def free_gb(p): return shutil.disk_usage(p).free / 1e9
+def check_or_record_params(outdir, prefix, params):
+ """#383-class guard, mirrored onto the --repo download loops from the --indir
+ path's resume manifest (below): a resumed run with DIFFERENT conversion
+ parameters (bits, group size, PROJ_BITS, ...) must not silently mix bit-widths
+ across shards in the same outdir -- the #355 failure mode (a second pass with
+ changed flags overwriting/interleaving with a finished container in silence).
+ Unlike the --indir manifest this doesn't need to track per-shard completion:
+ the --repo loops already do that via out-NNNNN.safetensors existence, since
+ shard index maps directly to output filename there. Only whether the params
+ used SO FAR match this run's needs checking. Returns False (caller should
+ abort) on a mismatch, True otherwise; records params on first use."""
+ path = os.path.join(outdir, f".{prefix}params.json")
+ if os.path.exists(path):
+ try: prev = json.loads(open(path).read())
+ except (OSError, ValueError): prev = None
+ if prev is not None and prev != params:
+ print(f"ERROR: {path} records a conversion with {prev};\n"
+ f" this run uses {params}. Refusing to mix conversions in the "
+ f"same outdir — use a fresh --outdir (or delete {path} and the "
+ f"{prefix}*.safetensors shards to redo).")
+ return False
+ tmp = path + ".tmp"
+ with open(tmp, "w") as f: json.dump(params, f, indent=1) # atomic write, same reasoning as the --indir manifest
+ os.replace(tmp, path)
+ return True
+
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--repo", default=None)
@@ -440,7 +466,8 @@ def main():
# EN: resume skips only what matches, and different parameters on the same
# EN: outdir are refused instead of mixing containers (the #355 failure mode).
params = {"ebits": a.ebits, "io_bits": a.io_bits, "xbits": a.xbits,
- "group_size": a.group_size, "n_layers": a.n_layers, "bits_map": bits_map}
+ "group_size": a.group_size, "n_layers": a.n_layers, "bits_map": bits_map,
+ "proj_bits": dict(PROJ_BITS)}
prog_path = os.path.join(a.outdir, f".{prefix}progress.json")
prog = {}
if os.path.exists(prog_path):
@@ -718,6 +745,10 @@ def main():
except Exception: pass
tmp = os.path.join(a.outdir, "_inflight"); os.makedirs(tmp, exist_ok=True)
if a.mtp:
+ params = {"ebits": a.ebits, "io_bits": a.io_bits, "xbits": a.xbits,
+ "group_size": a.group_size, "n_layers": a.n_layers, "bits_map": bits_map,
+ "proj_bits": dict(PROJ_BITS)}
+ if not check_or_record_params(a.outdir, "out-mtp-", params): return
import urllib.request
idx = json.loads(urllib.request.urlopen(
f"https://huggingface.co/{a.repo}/resolve/main/model.safetensors.index.json", timeout=30).read())["weight_map"]
@@ -737,6 +768,10 @@ def main():
print(f" -> {os.path.basename(outp)} ({os.path.getsize(outp)/1e9:.2f} GB, {len(out)} tensors)", flush=True)
shutil.rmtree(tmp, ignore_errors=True); print("[MTP] DONE."); return
if a.indexer:
+ params = {"ebits": a.ebits, "io_bits": a.io_bits, "xbits": a.xbits,
+ "group_size": a.group_size, "n_layers": a.n_layers, "bits_map": bits_map,
+ "proj_bits": dict(PROJ_BITS)}
+ if not check_or_record_params(a.outdir, "out-idx-", params): return
import urllib.request
idx = json.loads(urllib.request.urlopen(
f"https://huggingface.co/{a.repo}/resolve/main/model.safetensors.index.json", timeout=30).read())["weight_map"]
@@ -756,6 +791,10 @@ def main():
if os.path.isfile(blob): os.remove(blob)
print(f" -> {os.path.basename(outp)} ({len(out)} tensors)", flush=True)
shutil.rmtree(tmp, ignore_errors=True); print("[IDX] DONE."); return
+ params = {"ebits": a.ebits, "io_bits": a.io_bits, "xbits": a.xbits,
+ "group_size": a.group_size, "n_layers": a.n_layers, "bits_map": bits_map,
+ "proj_bits": dict(PROJ_BITS)}
+ if not check_or_record_params(a.outdir, "out-", params): return
for i, sh in enumerate(shards):
if free_gb(a.outdir) < a.min_free_gb:
print(f"STOP: free space is below {a.min_free_gb} GB. Free space and rerun to resume."); break
From e48346162a7cf3cd9ca8e00069fd46d66157a1cd Mon Sep 17 00:00:00 2001
From: ZacharyZcR
Date: Mon, 20 Jul 2026 22:22:46 +0800
Subject: [PATCH 49/71] =?UTF-8?q?tools:=20fmt=3D5=20index=20codec=20?=
=?UTF-8?q?=E2=80=94=20deployable=20bytes=20for=20the=20E8/IQ3=20container?=
=?UTF-8?q?=20(#452=20step=202)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
#453 settled the scheme; this produces the actual bytes. iq3_pack.encode/decode
implement the container layout so the converter, the engine and the decode
kernels can all be written against one spec:
98 bytes per 256 weights = 3.0625 bpw
[ 0..63] uint8 grid index per 4-dim magnitude block
[64..95] uint32 x8 — four 7-bit sign words + 4-bit sub-scale per 32
[96..97] fp16 super-scale
Signs use the published odd-parity trick: 7 of every 8 are stored and the 8th
is derived, so the encoder flips the smallest-magnitude weight of any block
whose true signs would violate parity — the cost the #453 ablation priced in,
now actually paid. Sub-scale is searched over all 16 codes against the stored
(fp16-rounded) super-scale, so encode-time and decode-time arithmetic agree.
GLM-5.2 routed experts under this container: 372.7 -> 281.2 GB (-24.6%), and a
176 GB VRAM tier holds ~12,180 experts instead of ~9,190 (+33%).
tests/test_iq3_pack.py: byte budget, deterministic encode, decode checked
value-by-value against an independent loop-based reader written straight from
the layout, sign-parity closure, and reconstruction quality in the band the
chosen scheme measured (rel-RMSE 0.195 vs the torch model's 0.195).
---
c/tests/test_iq3_pack.py | 88 ++++++++++++++++++++
c/tools/iq3_pack.py | 170 +++++++++++++++++++++++++++++++++++++++
2 files changed, 258 insertions(+)
create mode 100644 c/tests/test_iq3_pack.py
create mode 100644 c/tools/iq3_pack.py
diff --git a/c/tests/test_iq3_pack.py b/c/tests/test_iq3_pack.py
new file mode 100644
index 0000000..fd0a10a
--- /dev/null
+++ b/c/tests/test_iq3_pack.py
@@ -0,0 +1,88 @@
+"""fmt=5 codec oracle (#452 ladder step 2).
+
+Checks the properties the container, the converter and the decode kernels all
+depend on: exact byte budget, deterministic encode, decode agreeing with a
+straight-from-the-spec reader, sign parity closure, and reconstruction quality
+matching the ablation that chose this scheme (#453).
+"""
+import os
+import sys
+import unittest
+
+import numpy as np
+
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "tools"))
+import iq3_pack as P # noqa: E402
+
+
+def ref_decode(packed, K):
+ """Independent reader written straight from the layout comment — deliberately
+ naive and loop-based, so a shared bug in the vectorized path shows up."""
+ g = P.grid()
+ nsb = K // P.QK
+ rows = packed.reshape(-1, nsb * P.BLOCK_BYTES)
+ out = np.zeros((len(rows), K), dtype=np.float32)
+ for r in range(len(rows)):
+ for sb in range(nsb):
+ base = sb * P.BLOCK_BYTES
+ d = float(rows[r, base + 96:base + 98].copy().view(np.float16)[0])
+ for ib in range(P.QK // P.SUB):
+ word = int(np.ascontiguousarray(
+ rows[r, base + 64 + ib * 4:base + 64 + ib * 4 + 4]).view(np.uint32)[0])
+ db = d * (0.5 + ((word >> 28) & 0xF)) * 0.5
+ for l in range(4):
+ seven = (word >> (7 * l)) & 0x7F
+ bits = [(seven >> j) & 1 for j in range(7)]
+ bits.append(sum(bits) & 1) # odd parity closes the 8th
+ idx = int(rows[r, base + ib * 8 + l * 2 + 0])
+ idx2 = int(rows[r, base + ib * 8 + l * 2 + 1])
+ mags = list(g[idx]) + list(g[idx2])
+ for j in range(8):
+ pos = sb * P.QK + ib * P.SUB + l * 8 + j
+ out[r, pos] = mags[j] * db * (-1.0 if bits[j] else 1.0)
+ return out
+
+
+class TestIq3Pack(unittest.TestCase):
+ def setUp(self):
+ np.random.seed(4242)
+ self.x = (np.random.randn(6, 1024) * 0.05).astype(np.float32)
+
+ def test_byte_budget(self):
+ self.assertEqual(P.BLOCK_BYTES, 98)
+ self.assertAlmostEqual(P.bpw(), 3.0625, places=6)
+ packed = P.encode(self.x)
+ self.assertEqual(packed.shape, (6, 1024 // P.QK * 98))
+ self.assertEqual(packed.dtype, np.uint8)
+
+ def test_encode_is_deterministic(self):
+ self.assertTrue(np.array_equal(P.encode(self.x), P.encode(self.x)))
+
+ def test_decode_matches_spec_reader(self):
+ packed = P.encode(self.x)
+ fast = P.decode(packed, 1024)
+ slow = ref_decode(packed, 1024)
+ self.assertTrue(np.allclose(fast, slow, rtol=1e-6, atol=1e-8),
+ f"max |Δ| = {np.abs(fast - slow).max()}")
+
+ def test_sign_parity_closes(self):
+ """Every 8-weight block must have an even number of negatives — that is
+ what lets the 8th sign be derived instead of stored."""
+ y = P.decode(P.encode(self.x), 1024)
+ neg = (y < 0).reshape(-1, 8).sum(-1)
+ self.assertTrue(np.all(neg % 2 == 0), "a block stored odd negatives")
+
+ def test_reconstruction_quality(self):
+ y = P.decode(P.encode(self.x), 1024)
+ rel = np.sqrt(((y - self.x) ** 2).mean()) / np.sqrt((self.x ** 2).mean())
+ # the torch model that won the #453 A/B measures ~0.195 on this input class
+ self.assertLess(rel, 0.25, f"rel-RMSE {rel:.4f} — worse than the chosen scheme")
+ self.assertGreater(rel, 0.05, f"rel-RMSE {rel:.4f} — implausibly good, check the test")
+
+ def test_shape_guard(self):
+ with self.assertRaises(ValueError):
+ P.encode(np.zeros((2, 300), dtype=np.float32))
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/c/tools/iq3_pack.py b/c/tools/iq3_pack.py
new file mode 100644
index 0000000..03d139b
--- /dev/null
+++ b/c/tools/iq3_pack.py
@@ -0,0 +1,170 @@
+#!/usr/bin/env python3
+"""fmt=5 (E8/IQ3 grouped container) index codec — #452 ladder step 2.
+
+The ablation (#453) proved the SCHEME: an IQ3_XXS-style codebook plus rotation
+matches our simulated E8 ball (51.5% vs 51.5% on OLMoE). That code quantizes to
+lattice points and keeps floats. This module produces the DEPLOYABLE bytes and
+reads them back, so the container, the converter and the decode kernels all
+agree on one layout.
+
+Layout — one 256-weight super-block, 98 bytes, 3.0625 bpw:
+
+ [0 .. 63] uint8 grid index per 4-dim magnitude block (64 blocks)
+ [64 .. 95] uint32 x8, one per 32-weight sub-block:
+ bits 0..20 three 7-bit sign words (8 weights each,
+ bit i set => weight i negative; the 8th sign
+ is implied by odd parity)
+ bits 21..27 the fourth 7-bit sign word
+ bits 28..31 4-bit sub-scale code
+ [96 .. 97] fp16 super-scale d
+
+ value(w) = d * (0.5 + code) * 0.5 * grid[idx][j] * 0.5 * sign
+
+The last 0.5 is the half-unit convention of the published grid (magnitudes are
+stored doubled: 4,12,...,62 mean 2.0,6.0,...,31.0).
+
+Odd-parity signs: llama.cpp stores 7 of every 8 signs and derives the 8th so the
+product of the eight is +1. The encoder therefore flips the smallest-magnitude
+weight of any block whose true signs violate that — the same cost the ablation
+priced in, now applied for real.
+"""
+import json
+import os
+import numpy as np
+
+QK = 256 # weights per super-block
+SUB = 32 # weights per sub-block (one uint32 of signs+scale)
+BLOCK_BYTES = QK // 4 + (QK // SUB) * 4 + 2 # 64 + 32 + 2 = 98
+
+_GRID = None
+
+
+def grid():
+ """[256,4] float32 magnitudes in weight units (published table is doubled)."""
+ global _GRID
+ if _GRID is None:
+ path = os.path.join(os.path.dirname(__file__), "iq3xxs_grid.json")
+ _GRID = np.asarray(json.load(open(path)), dtype=np.float32) * 0.5
+ return _GRID
+
+
+def _nearest(mag4):
+ """[N,4] magnitudes -> [N] grid indices, argmin ||m-g||^2 without cdist."""
+ g = grid()
+ g2 = (g * g).sum(1)
+ out = np.empty(len(mag4), dtype=np.uint8)
+ for i in range(0, len(mag4), 1 << 16): # bounded working set
+ c = mag4[i:i + (1 << 16)]
+ out[i:i + len(c)] = np.argmin(g2 - 2.0 * (c @ g.T), axis=1).astype(np.uint8)
+ return out
+
+
+def encode(x):
+ """float32 [..., K] (K % 256 == 0) -> packed uint8 [..., K//256 * 98]."""
+ x = np.ascontiguousarray(x, dtype=np.float32)
+ K = x.shape[-1]
+ if K % QK:
+ raise ValueError(f"fmt=5 needs K % {QK} == 0, got {K}")
+ rows = x.reshape(-1, K)
+ nsb = K // QK
+ out = np.empty((len(rows), nsb * BLOCK_BYTES), dtype=np.uint8)
+
+ for sb in range(nsb):
+ blk = rows[:, sb * QK:(sb + 1) * QK] # [R,256]
+ sign = np.where(blk < 0, -1.0, 1.0).astype(np.float32)
+ mag = np.abs(blk)
+
+ # parity fix: flip the smallest magnitude of every 8 whose product is -1
+ s8 = sign.reshape(len(rows), QK // 8, 8)
+ m8 = mag.reshape(len(rows), QK // 8, 8)
+ viol = s8.prod(-1) < 0 # [R,32]
+ amin = m8.argmin(-1)
+ r, b = np.nonzero(viol)
+ s8[r, b, amin[r, b]] *= -1.0
+ sign = s8.reshape(len(rows), QK)
+
+ base = sb * BLOCK_BYTES
+ # super-scale: RMS anchor, same statistic the ablation searches around
+ d = np.sqrt((mag * mag).mean(-1, keepdims=True)) / 20.0 + 1e-12
+ out[:, base + 96:base + 98] = d.astype(np.float16).view(np.uint8)
+ d = d.astype(np.float16).astype(np.float32) # encode what we store
+
+ g = grid()
+ for ib in range(QK // SUB):
+ m = mag[:, ib * SUB:(ib + 1) * SUB] # [R,32]
+ best_err = None
+ best = None
+ for code in range(16):
+ db = d * (0.5 + code) * 0.5
+ q = (m / np.maximum(db, 1e-20)).reshape(-1, 4)
+ idx = _nearest(q)
+ rec = g[idx].reshape(len(rows), SUB) * db
+ err = ((rec - m) ** 2).sum(-1, keepdims=True)
+ if best_err is None:
+ best_err, best = err, (idx.reshape(len(rows), SUB // 4), code)
+ else:
+ take = (err < best_err)[:, 0]
+ if take.any():
+ keep_idx, keep_code = best
+ ni = idx.reshape(len(rows), SUB // 4)
+ keep_idx = np.where(take[:, None], ni, keep_idx)
+ # per-row code: store alongside, resolved below
+ keep_code = np.where(take, code, keep_code) if isinstance(
+ keep_code, np.ndarray) else np.where(
+ take, code, np.full(len(rows), keep_code))
+ best = (keep_idx, keep_code)
+ best_err = np.where(take[:, None], err, best_err)
+ bidx, bcode = best
+ if not isinstance(bcode, np.ndarray):
+ bcode = np.full(len(rows), bcode)
+ out[:, base + ib * 8:base + (ib + 1) * 8] = bidx.astype(np.uint8)
+
+ # signs: four 7-bit words for this sub-block + the 4-bit code
+ s = sign[:, ib * SUB:(ib + 1) * SUB].reshape(len(rows), 4, 8)
+ neg = (s < 0).astype(np.uint32)
+ word = np.zeros(len(rows), dtype=np.uint32)
+ for l in range(4):
+ seven = np.zeros(len(rows), dtype=np.uint32)
+ for j in range(7):
+ seven |= neg[:, l, j] << j
+ word |= seven << (7 * l)
+ word |= (bcode.astype(np.uint32) & 0xF) << 28
+ off = base + QK // 4 + ib * 4
+ out[:, off:off + 4] = word.view(np.uint8).reshape(len(rows), 4) if False else \
+ np.ascontiguousarray(word).view(np.uint8).reshape(len(rows), 4)
+ return out.reshape(*x.shape[:-1], nsb * BLOCK_BYTES)
+
+
+def decode(packed, K):
+ """packed uint8 [..., K//256*98] -> float32 [..., K]. The kernels' reference."""
+ packed = np.ascontiguousarray(packed, dtype=np.uint8)
+ nsb = K // QK
+ rows = packed.reshape(-1, nsb * BLOCK_BYTES)
+ out = np.empty((len(rows), K), dtype=np.float32)
+ g = grid()
+ for sb in range(nsb):
+ base = sb * BLOCK_BYTES
+ d = rows[:, base + 96:base + 98].copy().view(np.float16).astype(np.float32)
+ for ib in range(QK // SUB):
+ idx = rows[:, base + ib * 8:base + (ib + 1) * 8] # [R,8]
+ off = base + QK // 4 + ib * 4
+ word = np.ascontiguousarray(rows[:, off:off + 4]).view(np.uint32).reshape(-1)
+ code = (word >> 28) & 0xF
+ db = d[:, 0] * (0.5 + code) * 0.5 # [R]
+ mag = g[idx].reshape(len(rows), SUB) # [R,32]
+ sgn = np.ones((len(rows), 4, 8), dtype=np.float32)
+ for l in range(4):
+ seven = (word >> (7 * l)) & 0x7F
+ par = 0
+ for j in range(7):
+ bit = (seven >> j) & 1
+ sgn[:, l, j] = np.where(bit == 1, -1.0, 1.0)
+ par ^= bit
+ sgn[:, l, 7] = np.where(par == 1, -1.0, 1.0) # odd parity closes the block
+ out[:, sb * QK + ib * SUB:sb * QK + (ib + 1) * SUB] = \
+ mag * sgn.reshape(len(rows), SUB) * db[:, None]
+ return out.reshape(*packed.shape[:-1], K)
+
+
+def bpw():
+ return BLOCK_BYTES * 8 / QK
From d43b54534f4f6847fbcd0a8600a2766868df6c79 Mon Sep 17 00:00:00 2001
From: woolcoxm <13604288+woolcoxm@users.noreply.github.com>
Date: Fri, 17 Jul 2026 10:52:41 -0400
Subject: [PATCH 50/71] =?UTF-8?q?tests:=20efficiency=20suite=20=E2=80=94?=
=?UTF-8?q?=20tiny-model=20regression=20gates=20+=20full-model=20optimizat?=
=?UTF-8?q?ion=20dossier?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Two layers of efficiency coverage for the engine, both parsing the telemetry
glm.c already emits (REPLAY/PROFILE/[PROF]/CUDA-tier) but nothing previously
asserted on:
1. test_inefficiency.py — tiny-model asserted regression tests (8 tests, run
in make test via test-python). Gate on: throughput floor, PROFILE phase
accounting sanity, disk-wait not dominant on a resident model, CPU greedy
determinism, and (when a CUDA build is present) CUDA init, dense VRAM
upload, and CPU-vs-CUDA argmax agreement >= 70%. CUDA tests auto-skip with
a clear build hint on CPU-only binaries.
2. test_efficiency_report.py — opt-in optimization dossier for a real model.
Turns on every instrumentation flag (PROF, COLI_CUDA_PROFILE, CACHE_ROUTE,
DISK_SPLIT, LOOKA) and prints 9 sections (provenance, throughput + tail
latency, where-time-goes, attention breakdown, expert cache, disk I/O +
phase split, routing quality + predictability, speculation, GPU tiers),
each flagging inefficiency with the concrete knob to move tok/s. Never
fails CI.
tools/efficiency.py is the shared harness: parse_run() captures every signal,
run_engine() wraps the subprocess. Reuses PROFILE_RE/SPEED_RE from
tools/benchmark_cuda_fixture.py and extends the tok/s regex to also catch the
run_text (parenthesized) format the full-model PROMPT path uses.
Makefile adds: efficiency / efficiency-cuda / efficiency-report targets.
Verified end-to-end on the full glm52_i4_g64 model (CPU + CUDA).
---
c/Makefile | 23 ++
c/tests/README_efficiency.md | 98 +++++++
c/tests/test_efficiency_report.py | 315 ++++++++++++++++++++
c/tests/test_inefficiency.py | 239 ++++++++++++++++
c/tools/efficiency.py | 458 ++++++++++++++++++++++++++++++
5 files changed, 1133 insertions(+)
create mode 100644 c/tests/README_efficiency.md
create mode 100644 c/tests/test_efficiency_report.py
create mode 100644 c/tests/test_inefficiency.py
create mode 100644 c/tools/efficiency.py
diff --git a/c/Makefile b/c/Makefile
index af17357..1289968 100644
--- a/c/Makefile
+++ b/c/Makefile
@@ -363,6 +363,29 @@ test-python:
test: test-c test-python
+# --- Efficiency / regression suite (issue: "test the program for inefficiencies") ---
+# The tiny-model assertions live in test_inefficiency.py and run as part of
+# test-python (they're discovered by the test_*.py glob). These targets are
+# convenience entry points; the opt-in full-model report is NEVER in `make test`.
+#
+# make efficiency tiny-model asserted regression tests (CPU; part of test-python)
+# make efficiency-cuda the CUDA-path tests (requires a CUDA build — see below)
+# make efficiency-report opt-in full-model 🟢/🔴 diagnostic, never fails CI
+#
+# CUDA build (Windows): the CUDA tests need a host built with -DCOLI_CUDA plus
+# the runtime DLL. Do this FIRST — note CUDA_DLL=1 on BOTH the host and the
+# rule below, or `make glm.exe` will rebuild a CPU-only host and overwrite it:
+# make clean && make glm.exe CUDA_DLL=1 && make cuda-dll
+# The tests auto-skip with a clear message if the host is CPU-only.
+efficiency: test-python
+ $(PYTHON) -m unittest tests.test_inefficiency -v
+
+efficiency-cuda:
+ $(PYTHON) -m unittest tests.test_inefficiency.TinyCudaEfficiencyTest -v
+
+efficiency-report:
+ $(PYTHON) tests/test_efficiency_report.py
+
# Local validation: one portable CPU build and dependency-free tests.
check:
$(MAKE) clean
diff --git a/c/tests/README_efficiency.md b/c/tests/README_efficiency.md
new file mode 100644
index 0000000..1f8d339
--- /dev/null
+++ b/c/tests/README_efficiency.md
@@ -0,0 +1,98 @@
+# Efficiency suite — regression tests + optimization dossier
+
+Two layers:
+
+1. **`test_inefficiency.py`** — tiny-model *asserted* regression tests. Fast
+ (~0.15s/run), gate CI, catch breakage. Run as part of `make test`.
+2. **`test_efficiency_report.py`** — an *opt-in optimization dossier* for a real
+ model. Runs every instrumentation flag, prints a 9-section report answering
+ *what is doing what, when, with what, is it inefficient, how to improve*.
+ Never fails CI (it's a report, not a gate).
+
+## The dossier (what you run when optimizing)
+
+```bash
+# CPU-only (safe, fast to validate):
+COLI_EFFICIENCY_MODEL=../glm52_i4_g64 make efficiency-report
+
+# CUDA (dense + expert tiers — needs a CUDA build, see below):
+COLI_EFFICIENCY_MODEL=../glm52_i4_g64 COLI_EFFICIENCY_CUDA=1 make efficiency-report
+```
+
+It turns ON every observability flag the engine supports — `PROF=1`,
+`COLI_CUDA_PROFILE=1`, `CACHE_ROUTE=1` (auto-unlocks `route_agree`/`route_kl`),
+`DISK_SPLIT=1`, `LOOKA=1` — so nothing the engine can tell you is left dark.
+None of these change the computed output; they only add telemetry.
+
+The 9 sections, and the question each answers:
+
+| § | section | answers |
+|---|---|---|
+| 1 | PROVENANCE | what is running, on what CPU/backend, with what effective config |
+| 2 | THROUGHPUT | tok/s + forward-latency p50/p90/p99/max (is the tail healthy?) |
+| 3 | WHERE TIME GOES | the 5 PROFILE phases as % of decode + absolute seconds + verdict |
+| 3a | ATTENTION BREAKDOWN | attention split into projection/RoPE, score-softmax-value, output |
+| 4 | EXPERT CACHE | hit %, experts-loaded/token vs baseline topk |
+| 5 | DISK I/O | GB fetched, MB/token, GB/s, read-service vs felt-wait, phase split |
+| 5a | DISK-LOAD SPLIT | loads by decode phase (draft/absorb/verify) + MTP-vs-main bytes |
+| 6 | ROUTING QUALITY | route_agree %, route_kl, cache swaps |
+| 6a | ROUTING PREDICTABILITY | LOOKAHEAD recall per predictor (which prefetch wins) |
+| 7 | SPECULATION | tokens/forward, MTP acceptance % |
+| 8 | GPU TIERS | resident tensors, expert tier (count/GB/calls), H2D/kernel/D2H ms |
+
+Every line that crosses an advisory threshold is marked `[FLAG]` with the
+concrete lever to pull (raise RAM_GB, add PIN_GB, try DIRECT=1, lower CTX, …),
+and all flags repeat in a summary at the end.
+
+## Tunable thresholds
+
+The `IS IT INEFFICIENT?` lines are advisory constants at the top of
+`test_efficiency_report.py`:
+
+| constant | default | meaning |
+|---|---|---|
+| `DISK_WAIT_DOMINANT` | 0.40 | >40% decode waiting on expert reads → I/O-bound |
+| `LOW_HIT_RATE` | 0.30 | <30% cache hit → thrashing |
+| `LOW_ROUTE_AGREE` | 0.80 | <80% routing overlap → prefetch guessing wrong |
+| `HIGH_TAIL_RATIO` | 3.0 | p99 > 3× p50 → decode stalls |
+| `LOW_MTP_ACCEPT` | 0.20 | <20% MTP acceptance → draft decoder is dead weight |
+
+The tiny-model asserted floors live in `tools/efficiency.py` (`TINY_TOK_S_FLOOR`,
+`MAX_DISK_WAIT_SHARE`, `MIN_CPU_CUDA_AGREEMENT`).
+
+## The regression tests (what gates CI)
+
+`test_inefficiency.py` runs on the bundled `glm_tiny` model and asserts:
+
+- telemetry parses (no format drift)
+- tiny tok/s ≥ floor (throughput regression)
+- PROFILE phases present and non-negative (accounting sanity)
+- disk-wait not dominant on a resident model (I/O-path regression)
+- CPU determinism (two greedy runs agree)
+- **CUDA** (skip unless CUDA built): init path, dense uploads VRAM, CPU-vs-CUDA
+ argmax agreement ≥ 70% (kernel-correctness guard)
+
+```bash
+make efficiency # tiny CPU tests
+make efficiency-cuda # tiny CUDA tests (needs CUDA build)
+```
+
+## CUDA build prerequisite
+
+The default `make glm.exe` builds **without** CUDA. The CUDA tests and the CUDA
+dossier need a host built with `-DCOLI_CUDA` plus the runtime DLL:
+
+```bash
+make clean && make glm.exe CUDA_DLL=1 && make cuda-dll
+```
+
+`make efficiency-cuda` auto-skips with a clear message if the host is CPU-only
+(it scans the binary for the "CPU-only" marker the engine embeds).
+
+## Files
+
+- `tools/efficiency.py` — shared harness: `parse_run()` (captures every
+ telemetry signal), `run_engine()`, thresholds. Reuses `PROFILE_RE`/`SPEED_RE`
+ from `tools/benchmark_cuda_fixture.py`.
+- `tests/test_inefficiency.py` — tiny-model asserted tests (CPU + CUDA).
+- `tests/test_efficiency_report.py` — the opt-in optimization dossier.
diff --git a/c/tests/test_efficiency_report.py b/c/tests/test_efficiency_report.py
new file mode 100644
index 0000000..ba01b13
--- /dev/null
+++ b/c/tests/test_efficiency_report.py
@@ -0,0 +1,315 @@
+#!/usr/bin/env python3
+"""Exhaustive optimization dossier for a colibri engine run.
+
+This is NOT a pass/fail test. It runs the engine with every instrumentation flag
+on (PROF, COLI_CUDA_PROFILE, CACHE_ROUTE, DISK_SPLIT, LOOKA) and prints a section-
+by-section report answering, for each subsystem:
+
+ WHAT is doing it — which phase/kernel/tier
+ WHEN it is doing it — how much of decode wall-time it owns
+ WITH WHAT — the config/weights/tier it used
+ IS IT INEFFICIENT? — a verdict, with the threshold
+ HOW TO IMPROVE — the concrete knob, named
+
+Activation (opt-in only — NOT in `make test`):
+ COLI_EFFICIENCY_MODEL= python tests/test_efficiency_report.py
+
+Optional env:
+ COLI_EFFICIENCY_CUDA=1 also exercise the CUDA dense/expert tiers
+ COLI_EFFICIENCY_NGEN=N decode tokens (default 24)
+ COLI_EFFICIENCY_RAM_GB=N RAM budget (default 28)
+ COLI_EFFICIENCY_VRAM_GB=N CUDA expert-tier budget GB (default 4)
+ COLI_EFFICIENCY_PROMPT=... prompt (default: a code-gen prompt)
+
+Exit code is always 0 (it's a dossier, not a gate). Lines marked FLAG point at
+the most likely lever to move tok/s for the observed bottleneck.
+"""
+import os
+import sys
+import time
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
+from tools.efficiency import run_engine, disk_wait_share # noqa: E402
+
+
+# --- advisory thresholds (the "IS IT INEFFICIENT?" lines) ---
+DISK_WAIT_DOMINANT = 0.40 # >40% decode waiting on expert reads -> I/O-bound
+LOW_HIT_RATE = 0.30 # <30% cache hit -> thrashing (cap too small)
+LOW_ROUTE_AGREE = 0.80 # <80% routing overlap -> prefetch is guessing wrong
+HIGH_TAIL_RATIO = 3.0 # p99 > 3x p50 -> decode stalls (I/O hiccups / KV grow)
+LOW_MTP_ACCEPT = 0.20 # <20% MTP acceptance -> draft decoder is dead weight
+VRAM_WASTE_CALLS = 0 # experts pinned in VRAM but 0 calls served
+
+
+def _flag(ok): return "OK " if ok else "FLAG"
+
+
+def _bar(frac, width=24):
+ """A simple ASCII bar for share visualization."""
+ n = max(0, min(width, round(frac * width)))
+ return "#" * n + "." * (width - n)
+
+
+def _line(label, value, flag=None, note=""):
+ tag = f" [{flag}]" if flag else ""
+ print(f" {label:<22} {value}{tag} {note}" if note else f" {label:<22} {value}{tag}")
+
+
+def main() -> int:
+ model = os.environ.get("COLI_EFFICIENCY_MODEL")
+ if not model:
+ print(__doc__)
+ print("\nNot activated: set COLI_EFFICIENCY_MODEL= to run.")
+ return 0
+ model = str(Path(model).resolve())
+ if not Path(model).is_dir():
+ print(f"ERROR: {model} is not a directory", file=sys.stderr)
+ return 0
+
+ ngen = int(os.environ.get("COLI_EFFICIENCY_NGEN", "24"))
+ ram_gb = os.environ.get("COLI_EFFICIENCY_RAM_GB", "28")
+ vram_gb = os.environ.get("COLI_EFFICIENCY_VRAM_GB", "4")
+ prompt = os.environ.get(
+ "COLI_EFFICIENCY_PROMPT",
+ "Write a Python function that computes the factorial of a number. "
+ "Include error handling and a docstring.")
+ use_cuda = os.environ.get("COLI_EFFICIENCY_CUDA") == "1"
+
+ # Turn ON every instrumentation flag so the dossier has maximum detail.
+ # These are all observability toggles (PROF/COLI_CUDA_PROFILE/CACHE_ROUTE/
+ # DISK_SPLIT/LOOKA); none change the computed output.
+ overlay = dict(
+ NGEN=str(ngen), TEMP="0", RAM_GB=ram_gb, PROMPT=prompt,
+ PROF="1", CACHE_ROUTE="1", DISK_SPLIT="1", LOOKA="1", ROUTE_AGREE="1",
+ )
+ if use_cuda:
+ overlay.update(COLI_CUDA="1", COLI_GPU="0", CUDA_DENSE="1",
+ COLI_CUDA_PROFILE="1", CUDA_EXPERT_GB=vram_gb)
+
+ print("=" * 78)
+ print(f"OPTIMIZATION DOSSIER — {Path(model).name}")
+ print(f" mode : {'CUDA (dense+expert tiers)' if use_cuda else 'CPU-only'} "
+ f"ngen : {ngen} ram : {ram_gb} GB" +
+ (f" vram : {vram_gb} GB" if use_cuda else ""))
+ print("=" * 78)
+
+ t0 = time.time()
+ t, proc = run_engine(overlay, snap=model, timeout=3600.0)
+ wall = time.time() - t0
+ flags = [] # collected FLAG lines for the summary
+
+ print(f"\n[0] RUN")
+ _line("wall clock", f"{wall:.0f}s")
+ _line("exit code", proc.returncode,
+ None if proc.returncode == 0 else "FLAG",
+ "" if proc.returncode == 0 else "non-zero exit")
+ if proc.returncode != 0:
+ print(" stderr tail:")
+ for ln in proc.stderr.strip().splitlines()[-8:]:
+ print(f" {ln}")
+ return 0
+
+ # ---------------------------------------------------------------- [1] WHO ----
+ print(f"\n[1] PROVENANCE — what is running, on what, with what config")
+ if t.get("machine"):
+ m = t["machine"]
+ _line("CPU", m["cpu"])
+ _line("cores / omp", f"{m['cores']} cores")
+ _line("backend", m["backend"])
+ if t.get("load"):
+ ld = t["load"]
+ _line("model load time", f"{ld['load_s']:.2f}s")
+ _line("resident dense", f"{ld['resident_dense_mb']:.1f} MB")
+ _line("layers / experts", f"{ld['layers']} layers, {ld['experts']} experts")
+ _line("MTP", f"{ld['mtp_status']} (draft={ld['draft']})")
+ if t.get("config_str"):
+ _line("resolved config", t["config_str"])
+ print(" (this is the EFFECTIVE config after auto-budgeting — not your env verbatim)")
+
+ # ---------------------------------------------------------------- [2] SPEED --
+ print(f"\n[2] THROUGHPUT — is it fast, is the tail healthy")
+ if t.get("tok_s") is not None:
+ _line("tok/s", f"{t['tok_s']:.3f}")
+ else:
+ flags.append("throughput line missing — engine output format may have changed")
+ if t.get("latency"):
+ la = t["latency"]
+ _line("decode forwards", f"{int(la['forwards'])}")
+ _line("p50 / p90", f"{la['p50_ms']:.1f} / {la['p90_ms']:.1f} ms")
+ _line("p99 / max", f"{la['p99_ms']:.1f} / {la['max_ms']:.1f} ms")
+ tail_ok = la["p99_ms"] <= HIGH_TAIL_RATIO * la["p50_ms"]
+ _line("tail ratio (p99/p50)", f"{la['p99_ms']/max(la['p50_ms'],1e-9):.2f}x",
+ _flag(tail_ok),
+ "high tail = decode stalls (I/O hiccups, KV growth, re-pin)")
+ if not tail_ok:
+ flags.append(f"tail latency p99={la['p99_ms']:.1f}ms >> p50={la['p50_ms']:.1f}ms "
+ "(look for REPIN swaps or disk stalls)")
+
+ # ---------------------------------------------------------------- [3] TIME ---
+ print(f"\n[3] WHERE TIME GOES — what is doing it, when (share of decode)")
+ ts = t.get("time_shares")
+ prof = t.get("profile")
+ if ts:
+ order = [("io", "expert-disk I/O", DISK_WAIT_DOMINANT, "the cache is too small / disk is slow"),
+ ("matmul", "expert matmul", 0.40, "compute-bound; more cores or a GPU expert tier"),
+ ("attention", "attention", 0.35, "context length is the cost; lower CTX"),
+ ("head", "lm_head", 0.10, "vocab projection; unusual to dominate"),
+ ("other", "other", 0.30, "scheduling / KV bookkeeping overhead")]
+ for key, name, thresh, lever in order:
+ f = ts[key]
+ ok = f < thresh
+ _line(name, f"{f:5.0%} {_bar(f)}", _flag(ok),
+ "" if ok else f"->{lever}")
+ if not ok:
+ flags.append(f"{name} dominates ({f:.0%}) -> {lever}")
+ if t.get("verdict"):
+ print(f" engine verdict : {t['verdict']}")
+ elif prof:
+ print(" (no [PROF] time shares — set PROF=1 for phase percentages)")
+ if prof:
+ print(" absolute seconds :")
+ for k in ("disk", "expert_matmul", "attention", "lm_head", "other"):
+ _line(k, f"{prof[k]:.3f}s")
+
+ # attention sub-breakdown: how is attention being read
+ ab = t.get("attn_breakdown")
+ if ab:
+ print(f"\n[3a] ATTENTION BREAKDOWN — how the attention phase is spent")
+ atot = sum(ab.values()) or 1.0
+ for k, label in (("proj_rope", "projection + RoPE"),
+ ("score_sm_value", "score-softmax-value"),
+ ("out_proj", "output projection")):
+ _line(label, f"{ab[k]:.3f}s ({ab[k]/atot:.0%} of attn)")
+
+ # ---------------------------------------------------------------- [4] CACHE --
+ print(f"\n[4] EXPERT CACHE — is the cache efficient")
+ hit = t.get("hit_pct")
+ if hit is not None:
+ ok = hit >= LOW_HIT_RATE * 100
+ _line("hit rate", f"{hit:.1f}%", _flag(ok),
+ "" if ok else "<30% = thrashing; raise RAM_GB or cap")
+ if not ok:
+ flags.append(f"cache hit {hit:.1f}% is low -> raise RAM_GB (or cap), add PIN_GB")
+ el = t.get("experts_loaded")
+ if el:
+ per_tok = el["per_tok"]
+ _line("experts loaded/token", f"{per_tok:.1f}")
+ _line(" per-layer", f"{el['per_layer']:.2f} across {el['n_sparse_layers']} sparse layers")
+ _line(" baseline", f"topk={el['baseline_topk']} active experts/token")
+ base_topk = el["baseline_topk"]
+ if base_topk > 0 and per_tok > 2 * base_topk:
+ flags.append(f"loading {per_tok:.0f} experts/token vs topk={base_topk} "
+ "-> redundant I/O; cache is re-fetching evicted experts")
+
+ # ---------------------------------------------------------------- [5] DISK ---
+ print(f"\n[5] DISK I/O — is I/O the bottleneck, and where")
+ eio = t.get("expert_io")
+ if eio:
+ _line("total fetched", f"{eio['gb_fetched']:.3f} GB")
+ _line("per token", f"{eio['mb_per_tok']:.1f} MB/token")
+ _line("disk throughput", f"{eio['gb_per_s']:.2f} GB/s over the run")
+ _line("read service", f"{eio['read_service_s']:.2f}s (on I/O threads)")
+ _line("felt wait", f"{eio['felt_wait_s']:.2f}s (stall compute felt)")
+ if eio["felt_wait_s"] > eio["read_service_s"] * 0.5 and eio["read_service_s"] > 0:
+ flags.append("felt wait is a large fraction of read service -> PIPE=1 may not be "
+ "overlapping fully, or DIRECT=1 on NVMe")
+ ds = t.get("disk_split")
+ if ds:
+ print(f"\n[5a] DISK-LOAD SPLIT — which decode phase reads the bytes")
+ _line("draft phase", f"{ds['draft']} loads")
+ _line("absorb phase", f"{ds['absorb']} loads")
+ _line("verify/main", f"{ds['verify_main']} loads")
+ _line("MTP-layer bytes", f"{ds['mtp_loads']} loads, {ds['mtp_gb']:.2f} GB")
+ _line("main-layer bytes", f"{ds['main_loads']} loads, {ds['main_gb']:.2f} GB")
+ if ds.get("mtp_bytes_pct") is not None:
+ _line("MTP share of bytes", f"{ds['mtp_bytes_pct']:.1f}%")
+ share = disk_wait_share(t)
+ if share is not None:
+ ok = share < DISK_WAIT_DOMINANT
+ _line("disk-wait share", f"{share:.0%}", _flag(ok),
+ "" if ok else "I/O-bound (see levers in [3])")
+
+ # ---------------------------------------------------------------- [6] ROUTE --
+ print(f"\n[6] ROUTING QUALITY — is the router / prefetch accurate")
+ ra = t.get("route_agree")
+ if ra:
+ ok = ra["agree_pct"] >= LOW_ROUTE_AGREE * 100
+ _line("route_agree", f"{ra['agree_pct']:.1f}% overlap with true top-K",
+ _flag(ok),
+ "" if ok else "prefetch is guessing wrong; CACHE_ROUTE params may need tuning")
+ _line("route_kl", f"{ra['kl']:.4f} mean KL (lower = closer to true routing)")
+ if not ok:
+ flags.append(f"route_agree {ra['agree_pct']:.1f}% low -> tune ROUTE_J/M/P, "
+ "or prefetch is hurting more than helping")
+ sw = t.get("swap")
+ if sw:
+ _line("cache swaps", f"{sw['swaps']}/{sw['slots']} ({sw['pct']:.1f}%)",
+ None, "high swap = churn between turns")
+ la = t.get("lookahead")
+ if la:
+ print(f"\n[6a] ROUTING PREDICTABILITY — recall of true experts in predicted top-8")
+ print(" (which predictor should drive prefetch? highest recall wins)")
+ for row in la:
+ _line(row["predictor"][:34], f"{row['pct']:5.1f}% ({row['hit']}/{row['tot']})")
+
+ # ---------------------------------------------------------------- [7] SPEC ---
+ print(f"\n[7] SPECULATION — is the draft decoder pulling weight")
+ sp = t.get("speculation")
+ if sp:
+ _line("tokens/forward", f"{sp['tok_per_fw']:.2f} (>1.0 means speculation helps)")
+ _line("forwards/tokens", f"{sp['forwards']} forwards for {sp['tokens']} tokens")
+ # Speculation helps only if acceptance is high enough that tok/forward > 1.
+ # tok_per_fw already == 1.0 when nothing verifies, so judge by acceptance.
+ ok = sp["mtp_accept_pct"] >= LOW_MTP_ACCEPT * 100
+ _line("MTP acceptance", f"{sp['mtp_accept_pct']:.0f}%", _flag(ok),
+ "" if ok else "<20% -> drafts rarely verify; DRAFT=0 may be faster")
+ if not ok:
+ flags.append(f"MTP acceptance {sp['mtp_accept_pct']:.0f}% low -> "
+ "drafts cost more I/O than they save; try DRAFT=0")
+
+ # ---------------------------------------------------------------- [8] GPU ----
+ print(f"\n[8] GPU TIERS — is the GPU actually used")
+ cuda = t.get("cuda") or {}
+ if not cuda.get("enabled"):
+ print(" (CUDA not enabled — CPU-only run)")
+ else:
+ if cuda.get("resident_tensors") is not None:
+ _line("resident dense tensors", f"{cuda['resident_tensors']} tensors, "
+ f"{cuda['resident_gb']:.2f} GB")
+ if cuda.get("expert_count") is not None:
+ waste = cuda["calls_served"] <= VRAM_WASTE_CALLS
+ _line("expert tier", f"{cuda['expert_count']} experts pinned "
+ f"({cuda['expert_gb']:.2f} GB)", _flag(not waste))
+ _line(" calls served", f"{cuda['calls_served']} from VRAM",
+ _flag(not waste),
+ "" if not waste else "WASTE: pinned but never routed -> lower CUDA_EXPERT_GB")
+ if waste:
+ flags.append("VRAM expert tier has 0 calls served -> experts pinned but unused; "
+ "PIN stats may not match this workload")
+ if cuda.get("groups"):
+ g = cuda["groups"]
+ _line("expert groups", f"{g['calls']} calls, {g['experts']} experts, "
+ f"{g['rows']} rows ({g['experts_per_call']:.1f} experts/call)")
+ if cuda.get("groups_timing"):
+ gt = cuda["groups_timing"]
+ _line("GPU timing", f"H2D {gt['h2d_ms']:.1f} ms | kernel {gt['kernel_ms']:.1f} ms | "
+ f"D2H {gt['d2h_ms']:.1f} ms")
+ if gt["h2d_ms"] + gt["d2h_ms"] > gt["kernel_ms"]:
+ flags.append("CUDA H2D+D2H > kernel time -> transfer-bound; "
+ "consider larger expert tier to keep weights resident")
+
+ # ---------------------------------------------------------------- summary ----
+ print("\n" + "=" * 78)
+ if flags:
+ print(f" {len(flags)} FLAG(s) — the most likely levers to move tok/s:")
+ for f in flags:
+ print(f" - {f}")
+ else:
+ print(" no flags — every measured subsystem is within advisory thresholds.")
+ print("=" * 78)
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/c/tests/test_inefficiency.py b/c/tests/test_inefficiency.py
new file mode 100644
index 0000000..0a5eb65
--- /dev/null
+++ b/c/tests/test_inefficiency.py
@@ -0,0 +1,239 @@
+"""Inefficiency / regression tests for the colibri engine (tiny model, asserted).
+
+These run against the bundled glm_tiny model (~0.6 MB resident, ~0.1s/run) and
+gate CI: a regression here means something broke. They run on a plain CPU-only
+`glm.exe` build; the CUDA_* tests auto-skip if the engine wasn't built with
+CUDA_DLL=1 (see tests/README_efficiency.md for the build command).
+
+The signals under test, and the inefficiency each catches:
+ - tok/s floor : a throughput regression (broken build / bad config)
+ - profile phases sum : telemetry accounting bug (other balloons)
+ - disk-wait not dominant: a tiny resident model should never be I/O-bound
+ - CPU determinism : greedy decode is reproducible (no stray RNG/threading)
+ - CUDA init path : COLI_CUDA=1 initializes and does not silently exit 2
+ - CUDA dense uses VRAM : CUDA_DENSE=1 actually uploads tensors (no silent CPU fallback)
+ - CPU vs CUDA TF-match : identical weights+inputs → identical argmax (kernel bug guard)
+"""
+import os
+import shutil
+import unittest
+from pathlib import Path
+
+from tools.efficiency import (
+ parse_run, run_engine, disk_wait_share, tf_agreement,
+ TINY_TOK_S_FLOOR, MAX_DISK_WAIT_SHARE, MIN_CPU_CUDA_AGREEMENT,
+)
+
+HERE = Path(__file__).resolve().parent
+C_DIR = HERE.parent
+ENGINE = C_DIR / "glm.exe"
+TINY = C_DIR / "glm_tiny"
+
+
+def _engine_present() -> bool:
+ return ENGINE.exists()
+
+
+def _cuda_available() -> bool:
+ """True iff the engine binary has the CUDA loader compiled in AND the DLL is present.
+
+ The host is built with -DCOLI_CUDA only when CUDA_DLL=1 (Makefile). A binary
+ built without it embeds the string "this binary is CPU-only; rebuild" and
+ exits 2 on any CUDA env var — so we detect the CPU-only build by scanning
+ the binary for that marker (avoids a slow ldd/strings on every import; we
+ only read enough to find it). On Windows the DLL is also required
+ (backend_loader.c loads it at runtime); on Linux it's direct-linked.
+ """
+ if not _engine_present():
+ return False
+ try:
+ # Read once; the marker is near the read-only string table. 256 KB is
+ # plenty for this string and avoids loading a 1 MB binary into memory.
+ with open(ENGINE, "rb") as f:
+ blob = f.read(2 * 1024 * 1024)
+ if b"this binary is CPU-only" in blob:
+ return False # CPU-only build: COLI_CUDA=1 would exit 2.
+ except OSError:
+ pass
+ # Windows: DLL required at runtime. Linux: direct-linked (no DLL).
+ if os.name == "nt":
+ return (C_DIR / "coli_cuda.dll").exists()
+ import subprocess
+ try:
+ out = subprocess.run(["ldd", str(ENGINE)], capture_output=True, text=True)
+ return "libcudart" in out.stdout
+ except (FileNotFoundError, OSError):
+ return False
+
+
+@unittest.skipUnless(_engine_present(), "glm.exe not built (run: make glm.exe)")
+class TinyEfficiencyTest(unittest.TestCase):
+ """Asserted regression tests on the resident tiny model. Gates CI."""
+
+ def _run(self, **overlay):
+ return run_engine(overlay, engine=str(ENGINE), snap=str(TINY))[0]
+
+ # -- telemetry contract ---------------------------------------------------
+
+ def test_telemetry_parses(self):
+ """A REPLAY run must emit the throughput + PROFILE lines the suite keys on.
+
+ If this fails, either the engine changed its output format (update the
+ parsers in tools/efficiency.py) or the run crashed early."""
+ t = self._run(REPLAY="1", TEMP="0", NGEN="4")
+ self.assertEqual(t["returncode"], 0, f"engine exited non-zero:\n{t['stderr']}")
+ self.assertIn("tok_s", t["parsed"], f"missing tok/s line:\n{t['stderr']}")
+ self.assertIn("profile", t["parsed"], f"missing PROFILE line:\n{t['stderr']}")
+ self.assertIn("hit_pct", t["parsed"], f"missing expert hit line:\n{t['stderr']}")
+ self.assertIsNotNone(t["tok_s"])
+ self.assertIsNotNone(t["profile"])
+
+ # -- throughput floor -----------------------------------------------------
+
+ def test_tiny_tok_s_floor(self):
+ """Tiny decode must beat TINY_TOK_S_FLOOR.
+
+ The tiny model is fully resident and runs ~200 tok/s; the 20 tok/s
+ default floor is a 10x margin that catches broken builds or a pathological
+ config cascade (the cap=1 trap from ISSUE_new_model_resource_regression.md)
+ without flapping on machine noise."""
+ t = self._run(REPLAY="1", TEMP="0", NGEN="8")
+ self.assertEqual(t["returncode"], 0, f"engine exited non-zero:\n{t['stderr']}")
+ self.assertGreaterEqual(
+ t["tok_s"], TINY_TOK_S_FLOOR,
+ f"tok/s {t['tok_s']:.1f} below floor {TINY_TOK_S_FLOOR} "
+ f"(regression, or a config cascade starving the cache)",
+ )
+
+ # -- accounting sanity ----------------------------------------------------
+
+ def test_profile_phases_present_and_nonneg(self):
+ """Every PROFILE phase must be present and non-negative.
+
+ `other` can go slightly negative from timer overhead (the engine allows
+ it), but a large negative means the timers are double-counting."""
+ t = self._run(REPLAY="1", TEMP="0", NGEN="4")
+ p = t["profile"]
+ for phase in ("disk", "expert_matmul", "attention", "lm_head"):
+ self.assertGreaterEqual(p[phase], -0.01, f"{phase} went negative: {p}")
+ # 'other' is a residual; allow a small negative from timer overlap.
+ self.assertGreaterEqual(p["other"], -0.05, f"other too negative (double-count): {p}")
+
+ # -- disk-wait not dominant on a resident model ---------------------------
+
+ def test_disk_wait_not_dominant(self):
+ """A fully-resident tiny model must NOT be I/O-bound.
+
+ Everything fits in RAM; the expert-disk wait share should be ~0. If it
+ exceeds MAX_DISK_WAIT_SHARE, the cache/I/O path regressed — on a real
+ model this same regression would make decode I/O-bound (the exact
+ failure mode the [PROF] verdict flags)."""
+ t = self._run(REPLAY="1", TEMP="0", NGEN="8", PROF="1")
+ self.assertIn("time_shares", t["parsed"], f"missing [PROF] time shares:\n{t['stderr']}")
+ share = disk_wait_share(t)
+ self.assertIsNotNone(share)
+ self.assertLess(
+ share, MAX_DISK_WAIT_SHARE,
+ f"expert-I/O share {share:.0%} on a resident model — I/O path regressed",
+ )
+
+ # -- determinism ----------------------------------------------------------
+
+ def test_cpu_vs_cpu_determinism(self):
+ """Two greedy REPLAY runs with the same seed produce identical telemetry.
+
+ TEMP=0 = greedy (no sampling), so tok/s and hit-rate must be reproducible.
+ A drift here means non-determinism crept into the decode path (stray
+ threading, uninitialized state) — which on a real model would make A/B
+ comparisons meaningless."""
+ a = self._run(REPLAY="1", TEMP="0", NGEN="8", SEED="1")
+ b = self._run(REPLAY="1", TEMP="0", NGEN="8", SEED="1")
+ self.assertEqual(a["returncode"], 0)
+ self.assertEqual(b["returncode"], 0)
+ self.assertEqual(a["hit_pct"], b["hit_pct"], "greedy hit-rate drifted between runs")
+ # tok/s within 25% — exact equality is too strict across scheduler noise.
+ self.assertLess(abs(a["tok_s"] - b["tok_s"]) / max(a["tok_s"], b["tok_s"]), 0.25)
+
+
+@unittest.skipUnless(_cuda_available(),
+ "CUDA build not present (run: make clean && make glm.exe CUDA_DLL=1 && make cuda-dll)")
+class TinyCudaEfficiencyTest(unittest.TestCase):
+ """CUDA-path regression tests on the tiny model. Skip unless CUDA built.
+
+ glm_tiny is small and fully resident, so CUDA here is fast and exercises the
+ real GPU code path (init, dense upload, kernel correctness) without the
+ long load time or memory pressure of the full model. These guard the
+ silent-failure modes that are otherwise invisible:
+ - COLI_CUDA=1 silently falling back to CPU (loader/DLL missing)
+ - CUDA_DENSE=1 uploading nothing
+ - a CUDA kernel producing different argmax than CPU on identical inputs
+ """
+
+ def _run(self, **overlay):
+ return run_engine(overlay, engine=str(ENGINE), snap=str(TINY))[0]
+
+ def test_cuda_init_path(self):
+ """COLI_CUDA=1 must initialize the device and NOT exit 2.
+
+ Exit 2 is the engine's "requested backend is unavailable" path
+ (glm.c: g_cuda_enabled check). A clean init prints the [CUDA] device
+ banner to stderr. If this fails, the DLL is broken or the loader can't
+ resolve symbols (ABI drift between backend_cuda.h and the dll)."""
+ t = self._run(COLI_CUDA="1", COLI_GPU="0", REPLAY="1", TEMP="0", NGEN="4")
+ self.assertNotEqual(t["returncode"], 2,
+ f"engine refused CUDA backend:\n{t['stderr']}")
+ self.assertTrue(t["cuda"]["enabled"],
+ f"no [CUDA] device banner on stderr:\n{t['stderr']}")
+
+ def test_cuda_dense_uses_vram(self):
+ """CUDA_DENSE=1 must actually upload dense tensors to VRAM.
+
+ The minimal GPU-exercising config (per backend_loader.c analysis):
+ COLI_CUDA=1 + CUDA_DENSE=1. Without CUDA_DENSE the dense path stays on
+ CPU and [CUDA] resident set reports 0 tensors — a silent no-op. This
+ catches that regression: after the run, resident_tensors > 0."""
+ t = self._run(COLI_CUDA="1", COLI_GPU="0", CUDA_DENSE="1",
+ REPLAY="1", TEMP="0", NGEN="4")
+ self.assertEqual(t["returncode"], 0, f"engine exited non-zero:\n{t['stderr']}")
+ rt = t["cuda"]["resident_tensors"]
+ self.assertIsNotNone(rt, f"no [CUDA] resident set line:\n{t['stderr']}")
+ self.assertGreater(rt, 0,
+ f"CUDA_DENSE=1 but {rt} tensors resident — silent CPU fallback")
+
+ def test_cpu_vs_cuda_tf_match(self):
+ """CPU and CUDA teacher-forcing must agree on most positions (DIRECTLY).
+
+ Both paths prefill the SAME oracle sequence on the SAME weights, so their
+ argmaxes should match position-for-position — but not exactly: the two
+ backends accumulate dot-products in different orders (x86 SIMD vs CUDA
+ kernel), so a few near-tied logits flip. That divergence is expected
+ numeric behavior, not a kernel bug. A *catastrophic* kernel regression
+ (wrong GEMM, wrong scale, wrong fmt) would collapse agreement toward
+ random (~1/vocab = ~4%); the MIN_CPU_CUDA_AGREEMENT floor (default 70%)
+ catches that while tolerating harmless drift.
+
+ We compare CPU-vs-CUDA *directly* (not via the oracle match-counts),
+ because both backends differ from the oracle at different positions and
+ the summary line can't tell "CPU≠CUDA" from "CPU≠oracle"."""
+ import json
+ ref = json.loads((C_DIR / "ref_glm.json").read_text())
+ oracle = ref["tf_pred"]
+
+ cpu = self._run(TF="1", TEMP="0")
+ cuda = self._run(COLI_CUDA="1", COLI_GPU="0", CUDA_DENSE="1", TF="1", TEMP="0")
+ self.assertEqual(cpu["returncode"], 0, f"CPU TF run failed:\n{cpu['stderr']}")
+ self.assertEqual(cuda["returncode"], 0, f"CUDA TF run failed:\n{cuda['stderr']}")
+ self.assertIn("tf_mismatches", cpu["parsed"], "CPU run missing per-position mismatches")
+ self.assertIn("tf_mismatches", cuda["parsed"], "CUDA run missing per-position mismatches")
+
+ agree, diff = tf_agreement(cpu, cuda, oracle)
+ self.assertGreaterEqual(
+ agree, MIN_CPU_CUDA_AGREEMENT,
+ f"CPU-vs-CUDA argmax agreement {agree:.0%} below floor "
+ f"{MIN_CPU_CUDA_AGREEMENT:.0%} — CUDA kernel likely regressed. "
+ f"Differing positions: {diff[:10]}{'...' if len(diff)>10 else ''}",
+ )
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/c/tools/efficiency.py b/c/tools/efficiency.py
new file mode 100644
index 0000000..775e8cf
--- /dev/null
+++ b/c/tools/efficiency.py
@@ -0,0 +1,458 @@
+"""Efficiency / regression harness for the colibri engine.
+
+The engine already emits rich telemetry (REPLAY tok/s, PROFILE phase timings,
+[PROF] time shares + verdict, CUDA expert-tier utilization). Until now every
+consumer of that telemetry — `benchmark_cuda_fixture.py`, `bench_full.sh`,
+`bench_ux.sh` — has only *printed* it for a human to eyeball. This module turns
+each signal into a parseable field so tests can assert on it.
+
+Design:
+ - Reuses SPEED_RE / PROFILE_RE from tools.benchmark_cuda_fixture (no drift).
+ - parse_run() is pure: stdout+stderr in, dict out. Easy to unit-test against
+ captured strings (like the existing test_benchmark_cuda_fixture does).
+ - run_engine() is the subprocess wrapper. Captures stdout and stderr
+ separately, because the engine splits them: PROFILE/REPLAY/CUDA-tier go to
+ stdout, the [CUDA]/[PROF]/[prefill] banners go to stderr.
+ - Floor defaults are module constants (tunable in one place, not scattered).
+
+No model file is required to import this module; only run_engine() invokes the
+binary. parse_run() works on any captured text, so most test surface is covered
+by string fixtures without spinning the engine at all.
+"""
+from __future__ import annotations
+
+import os
+import re
+import subprocess
+from pathlib import Path
+from typing import Optional
+
+# Reuse the validated regexes from the existing A/B benchmark harness so the
+# PROFILE field order (disk, expert_matmul, attention, lm_head, other) and the
+# tok/s capture stay identical. Drift here would silently break every consumer.
+from tools.benchmark_cuda_fixture import SPEED_RE as _SPEED_RE_REPLAY, PROFILE_RE, PROFILE_KEYS
+
+# SPEED_RE (from benchmark_cuda_fixture) matches the REPLAY-mode line only:
+# "REPLAY decode: ... | 12.34 tok/s | ..."
+# run_text / PROMPT mode uses a DIFFERENT format (glm.c:4682):
+# "decode N tokens in X.XXs (12.34 tok/s) | expert hit rate ..."
+# This alt regex catches the parenthesized form so the full-model report (which
+# uses PROMPT mode) gets a real tok/s instead of reporting it missing.
+SPEED_RE_TEXT = re.compile(r"decode \d+ tokens in [0-9.]+s \(([0-9.]+) tok/s\)")
+
+
+def _first_speed(stdout: str):
+ """Find tok/s in whichever run-mode format the engine used."""
+ for rx in (_SPEED_RE_REPLAY, SPEED_RE_TEXT):
+ m = rx.search(stdout)
+ if m:
+ return m
+ return None
+
+
+# Public alias so existing imports keep working (tests reference SPEED_RE).
+SPEED_RE = _SPEED_RE_REPLAY
+
+
+# --- additional parsers (formats verified against glm.c printf strings) ---
+
+# "expert hit rate 88.1%" (summary line) | "expert hit 95.0%" (REPLAY line)
+HIT_RE = re.compile(r"expert hit(?:\s+rate)?\s+([0-9.]+)%")
+
+# "[PROF] time shares: expert-I/O 3% | expert-matmul 12% | attention 56% | lm_head 2% | other 27%"
+SHARES_RE = re.compile(
+ r"\[PROF\] time shares: expert-I/O\s+([0-9.]+)%\s*\|\s*expert-matmul\s+([0-9.]+)%\s*"
+ r"\|\s*attention\s+([0-9.]+)%\s*\|\s*lm_head\s+([0-9.]+)%\s*\|\s*other\s+([0-9.]+)%"
+)
+
+# "[PROF] verdict: I/O-bound — 60% of the time ..." (also compute-bound / attention-bound / balanced)
+VERDICT_RE = re.compile(r"\[PROF\] verdict:\s*(I/O-bound|compute-bound|attention-bound|balanced)")
+
+# "[PROF] expert I/O: ... hit 95.0% (76 hit / 4 load) | 4.0 loads/token"
+LOADS_PER_TOK_RE = re.compile(r"\|\s*([0-9.]+)\s+loads/token")
+
+# "CUDA expert tier: 111 resident experts (2.36 GB) | 5400 calls served from VRAM" (stdout)
+CUDA_TIER_RE = re.compile(
+ r"CUDA expert tier:\s+(\d+)\s+resident experts\s+\(([0-9.]+)\s+GB\)\s*\|\s+(\d+)\s+calls served from VRAM"
+)
+
+# "[CUDA] device 0: NVIDIA ..., 14.4 GB VRAM, sm_120" (stderr, per device at init)
+CUDA_DEVICE_RE = re.compile(r"\[CUDA\] device\s+\d+:")
+
+# "[CUDA] resident set: 12 tensors, 0.45 GB VRAM" (stderr, cuda_stats_print)
+CUDA_RESIDENT_RE = re.compile(
+ r"\[CUDA\] resident set:\s+(\d+)\s+tensors,\s+([0-9.]+)\s+GB\s+VRAM"
+)
+
+# "PREFILL (teacher-forcing) C vs oracle: 11/32 positions | 1700.4 pos/s" (TF=1 mode, stdout)
+TF_MATCH_RE = re.compile(r"PREFILL \(teacher-forcing\).*:\s+(\d+)/(\d+)\s+positions")
+
+# --- the six deeper signals (added after "are you gathering everything?" audit) ---
+
+# "ATTENTION: projection/RoPE 0.050s | score-softmax-value 0.009s | output projection 0.011s"
+# Sub-breakdown of the attention phase — answers "how is attention being read".
+ATTN_BREAKDOWN_RE = re.compile(
+ r"ATTENTION: projection/RoPE\s+([0-9.]+)s\s*\|\s*score-softmax-value\s+([0-9.]+)s\s*"
+ r"\|\s*output projection\s+([0-9.]+)s"
+)
+
+# "[PROF] decode forwards: 20 | latency p50 7.3 ms | p90 8.0 ms | p99 8.1 ms | max 8.2 ms | 1.00 tok/forward"
+# Per-forward tail latency — a p99 >> p50 means decode stalls (I/O hiccups, KV grow).
+LATENCY_RE = re.compile(
+ r"\[PROF\] decode forwards:\s+(\d+)\s*\| latency p50\s+([0-9.]+)\s*ms\s*\| "
+ r"p90\s+([0-9.]+)\s*ms\s*\|\s*p99\s+([0-9.]+)\s*ms\s*\|\s*max\s+([0-9.]+)\s*ms"
+)
+
+# "[PROF] expert I/O: 0.004 GB fetched (0.2 MB/token, 0.03 GB/s over the run) | hit 95.0% ... |
+# 4.0 loads/token | 0.0s read service / 0.0s felt wait"
+# Absolute disk throughput + the felt-wait split (the [PROF] version, more detailed than PROFILE).
+EXPERT_IO_RE = re.compile(
+ r"\[PROF\] expert I/O:\s+([0-9.]+)\s+GB fetched\s+\(([0-9.]+)\s+MB/token,\s*([0-9.]+)\s+GB/s"
+ r".*?\|\s*([0-9.]+)\s+loads/token\s*\|\s*([0-9.]+)s\s+read service\s*/\s*([0-9.]+)s\s+felt wait"
+)
+
+# "speculation: 1.05 tokens/forward (19 forwards per 20 tokens) | MTP acceptance 44% (7/16)"
+# Draft efficiency — is the speculative decoder pulling weight or dead overhead?
+SPECULATION_RE = re.compile(
+ r"speculation:\s+([0-9.]+)\s+tokens/forward\s+\((\d+)\s+forwards per\s+(\d+)\s+tokens\)"
+ r"\s*\|\s*MTP acceptance\s+([0-9.]+)%"
+)
+
+# "experts loaded/token: 450.0 (per-layer 56.25 across 8; baseline topk=8) | TOPK=0 TOPP=0.00"
+# Fuller than loads_per_tok: includes the per-layer spread + the active topk/topp.
+EXPERTS_LOADED_RE = re.compile(
+ r"experts loaded/token:\s+([0-9.]+)\s+\(per-layer\s+([0-9.]+)\s+across\s+(\d+);\s*baseline topk=(\d+)\)"
+)
+
+# "[PROF] machine: Intel(...) | 22 cores (22 omp threads) | RAM 34.1 GB total, 27.1 GB available | backend CUDA"
+# Provenance — makes a report reproducible across machines/runs.
+MACHINE_RE = re.compile(r"\[PROF\] machine:\s*(.*)\|\s*(\d+)\s+cores.*backend\s+(\S+)")
+
+# "[PROF] config: RAM_GB=auto 23.9 CTX=4096 | expert cache cap 8/layer ... | DRAFT=0 PIPE=1 DIRECT=0 ..."
+# Effective resolved config (after auto-budgeting). Answers "what config actually ran".
+CONFIG_RE = re.compile(r"\[PROF\] config:\s*(.*)")
+
+# "[ORACLE] mismatch pos=7 expected=197 got=22" (TF=1 mode, stderr, per position)
+# Captures the engine's *actual* argmax at each position, so two backends can be
+# compared DIRECTLY (independent of how each relates to the oracle).
+TF_MISMATCH_RE = re.compile(r"\[ORACLE\] mismatch pos=(\d+) expected=\d+ got=(\d+)")
+
+# --- routing-quality + disk-split + cuda-groups (the deep signals) ---
+
+# summary suffix: " | swap 3.1% (12/384)" (CACHE_ROUTE inline)
+SWAP_RE = re.compile(r"swap\s+([0-9.]+)%\s+\((\d+)/(\d+)\)")
+
+# summary suffix: " | route_agree 85.0% | route_kl 0.0123" (CACHE_ROUTE/ROUTE_AGREE)
+ROUTE_AGREE_RE = re.compile(r"route_agree\s+([0-9.]+)%\s*\|\s*route_kl\s+([0-9.]+)")
+
+# "disk-load split: draft 8 + absorb 0 + verify/main 3 misses | MTP-layer 0 loads 0.00 GB |
+# main-layers 11 loads 0.01 GB (MTP 0.0% of bytes)" (DISK_SPLIT=1)
+DISK_SPLIT_RE = re.compile(
+ r"disk-load split: draft\s+(\d+)\s+\+\s+absorb\s+(\d+)\s+\+\s+verify/main\s+(\d+)\s+misses"
+ r"\s*\|\s*MTP-layer\s+(\d+)\s+loads\s+([0-9.]+)\s+GB\s*\|\s*main-layers\s+(\d+)\s+loads\s+([0-9.]+)\s+GB"
+ r"(?:\s+\(MTP\s+([0-9.]+)%\s+of bytes\))?"
+)
+
+# "[CUDA] expert groups: 120 call, 840 expert, 1200 righe (7.00 expert/call)"
+CUDA_GROUPS_RE = re.compile(
+ r"\[CUDA\] expert groups:\s+(\d+)\s+call,\s+(\d+)\s+expert,\s+(\d+)\s+righe\s+\(([0-9.]+)\s+expert/call\)"
+)
+
+# "[CUDA] expert groups timing: H2D 12.3 ms | kernel 45.6 ms | D2H 7.8 ms" (COLI_CUDA_PROFILE=1)
+CUDA_GROUPS_TIME_RE = re.compile(
+ r"\[CUDA\] expert groups timing: H2D\s+([0-9.]+)\s+ms\s*\|\s*kernel\s+([0-9.]+)\s+ms\s*\|\s*D2H\s+([0-9.]+)\s+ms"
+)
+
+# LOOKAHEAD recall block — 4 named rows. (name, pct, hit, tot)
+LOOKAHEAD_RE = re.compile(
+ r"^\s*(.+?)\s+([0-9.]+)%\s+\((\d+)/(\d+)\)\s*$", re.MULTILINE
+)
+
+# "loaded in 0.02s | resident dense: 0.21 MB | layers=5 experts=8 | MTP absent (draft=0)"
+LOAD_BANNER_RE = re.compile(
+ r"loaded in\s+([0-9.]+)s\s*\|\s*resident dense:\s+([0-9.]+)\s+MB\s*\|"
+ r"\s*layers=(\d+)\s+experts=(\d+)\s*\|\s*MTP\s+(\w+)\s+\(draft=(\d+)\)"
+)
+
+
+# --- tunable floors -----------------------------------------------------------
+# These are deliberately generous so they catch *regressions* (broken builds,
+# pathological configs, telemetry accounting bugs) without flapping on machine
+# noise. The tiny model is fully resident at ~200 tok/s, so a 20 tok/s floor is
+# a 10x margin. Tune per-host via env if needed (documented in README).
+TINY_TOK_S_FLOOR = float(os.environ.get("COLI_TINY_TOK_S_FLOOR", "20.0"))
+# On a fully-resident tiny model the expert-disk wait share should be tiny.
+# If it exceeds this, something regressed in the I/O accounting or cache path.
+MAX_DISK_WAIT_SHARE = float(os.environ.get("COLI_MAX_DISK_WAIT_SHARE", "0.20"))
+# decode wall-time should be roughly the sum of PROFILE phases (other = residual).
+PROFILE_SUM_TOLERANCE = float(os.environ.get("COLI_PROFILE_SUM_TOL", "0.05"))
+# Minimum direct CPU-vs-CUDA argmax agreement on the tiny TF fixture. The two
+# backends use different accumulation orders (SIMD dot vs CUDA kernel), so a
+# few near-tied positions flip argmax — that's expected numeric divergence, not
+# a kernel bug. Measured baseline ~84% (27/32) on this fixture; the 70% floor
+# leaves headroom for machine noise while still catching a catastrophic kernel
+# regression (e.g. a wrong GEMM would drop this to ~random = ~4%).
+MIN_CPU_CUDA_AGREEMENT = float(os.environ.get("COLI_MIN_CPU_CUDA_AGREE", "0.70"))
+
+
+def parse_run(stdout: str, stderr: str = "") -> dict:
+ """Parse one engine run's output into a telemetry dict.
+
+ Returns keys: tok_s, hit_pct, profile (dict, seconds), profile_sum,
+ time_shares (dict, fractions 0..1), verdict, loads_per_tok, cuda (dict),
+ tf_match (tuple or None), parsed (set of field names found).
+
+ Raises RuntimeError only if the core throughput line is missing — everything
+ else is optional and absent on some run modes (e.g. [PROF] needs PROF=1,
+ CUDA tier needs gpu_expert_count>0).
+ """
+ out = dict(
+ tok_s=None, hit_pct=None, profile=None, profile_sum=None,
+ time_shares=None, verdict=None, loads_per_tok=None,
+ cuda=None, tf_match=None, stderr=stderr,
+ )
+ parsed = set()
+ blob = stdout + "\n" + stderr # [PROF]/[CUDA] live on stderr; scan both.
+
+ m = _first_speed(stdout)
+ if m:
+ out["tok_s"] = float(m.group(1)); parsed.add("tok_s")
+
+ m = HIT_RE.search(blob)
+ if m:
+ out["hit_pct"] = float(m.group(1)); parsed.add("hit_pct")
+
+ m = PROFILE_RE.search(stdout)
+ if m:
+ service, wait, emm, attn, head, other = (float(x) for x in m.groups())
+ disk = service + (wait or 0.0)
+ out["profile"] = dict(zip(PROFILE_KEYS, (disk, emm, attn, head, other)))
+ out["profile_sum"] = disk + emm + attn + head + other
+ parsed.add("profile")
+
+ # ATTENTION sub-breakdown: projection/RoPE | score-softmax-value | output.
+ m = ATTN_BREAKDOWN_RE.search(stdout)
+ if m:
+ out["attn_breakdown"] = dict(zip(
+ ("proj_rope", "score_sm_value", "out_proj"),
+ (float(x) for x in m.groups())))
+ parsed.add("attn_breakdown")
+
+ m = SHARES_RE.search(blob)
+ if m:
+ io, emm, attn, head, other = (float(x) / 100.0 for x in m.groups())
+ out["time_shares"] = dict(io=io, matmul=emm, attention=attn, head=head, other=other)
+ parsed.add("time_shares")
+
+ m = VERDICT_RE.search(blob)
+ if m:
+ out["verdict"] = m.group(1); parsed.add("verdict")
+
+ # [PROF] decode forwards + latency p50/p90/p99/max.
+ m = LATENCY_RE.search(blob)
+ if m:
+ out["latency"] = dict(zip(
+ ("forwards", "p50_ms", "p90_ms", "p99_ms", "max_ms"),
+ (float(x) for x in m.groups())))
+ parsed.add("latency")
+
+ # [PROF] expert I/O throughput: GB fetched, MB/token, GB/s, service vs felt wait.
+ m = EXPERT_IO_RE.search(blob)
+ if m:
+ out["expert_io"] = dict(zip(
+ ("gb_fetched", "mb_per_tok", "gb_per_s", "loads_per_tok",
+ "read_service_s", "felt_wait_s"),
+ (float(x) for x in m.groups())))
+ parsed.add("expert_io")
+
+ m = LOADS_PER_TOK_RE.search(blob)
+ if m:
+ out["loads_per_tok"] = float(m.group(1)); parsed.add("loads_per_tok")
+
+ # experts loaded/token with per-layer spread + baseline topk (run_text summary).
+ m = EXPERTS_LOADED_RE.search(stdout)
+ if m:
+ out["experts_loaded"] = dict(
+ per_tok=float(m.group(1)), per_layer=float(m.group(2)),
+ n_sparse_layers=int(m.group(3)), baseline_topk=int(m.group(4)))
+ parsed.add("experts_loaded")
+
+ # speculation: tokens/forward, forwards, tokens, MTP acceptance%.
+ m = SPECULATION_RE.search(stdout)
+ if m:
+ out["speculation"] = dict(zip(
+ ("tok_per_fw", "forwards", "tokens", "mtp_accept_pct"),
+ (float(m.group(1)), int(m.group(2)), int(m.group(3)), float(m.group(4)))))
+ parsed.add("speculation")
+
+ # routing quality (CACHE_ROUTE inline suffixes on the summary line).
+ m = SWAP_RE.search(stdout)
+ if m:
+ out["swap"] = dict(pct=float(m.group(1)), swaps=int(m.group(2)), slots=int(m.group(3)))
+ parsed.add("swap")
+ m = ROUTE_AGREE_RE.search(stdout)
+ if m:
+ out["route_agree"] = dict(agree_pct=float(m.group(1)), kl=float(m.group(2)))
+ parsed.add("route_agree")
+
+ # disk-load split by decode phase (DISK_SPLIT=1).
+ m = DISK_SPLIT_RE.search(stdout)
+ if m:
+ out["disk_split"] = dict(zip(
+ ("draft", "absorb", "verify_main", "mtp_loads", "mtp_gb",
+ "main_loads", "main_gb", "mtp_bytes_pct"),
+ (int(m.group(1)), int(m.group(2)), int(m.group(3)), int(m.group(4)),
+ float(m.group(5)), int(m.group(6)), float(m.group(7)),
+ float(m.group(8)) if m.group(8) else None)))
+ parsed.add("disk_split")
+
+ # provenance: machine + resolved config.
+ m = MACHINE_RE.search(blob)
+ if m:
+ out["machine"] = dict(cpu=m.group(1).strip(), cores=int(m.group(2)), backend=m.group(3))
+ parsed.add("machine")
+ m = CONFIG_RE.search(blob)
+ if m:
+ out["config_str"] = m.group(1).strip(); parsed.add("config")
+
+ # load banner: load time, resident dense MB, layers, experts, MTP status.
+ m = LOAD_BANNER_RE.search(stdout)
+ if m:
+ out["load"] = dict(zip(
+ ("load_s", "resident_dense_mb", "layers", "experts", "mtp_status", "draft"),
+ (float(m.group(1)), float(m.group(2)), int(m.group(3)),
+ int(m.group(4)), m.group(5), int(m.group(6)))))
+ parsed.add("load")
+
+ # LOOKAHEAD routing-recall block (LOOKA=1): list of {predictor, pct, hit, tot}.
+ la_block = re.search(
+ r"LOOKAHEAD routing.*?recall.*?:\n((?:^\s+.+?\s+[0-9.]+%\s+\(\d+/\d+\)\s*$\n?)+)",
+ blob, re.MULTILINE)
+ if la_block:
+ out["lookahead"] = []
+ for row in LOOKAHEAD_RE.finditer(la_block.group(1)):
+ out["lookahead"].append(dict(
+ predictor=row.group(1).strip(), pct=float(row.group(2)),
+ hit=int(row.group(3)), tot=int(row.group(4))))
+ parsed.add("lookahead")
+
+ cuda = dict(enabled=False, expert_count=None, expert_gb=None,
+ calls_served=None, resident_tensors=None, resident_gb=None,
+ groups=None, groups_timing=None)
+ if CUDA_DEVICE_RE.search(stderr):
+ cuda["enabled"] = True
+ m = CUDA_TIER_RE.search(stdout)
+ if m:
+ cuda["expert_count"] = int(m.group(1))
+ cuda["expert_gb"] = float(m.group(2))
+ cuda["calls_served"] = int(m.group(3))
+ m = CUDA_RESIDENT_RE.search(stderr)
+ if m:
+ cuda["resident_tensors"] = int(m.group(1))
+ cuda["resident_gb"] = float(m.group(2))
+ m = CUDA_GROUPS_RE.search(stderr)
+ if m:
+ cuda["groups"] = dict(zip(
+ ("calls", "experts", "rows", "experts_per_call"),
+ (int(m.group(1)), int(m.group(2)), int(m.group(3)), float(m.group(4)))))
+ m = CUDA_GROUPS_TIME_RE.search(stderr)
+ if m:
+ cuda["groups_timing"] = dict(zip(
+ ("h2d_ms", "kernel_ms", "d2h_ms"),
+ (float(m.group(1)), float(m.group(2)), float(m.group(3)))))
+ out["cuda"] = cuda
+
+ m = TF_MATCH_RE.search(stdout)
+ if m:
+ out["tf_match"] = (int(m.group(1)), int(m.group(2))); parsed.add("tf_match")
+
+ # Capture per-position argmax divergences from the oracle, keyed by position.
+ mismatches = {int(mm.group(1)): int(mm.group(2))
+ for mm in TF_MISMATCH_RE.finditer(blob)}
+ if out["tf_match"] is not None:
+ out["tf_mismatches"] = mismatches
+ parsed.add("tf_mismatches")
+
+ out["parsed"] = parsed
+ return out
+
+
+def run_engine(
+ env_overlay: dict,
+ *,
+ engine: Optional[str] = None,
+ cap: int = 4,
+ ebits: int = 4,
+ dbits: int = 4,
+ timeout: float = 600.0,
+ snap: Optional[str] = None,
+) -> tuple[dict, subprocess.CompletedProcess]:
+ """Run the engine with an env overlay; return (parsed_telemetry, proc).
+
+ `engine` defaults to ./glm.exe (colibri's Windows host). `snap` defaults to
+ the bundled tiny model (glm_tiny) so callers can omit it for fast tests.
+ The positional argv is `cap ebits dbits`, matching the engine's main().
+ """
+ if engine is None:
+ engine = str(Path(__file__).resolve().parent.parent / "glm.exe")
+ env = os.environ.copy()
+ # Strip CUDA vars by default so a CPU run isn't accidentally GPU-accelerated
+ # by a leftover env; callers opt in by passing them in env_overlay.
+ for k in ("COLI_CUDA", "COLI_GPU", "COLI_GPUS", "CUDA_DENSE", "CUDA_EXPERT_GB"):
+ env.pop(k, None)
+ env.update(env_overlay)
+ if snap is not None:
+ env["SNAP"] = snap
+ elif "SNAP" not in env:
+ env["SNAP"] = str(Path(__file__).resolve().parent.parent / "glm_tiny")
+
+ proc = subprocess.run(
+ [engine, str(cap), str(ebits), str(dbits)],
+ env=env, capture_output=True, text=True, timeout=timeout,
+ )
+ telemetry = parse_run(proc.stdout, proc.stderr)
+ telemetry["returncode"] = proc.returncode
+ telemetry["env"] = {k: env_overlay[k] for k in env_overlay}
+ return telemetry, proc
+
+
+def disk_wait_share(t: dict) -> Optional[float]:
+ """Fraction of decode wall-time spent waiting on expert disk reads.
+
+ Preferred source: [PROF] time_shares (the engine's own accounting, which
+ separates felt-wait from read-service). Falls back to PROFILE disk / sum if
+ [PROF] wasn't emitted (PROF=0 runs). None if neither is available.
+ """
+ if t.get("time_shares"):
+ return t["time_shares"]["io"]
+ if t.get("profile") and t.get("profile_sum"):
+ return t["profile"]["disk"] / t["profile_sum"]
+ return None
+
+
+def tf_agreement(cpu: dict, cuda: dict, oracle: list[int]) -> tuple[float, list[int]]:
+ """Direct CPU-vs-CUDA argmax agreement on the TF fixture.
+
+ Both runs prefilled the SAME oracle sequence; tf_mismatches holds each
+ backend's actual argmax where it diverged from the oracle. Where a backend
+ is ABSENT from the mismatch map, its prediction equals the oracle token at
+ that position. So the reconstructed per-position prediction is:
+ oracle[i] if i not in mismatches else mismatches[i]
+ and agreement is the fraction of positions where CPU and CUDA predictions
+ are identical — independent of how each relates to the oracle.
+
+ `oracle` is ref_glm.json's tf_pred (the per-position oracle argmax). Pass
+ n_positions = len(oracle).
+
+ Returns (agreement_fraction, list_of_differing_positions).
+ """
+ cm = cpu.get("tf_mismatches") or {}
+ gm = cuda.get("tf_mismatches") or {}
+ diff = []
+ for i, orc in enumerate(oracle):
+ cpu_tok = cm.get(i, orc) # matched oracle => oracle token
+ cuda_tok = gm.get(i, orc)
+ if cpu_tok != cuda_tok:
+ diff.append(i)
+ agree = (len(oracle) - len(diff)) / len(oracle) if oracle else 0.0
+ return agree, diff
From cf126cbcf969396c48ce27838faca7d81947efee Mon Sep 17 00:00:00 2001
From: woolcoxm <13604288+woolcoxm@users.noreply.github.com>
Date: Fri, 17 Jul 2026 11:05:25 -0400
Subject: [PATCH 51/71] fix(tests): skip efficiency tests when glm_tiny fixture
is absent (CI #360)
CI runs 'make check' = dependency-free tests, no model downloads (by design,
#140). glm_tiny/ is a gitignored generated fixture, so test_inefficiency.py
hard-failed on the Windows/macOS/Linux runners with 'config.json: No such file
or directory' instead of skipping.
_engine_present() now requires BOTH glm.exe AND glm_tiny/config.json, and
_skip_reason() names exactly which prerequisite is missing so the skip is
actionable. Verified: 8 skipped (0 failed) with the fixture absent; 5 pass +
3 CUDA-skip with it present.
---
c/tests/test_inefficiency.py | 24 +++++++++++++++++++++---
1 file changed, 21 insertions(+), 3 deletions(-)
diff --git a/c/tests/test_inefficiency.py b/c/tests/test_inefficiency.py
index 0a5eb65..00d8b58 100644
--- a/c/tests/test_inefficiency.py
+++ b/c/tests/test_inefficiency.py
@@ -31,7 +31,25 @@ TINY = C_DIR / "glm_tiny"
def _engine_present() -> bool:
- return ENGINE.exists()
+ """True iff BOTH the built engine AND the tiny fixture are available.
+
+ These tests need glm.exe (a build artifact) AND glm_tiny/ (a generated
+ fixture, gitignored). CI runs `make check` = "dependency-free tests, no
+ model downloads" (workflow .github/workflows/check.yml, by design #140), so
+ neither is present there and these tests must SKIP rather than fail. They
+ run locally after `make glm.exe` (glm_tiny ships alongside the source, or
+ is regenerated by tools/make_glm_oracle.py).
+ """
+ return ENGINE.exists() and (TINY / "config.json").exists()
+
+
+def _skip_reason() -> str:
+ """Name exactly which prerequisite is missing, so the skip is actionable."""
+ if not ENGINE.exists():
+ return "glm.exe not built (run: make glm.exe)"
+ if not (TINY / "config.json").exists():
+ return "glm_tiny fixture absent (gitignored; ship it locally or run tools/make_glm_oracle.py)"
+ return ""
def _cuda_available() -> bool:
@@ -66,7 +84,7 @@ def _cuda_available() -> bool:
return False
-@unittest.skipUnless(_engine_present(), "glm.exe not built (run: make glm.exe)")
+@unittest.skipUnless(_engine_present(), _skip_reason() or "glm.exe + glm_tiny required")
class TinyEfficiencyTest(unittest.TestCase):
"""Asserted regression tests on the resident tiny model. Gates CI."""
@@ -156,7 +174,7 @@ class TinyEfficiencyTest(unittest.TestCase):
@unittest.skipUnless(_cuda_available(),
- "CUDA build not present (run: make clean && make glm.exe CUDA_DLL=1 && make cuda-dll)")
+ _skip_reason() or "CUDA build not present (run: make clean && make glm.exe CUDA_DLL=1 && make cuda-dll)")
class TinyCudaEfficiencyTest(unittest.TestCase):
"""CUDA-path regression tests on the tiny model. Skip unless CUDA built.
From f27a89ea8212de0627fbacb9226222d7708913f5 Mon Sep 17 00:00:00 2001
From: woolcoxm <13604288+woolcoxm@users.noreply.github.com>
Date: Thu, 16 Jul 2026 16:23:29 -0400
Subject: [PATCH 52/71] resource_plan: fix --auto-tier pinning decode to one
core (#325)
physical_cpu_count() silently returned 1 in two situations, and that value
flowed through build_plan -> OMP_NUM_THREADS to pin every matmul region to a
single thread under --auto-tier (reported on Rocky Linux 9, 512 GB RAM).
Two root causes:
1. The lscpu parse counted the wrong thing. `lscpu -p=core,socket` prepends the
CPU column, so the output is actually CPU,Core,Socket; the old set
comprehension collected (CPU,core,socket) tuples that were unique per logical
CPU. Now parse CPU,Core,Socket and dedupe on (core, socket) to get true
physical cores (the SMT-doubling the surrounding comments warn against was
the actual behavior).
2. Any probe failure fell through to `os.cpu_count() or 1`. On a cgroup'd or
otherwise constrained box os.cpu_count() can be 1 (or None), silently
capping the run. Skip offline core/socket fields ("-" instead of raising
ValueError) so a single offline row no longer discards the whole parse, and
replace the silent `or 1` fallback with os.cpu_count() (logical) plus an
explicit warning. Only return 1 when nothing at all is detected, and warn.
Also harden the win32 branch: declare argtypes/restype on
GetLogicalProcessorInformationEx (an undeclared 64-bit WinAPI returns c_int and
takes c_int pointers, so the probe could silently fail), and warn on its
fallbacks. Replace the silent max(1, int()) clamp in build_plan with
_resolve_physical_cores() that clamps to 1 with a warning instead of masking.
Tests: the existing OMP_NUM_THREADS test passed physical_cpus=24 explicitly, so
it never exercised the real probe -- that is why this regressed. Add regression
coverage for the lscpu physical-core dedup, offline fields, lscpu-missing
fallback, zero-cores degenerate case, and an end-to-end build_plan +
environment_for_plan check that OMP_NUM_THREADS reflects physical (not logical)
cores.
---
c/resource_plan.py | 87 +++++++++++++++++++++----
c/tests/test_resource_plan.py | 115 ++++++++++++++++++++++++++++++++++
2 files changed, 190 insertions(+), 12 deletions(-)
diff --git a/c/resource_plan.py b/c/resource_plan.py
index 15ce02a..056fb58 100644
--- a/c/resource_plan.py
+++ b/c/resource_plan.py
@@ -166,14 +166,33 @@ def discover_gpus():
return devices
+def _physical_cores_warn(message):
+ """Visibility for a mis-detected core count: a silent "1" here becomes
+ OMP_NUM_THREADS=1 and pins the whole run to a single core (#325). Emit on
+ stderr so it surfaces in the [PLAN]/[OMP] stream without being swallowed."""
+ print(f"[plan] warning: {message}", file=sys.stderr)
+
+
def physical_cpu_count():
+ """Number of physical CPU cores (not SMT siblings).
+
+ Per-expert matmul regions are tiny and back-to-back; two SMT siblings share
+ one AVX-512 unit and contend, so logical (SMT) counts over-subscribe and
+ hurt throughput. We want true physical cores. A silent 1 here propagates to
+ OMP_NUM_THREADS=1 and pins the run to one core (#325), so every fallback
+ must be visible, never just ``or 1``.
+ """
if sys.platform == "win32":
- # os.cpu_count() conta i processori logici (SMT): 2 thread/core saturano
- # le unita' AVX-512 e peggiorano il matmul. Contiamo i core fisici veri
- # con GetLogicalProcessorInformationEx(RelationProcessorCore).
+ # Contiamo i core fisici veri con GetLogicalProcessorInformationEx
+ # (RelationProcessorCore). Le firme vanno dichiarate: su Python a 64 bit
+ # una WinAPI non dichiarata ritorna c_int (32 bit) e riceve i puntatori
+ # come c_int di default, quindi il probe puo' fallire silenziosamente.
try:
import ctypes
k32 = ctypes.windll.kernel32
+ k32.GetLogicalProcessorInformationEx.argtypes = [
+ ctypes.c_uint, ctypes.c_void_p, ctypes.POINTER(ctypes.c_ulong)]
+ k32.GetLogicalProcessorInformationEx.restype = ctypes.c_int
need = ctypes.c_ulong(0)
k32.GetLogicalProcessorInformationEx(0, None, ctypes.byref(need))
buf = (ctypes.c_char * need.value)()
@@ -189,18 +208,62 @@ def physical_cpu_count():
off += size
if cores:
return cores
- except (OSError, ValueError, AttributeError):
- pass
+ _physical_cores_warn("GetLogicalProcessorInformationEx returned no cores")
+ except (OSError, ValueError, AttributeError) as error:
+ _physical_cores_warn(f"Windows core probe failed: {error}")
try:
- result = subprocess.run(["lscpu", "-p=core,socket"], text=True,
+ # lscpu -p prepende SEMPRE la colonna CPU, quindi `-p=core,socket` emette
+ # in realta' "CPU,Core,Socket" (3 colonne). Dobbiamo leggere esplicitamente
+ # CPU/Core/Socket e deduplicare su (core, socket): contare le righe non
+ # deduplicate restituirebbe i thread logici (SMT) - l'errore originale.
+ # I campi vuoti ("-") marcano core/socket offline e non sono interi.
+ result = subprocess.run(["lscpu", "-p=CPU,Core,Socket"], text=True,
capture_output=True, check=True, timeout=5)
- cores = {tuple(map(int, line.split(","))) for line in result.stdout.splitlines()
- if line and not line.startswith("#")}
+ cores = set()
+ for line in result.stdout.splitlines():
+ if not line or line.startswith("#"):
+ continue
+ fields = line.split(",")
+ if len(fields) < 3:
+ continue
+ try:
+ core, socket = int(fields[1]), int(fields[2])
+ except ValueError:
+ continue # "-" per un core/socket offline
+ cores.add((core, socket))
if cores:
return len(cores)
- except (OSError, ValueError, subprocess.SubprocessError):
- pass
- return os.cpu_count() or 1
+ except (OSError, ValueError, subprocess.SubprocessError) as error:
+ _physical_cores_warn(f"lscpu core probe failed: {error}")
+ logical = os.cpu_count()
+ if not logical:
+ _physical_cores_warn(
+ "could not detect any CPU cores; falling back to 1. "
+ "Set OMP_NUM_THREADS manually to fix single-core decode (#325).")
+ return 1
+ _physical_cores_warn(
+ f"physical-core probes unavailable; using {logical} logical CPUs "
+ f"(SMT may over-subscribe). Set OMP_NUM_THREADS to physical cores if slow.")
+ return logical
+
+
+def _resolve_physical_cores(physical_cpus):
+ """Coerce the build_plan() physical-core argument to a sane positive int.
+
+ A None/0/None-ish value reaching here means physical_cpu_count() already
+ warned; clamp to 1 (so the engine always gets a positive team size) but keep
+ that clamp visible rather than silently masking it as the old ``max(1, int())``
+ did (#325)."""
+ try:
+ count = int(physical_cpus or 0)
+ except (TypeError, ValueError):
+ count = 0
+ if count < 1:
+ _physical_cores_warn(
+ "physical core count resolved to 0; defaulting to 1. "
+ "Set OMP_NUM_THREADS to fix single-core decode (#325).")
+ return 1
+ return count
def cpu_socket_count():
@@ -366,7 +429,7 @@ def build_plan(model, ram_gb=0, context=4096, gpu_indices=None, vram_gb=0,
"policy": {"name": policy, **POLICIES[policy],
"quality_preserving": policy != "experimental-fast"},
"model": {key: value for key, value in info.items() if key != "config"},
- "cpu": {"physical_cores": max(1, int(physical_cpus)),
+ "cpu": {"physical_cores": _resolve_physical_cores(physical_cpus),
"sockets": max(1, int(cpu_sockets)),
"thread_policy": "physical-cores"},
"tiers": {
diff --git a/c/tests/test_resource_plan.py b/c/tests/test_resource_plan.py
index 4ef4706..f413878 100644
--- a/c/tests/test_resource_plan.py
+++ b/c/tests/test_resource_plan.py
@@ -5,6 +5,7 @@ import sys
import tempfile
import unittest
from pathlib import Path
+from unittest import mock
from resource_plan import (
GB,
@@ -14,6 +15,7 @@ from resource_plan import (
environment_for_plan,
format_plan,
memory_available,
+ physical_cpu_count,
)
@@ -87,6 +89,51 @@ class ResourcePlanTest(unittest.TestCase):
self.assertIn("clamped", plan["warnings"][0])
self.assertIn("0:test-gpu", format_plan(plan))
+ def test_auto_tier_thread_count_uses_physical_cores(self):
+ # End-to-end for #325: build_plan + environment_for_plan must export the
+ # physical (not logical SMT) core count as OMP_NUM_THREADS. The original
+ # suite passed physical_cpus=24 explicitly, so it never exercised the
+ # real physical_cpu_count() probe whose single-core failure pinned decode.
+ def lscpu(stdout):
+ return subprocess.CompletedProcess(args=[], returncode=0,
+ stdout=stdout, stderr="")
+ # 1 socket, 12 cores, 2 SMT siblings -> 24 threads, 12 physical cores.
+ # lscpu prepends the CPU column, so output is CPU,Core,Socket.
+ rows = [f"{cpu},{core},0" for core in range(12) for cpu in range(2)]
+ blob = "# CPU,Core,Socket\n" + "\n".join(rows)
+ with mock.patch("resource_plan.subprocess.run", return_value=lscpu(blob)), \
+ mock.patch.object(sys, "platform", "linux"):
+ plan = build_plan(self.model, available_memory=16 * GB,
+ available_disk=1, gpus=[])
+ env = environment_for_plan(plan)
+ self.assertEqual(plan["cpu"]["physical_cores"], 12)
+ self.assertEqual(env["OMP_NUM_THREADS"], "12")
+
+ def test_plan_conserves_budget_and_experts_above_256gb(self):
+ # Regression for #325's reporter: a 512 GB machine loading the whole
+ # model into RAM. Verify the budget math stays exact at large RAM sizes
+ # (no integer truncation, no over-allocation, no experts lost between
+ # tiers). Checked at 256/512/800 GB to bracket the reporter's box.
+ for ram_gb in (256, 512, 800):
+ plan = build_plan(self.model, ram_gb=ram_gb, available_disk=1,
+ gpus=[], physical_cpus=64)
+ ram = plan["tiers"]["ram"]
+ # RAM budget never over-allocated: dense + runtime + cache <= budget.
+ allocated = (ram["dense_bytes"] + ram["runtime_bytes"]
+ + ram["expert_cache_bytes"])
+ self.assertLessEqual(allocated, ram["budget_bytes"],
+ f"over-allocated RAM at {ram_gb} GB")
+ # Every expert byte is accounted for exactly once across the tiers.
+ tiers = plan["tiers"]
+ tiered = (tiers["vram"]["hot_expert_bytes"]
+ + ram["warm_expert_bytes"]
+ + tiers["disk"]["cold_expert_bytes"])
+ self.assertEqual(tiered, plan["model"]["expert_bytes"],
+ f"expert bytes lost/duplicated at {ram_gb} GB")
+ # A positive RAM budget yields a non-negative cache and a sensible cap.
+ self.assertGreaterEqual(ram["expert_cache_bytes"], 0)
+ self.assertGreaterEqual(ram["cache_slots_per_layer"], 0)
+
def test_filters_requested_devices(self):
gpus = [{"index": 0, "name": "a", "total_bytes": 8 * GB, "free_bytes": 8 * GB}]
plan = build_plan(self.model, available_memory=16 * GB, available_disk=1,
@@ -253,5 +300,73 @@ class ResourcePlanTest(unittest.TestCase):
self.assertIn("expected_bottleneck", plan)
+class PhysicalCpuCountTest(unittest.TestCase):
+ """Regression for #325: --auto-tier pinned decode to one core because
+ physical_cpu_count() silently returned 1.
+
+ Two root causes this locks down:
+ 1. lscpu -p prepends a CPU column, so `-p=core,socket` emits
+ CPU,Core,Socket; counting rows counted logical SMT siblings.
+ 2. any probe failure fell through to ``os.cpu_count() or 1`` and the
+ ``or 1`` could pin a constrained/cgroup'd box to a single core.
+ """
+
+ def _lscpu(self, stdout):
+ return subprocess.CompletedProcess(args=[], returncode=0,
+ stdout=stdout, stderr="")
+
+ def _lscpu_topology(self, sockets, cores_per_socket, threads_per_core):
+ # Real lscpu shape: socket-local core IDs repeat across sockets; the
+ # CPU column (always prepended) is a unique logical-CPU index.
+ rows, cpu = [], 0
+ for sock in range(sockets):
+ for core in range(cores_per_socket):
+ for _ in range(threads_per_core):
+ rows.append(f"{cpu},{core},{sock}")
+ cpu += 1
+ return "# CPU,Core,Socket\n" + "\n".join(rows)
+
+ def test_counts_physical_cores_not_smt_threads(self):
+ blob = self._lscpu_topology(sockets=2, cores_per_socket=16, threads_per_core=2)
+ with mock.patch("resource_plan.subprocess.run", return_value=self._lscpu(blob)), \
+ mock.patch.object(sys, "platform", "linux"):
+ self.assertEqual(physical_cpu_count(), 32)
+
+ def test_single_socket_no_smt(self):
+ blob = self._lscpu_topology(sockets=1, cores_per_socket=8, threads_per_core=1)
+ with mock.patch("resource_plan.subprocess.run", return_value=self._lscpu(blob)), \
+ mock.patch.object(sys, "platform", "linux"):
+ self.assertEqual(physical_cpu_count(), 8)
+
+ def test_skips_offline_core_socket_fields(self):
+ # VMs / large NUMA boxes emit "-" for offline core or socket IDs; that
+ # used to raise ValueError, discard the whole parse, and fall through
+ # to the single-core fallback.
+ blob = "# CPU,Core,Socket\n0,0,0\n1,-,0\n2,1,0\n3,1,0\n"
+ with mock.patch("resource_plan.subprocess.run", return_value=self._lscpu(blob)), \
+ mock.patch.object(sys, "platform", "linux"):
+ self.assertEqual(physical_cpu_count(), 2)
+
+ def test_lscpu_missing_falls_back_to_logical_not_silent_one(self):
+ # The bug: lscpu absent -> os.cpu_count() or 1. On a constrained box
+ # os.cpu_count() can be 1. We still must never silently pick 1 without
+ # a warning, and when logical cores exist they must be used.
+ import os
+ with mock.patch("resource_plan.subprocess.run", side_effect=FileNotFoundError), \
+ mock.patch.object(sys, "platform", "linux"), \
+ mock.patch("resource_plan.os.cpu_count", return_value=16), \
+ mock.patch("sys.stderr"):
+ self.assertEqual(physical_cpu_count(), 16)
+
+ def test_zero_logical_cores_warns_and_returns_one(self):
+ # The genuine degenerate case: no probe works and os.cpu_count() is
+ # None/1. Must return 1 (engine needs a positive team size) but warn.
+ with mock.patch("resource_plan.subprocess.run", side_effect=FileNotFoundError), \
+ mock.patch.object(sys, "platform", "linux"), \
+ mock.patch("resource_plan.os.cpu_count", return_value=None), \
+ mock.patch("sys.stderr"):
+ self.assertEqual(physical_cpu_count(), 1)
+
+
if __name__ == "__main__":
unittest.main()
From bd3efb1c9764c5dc61a83e9bd6b2f20b70f5e520 Mon Sep 17 00:00:00 2001
From: woolcoxm <13604288+woolcoxm@users.noreply.github.com>
Date: Thu, 16 Jul 2026 16:57:38 -0400
Subject: [PATCH 53/71] resource_plan: stop setting OMP_PROC_BIND/OMP_PLACES
from --auto-tier (#325)
The core-count fix was necessary but not sufficient: @liangstein confirmed
physical_cpu_count() now returns 64 on his box, yet --auto-tier STILL pinned
decode to one core. A complete env diff between the plain (working) and
--auto-tier (broken) paths showed exactly three keys the plan adds:
OMP_NUM_THREADS = 64 (correct, verified)
OMP_PROC_BIND = spread
OMP_PLACES = cores
Since OMP_NUM_THREADS was already correct, the culprit is the affinity pair.
The mechanism: environment_for_plan() sets OMP_PROC_BIND=spread + OMP_PLACES=cores
in the launcher's env. The engine's hot-thread tuning (glm.c main, the
COLI_OMP_TUNED self-exec) then tries setenv("OMP_PROC_BIND","close", overwrite=0)
-- but overwrite=0 cannot replace an already-set var, so the plan's "spread"
wins. On the reporter's libgomp + multi-socket topology, spread + places=cores
collapsed the team to a single CPU even with 64 threads configured.
Fix: don't set affinity from the plan at all. The engine deliberately chose
"close" for cache locality (the tiny back-to-back per-expert matmuls want
adjacent cores), and the plain path already leaves affinity to the engine.
Removing the plan's spread/places makes --auto-tier match the working plain
path; a user wanting a specific policy can still set OMP_PROC_BIND/OMP_PLACES
in their own environment (environment_for_plan only setdefaults OMP_NUM_THREADS).
Verified via env diff: after the fix, --auto-tier adds ONLY OMP_NUM_THREADS
beyond the plain env -- the engine's own close/bind tuning now wins on both
paths identically.
Note: this could not be reproduced on Windows (MinGW libgomp prints "Affinity
not supported on this configuration" and ignores the vars entirely); it is
Linux-libgomp-specific, matching the reporter's Rocky 9 box.
Tests: rewrite test_applies_plan_without_overriding_explicit_settings to assert
the plan sets NO affinity vars on any platform (the old test encoded the buggy
platform-dependent spread/cores contract). Add
test_plan_does_not_set_omp_affinity_vars as a focused regression. 78/78 pass.
---
c/resource_plan.py | 17 ++++++++++++-----
c/tests/test_resource_plan.py | 27 ++++++++++++++++++++-------
2 files changed, 32 insertions(+), 12 deletions(-)
diff --git a/c/resource_plan.py b/c/resource_plan.py
index 056fb58..27b5487 100644
--- a/c/resource_plan.py
+++ b/c/resource_plan.py
@@ -461,11 +461,18 @@ def environment_for_plan(plan, env=None, cuda_enabled=True):
result = dict(env or {})
result.setdefault("COLI_POLICY", plan["policy"]["name"])
result.setdefault("OMP_NUM_THREADS", str(plan["cpu"]["physical_cores"]))
- if sys.platform != "win32":
- # la libgomp di MinGW non supporta l'affinity su Windows
- # ("Affinity not supported on this configuration"): non impostarle li'.
- result.setdefault("OMP_PROC_BIND", "spread")
- result.setdefault("OMP_PLACES", "cores")
+ # NOTE: we intentionally do NOT set OMP_PROC_BIND / OMP_PLACES here.
+ # The engine's own hot-thread tuning (glm.c main(), the COLI_OMP_TUNED
+ # self-exec) sets OMP_PROC_BIND=close with overwrite=0 -- it prefers
+ # packing the team onto adjacent cores for the tiny back-to-back per-expert
+ # matmuls. Pre-setting OMP_PROC_BIND=spread here ran first and won (the
+ # engine's overwrite=0 setenv could not override an already-set var), and
+ # spread + OMP_PLACES=cores collapsed the team to one CPU on some libgomp /
+ # multi-socket topologies (#325: --auto-tier pinned decode to 1 core on a
+ # 64-core box even with OMP_NUM_THREADS=64). Leaving affinity to the engine
+ # makes --auto-tier match the plain (working) path. A user who wants a
+ # specific policy can still set OMP_PROC_BIND/OMP_PLACES in the environment
+ # themselves -- setdefault above only covers OMP_NUM_THREADS.
tune = plan.get("tune", {})
for key, entry in tune.items():
if key.startswith("_"):
diff --git a/c/tests/test_resource_plan.py b/c/tests/test_resource_plan.py
index f413878..7799af8 100644
--- a/c/tests/test_resource_plan.py
+++ b/c/tests/test_resource_plan.py
@@ -109,6 +109,19 @@ class ResourcePlanTest(unittest.TestCase):
self.assertEqual(plan["cpu"]["physical_cores"], 12)
self.assertEqual(env["OMP_NUM_THREADS"], "12")
+ def test_plan_does_not_set_omp_affinity_vars(self):
+ # The real #325 regression: --auto-tier set OMP_PROC_BIND=spread +
+ # OMP_PLACES=cores, which ran before the engine's overwrite=0 setenv and
+ # so won, collapsing the OpenMP team to one CPU on the reporter's 64-core
+ # Linux box even though OMP_NUM_THREADS was correct. The plan must leave
+ # affinity to the engine's own hot-thread tuning (which prefers 'close').
+ plan = build_plan(self.model, available_memory=16 * GB,
+ available_disk=1, gpus=[], physical_cpus=64)
+ env = environment_for_plan(plan)
+ self.assertEqual(env["OMP_NUM_THREADS"], "64")
+ self.assertNotIn("OMP_PROC_BIND", env)
+ self.assertNotIn("OMP_PLACES", env)
+
def test_plan_conserves_budget_and_experts_above_256gb(self):
# Regression for #325's reporter: a 512 GB machine loading the whole
# model into RAM. Verify the budget math stays exact at large RAM sizes
@@ -164,13 +177,13 @@ class ResourcePlanTest(unittest.TestCase):
self.assertEqual(env["COLI_CUDA"], "1")
self.assertEqual(env["COLI_GPUS"], "1")
self.assertEqual(env["OMP_NUM_THREADS"], str(plan["cpu"]["physical_cores"]))
- if sys.platform == "win32":
- # MinGW libgomp: niente affinity su Windows, le chiavi non vanno emesse
- self.assertNotIn("OMP_PROC_BIND", env)
- self.assertNotIn("OMP_PLACES", env)
- else:
- self.assertEqual(env["OMP_PROC_BIND"], "spread")
- self.assertEqual(env["OMP_PLACES"], "cores")
+ # The plan must NOT set OMP_PROC_BIND / OMP_PLACES on any platform:
+ # the engine's own hot-thread tuning owns affinity (it prefers
+ # OMP_PROC_BIND=close for the back-to-back per-expert matmuls). Setting
+ # spread + cores here ran before the engine's overwrite=0 setenv and so
+ # won, collapsing the team to one CPU on some libgomp topologies (#325).
+ self.assertNotIn("OMP_PROC_BIND", env)
+ self.assertNotIn("OMP_PLACES", env)
self.assertEqual(env["PIN_GB"], env["CUDA_EXPERT_GB"])
explicit_threads = environment_for_plan(plan, {"OMP_NUM_THREADS": "7",
From 171aa69fcb71798fc2c4e0062b9f43f33d3ce0d8 Mon Sep 17 00:00:00 2001
From: woolcoxm <13604288+woolcoxm@users.noreply.github.com>
Date: Fri, 17 Jul 2026 15:23:51 -0400
Subject: [PATCH 54/71] fix(resource_plan): take last two lscpu fields, not
[1]/[2] (#332 review)
JustVugg caught a regression by running the PR: the code asked
`lscpu -p=CPU,Core,Socket` and read fields[1]/fields[2], but the comment's
claim was inverted -- `lscpu -p=` emits EXACTLY the requested columns
(no CPU prefix), while bare `lscpu -p` prepends CPU.
On machines where the requested list is short or lscpu collapses to two
columns, every line was skipped by the `< 3` guard, cores stayed empty, and
the code fell through to os.cpu_count() -- the LOGICAL count. Result: 6
physical cores reported as 12 (SMT over-subscription), the opposite of the
fix.
Correct per review:
- ask lscpu for exactly 'core,socket'
- take fields[-2], fields[-1] (correct whether or not CPU is prepended)
- keep the warning scaffolding + the _resolve_physical_cores clamp
(those are the actual #325 fix; only the indexing was wrong)
Test now exercises BOTH the 2-column (-p=core,socket) and 3-column
(bare -p, CPU prefix) layouts, asserting 12 physical cores each. The
2-column case is the one that regressed: old parser returns 24, new
returns 12.
---
c/resource_plan.py | 25 ++++++++++++++++---------
c/tests/test_resource_plan.py | 35 +++++++++++++++++++++++++----------
2 files changed, 41 insertions(+), 19 deletions(-)
diff --git a/c/resource_plan.py b/c/resource_plan.py
index 27b5487..1374a3b 100644
--- a/c/resource_plan.py
+++ b/c/resource_plan.py
@@ -212,24 +212,31 @@ def physical_cpu_count():
except (OSError, ValueError, AttributeError) as error:
_physical_cores_warn(f"Windows core probe failed: {error}")
try:
- # lscpu -p prepende SEMPRE la colonna CPU, quindi `-p=core,socket` emette
- # in realta' "CPU,Core,Socket" (3 colonne). Dobbiamo leggere esplicitamente
- # CPU/Core/Socket e deduplicare su (core, socket): contare le righe non
- # deduplicate restituirebbe i thread logici (SMT) - l'errore originale.
- # I campi vuoti ("-") marcano core/socket offline e non sono interi.
- result = subprocess.run(["lscpu", "-p=CPU,Core,Socket"], text=True,
+ # Ask lscpu for exactly core,socket and dedupe on (core, socket).
+ # Counting un-deduplicated rows would return logical threads (SMT),
+ # which was the original over-subscription bug. Empty fields ("-")
+ # mark an offline core/socket and fail int() -> skipped.
+ #
+ # Column layout robustness: `lscpu -p=` emits *exactly* the
+ # requested columns (no CPU prefix), while bare `lscpu -p` prepends
+ # CPU. We requested two columns, but take the LAST TWO fields so the
+ # parser stays correct whether or not a CPU column is present
+ # (JustVugg review: the previous fields[1]/fields[2] indexing assumed
+ # a 3-column layout and regressed 2-column output to the logical
+ # count -- the opposite of the fix).
+ result = subprocess.run(["lscpu", "-p=core,socket"], text=True,
capture_output=True, check=True, timeout=5)
cores = set()
for line in result.stdout.splitlines():
if not line or line.startswith("#"):
continue
fields = line.split(",")
- if len(fields) < 3:
+ if len(fields) < 2:
continue
try:
- core, socket = int(fields[1]), int(fields[2])
+ core, socket = int(fields[-2]), int(fields[-1])
except ValueError:
- continue # "-" per un core/socket offline
+ continue # "-" for an offline core/socket
cores.add((core, socket))
if cores:
return len(cores)
diff --git a/c/tests/test_resource_plan.py b/c/tests/test_resource_plan.py
index 7799af8..f028c31 100644
--- a/c/tests/test_resource_plan.py
+++ b/c/tests/test_resource_plan.py
@@ -98,16 +98,31 @@ class ResourcePlanTest(unittest.TestCase):
return subprocess.CompletedProcess(args=[], returncode=0,
stdout=stdout, stderr="")
# 1 socket, 12 cores, 2 SMT siblings -> 24 threads, 12 physical cores.
- # lscpu prepends the CPU column, so output is CPU,Core,Socket.
- rows = [f"{cpu},{core},0" for core in range(12) for cpu in range(2)]
- blob = "# CPU,Core,Socket\n" + "\n".join(rows)
- with mock.patch("resource_plan.subprocess.run", return_value=lscpu(blob)), \
- mock.patch.object(sys, "platform", "linux"):
- plan = build_plan(self.model, available_memory=16 * GB,
- available_disk=1, gpus=[])
- env = environment_for_plan(plan)
- self.assertEqual(plan["cpu"]["physical_cores"], 12)
- self.assertEqual(env["OMP_NUM_THREADS"], "12")
+
+ # The parser must return 12 physical cores under BOTH lscpu layouts:
+ # - 2-col: `lscpu -p=core,socket` emits exactly [core,socket] (this is
+ # what the probe actually requests; the previous fields[1]/[2]
+ # indexing skipped every line here and fell through to the
+ # logical count -> the regression JustVugg caught).
+ # - 3-col: bare `lscpu -p` prepends a CPU column -> [cpu,core,socket].
+ # Taking the last two fields is correct in both cases.
+ layouts = {
+ "2-col (-p=core,socket)": (
+ "# core,socket\n" +
+ "\n".join(f"{core},0" for core in range(12) for _ in range(2))),
+ "3-col (bare -p, CPU prefix)": (
+ "# CPU,Core,Socket\n" +
+ "\n".join(f"{cpu},{core},0" for core in range(12) for cpu in range(2))),
+ }
+ for label, blob in layouts.items():
+ with mock.patch("resource_plan.subprocess.run",
+ return_value=lscpu(blob)), \
+ mock.patch.object(sys, "platform", "linux"):
+ plan = build_plan(self.model, available_memory=16 * GB,
+ available_disk=1, gpus=[])
+ env = environment_for_plan(plan)
+ self.assertEqual(plan["cpu"]["physical_cores"], 12, label)
+ self.assertEqual(env["OMP_NUM_THREADS"], "12", label)
def test_plan_does_not_set_omp_affinity_vars(self):
# The real #325 regression: --auto-tier set OMP_PROC_BIND=spread +
From 0b05f6a76bc82cf1e26415be8fb4758036d9d018 Mon Sep 17 00:00:00 2001
From: woolcoxm <13604288+woolcoxm@users.noreply.github.com>
Date: Fri, 17 Jul 2026 12:03:43 -0400
Subject: [PATCH 55/71] win32: auto-enable the GPU in bare coli chat
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
On Windows a bare 'coli chat' (no --gpu/--vram/--auto-tier) ALWAYS ran
CPU-only, even on a CUDA build with a GPU present. Two defects:
1. cuda_binary() returned False on Windows. It detects CUDA by running
'ldd glm | grep libcudart', which is Linux-only (no ldd on win32) and
meaningless anyway because the Windows engine links cudart only inside a
runtime-loaded coli_cuda.dll, not as a libcudart symbol in glm.exe. So
the --gpu/--vram/--auto-tier gates (which call cuda_binary()) never opened.
2. Even with detection fixed, bare 'coli chat' set no CUDA env: env_for's
else-branch only enables CUDA when --gpu/--vram is passed. Nothing
auto-enabled the GPU.
Now: cuda_binary() on non-Linux returns True iff coli_cuda.dll exists next
to glm.exe — the exact file backend_loader.c loads from the engine's own
directory, so its presence is a faithful, cheap, DLL-hijack-safe proxy for a
CUDA-capable build. And env_for, scoped to win32 (Linux keeps its working
explicit-flag UX), auto-enables CUDA when a bare chat detects a CUDA build
plus a GPU via nvidia-smi, sizing the expert-tier VRAM budget from real free
VRAM via the existing build_plan/environment_for_plan machinery (same as
--auto-tier, no guessed budget). If nvidia-smi is missing it falls back to
CPU with a clear warning; --gpu none still forces CPU; explicit --vram/--gpu
still win. CUDA_DENSE stays an explicit opt-in (matches --auto-tier).
Verified on a Windows + RTX 5070 Ti box: bare 'coli chat --model ' now
prints '[GPU] auto-enabled CUDA ... 13.0 GB expert tier' and emits
COLI_CUDA=1 / COLI_GPUS=0 / CUDA_EXPERT_GB=13.044 (was: all unset, CPU-only).
Tests: 4 new cases (auto-enable, nvidia-smi-missing fallback, CPU-build
silent, Linux-unchanged) plus the 4 existing default-I/O tests guarded to
mock cuda_binary() so they stay host-independent. Full python suite green
(env_defaults 8, resource_plan 10, doctor 8, makefile_platform 3, cli_output 3).
Out of scope: doctor.cuda_linkage is also POSIX-only and mis-reports on
Windows — separate follow-up.
---
c/coli | 37 ++++++++++++++++
c/tests/test_env_defaults.py | 86 +++++++++++++++++++++++++++++++++++-
docs/ENVIRONMENT.md | 9 ++++
3 files changed, 130 insertions(+), 2 deletions(-)
diff --git a/c/coli b/c/coli
index d6728fe..b00a1cd 100755
--- a/c/coli
+++ b/c/coli
@@ -237,6 +237,43 @@ def env_for(a):
gpu=f" · VRAM {format_bytes(vt['budget_bytes'])}" if has_cuda and vt["devices"] else " · CPU"
print(f" {C.dim}[PLAN] RAM {format_bytes(rt['budget_bytes'])} · cap {rt['cache_slots_per_layer']}/layer{gpu}{C.r}",file=sys.stderr)
else:
+ # Windows: a bare `coli chat` (no --gpu/--vram/--auto-tier) used to ALWAYS
+ # run CPU-only, even on a CUDA build with a GPU present — cuda_binary()
+ # returned False on Windows (see above), and nothing set COLI_CUDA without
+ # an explicit flag. Now that detection works, auto-enable the GPU when one
+ # is detected so `coli chat` Just Works. Scoped to Windows: Linux already
+ # has working detection + the explicit-flag UX, and changing bare-chat
+ # semantics there is out of scope. Falls back to CPU with a warning if
+ # nvidia-smi is missing (discover_gpus can't size VRAM without it).
+ if (sys.platform == "win32" and a.gpu is None and not a.vram):
+ if cuda_binary():
+ from resource_plan import discover_gpus, build_plan, environment_for_plan, format_bytes
+ gpus = discover_gpus()
+ if gpus:
+ e["COLI_CUDA"]="1"
+ e.setdefault("COLI_GPUS", ",".join(str(g["index"]) for g in gpus))
+ # Reuse the planner so the expert-tier VRAM budget is the real
+ # free VRAM minus the 2 GB reserve — not a guess. Same machinery
+ # as --auto-tier, just without requiring the user to pass it.
+ ram,ctx,devices,vram_req = resource_request(a, e)
+ try:
+ plan=build_plan(a.model,ram,ctx,devices,vram_req,policy=a.policy)
+ e.update(environment_for_plan(plan,e,cuda_enabled=True))
+ vt=plan["tiers"]["vram"]
+ names=",".join(g["name"].strip() for g in gpus)
+ print(f" {C.dim}[GPU] auto-enabled CUDA · {names} · "
+ f"{format_bytes(vt['budget_bytes'])} expert tier{C.r}", file=sys.stderr)
+ except (OSError,ValueError,json.JSONDecodeError) as error:
+ # Plan failed (e.g. model dir unreadable): don't block the
+ # run, just leave the unsized COLI_CUDA=1 and let the engine
+ # pick its own budget. Engine handles a missing budget.
+ print(f" {C.yel}[GPU] auto-enable: could not size VRAM ({error}); "
+ f"using engine default{C.r}", file=sys.stderr)
+ else:
+ print(f" {C.yel}[GPU] coli_cuda.dll present but nvidia-smi not found on PATH "
+ f"(cannot size VRAM); running CPU-only. Add nvidia-smi to PATH or pass "
+ f"--vram N to enable CUDA.{C.r}", file=sys.stderr)
+ # else: CPU build (no coli_cuda.dll) — stay silent, CPU is correct.
# --gpu/--vram SENZA --auto-tier: prima venivano ignorati in silenzio e il run
# partiva CPU-only senza alcun avviso — benchmark "GPU" pubblicati per errore (#121).
if a.gpu is not None:
diff --git a/c/tests/test_env_defaults.py b/c/tests/test_env_defaults.py
index 12a3c99..3bb08fc 100644
--- a/c/tests/test_env_defaults.py
+++ b/c/tests/test_env_defaults.py
@@ -32,9 +32,16 @@ def args(**over):
class EnvDefaultsTest(unittest.TestCase):
- def env_for_with(self, environ, platform):
+ def env_for_with(self, environ, platform, cuda=False):
+ """Run env_for on a bare-chat args() under a faked env + platform.
+
+ cuda=False by default so the existing default-I/O tests stay
+ deterministic: the Windows auto-enable branch calls cuda_binary() and
+ (if True) discover_gpus(), both of which reach the real machine — faking
+ False keeps these tests independent of the host's GPU."""
with mock.patch.dict(os.environ, environ, clear=True), \
- mock.patch.object(sys, "platform", platform):
+ mock.patch.object(sys, "platform", platform), \
+ mock.patch.object(coli, "cuda_binary", return_value=cuda):
return coli.env_for(args())
def test_win32_sets_measured_defaults(self):
@@ -64,5 +71,80 @@ class EnvDefaultsTest(unittest.TestCase):
self.assertNotIn(k, e)
+class CudaAutoEnableTest(unittest.TestCase):
+ """Windows bare `coli chat` (no --gpu/--vram/--auto-tier) used to ALWAYS run
+ CPU-only even on a CUDA build with a GPU present. env_for now auto-enables
+ CUDA on win32 when cuda_binary() is True and a GPU is discoverable; falls
+ back to CPU with a warning if nvidia-smi (discover_gpus) is missing; stays
+ silent on a CPU build; and never touches the Linux path."""
+
+ def _env_for(self, platform, cuda, gpus, plan=None):
+ # Patch discover_gpus / build_plan / environment_for_plan at the
+ # resource_plan module (env_for imports them lazily on each call, so the
+ # patches are live when those imports run). Stubbing the planner keeps
+ # the test independent of a real model dir (args().model == "X").
+ import resource_plan
+ a = args()
+ GPB = 1024 ** 3
+ if plan is None:
+ plan = {"tiers": {"ram": {"budget_bytes": 16 * GPB, "cache_slots_per_layer": 4},
+ "vram": {"budget_bytes": int(8.0 * GPB), "devices": gpus}}}
+
+ def fake_environment_for_plan(p, env, cuda_enabled=True):
+ # Mirror the real contract: size CUDA_EXPERT_GB from the plan's VRAM
+ # budget (this is the value env_for propagates into the engine env).
+ r = dict(env)
+ if cuda_enabled and p["tiers"]["vram"]["devices"] and p["tiers"]["vram"]["budget_bytes"] > 0:
+ r["CUDA_EXPERT_GB"] = f"{p['tiers']['vram']['budget_bytes'] / GPB:.3f}"
+ return r
+
+ with mock.patch.dict(os.environ, {}, clear=True), \
+ mock.patch.object(sys, "platform", platform), \
+ mock.patch.object(coli, "cuda_binary", return_value=cuda), \
+ mock.patch.object(resource_plan, "discover_gpus", return_value=gpus), \
+ mock.patch.object(resource_plan, "build_plan", return_value=plan), \
+ mock.patch.object(resource_plan, "environment_for_plan",
+ side_effect=fake_environment_for_plan):
+ return coli.env_for(a)
+
+ def _fake_gpu(self, index=0, name="NVIDIA GeForce RTX 5070 Ti",
+ total_mib=16384, free_mib=15000):
+ return {"index": index, "name": name,
+ "total_bytes": total_mib * 1024 * 1024,
+ "free_bytes": free_mib * 1024 * 1024}
+
+ def test_win32_auto_enables_cuda_when_gpu_present(self):
+ e = self._env_for("win32", cuda=True, gpus=[self._fake_gpu()])
+ self.assertEqual(e["COLI_CUDA"], "1")
+ self.assertEqual(e["COLI_GPUS"], "0")
+ # VRAM budget is sized from free VRAM by build_plan (real minus reserve),
+ # so it must be present and positive — never a guess or zero.
+ self.assertIn("CUDA_EXPERT_GB", e)
+ self.assertGreater(float(e["CUDA_EXPERT_GB"]), 0.0)
+ # Dense offload is an explicit opt-in (matches --auto-tier): not set here.
+ self.assertNotIn("CUDA_DENSE", e)
+
+ def test_win32_falls_back_to_cpu_when_nvidia_smi_missing(self):
+ # coli_cuda.dll present (cuda=True) but nvidia-smi absent (no GPUs found)
+ # -> warn + CPU-only, never crash, never set COLI_CUDA.
+ e = self._env_for("win32", cuda=True, gpus=[])
+ self.assertNotIn("COLI_CUDA", e)
+ self.assertNotIn("COLI_GPUS", e)
+ self.assertNotIn("CUDA_EXPERT_GB", e)
+
+ def test_win32_cpu_build_stays_silent(self):
+ # No coli_cuda.dll (cuda=False) -> CPU build, nothing GPU-related emitted.
+ e = self._env_for("win32", cuda=False, gpus=[self._fake_gpu()])
+ self.assertNotIn("COLI_CUDA", e)
+ self.assertNotIn("COLI_GPUS", e)
+
+ def test_linux_bare_chat_not_auto_enabled(self):
+ # The auto-enable is scoped to win32: a Linux bare chat with a GPU
+ # present must NOT turn CUDA on (Linux keeps the explicit-flag UX).
+ e = self._env_for("linux", cuda=True, gpus=[self._fake_gpu()])
+ self.assertNotIn("COLI_CUDA", e)
+ self.assertNotIn("CUDA_EXPERT_GB", e)
+
+
if __name__ == "__main__":
unittest.main()
diff --git a/docs/ENVIRONMENT.md b/docs/ENVIRONMENT.md
index 9f0d749..c3694c6 100644
--- a/docs/ENVIRONMENT.md
+++ b/docs/ENVIRONMENT.md
@@ -105,6 +105,15 @@ Format: `VAR` — default — effect.
| `COLI_CUDA_SHARED_W4A16_MIN_ROWS` | `32` | Min row count to engage the shared-MLP W4A16 kernel. |
| `COLI_METAL_UNTRACKED` | off (Metal only) | `=1` sets `MTLResourceHazardTrackingModeUntracked` on Metal buffers (reduces hazard-tracking overhead). |
+> **Windows note.** On Windows, a bare `coli chat` / `coli run` / `coli serve`
+> (no `--gpu`/`--vram`/`--auto-tier`) **auto-enables the GPU** when it detects a
+> CUDA build (`coli_cuda.dll` next to the engine) and at least one GPU via
+> `nvidia-smi`. The expert-tier VRAM budget is then sized automatically from the
+> card's free VRAM (same computation as `--auto-tier`). If `nvidia-smi` is not on
+> `PATH` the run falls back to CPU with a warning — pass `--vram N` (or add
+> `nvidia-smi` to `PATH`) to enable CUDA in that case. `--gpu none` forces
+> CPU-only. (Linux/macOS behaviour is unchanged: pass a flag to enable CUDA.)
+
---
## Advanced / experimental / debug
From 7de49fa02d0d37dcb2145157d40c4dd68b6d3983 Mon Sep 17 00:00:00 2001
From: JustVugg
Date: Mon, 20 Jul 2026 17:23:21 +0200
Subject: [PATCH 56/71] =?UTF-8?q?docs:=20explain=20the=20prebuilt=20Window?=
=?UTF-8?q?s=20binary=20=E2=80=94=20what=20the=20.exe=20is=20and=20how=20t?=
=?UTF-8?q?o=20run=20it=20(#450)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The release ships colibri--windows-x86_64.zip but nothing said what the
.exe is for or how to use it. The quickstart's Windows Option A now lists the
zip contents (engine .exe / coli launcher / Python support), and gives the two
missing steps: rename the engine to glm.exe so the coli launcher finds it, and
install Python 3 for the launcher/gateway. README's run section gets a short
Windows-prebuilt pointer to the same. Docs-only.
Closes #450
Co-Authored-By: Claude Fable 5
---
README.md | 7 +++++++
docs/quickstart.md | 30 +++++++++++++++++++++++++-----
2 files changed, 32 insertions(+), 5 deletions(-)
diff --git a/README.md b/README.md
index d6f9bcb..48d0209 100644
--- a/README.md
+++ b/README.md
@@ -187,6 +187,13 @@ COLI_MODEL=/nvme/glm52_i4 ./coli doctor # read-only readiness check
The engine at runtime is pure C — python is only used by the one-time converter
and the optional API gateway.
+**On Windows?** You don't need to build. Download the
+`colibri--windows-x86_64.zip` from
+[Releases](https://github.com/JustVugg/colibri/releases), unzip it, rename
+`colibri-*-windows-x86_64.exe` → `glm.exe` (so the `coli` launcher finds the
+engine), install [Python 3](https://www.python.org/downloads/), then run
+`coli chat`. Full walkthrough in the [Quick Start guide](docs/quickstart.md#windows).
+
Prefer a `coli` command on your PATH? From a checkout, `pip install -e .`
registers it (the engine itself still lives in `c/` — this is an editable
install from the clone, not a standalone wheel).
diff --git a/docs/quickstart.md b/docs/quickstart.md
index 7d6a9bf..60d4877 100644
--- a/docs/quickstart.md
+++ b/docs/quickstart.md
@@ -42,11 +42,31 @@ engine needs.
You have two options.
**Option A — download a prebuilt binary (no compiler needed).**
-Grab the latest `colibri--windows-x86_64.zip` from the
-[Releases page](https://github.com/JustVugg/colibri/releases), unzip it, and
-skip to [step 3](#3-get-the-model). Python 3 (from
-[python.org](https://www.python.org/downloads/)) is still needed if you want to
-convert a model yourself.
+Grab `colibri--windows-x86_64.zip` from the
+[Releases page](https://github.com/JustVugg/colibri/releases) and unzip it.
+Inside you'll find:
+
+| File | What it is |
+|---|---|
+| `colibri--windows-x86_64.exe` | **the engine** — the C program that actually runs the model |
+| `coli` | the command-line launcher (`chat`, `serve`, `convert`, `doctor`, …) |
+| `openai_server.py`, `resource_plan.py`, `doctor.py` | Python support for the API server and placement planner |
+
+Two setup steps:
+
+1. **Rename the engine to `glm.exe`** so the launcher can find it (it looks for a
+ binary named `glm`):
+ ```powershell
+ Rename-Item colibri-*-windows-x86_64.exe glm.exe
+ ```
+2. **Install Python 3** from [python.org](https://www.python.org/downloads/) — the
+ `coli` launcher and the API gateway are Python scripts (the engine itself is
+ pure C and needs nothing).
+
+Then continue to [step 3](#3-get-the-model). Prefer to skip the launcher? You can
+run the engine directly — `.\glm.exe` reads the model path from the `SNAP`
+environment variable (see [docs/windows.md](windows.md)) — but `coli chat` is the
+easy path.
**Option B — build from source with MSYS2.**
Install [MSYS2](https://www.msys2.org/), open the **UCRT64** shell, and run:
From 31526ae619eaaa27479febb8407d55199c479cec Mon Sep 17 00:00:00 2001
From: JustVugg
Date: Mon, 20 Jul 2026 18:15:39 +0200
Subject: [PATCH 57/71] fix(build): test_pipe_block references glm.c, renamed
to colibri.c by #391
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The #270 rebase resolved the TEST_BINS list but missed the test_pipe_block
rule prerequisite and its #include, both still pointing at glm.c (which #391
renamed to colibri.c) — 'No rule to make target glm.c' broke the Linux C test
suite on dev. Point both at colibri.c. Build-verified locally.
Co-Authored-By: Claude Fable 5
---
c/Makefile | 2 +-
c/tests/test_pipe_block.c | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/c/Makefile b/c/Makefile
index 35eccb5..3ebf7d8 100644
--- a/c/Makefile
+++ b/c/Makefile
@@ -368,7 +368,7 @@ tests/bench_idot$(EXE): tests/bench_idot.c colibri.c st.h uring.h json.h tok.h t
tests/test_uring$(EXE): tests/test_uring.c colibri.c st.h uring.h json.h tok.h tok_unicode.h compat.h grammar.h tier.h quant.h sample.h kv_persist.h telemetry.h
$(CC) $(CFLAGS) $< -o $@ $(LDFLAGS)
-tests/test_pipe_block$(EXE): tests/test_pipe_block.c glm.c st.h uring.h json.h tok.h tok_unicode.h compat.h grammar.h tier.h
+tests/test_pipe_block$(EXE): tests/test_pipe_block.c colibri.c st.h uring.h json.h tok.h tok_unicode.h compat.h grammar.h tier.h
$(CC) $(CFLAGS) $< -o $@ $(LDFLAGS)
test-c: $(TEST_BINS)
diff --git a/c/tests/test_pipe_block.c b/c/tests/test_pipe_block.c
index 64fa276..8571695 100644
--- a/c/tests/test_pipe_block.c
+++ b/c/tests/test_pipe_block.c
@@ -19,7 +19,7 @@
#include
#include
#define main coli_glm_main_unused
-#include "../glm.c"
+#include "../colibri.c"
#undef main
static int fail(const char *s){ fprintf(stderr,"FAIL: %s\n",s); return 1; }
From 49c11539b22b46161119375fb0b5c00e22414849 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Attila=20Ol=C3=A1h?=
Date: Mon, 20 Jul 2026 18:22:58 +0200
Subject: [PATCH 58/71] nix: re-use existing pkgs binding
---
flake.nix | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/flake.nix b/flake.nix
index 55f95ee..a408386 100644
--- a/flake.nix
+++ b/flake.nix
@@ -120,7 +120,7 @@
};
};
- formatter = (import nixpkgs {inherit system;}).alejandra;
+ formatter = pkgs.alejandra;
devShells.default = pkgs.mkShell {
inputsFrom = [colibri];
From d7211632b4e7223e0a4f3dd21bfab2ed6ed0478e Mon Sep 17 00:00:00 2001
From: FABIOTESS
Date: Tue, 14 Jul 2026 15:14:03 +0100
Subject: [PATCH 59/71] =?UTF-8?q?refactor:=20grammar-draft=20state=20into?=
=?UTF-8?q?=20GrDraft=20struct=20(mechanical,=20no=20behavior=20change)=20?=
=?UTF-8?q?=E2=80=94=20groundwork=20for=20per-request=20grammars=20in=20se?=
=?UTF-8?q?rve=5Fmux?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-Authored-By: Claude Fable 5
---
c/colibri.c | 105 ++++++++++++++++++++++++++++------------------------
1 file changed, 56 insertions(+), 49 deletions(-)
diff --git a/c/colibri.c b/c/colibri.c
index fe1b383..a24d540 100644
--- a/c/colibri.c
+++ b/c/colibri.c
@@ -512,12 +512,6 @@ static int g_draft=0; /* metodo E: DRAFT=n token auto-speculati per forward v
* parte (#8). MAI un vincolo sul sampling: solo proposte, la verifica batch-union
* decide — grammatica sbagliata = draft rifiutati, output identico.
* GRAMMAR_DRAFT=n (default 24) limita i token forzati per forward. */
-static Grammar g_gram; static GrState g_gst;
-static Tok *g_gr_T=NULL;
-static int g_gr_on=0; /* grammatica caricata e walker vivo */
-static int g_gr_armed=0; /* lazy: parte dal primo byte ammesso dalla radice (salta i preamboli) */
-static int g_gr_max=24;
-static uint64_t g_gr_prop=0, g_gr_acc=0;
static FILE *g_route_fp=NULL; /* ROUTE_TRACE=: dump per-position top-K routing (ids:gates)
* per layer — offline co-activation / coupling analysis. Zero
* effect on computation; measurement only. */
@@ -537,6 +531,19 @@ static int g_couple=0, g_couple_k=8, g_couple_d=1;
static int16_t *cp_pred=NULL; /* [(L*2+(dL-1))*E + e]*CP_M + j -> target id (-1 none) */
static float *cp_cnt=NULL;
static long g_cp_enq=0;
+/* All grammar-forced-draft state in one struct so it can become per-request
+ * in the multiplexed server. Fields (same semantics as the former globals):
+ * on = grammar loaded and walker alive; armed = lazy start from the first byte
+ * accepted at the root (skips preambles); max = forced-span cap per forward;
+ * prop/acc = proposed/accepted forced-draft counters. */
+typedef struct {
+ Grammar gram;
+ GrState st;
+ Tok *T;
+ int on, armed, max;
+ uint64_t prop, acc;
+} GrDraft;
+static GrDraft g_grd={.max=24}; /* process-level instance: PROMPT mode + run_serve keep using this */
static void couple_prefetch(Model *m, int layer, const int *idx, int Ke);
static int g_looka=0; /* LOOKA=1: misura (solo contatori, zero effetti) quanto il routing MoE
* e' predicibile IN ANTICIPO — la domanda che decide se un prefetch
@@ -3955,7 +3962,7 @@ static void repin_pass(Model *m){ repin_pass_limit(m,16); }
* grammar_draft propone lo span FORZATO successivo (un solo byte legale per posizione)
* gia' tokenizzato. Il confine di tokenizzazione non e' garantito coincidere con quello
* del modello: la verifica assorbe la differenza (al peggio l'ultimo draft e' rifiutato). */
-static void grammar_setup(Tok *T){
+static void grammar_setup(GrDraft *g, Tok *T){
/* GRAMMAR= takes precedence; SCHEMA= compiles a JSON-Schema
* to GBNF (schema_gbnf.h) for the same draft source. Both fail soft: the engine
* runs without a grammar and output is unchanged. */
@@ -3977,52 +3984,52 @@ static void grammar_setup(Tok *T){
if(!gbnf){ fprintf(stderr,"[SCHEMA] %s: %s (running without grammar)\n",sf,serr); return; }
txt=gbnf;
}
- if(gr_parse(&g_gram,txt)){ fprintf(stderr,"[GRAMMAR] %s: %s\n",path,g_gram.err); free(txt); return; }
+ if(gr_parse(&g->gram,txt)){ fprintf(stderr,"[GRAMMAR] %s: %s\n",path,g->gram.err); free(txt); return; }
free(txt);
- gr_state_init(&g_gst,&g_gram);
- if(!g_gst.alive){ fprintf(stderr,"[GRAMMAR] %s: grammar cannot be evaluated (left recursion?)\n",path); return; }
- if(getenv("GRAMMAR_DRAFT")) g_gr_max=atoi(getenv("GRAMMAR_DRAFT"));
- if(g_gr_max<1) g_gr_max=1;
- if(g_gr_max>48) g_gr_max=48;
- g_gr_T=T; g_gr_on=1;
- fprintf(stderr,"[GRAMMAR] %s: %d rules, forced span capped at %d tokens/forward\n",path,g_gram.n,g_gr_max);
+ gr_state_init(&g->st,&g->gram);
+ if(!g->st.alive){ fprintf(stderr,"[GRAMMAR] %s: grammar cannot be evaluated (left recursion?)\n",path); return; }
+ if(getenv("GRAMMAR_DRAFT")) g->max=atoi(getenv("GRAMMAR_DRAFT"));
+ if(g->max<1) g->max=1;
+ if(g->max>48) g->max=48;
+ g->T=T; g->on=1;
+ fprintf(stderr,"[GRAMMAR] %s: %d rules, forced span capped at %d tokens/forward\n",path,g->gram.n,g->max);
}
/* stato pulito all'inizio di ogni RISPOSTA (non tra i \x02MORE, che continuano) */
-static void grammar_reset(void){
- if(!g_gr_on) return;
- gr_state_init(&g_gst,&g_gram); g_gr_armed=0;
- if(!g_gst.alive) g_gr_on=0;
+static void grammar_reset(GrDraft *g){
+ if(!g->on) return;
+ gr_state_init(&g->st,&g->gram); g->armed=0;
+ if(!g->st.alive) g->on=0;
}
/* consuma i byte di un token emesso. Preambolo (prima dell'arming): ignorato.
* Desync dopo l'arming: si riarma in attesa del prossimo inizio valido — al peggio
* i draft vengono rifiutati dalla verifica, l'output non cambia MAI. */
-static void gr_feed(int t){
- if(!g_gr_on||!g_gr_T) return;
- char b[64]; int n=tok_decode(g_gr_T,&t,1,b,63);
+static void gr_feed(GrDraft *g, int t){
+ if(!g->on||!g->T) return;
+ char b[64]; int n=tok_decode(g->T,&t,1,b,63);
for(int i=0;ist,(unsigned char)b[i]);
+ if(r==1){ g->armed=1; continue; }
+ if(r<0){ g->on=0; return; } /* walker spento: fine dei draft */
+ if(!g->armed) continue; /* preambolo: aspetta l'inizio */
+ gr_state_init(&g->st,&g->gram); g->armed=0; /* desync: riparti dalla radice */
+ if(!g->st.alive){ g->on=0; return; }
+ if(gr_accept(&g->st,(unsigned char)b[i])==1) g->armed=1;
}
}
/* propone lo span forzato come token (max cap); 0 se la grammatica dirama qui */
-static int grammar_draft(int *draft, int cap){
- if(!g_gr_on||!g_gr_armed||!g_gr_T||cap<1) return 0;
- if(g_gr_prop>=32 && g_gr_acc*2on||!g->armed||!g->T||cap<1) return 0;
+ if(g->prop>=32 && g->acc*2prop){ /* guardia adattiva, come per MTP:
acceptance sotto il 50% = tokenizzazione fuori asse, meglio spegnersi */
- g_gr_on=0;
+ g->on=0;
fprintf(stderr,"[GRAMMAR] %.0f%% acceptance after %llu proposals: grammar drafts disabled\n",
- 100.0*g_gr_acc/g_gr_prop,(unsigned long long)g_gr_prop);
+ 100.0*g->acc/g->prop,(unsigned long long)g->prop);
return 0;
}
- char fb[512]; int nb=gr_forced(&g_gst,fb,(int)sizeof fb-1);
+ char fb[512]; int nb=gr_forced(&g->st,fb,(int)sizeof fb-1);
if(nb<=0) return 0;
- int g=tok_encode(g_gr_T,fb,nb,draft,cap);
- return g>0?g:0;
+ int nt=tok_encode(g->T,fb,nb,draft,cap); /* renamed local: 'g' is now the state param */
+ return nt>0?nt:0;
}
/* ---- SAMPLING (temperatura + nucleus) con verifica speculativa LOSSLESS ----
@@ -4080,12 +4087,12 @@ static int spec_decode(Model *m, int *all, int kv, int n_new, int eos, float *lo
int next=pick_tok(logit,V,carry_ban); carry_ban=-1; free(logit); logit=NULL;
if((eos>=0 && next==eos) || is_stop(next)) break;
emit(next,ud); all[kv]=next; emitted++; m->n_emit++;
- gr_feed(next); /* il walker segue l'output emesso */
+ gr_feed(&g_grd,next); /* il walker segue l'output emesso */
if(emitted>=n_new) break; /* l'ultimo token non serve forwardarlo */
int g = 0, gsrc = 0; /* sorgente: 1=grammatica 2=MTP/n-gram */
- if(g_gr_on){ /* metodo F: prima la grammatica — dove
+ if(g_grd.on){ /* metodo F: prima la grammatica — dove
* forza, l'acceptance e' ~1 (#48) */
- g=grammar_draft(draft,g_gr_max);
+ g=grammar_draft(&g_grd,draft,g_grd.max);
if(g>0) gsrc=1;
}
if(!g && g_draft>0 && m->has_mtp){
@@ -4107,7 +4114,7 @@ static int spec_decode(Model *m, int *all, int kv, int n_new, int eos, float *lo
if(g>n_new-emitted) g=n_new-emitted;
if(kv+1+g+1>m->max_t) g=m->max_t-kv-2;
if(g<0) g=0;
- if(gsrc==1) g_gr_prop+=(uint64_t)g;
+ if(gsrc==1) g_grd.prop+=(uint64_t)g;
int S=1+g; int batch[64]; batch[0]=next; memcpy(batch+1,draft,g*sizeof(int));
double tf0=g_prof?now_s():0;
float *lo=step_all(m,batch,S,kv); m->n_fw++;
@@ -4123,9 +4130,9 @@ static int spec_decode(Model *m, int *all, int kv, int n_new, int eos, float *lo
if(!accept){ if(g_temp>0) carry_ban=draft[k]; break; }
if((eos>=0 && draft[k]==eos) || is_stop(draft[k])){ done=1; break; }
emit(draft[k],ud); all[kv+1+k]=draft[k]; emitted++; m->n_emit++;
- gr_feed(draft[k]); k++;
+ gr_feed(&g_grd,draft[k]); k++;
}
- if(gsrc==1) g_gr_acc+=(uint64_t)k;
+ if(gsrc==1) g_grd.acc+=(uint64_t)k;
else if(gsrc==2 && m->has_mtp) m->mtp_acc+=k;
if(m->has_mtp && k>=1) mtp_absorb(m, all+kv+1, m->h_all, k, kv); /* KV MTP in sync coi verificati */
/* hlast deve corrispondere all'ultima posizione ACCETTATA (kv+k), non a fine batch */
@@ -4427,7 +4434,7 @@ static void run_text(Model *m, const char *snap, const char *prompt, int ngen){
Tok T; tok_load(&T,tkp);
int eos=tok_id_of(&T,"<|endoftext|>");
stops_arm_tok(&m->c, eos, &T);
- grammar_setup(&T); /* metodo F: GRAMMAR=file.gbnf (#48) */
+ grammar_setup(&g_grd,&T); /* metodo F: GRAMMAR=file.gbnf (#48) */
if(g_temp<0) g_temp=0.7f; /* auto: 0.7, NON l'1.0 ufficiale — la coda della
* distribuzione int4 e' rumore di quantizzazione */
int cap=(int)strlen(prompt)+16; int *pids=malloc(cap*sizeof(int));
@@ -4456,7 +4463,7 @@ static void run_text(Model *m, const char *snap, const char *prompt, int ngen){
ProfBase pb; prof_base(m,&pb);
double t=now_s();
EmitStream es={&T,m,t,0,0};
- grammar_reset();
+ grammar_reset(&g_grd);
int produced=spec_decode(m,all,np,ngen,eos,logit,emit_stream,&es,NULL);
double dt=now_s()-t;
double tot=m->hits+m->miss;
@@ -4483,8 +4490,8 @@ static void run_text(Model *m, const char *snap, const char *prompt, int ngen){
m->n_fw?(double)m->n_emit/m->n_fw:1.0, (unsigned long long)m->n_fw, (unsigned long long)m->n_emit,
m->mtp_prop?100.0*m->mtp_acc/m->mtp_prop:0.0, (unsigned long long)m->mtp_acc, (unsigned long long)m->mtp_prop);
if(g_cp_enq) printf("couple: %ld cross-layer prefetch hints enqueued\n", g_cp_enq);
- if(g_gr_prop) printf("grammar: %.0f%% acceptance (%llu/%llu forced drafts)\n",
- 100.0*g_gr_acc/g_gr_prop, (unsigned long long)g_gr_acc, (unsigned long long)g_gr_prop);
+ if(g_grd.prop) printf("grammar: %.0f%% acceptance (%llu/%llu forced drafts)\n",
+ 100.0*g_grd.acc/g_grd.prop, (unsigned long long)g_grd.acc, (unsigned long long)g_grd.prop);
if(g_disk_split) printf("disk-load split: draft %llu + absorb %llu + verify/main %llu misses | "
"MTP-layer %llu loads %.2f GB | main-layers %llu loads %.2f GB (MTP %.1f%% of bytes)\n",
(unsigned long long)m->miss_draft, (unsigned long long)m->miss_absorb,
@@ -4977,7 +4984,7 @@ static void run_serve(Model *m, const char *snap){
Tok T; tok_load(&T,tkp);
int eos=tok_id_of(&T,"<|endoftext|>");
stops_arm_tok(&m->c, eos, &T);
- grammar_setup(&T); /* metodo F: GRAMMAR=file.gbnf (#48) */
+ grammar_setup(&g_grd,&T); /* metodo F: GRAMMAR=file.gbnf (#48) */
if(g_temp<0) g_temp=0.7f; /* auto: 0.7, NON l'1.0 ufficiale — la coda della
* distribuzione int4 e' rumore di quantizzazione */
int ngen=getenv("NGEN")?atoi(getenv("NGEN")):256;
@@ -5096,7 +5103,7 @@ static void run_serve(Model *m, const char *snap){
else logit=step(m,hist+len-1,1,len-1); /* prompt identico/prefisso: rigenera i logits */
EmitStream es={&T,m,now_s(),0,1};
int prod=0;
- grammar_reset(); /* nuova risposta = nuovo documento (MORE invece continua) */
+ grammar_reset(&g_grd); /* nuova risposta = nuovo documento (MORE invece continua) */
if(cur>0) prod=spec_decode(m,hist,len,cur,eos,logit,emit_stream,&es,&len);
else free(logit);
double tdt=now_s()-tt0; if(tdt<1e-6) tdt=1e-6;
From d7b855f43fbf0d8c77ed8f7d9fb0d910f88212ea Mon Sep 17 00:00:00 2001
From: FABIOTESS
Date: Tue, 14 Jul 2026 15:26:32 +0100
Subject: [PATCH 60/71] =?UTF-8?q?serve=20stage=202:=20per-request=20gramma?=
=?UTF-8?q?rs=20end-to-end=20=E2=80=94=20response=5Fformat=20->=20SUBMIT?=
=?UTF-8?q?=20->=20grammar-forced=20drafts=20in=20the=20multiplexed=20serv?=
=?UTF-8?q?er?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The OpenAI gateway previously 400'd every response_format; the mux engine path
ran with speculation disabled entirely. Now:
- openai_server.py: response_format {"type":"json_object"} (generic ws-tolerant
JSON grammar), {"type":"json_schema"} (schema forwarded as-is, compiled
engine-side by schema_gbnf.h), and a raw-GBNF {"type":"gbnf"} extension.
Draft-source semantics throughout: a schema the engine cannot compile costs
the speedup, never the request and never the output.
- SUBMIT protocol: optional 7th field gbytes; grammar text appended to the
payload after the prompt. 6-field headers unchanged (back-compatible).
- Engine: per-slot GrDraft (grammar_setup_text/grammar_teardown split out of
the env-driven setup); walkers fed on every emitted token. Grammar-forced
drafting in run_serve_mux for greedy requests: a drafting slot leaves the
shared batch for one forward and runs the proven single-sequence verify path
(kv_bind + step_all) — the same primitives prefill already uses per
submission — then rejoins; rejected drafts' KV entries are overwritten by the
next forward exactly like the existing prefix-truncation path. Sampling
requests never draft (verification under sampling needs rejection resampling;
out of scope).
Tests: 7-field SUBMIT parse cases; response_format->grammar plumbing incl.
fail cases; test doubles updated; generic JSON grammar parse+walk validated
against grammar.h. make test-c green; python suite green except the known
environmental memory_available failure (#150 fixes it).
Co-Authored-By: Claude Fable 5
---
c/colibri.c | 118 ++++++++++++++++++++++++++--------
c/decode_batch.h | 24 +++++--
c/openai_server.py | 66 +++++++++++++++----
c/tests/test_decode_batch.c | 6 ++
c/tests/test_openai_server.py | 26 ++++++--
5 files changed, 188 insertions(+), 52 deletions(-)
diff --git a/c/colibri.c b/c/colibri.c
index a24d540..9a23164 100644
--- a/c/colibri.c
+++ b/c/colibri.c
@@ -3962,10 +3962,36 @@ static void repin_pass(Model *m){ repin_pass_limit(m,16); }
* grammar_draft propone lo span FORZATO successivo (un solo byte legale per posizione)
* gia' tokenizzato. Il confine di tokenizzazione non e' garantito coincidere con quello
* del modello: la verifica assorbe la differenza (al peggio l'ultimo draft e' rifiutato). */
+/* Compile grammar TEXT into g: raw GBNF, or — if the first non-space byte is '{'
+ * — a JSON-Schema compiled via schema_gbnf.h. Takes ownership of txt (always
+ * freed). Fail-soft: returns -1 with g->on=0, engine runs without a grammar. */
+static int grammar_setup_text(GrDraft *g, Tok *T, char *txt, const char *label){
+ const char *p=txt; while(*p==' '||*p=='\t'||*p=='\n'||*p=='\r') p++;
+ if(*p=='{'){
+ char serr[160];
+ char *gbnf=schema_to_gbnf(txt,serr,sizeof serr);
+ free(txt);
+ if(!gbnf){ fprintf(stderr,"[SCHEMA] %s: %s (running without grammar)\n",label,serr); return -1; }
+ txt=gbnf;
+ }
+ if(gr_parse(&g->gram,txt)){ fprintf(stderr,"[GRAMMAR] %s: %s\n",label,g->gram.err); free(txt); return -1; }
+ free(txt);
+ gr_state_init(&g->st,&g->gram);
+ if(!g->st.alive){ fprintf(stderr,"[GRAMMAR] %s: grammar cannot be evaluated (left recursion?)\n",label); return -1; }
+ if(g->max<1) g->max=24;
+ if(g->max>48) g->max=48;
+ g->T=T; g->on=1; g->armed=0;
+ fprintf(stderr,"[GRAMMAR] %s: %d rules, forced span capped at %d tokens/forward\n",label,g->gram.n,g->max);
+ return 0;
+}
+/* Release a per-request grammar so the slot can host the next request (keeps max). */
+static void grammar_teardown(GrDraft *g){
+ if(g->gram.n) gr_free(&g->gram);
+ int max=g->max; memset(g,0,sizeof(*g)); g->max=max;
+}
static void grammar_setup(GrDraft *g, Tok *T){
/* GRAMMAR= takes precedence; SCHEMA= compiles a JSON-Schema
- * to GBNF (schema_gbnf.h) for the same draft source. Both fail soft: the engine
- * runs without a grammar and output is unchanged. */
+ * to GBNF. Both fail soft: the engine runs without a grammar, output unchanged. */
const char *gf=getenv("GRAMMAR");
const char *sf=(gf&&*gf)?NULL:getenv("SCHEMA");
if((!gf||!*gf)&&(!sf||!*sf)) return;
@@ -3977,22 +4003,8 @@ static void grammar_setup(GrDraft *g, Tok *T){
if(!txt || fread(txt,1,(size_t)n,f)!=(size_t)n){
fprintf(stderr,"[GRAMMAR] failed to read %s\n",path); fclose(f); free(txt); return; }
fclose(f); txt[n]=0;
- if(sf){ /* schema -> GBNF, then the same gr_parse as the GRAMMAR path */
- char serr[160];
- char *gbnf=schema_to_gbnf(txt,serr,sizeof serr);
- free(txt);
- if(!gbnf){ fprintf(stderr,"[SCHEMA] %s: %s (running without grammar)\n",sf,serr); return; }
- txt=gbnf;
- }
- if(gr_parse(&g->gram,txt)){ fprintf(stderr,"[GRAMMAR] %s: %s\n",path,g->gram.err); free(txt); return; }
- free(txt);
- gr_state_init(&g->st,&g->gram);
- if(!g->st.alive){ fprintf(stderr,"[GRAMMAR] %s: grammar cannot be evaluated (left recursion?)\n",path); return; }
if(getenv("GRAMMAR_DRAFT")) g->max=atoi(getenv("GRAMMAR_DRAFT"));
- if(g->max<1) g->max=1;
- if(g->max>48) g->max=48;
- g->T=T; g->on=1;
- fprintf(stderr,"[GRAMMAR] %s: %d rules, forced span capped at %d tokens/forward\n",path,g->gram.n,g->max);
+ grammar_setup_text(g,T,txt,path);
}
/* stato pulito all'inizio di ogni RISPOSTA (non tra i \x02MORE, che continuano) */
static void grammar_reset(GrDraft *g){
@@ -4811,8 +4823,8 @@ static void mux_done(Model *m, ServeCtx *sc, ServeReq *r){
/* Read and prefill one request. Returns -1 on EOF, 0 for a rejected frame and
* 1 for an accepted request. Prefill deliberately remains serial: continuous
* batching starts at decode, where every active slot contributes one row. */
-static int mux_submit(Model *m, Tok *T, ServeCtx *ctx, ServeReq *req, int nctx,
- int maxctx, int eos){
+static int mux_submit(Model *m, Tok *T, ServeCtx *ctx, ServeReq *req, GrDraft *grd,
+ int nctx, int maxctx, int eos){
char *line=NULL; size_t cap=0; ssize_t nr=getline(&line,&cap,stdin);
if(nr<0){ free(line); return -1; }
if(nr && line[nr-1]=='\n') line[--nr]=0;
@@ -4833,21 +4845,31 @@ static int mux_submit(Model *m, Tok *T, ServeCtx *ctx, ServeReq *req, int nctx,
char *raw=malloc((size_t)sub.bytes+1);
if(!raw){ fprintf(stderr,"OOM multiplex payload\n"); exit(1); }
if(fread(raw,1,(size_t)sub.bytes,stdin)!=(size_t)sub.bytes){ free(raw); free(line); return -1; }
+ char *gtxt=NULL; /* optional per-request grammar/schema text */
+ if(sub.gbytes){
+ gtxt=malloc((size_t)sub.gbytes+1);
+ if(!gtxt){ fprintf(stderr,"OOM multiplex payload\n"); exit(1); }
+ if(fread(gtxt,1,(size_t)sub.gbytes,stdin)!=(size_t)sub.gbytes){
+ free(gtxt); free(raw); free(line); return -1; }
+ gtxt[sub.gbytes]=0;
+ }
int delim=fgetc(stdin);
if(delim!='\n'){
printf("ERROR %llu BAD_FRAME\n",sub.id); fflush(stdout);
- free(raw); free(line); return -1;
+ free(gtxt); free(raw); free(line); return -1;
}
raw[sub.bytes]=0;
if(sub.slot>=nctx || memchr(raw,0,(size_t)sub.bytes)){
- printf("ERROR %llu BAD_REQUEST\n",sub.id); fflush(stdout); free(raw); free(line); return 0;
+ printf("ERROR %llu BAD_REQUEST\n",sub.id); fflush(stdout); free(gtxt); free(raw); free(line); return 0;
}
if(req[sub.slot].active){
- printf("ERROR %llu SLOT_BUSY\n",sub.id); fflush(stdout); free(raw); free(line); return 0;
+ printf("ERROR %llu SLOT_BUSY\n",sub.id); fflush(stdout); free(gtxt); free(raw); free(line); return 0;
}
for(int i=0;ikv);
int *tmp=malloc(maxctx*sizeof(int));
if(!tmp){ fprintf(stderr,"OOM mux_submit tmp\n"); free(raw); free(line); exit(1); }
@@ -4873,6 +4895,7 @@ static int mux_submit(Model *m, Tok *T, ServeCtx *ctx, ServeReq *req, int nctx,
int next=pick_tok(logit,m->c.vocab,-1); free(logit);
if(r->maximum<=0 || next==eos || is_stop(next)){ mux_done(m,sc,r); return 1; }
r->pending=next; r->emitted=1; r->active=1; sc->hist[sc->len]=next; m->n_emit++;
+ if(grd[sub.slot].on){ grammar_reset(&grd[sub.slot]); gr_feed(&grd[sub.slot],next); }
mux_data(T,r->id,next);
if(r->emitted>=r->maximum) mux_done(m,sc,r);
return 1;
@@ -4881,14 +4904,18 @@ static int mux_submit(Model *m, Tok *T, ServeCtx *ctx, ServeReq *req, int nctx,
static void run_serve_mux(Model *m, const char *snap){
char tkp[2048]; snprintf(tkp,sizeof(tkp),"%s/tokenizer.json",snap);
Tok T; tok_load(&T,tkp); int eos=tok_id_of(&T,"<|endoftext|>"); stops_arm_tok(&m->c,eos,&T);
- g_draft=0; /* one scheduler owns every forward; MTP/speculation is not ragged-safe */
+ g_draft=0; /* one scheduler owns every forward; MTP/n-gram speculation is not ragged-safe.
+ * Grammar-forced drafts ARE mux-safe (below): a drafting slot leaves the shared
+ * batch for one forward and runs the proven single-sequence verify path
+ * (kv_bind + step_all), exactly like prefill already does per submission. */
int maxctx=getenv("CTX")?atoi(getenv("CTX")):4096;
int nctx=getenv("KV_SLOTS")?atoi(getenv("KV_SLOTS")):1;
if(nctx<1||nctx>512){fprintf(stderr,"KV_SLOTS must be between 1 and 512\n");exit(2);}
g_kvsave=getenv("KVSAVE")?atoi(getenv("KVSAVE")):1;
KVState *initial=m->kv; free(initial->kv_start); free(initial);
ServeCtx *ctx=calloc(nctx,sizeof(*ctx)); ServeReq *req=calloc(nctx,sizeof(*req));
- for(int i=0;i0)?1:0;
if(ready)
#endif
- if(mux_submit(m,&T,ctx,req,nctx,maxctx,eos)<0) eof=1;
+ if(mux_submit(m,&T,ctx,req,grd,nctx,maxctx,eos)<0) eof=1;
}
active=0; for(int i=0;ion && r->temp==0) k=grammar_draft(gd,draft,gd->max);
+ if(k>0 && sc->len+1+klen+1+k>=(int)sc->kv.max_t) k=(int)sc->kv.max_t-sc->len-2;
+ if(k<1){ rows[S]=(DecodeRow){&sc->kv,r->pending,sc->len}; slots[S++]=i; continue; }
+ kv_bind(m,&sc->kv);
+ int seq[50]; seq[0]=r->pending;
+ memcpy(seq+1,draft,(size_t)k*sizeof(int));
+ float *lo=step_all(m,seq,1+k,sc->len); m->n_fw++;
+ gd->prop+=(uint64_t)k;
+ int done=0;
+ for(int j=0;j<=k && !done;j++){
+ g_temp=r->temp; g_nuc=r->top_p;
+ int next=pick_tok(lo+(int64_t)j*m->c.vocab,m->c.vocab,-1);
+ sc->len++; /* seq[j] joins the committed history */
+ if(next==eos || is_stop(next)){ mux_done(m,sc,r); done=1; break; }
+ r->pending=next; sc->hist[sc->len]=next; r->emitted++; m->n_emit++;
+ if(gd->on) gr_feed(gd,next);
+ mux_data(&T,r->id,next);
+ if(r->emitted>=r->maximum){ mux_done(m,sc,r); done=1; break; }
+ if(jacc++;
+ }
+ }
+ free(lo);
+ continue; /* handled outside the shared batch */
+ }
+ rows[S]=(DecodeRow){&sc->kv,r->pending,sc->len}; slots[S++]=i;
}
+ if(S==0) continue; /* every active slot drafted this round */
double tf0=g_prof?now_s():0;
float *lo=step_decode_batch(m,rows,S); if(!lo){fprintf(stderr,"decode batch failed\n");break;}
m->n_fw++;
@@ -4954,13 +5014,15 @@ static void run_serve_mux(Model *m, const char *snap){
int next=pick_tok(lo+(int64_t)s*m->c.vocab,m->c.vocab,-1);
if(next==eos || is_stop(next)){mux_done(m,sc,r);continue;}
r->pending=next; sc->hist[sc->len]=next; r->emitted++; m->n_emit++;
+ if(grd[i].on) gr_feed(&grd[i],next); /* walker stays in sync when not drafting */
mux_data(&T,r->id,next);
if(r->emitted>=r->maximum) mux_done(m,sc,r);
}
free(lo);
}
usage_save(m);
- for(int i=0;ikv=NULL; m->Lc=m->Rc=m->Ic=NULL; m->kv_start=NULL; m->max_t=0;
}
diff --git a/c/decode_batch.h b/c/decode_batch.h
index ad3d2ed..9f4dd34 100644
--- a/c/decode_batch.h
+++ b/c/decode_batch.h
@@ -13,22 +13,32 @@ static inline float *coli_kv_row(float *base, int position, int width)
}
typedef struct {
- unsigned long long id, bytes;
+ unsigned long long id, bytes, gbytes;
int slot, max_tokens;
float temperature, top_p;
} ColiSubmit;
/* Parse the textual header. The payload is read separately using `bytes`, so
- * it may contain newlines. Reject trailing fields to keep framing unambiguous. */
+ * it may contain newlines. Reject trailing fields to keep framing unambiguous.
+ * Optional 7th field `gbytes`: length of a per-request grammar (raw GBNF, or a
+ * JSON-Schema compiled engine-side) appended to the payload AFTER the prompt
+ * bytes. 6-field headers remain valid (gbytes = 0). */
static inline int coli_submit_parse(const char *line, ColiSubmit *s)
{
char tail;
- if (!line || !s ||
- sscanf(line, "SUBMIT %llu %d %llu %d %f %f %c", &s->id, &s->slot,
+ if (!line || !s) return 0;
+ s->gbytes = 0;
+ if (sscanf(line, "SUBMIT %llu %d %llu %d %f %f %llu %c", &s->id, &s->slot,
&s->bytes, &s->max_tokens, &s->temperature, &s->top_p,
- &tail) != 6)
- return 0;
- return s->id > 0 && s->bytes <= (16u << 20) && s->slot >= 0 && s->max_tokens >= 1 &&
+ &s->gbytes, &tail) != 7) {
+ s->gbytes = 0;
+ if (sscanf(line, "SUBMIT %llu %d %llu %d %f %f %c", &s->id, &s->slot,
+ &s->bytes, &s->max_tokens, &s->temperature, &s->top_p,
+ &tail) != 6)
+ return 0;
+ }
+ return s->id > 0 && s->bytes <= (16u << 20) && s->gbytes <= (1u << 20) &&
+ s->slot >= 0 && s->max_tokens >= 1 &&
isfinite(s->temperature) && isfinite(s->top_p) &&
s->temperature >= 0 && s->temperature <= 2 &&
s->top_p > 0 && s->top_p <= 1;
diff --git a/c/openai_server.py b/c/openai_server.py
index be21d91..1dd987b 100644
--- a/c/openai_server.py
+++ b/c/openai_server.py
@@ -370,6 +370,21 @@ def render_chat(messages, enable_thinking=False, reasoning_effort=None, tools=No
return "".join(prompt)
+# Generic whitespace-tolerant JSON grammar for response_format {"type": "json_object"}.
+# Draft-source semantics: positions with one legal byte draft; jws points just keep
+# the walker alive through the model's own spacing (see docs/grammar-draft.md).
+GENERIC_JSON_GBNF = (
+ 'root ::= jws jval jws\n'
+ 'jval ::= jobj | jarr | jstr | jnum | "true" | "false" | "null"\n'
+ 'jobj ::= "{" jws ( jstr jws ":" jws jval jws ( "," jws jstr jws ":" jws jval jws )* )? "}"\n'
+ 'jarr ::= "[" jws ( jval jws ( "," jws jval jws )* )? "]"\n'
+ 'jstr ::= "\\"" jchar* "\\""\n'
+ 'jchar ::= [^"\\\\\\x00-\\x1f] | "\\\\" ( ["\\\\/bfnrt] | "u" jhex jhex jhex jhex )\n'
+ 'jhex ::= [0-9a-fA-F]\n'
+ 'jnum ::= "-"? ( "0" | [1-9] [0-9]* ) ( "." [0-9]+ )? ( ( "e" | "E" ) ( "+" | "-" )? [0-9]+ )?\n'
+ 'jws ::= ( " " | "\\t" | "\\n" | "\\r" )*\n'
+)
+
def generation_options(body, limit):
if body.get("n", 1) != 1:
raise APIError(400, "Colibri currently supports `n=1` only.", "n", "unsupported_value")
@@ -424,10 +439,35 @@ def generation_options(body, limit):
raise APIError(400, "Token penalties are not supported yet.", None, "unsupported_parameter")
if body.get("seed") is not None:
raise APIError(400, "Per-request seeds are not supported yet.", "seed", "unsupported_parameter")
+ # response_format -> optional per-request grammar for the engine's grammar-forced
+ # draft source (#70/#148). NEVER a sampling constraint: drafts are verified, so a
+ # schema the engine cannot compile degrades to "no speedup", not to an error and
+ # not to changed output. json_schema payloads are forwarded as-is (the engine
+ # compiles them via schema_gbnf.h); {"type": "gbnf"} is a raw-GBNF extension.
+ grammar = None
response_format = body.get("response_format")
- if response_format not in (None, {"type": "text"}):
- raise APIError(400, "Only the default text response format is supported.",
- "response_format", "unsupported_parameter")
+ if response_format is not None and response_format != {"type": "text"}:
+ if not isinstance(response_format, dict) or "type" not in response_format:
+ raise APIError(400, "`response_format` must be an object with a `type`.",
+ "response_format", "invalid_value")
+ ftype = response_format["type"]
+ if ftype == "json_object":
+ grammar = GENERIC_JSON_GBNF
+ elif ftype == "json_schema":
+ schema = (response_format.get("json_schema") or {}).get("schema")
+ if not isinstance(schema, dict):
+ raise APIError(400, "`response_format.json_schema.schema` must be an object.",
+ "response_format", "invalid_value")
+ grammar = json.dumps(schema)
+ elif ftype == "gbnf":
+ grammar = response_format.get("grammar")
+ if not isinstance(grammar, str) or not grammar.strip():
+ raise APIError(400, "`response_format.grammar` must be a non-empty GBNF string.",
+ "response_format", "invalid_value")
+ else:
+ raise APIError(400, "`response_format.type` must be \"text\", \"json_object\", "
+ "\"json_schema\" or \"gbnf\".",
+ "response_format", "unsupported_value")
maximum = body.get("max_completion_tokens")
maximum_param = "max_completion_tokens"
@@ -454,7 +494,7 @@ def generation_options(body, limit):
if (isinstance(top_p, bool) or not isinstance(top_p, (int, float)) or
not math.isfinite(top_p) or not 0 < top_p <= 1):
raise APIError(400, "`top_p` must be greater than 0 and at most 1.", "top_p")
- return maximum, float(temperature), float(top_p)
+ return maximum, float(temperature), float(top_p), grammar
def read_engine_turn(stream, sentinel, on_bytes):
@@ -618,12 +658,15 @@ class Engine:
self._fail_pending(error)
def generate(self, prompt, max_tokens, temperature, top_p, on_text, cache_slot=0,
- cancelled=None):
+ cancelled=None, grammar=None):
if isinstance(cache_slot, bool) or not isinstance(cache_slot, int) or not 0 <= cache_slot < self.kv_slots:
raise APIError(400, "Invalid cache slot.", "cache_slot")
payload = prompt.encode("utf-8")
if b"\0" in payload:
raise APIError(400, "NUL bytes are not supported in prompts.", "messages")
+ gpayload = grammar.encode("utf-8") if grammar else b""
+ if b"\0" in gpayload:
+ raise APIError(400, "NUL bytes are not supported in grammars.", "response_format")
decoder = codecs.getincrementaldecoder("utf-8")("replace")
def decode(data):
@@ -643,12 +686,13 @@ class Engine:
self.next_request_id += 1
self.pending[request_id] = events
header = (f"SUBMIT {request_id} {cache_slot} {len(payload)} {max_tokens} "
- f"{temperature:.8g} {top_p:.8g}\n").encode()
+ f"{temperature:.8g} {top_p:.8g}"
+ + (f" {len(gpayload)}" if gpayload else "") + "\n").encode()
try:
with self.write_lock:
if self.process.poll() is not None:
raise RuntimeError("colibri engine is not running")
- self.process.stdin.write(header + payload + b"\n")
+ self.process.stdin.write(header + payload + gpayload + b"\n")
self.process.stdin.flush()
except Exception:
with self.pending_lock:
@@ -895,7 +939,7 @@ class APIHandler(BaseHTTPRequestHandler):
if dbg >= 2:
sys.stderr.write(f"\n===== PROMPT [{request_id}] =====\n{prompt}\n===== OUTPUT [{request_id}] =====\n")
sys.stderr.flush()
- maximum, temperature, top_p = generation_options(body, self.server.max_tokens)
+ maximum, temperature, top_p, grammar = generation_options(body, self.server.max_tokens)
# tools and tool_choice come from chat_completion() already processed/filtered
if chat and tool_choice == "none":
tools = None # client forbade tools: never surface tool_calls
@@ -924,7 +968,7 @@ class APIHandler(BaseHTTPRequestHandler):
output = []
stats = self.server.engine.generate(
prompt, maximum, temperature, top_p, output.append, cache_slot,
- self.client_disconnected)
+ self.client_disconnected, grammar=grammar)
text = "".join(output)
length_finish = "length" if stats["length_limited"] else "stop"
if chat and tools:
@@ -1031,7 +1075,7 @@ class APIHandler(BaseHTTPRequestHandler):
sp["buf"] = sp["buf"][flush:]
stats = self.server.engine.generate(
prompt, maximum, temperature, top_p, emit_tools, cache_slot,
- lambda: not connected)
+ lambda: not connected, grammar=grammar)
if not sp["tool"] and sp["buf"]:
emit(sp["buf"]) # no tool call happened: flush held tail
_content, calls = parse_tool_calls("".join(raw), tools)
@@ -1048,7 +1092,7 @@ class APIHandler(BaseHTTPRequestHandler):
emit(chunk)
stats = self.server.engine.generate(
prompt, maximum, temperature, top_p, emit_plain, cache_slot,
- lambda: not connected)
+ lambda: not connected, grammar=grammar)
finish = "length" if stats["length_limited"] else "stop"
ka_stop.set() # generation done: stop the keepalive pump
ka_thread.join(timeout=2)
diff --git a/c/tests/test_decode_batch.c b/c/tests/test_decode_batch.c
index d370d2d..f5b216e 100644
--- a/c/tests/test_decode_batch.c
+++ b/c/tests/test_decode_batch.c
@@ -44,6 +44,12 @@ static void test_submit_header(void)
assert(coli_submit_parse("SUBMIT 1 0 16777216 3 1 1", &sub));
assert(!coli_submit_parse("SUBMIT 1 0 16777217 3 1 1", &sub));
assert(!coli_submit_parse("SUBMIT 1 0 2 3 1 1 trailing", &sub));
+ /* optional 7th field: per-request grammar length (0 when absent) */
+ assert(coli_submit_parse("SUBMIT 42 3 17 64 0.7 0.95", &sub) && sub.gbytes == 0);
+ assert(coli_submit_parse("SUBMIT 42 3 17 64 0.7 0.95 512", &sub) && sub.gbytes == 512);
+ assert(coli_submit_parse("SUBMIT 42 3 17 64 0.7 0.95 1048576", &sub));
+ assert(!coli_submit_parse("SUBMIT 42 3 17 64 0.7 0.95 1048577", &sub));
+ assert(!coli_submit_parse("SUBMIT 42 3 17 64 0.7 0.95 512 extra", &sub));
}
int main(void)
diff --git a/c/tests/test_openai_server.py b/c/tests/test_openai_server.py
index 4cda9ef..7cbbc04 100644
--- a/c/tests/test_openai_server.py
+++ b/c/tests/test_openai_server.py
@@ -20,8 +20,8 @@ class FakeEngine:
self.calls = []
def generate(self, prompt, maximum, temperature, top_p, on_text, cache_slot=0,
- cancelled=None):
- self.calls.append((prompt, maximum, temperature, top_p, cache_slot))
+ cancelled=None, grammar=None):
+ self.calls.append((prompt, maximum, temperature, top_p, cache_slot, grammar))
on_text("Hé")
on_text("llo")
return {"prompt_tokens": 7, "completion_tokens": 2, "length_limited": False}
@@ -34,7 +34,7 @@ class BlockingEngine(FakeEngine):
self.release = threading.Event()
def generate(self, prompt, maximum, temperature, top_p, on_text, cache_slot=0,
- cancelled=None):
+ cancelled=None, grammar=None):
self.entered.set()
self.release.wait(2)
return super().generate(prompt, maximum, temperature, top_p, on_text, cache_slot,
@@ -71,11 +71,11 @@ class TemplateTest(unittest.TestCase):
def test_validates_generation_limits(self):
self.assertEqual(generation_options({"max_tokens": 4, "temperature": 0, "top_p": 1}, 8),
- (4, 0.0, 1.0))
+ (4, 0.0, 1.0, None))
# max_tokens above the server cap is clamped, not rejected (#260): OpenAI
# clients default to large values; erroring breaks them.
self.assertEqual(generation_options({"max_tokens": 9, "temperature": 0, "top_p": 1}, 8),
- (8, 0.0, 1.0))
+ (8, 0.0, 1.0, None))
# non-positive / non-int max_tokens is still a hard error
with self.assertRaises(APIError):
generation_options({"max_tokens": 0}, 8)
@@ -84,7 +84,21 @@ class TemplateTest(unittest.TestCase):
with self.assertRaises(APIError):
generation_options({"top_p": math.inf}, 8)
self.assertEqual(generation_options({"temperature": None, "top_p": None}, 8),
- (8, 0.7, 0.9))
+ (8, 0.7, 0.9, None))
+ # response_format -> grammar plumbing (draft source, never a constraint)
+ opts = generation_options({"max_tokens": 4, "response_format": {"type": "json_object"}}, 8)
+ self.assertIn("root ::=", opts[3])
+ schema = {"type": "object", "properties": {"a": {"type": "string"}}, "required": ["a"]}
+ opts = generation_options({"max_tokens": 4, "response_format":
+ {"type": "json_schema", "json_schema": {"schema": schema}}}, 8)
+ self.assertEqual(json.loads(opts[3]), schema)
+ opts = generation_options({"max_tokens": 4, "response_format":
+ {"type": "gbnf", "grammar": 'root ::= "x"'}}, 8)
+ self.assertEqual(opts[3], 'root ::= "x"')
+ with self.assertRaises(APIError):
+ generation_options({"response_format": {"type": "yaml"}}, 8)
+ with self.assertRaises(APIError):
+ generation_options({"response_format": {"type": "json_schema", "json_schema": {}}}, 8)
class ProtocolTest(unittest.TestCase):
From 84514a5f395aa1e3a8cdbe9eec04dcbb54534db3 Mon Sep 17 00:00:00 2001
From: FABIOTESS
Date: Tue, 14 Jul 2026 15:44:17 +0100
Subject: [PATCH 61/71] review: docs section, 1 MiB grammar pre-check, negative
tests, measured compile overhead
Addresses the #192 review: server usage documented in docs/grammar-draft.md
(incl. back-compat statement for the additive SUBMIT field and the #100-class
near-tie caveat); gateway pre-checks grammar payloads at 1 MiB (matching the
engine's gbytes bound); negative tests for non-dict response_format, empty and
oversized grammars, plus an explicit test that malformed GBNF passes the
gateway by design (engine fail-soft, draft-source semantics). Measured compile
overhead: 7.8 us/request typical schema, 17.9 us at the 32-level nesting cap.
Co-Authored-By: Claude Fable 5
---
c/openai_server.py | 3 +++
c/tests/test_openai_server.py | 10 ++++++++++
docs/grammar-draft.md | 24 ++++++++++++++++++++++++
3 files changed, 37 insertions(+)
diff --git a/c/openai_server.py b/c/openai_server.py
index 1dd987b..ae1032c 100644
--- a/c/openai_server.py
+++ b/c/openai_server.py
@@ -468,6 +468,9 @@ def generation_options(body, limit):
raise APIError(400, "`response_format.type` must be \"text\", \"json_object\", "
"\"json_schema\" or \"gbnf\".",
"response_format", "unsupported_value")
+ if grammar is not None and len(grammar.encode("utf-8")) > (1 << 20):
+ raise APIError(400, "`response_format` grammar/schema exceeds 1 MiB.",
+ "response_format", "invalid_value")
maximum = body.get("max_completion_tokens")
maximum_param = "max_completion_tokens"
diff --git a/c/tests/test_openai_server.py b/c/tests/test_openai_server.py
index 7cbbc04..8000b40 100644
--- a/c/tests/test_openai_server.py
+++ b/c/tests/test_openai_server.py
@@ -99,6 +99,16 @@ class TemplateTest(unittest.TestCase):
generation_options({"response_format": {"type": "yaml"}}, 8)
with self.assertRaises(APIError):
generation_options({"response_format": {"type": "json_schema", "json_schema": {}}}, 8)
+ with self.assertRaises(APIError): # non-dict response_format
+ generation_options({"response_format": "json"}, 8)
+ with self.assertRaises(APIError): # empty gbnf
+ generation_options({"response_format": {"type": "gbnf", "grammar": " "}}, 8)
+ with self.assertRaises(APIError): # oversized grammar (> 1 MiB pre-check)
+ generation_options({"response_format": {"type": "gbnf", "grammar": "x" * ((1 << 20) + 1)}}, 8)
+ # malformed GBNF passes the gateway by design: the ENGINE fail-softs it
+ # (draft source only — bad grammar costs the speedup, never the request)
+ opts = generation_options({"response_format": {"type": "gbnf", "grammar": "not a grammar ::="}}, 8)
+ self.assertEqual(opts[3], "not a grammar ::=")
class ProtocolTest(unittest.TestCase):
diff --git a/docs/grammar-draft.md b/docs/grammar-draft.md
index 707fe5c..6140cbb 100644
--- a/docs/grammar-draft.md
+++ b/docs/grammar-draft.md
@@ -83,3 +83,27 @@ this implementation adds: forced spans as a **draft source verified in the targe
own forward** (lossless even under a wrong grammar, composes with MTP/n-gram in one
union batch), deployed where the win is denominated in expert I/O rather than
forward passes.
+
+## Server usage: `response_format` (OpenAI API)
+
+The gateway (`openai_server.py`) turns `response_format` into a **per-request**
+grammar carried to the engine over the `SUBMIT` protocol (optional 7th header
+field, `gbytes`; 6-field headers from older clients remain valid — the field is
+additive and back-compatible in both directions):
+
+```jsonc
+{"response_format": {"type": "json_object"}} // generic JSON grammar
+{"response_format": {"type": "json_schema",
+ "json_schema": {"schema": { ... }}}} // compiled by schema_gbnf.h
+{"response_format": {"type": "gbnf", "grammar": "root ::= ..."}} // raw GBNF (extension)
+```
+
+Semantics are identical to `GRAMMAR=`/`SCHEMA=`: a **draft source, never a
+sampling constraint**. A schema outside the supported subset, or malformed GBNF,
+costs the speedup — never the request, never the output. Drafting engages for
+greedy requests (`temperature: 0`); sampled requests run undrafted. Compile
+overhead is negligible: ~8 µs/request for a typical schema, ~18 µs at the
+32-level nesting cap (measured, M3 Max). Grammar payloads are capped at 1 MiB.
+As with MTP (#100), a drafted greedy run may differ from an undrafted one in
+near-tie tokens (the verify forward has a different batch shape); each output
+is a valid greedy stream of its own forward shapes.
From 5e42e70ec3744ea018187a38d9a4a88470f9d59f Mon Sep 17 00:00:00 2001
From: FABIOTESS
Date: Mon, 20 Jul 2026 17:31:05 +0100
Subject: [PATCH 62/71] =?UTF-8?q?int3-g64=20(fmt=3D5):=20per-group-scale?=
=?UTF-8?q?=203-bit=20weight=20format=20=E2=80=94=20engine,=20converter,?=
=?UTF-8?q?=20tests?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
New weight format: int3 with ONE f32 scale per 64-input group (3.5 bits/weight
effective). Per group: 16B low plane (2 bits/val, int2 layout) + 8B high plane
(1 bit/val), values [-4,3] stored v+4. Same quantization math as
tools/quant_ablation.py _quant_last_dim(bits=3, group=64) from #132, whose
OLMoE ablation measured int3-g64 BEATING the shipped per-row int4 on quality
(-7.5 vs -9.3pp) at ~14% fewer bits.
Engine (placed per the #391 split): matmul_i3 + pack_int3_g64 + I3_* layout
helpers in quant.h next to their kernel family; fmt=5 branches in colibri.c's
qt_bytes/qt_alloc/qt_fill/matmul_qt/embed_row/qt_addrow/qt_matvec_rows.
Format detection now goes through #413's qt_resolve_fmt: fmt=5 registers its
distinct weight-byte layout O*ceil(I/64)*24 and its scale cardinality
O*ceil(I/64) there, validated against [O,I] like every other format. int3-g64
and grouped-int4-at-gs=64 carry the SAME scale count, so the weight bytes are
the int3 tag; row formats keep precedence for the small-I shapes where byte
counts coincide. The io_uring expert path still used the raw ?1:?2:3 byte
inference (it missed fmt=4 grouping entirely and never set gs) — converted to
qt_resolve_fmt like the other two expert paths.
Backends: qt_cuda_upload returns 0 for fmt=5 (tensor stays CPU-side, the
documented fallback), the dense CUDA matmul gate excludes fmt=5, and Metal's
existing fmt gates (gemm fmt<=3, moe fmt 1/2) already reject it.
Converter: quant_int3_g64 in convert_fp8_to_int4.py; --ebits 3/--xbits 3 now
emit it (previously bits=3 silently produced int4).
Tests: tests/test_int3.c (bit-exact pack/unpack vs reference, matmul_i3 vs
dequant-matmul incl. short tail groups and the real GLM I=7168, QT plumbing,
qt_resolve_fmt disambiguation incl. the same-scale-count fmt=4/fmt=5 pair,
outlier-rows RMS: int3-g64 3.3x lower error than per-row int4),
tests/test_int3_load.c (hand-rolled .safetensors fixture through st_init +
qt_from_disk: fmt=5 detected and loaded next to an int4 control tensor),
tests/test_int3_convert.py (NumPy pack round-trip vs independent decoder).
Co-Authored-By: Claude Fable 5
---
c/Makefile | 8 +-
c/colibri.c | 69 +++++++++++++---
c/quant.h | 90 ++++++++++++++++++++
c/tests/test_int3.c | 147 +++++++++++++++++++++++++++++++++
c/tests/test_int3_convert.py | 70 ++++++++++++++++
c/tests/test_int3_load.c | 103 +++++++++++++++++++++++
c/tools/convert_fp8_to_int4.py | 28 ++++++-
7 files changed, 503 insertions(+), 12 deletions(-)
create mode 100644 c/tests/test_int3.c
create mode 100644 c/tests/test_int3_convert.py
create mode 100644 c/tests/test_int3_load.c
diff --git a/c/Makefile b/c/Makefile
index 3ebf7d8..06fe176 100644
--- a/c/Makefile
+++ b/c/Makefile
@@ -171,7 +171,7 @@ else
PYTHON ?= python3
endif
CUDA_OBJ =
-TEST_BINS = tests/test_json$(EXE) tests/test_st$(EXE) tests/test_st_pread$(EXE) tests/test_tier$(EXE) tests/test_grammar$(EXE) tests/test_schema_gbnf$(EXE) tests/test_decode_batch$(EXE) tests/test_idot$(EXE) tests/test_i4_grouped$(EXE) tests/test_stops$(EXE) tests/test_topp$(EXE) tests/test_sample_nan$(EXE) tests/test_tok_o200k$(EXE) tests/test_kv_alloc$(EXE) tests/test_i4_acc512$(EXE) tests/test_compat_direct$(EXE) tests/test_dsa_select$(EXE) tests/test_logit_nan$(EXE) tests/test_pipe_block$(EXE)
+TEST_BINS = tests/test_json$(EXE) tests/test_st$(EXE) tests/test_st_pread$(EXE) tests/test_tier$(EXE) tests/test_grammar$(EXE) tests/test_schema_gbnf$(EXE) tests/test_decode_batch$(EXE) tests/test_idot$(EXE) tests/test_i4_grouped$(EXE) tests/test_stops$(EXE) tests/test_topp$(EXE) tests/test_sample_nan$(EXE) tests/test_tok_o200k$(EXE) tests/test_kv_alloc$(EXE) tests/test_int3$(EXE) tests/test_int3_load$(EXE) tests/test_i4_acc512$(EXE) tests/test_compat_direct$(EXE) tests/test_dsa_select$(EXE) tests/test_logit_nan$(EXE) tests/test_pipe_block$(EXE)
ifneq (,$(LINUX))
TEST_BINS += tests/test_uring$(EXE)
endif
@@ -343,6 +343,12 @@ tests/test_sample_nan$(EXE): tests/test_sample_nan.c colibri.c st.h uring.h json
tests/test_kv_alloc$(EXE): tests/test_kv_alloc.c colibri.c st.h json.h tok.h tok_unicode.h compat.h grammar.h tier.h quant.h sample.h kv_persist.h telemetry.h
$(CC) $(CFLAGS) $< -o $@ $(LDFLAGS)
+tests/test_int3$(EXE): tests/test_int3.c colibri.c st.h uring.h json.h tok.h tok_unicode.h compat.h grammar.h tier.h quant.h sample.h kv_persist.h telemetry.h
+ $(CC) $(CFLAGS) $< -o $@ $(LDFLAGS)
+
+tests/test_int3_load$(EXE): tests/test_int3_load.c colibri.c st.h uring.h json.h tok.h tok_unicode.h compat.h grammar.h tier.h quant.h sample.h kv_persist.h telemetry.h
+ $(CC) $(CFLAGS) $< -o $@ $(LDFLAGS)
+
tests/test_logit_nan$(EXE): tests/test_logit_nan.c colibri.c st.h uring.h json.h tok.h tok_unicode.h compat.h grammar.h tier.h
$(CC) $(CFLAGS) $< -o $@ $(LDFLAGS)
diff --git a/c/colibri.c b/c/colibri.c
index fe1b383..ef32b01 100644
--- a/c/colibri.c
+++ b/c/colibri.c
@@ -94,7 +94,13 @@ typedef struct {
* fmt=1 INT8 -> q8 (1 byte/param) + scala per riga
* fmt=2 INT4 -> q4 (2 valori per byte, impacchettati) + scala per riga
* INT4 e' cio' che fa stare la densa residente nei 15 GB (0.5 byte/param). */
-/* fmt: 0 F32, 1 INT8, 2 INT4 (2/byte), 3 INT2 (4/byte). q4 ospita sia int4 che int2 packed. */
+/* fmt: 0 F32, 1 INT8, 2 INT4 (2/byte), 3 INT2 (4/byte), 4 INT4-GROUPED, 5 INT3-G64.
+ * q4 ospita int4/int2/int3 packed. fmt=4 (grouped int4, #242): per-row nibbles + one f32
+ * scale per group of `gs` inputs (s has O*ceil(I/gs) entries).
+ * fmt=5 (int3, per-GROUP scales, group=64, see quant.h I3_*): values in [-4,3] stored per
+ * 64-input group as 24 bytes = 16B low plane (2 bits/val, int2 layout) + 8B high plane
+ * (1 bit/val), plus ONE f32 scale PER GROUP (s has O*ceil(I/64) entries, not O). 3.5
+ * bits/weight effective — the quality/size sweet spot measured in the #132 ablation. */
typedef struct {
int fmt; float *qf; int8_t *q8; uint8_t *q4; float *s; int O, I, gs; /* gs=group size (0=per-row, 128=grouped) */
#ifdef COLI_CUDA
@@ -110,6 +116,10 @@ static int64_t qt_bytes(const QT *t){ /* byte residenti del tensore */
if(t->fmt==4){ /* int4 grouped: packed nibbles + O*ceil(I/gs) scales */
int ng=(t->I+t->gs-1)/t->gs;
return (int64_t)t->O*((t->I+1)/2) + (int64_t)t->O*ng*4; }
+ if(t->fmt==5){ /* int3-g64: 24B/group weights + one f32 scale per group (I3_* in quant.h,
+ * included below — keep the arithmetic literal here) */
+ int64_t ng=((int64_t)t->I+63)/64;
+ return (int64_t)t->O*ng*24 + (int64_t)t->O*ng*4; }
return (int64_t)t->O*((t->I+1)/2) + (int64_t)t->O*4; /* fmt=2 int4 per-row */
}
@@ -267,6 +277,7 @@ static void qt_cuda_reset(QT *t){
t->cuda_failed=0;
}
static int qt_cuda_upload(QT *t){
+ if(t->fmt==5) return 0; /* int3-g64: no CUDA kernel yet — tensor stays CPU-side */
const void *weights = t->fmt==0 ? (const void*)t->qf
: t->fmt==1 ? (const void*)t->q8 : (const void*)t->q4;
if(t->fmt==4) /* grouped int4 (#334): scales are [O, ceil(I/gs)] — the plain
@@ -445,7 +456,7 @@ static void matmul_qt_ex(float *y, const float *x, QT *w, int S, int allow_idot)
}
#endif
#ifdef COLI_CUDA
- if(g_cuda_enabled && w->cuda_eligible && !w->cuda_failed && !omp_in_parallel()){
+ if(g_cuda_enabled && w->cuda_eligible && !w->cuda_failed && w->fmt!=5 && !omp_in_parallel()){
const void *weights = w->fmt==0 ? (const void*)w->qf
: w->fmt==1 ? (const void*)w->q8 : (const void*)w->q4;
if(coli_cuda_matmul(&w->cuda,y,x,weights,w->s,w->fmt,S,w->I,w->O,w->cuda_device)) return;
@@ -467,6 +478,7 @@ static void matmul_qt_ex(float *y, const float *x, QT *w, int S, int allow_idot)
}
if(w->fmt==1) matmul_q(y,x,w->q8,w->s,S,w->I,w->O);
else if(w->fmt==3) matmul_i2(y,x,w->q4,w->s,S,w->I,w->O);
+ else if(w->fmt==5) matmul_i3(y,x,w->q4,w->s,S,w->I,w->O);
else matmul_i4(y,x,w->q4,w->s,S,w->I,w->O);
}
@@ -659,18 +671,21 @@ static _Atomic int g_cur_moe_layer=-1; /* massimo layer moe in cui il MAIN e'
static int g_pilot_inflight[256]; /* protected by g_pilot_mx; URING can load a layer concurrently */
static _Atomic long g_pilot_loads=0; /* load cross-layer VERI completati (banda spesa) */
static _Atomic long g_pilot_drops=0; /* predizioni scartate perche' il main possiede gia' il layer */
-/* sceglie il formato da `bits`: >=16 f32, 5..8 int8, <=4 int4-packed */
+/* format from `bits`: >=16 f32, 5..8 int8, 4 int4-packed, 3 int3-g64 (group scales), <=2 int2 */
static void qt_alloc(QT *t, int O, int I, int bits){
t->O=O; t->I=I; t->qf=NULL; t->q8=NULL; t->q4=NULL; t->s=NULL;
if(bits>=16){ t->fmt=0; t->qf=falloc((int64_t)O*I); }
else if(bits>=5 || g_nopack){ t->fmt=1; t->q8=qalloc((int64_t)O*I); t->s=qsalloc(O); }
- else if(bits>=3){ t->fmt=2; t->q4=qalloc((int64_t)O*((I+1)/2)); t->s=qsalloc(O); }
+ else if(bits>=4){ t->fmt=2; t->q4=qalloc((int64_t)O*((I+1)/2)); t->s=qsalloc(O); }
+ else if(bits==3){ t->fmt=5; t->q4=qalloc((int64_t)O*i3_rowbytes(I));
+ t->s=(float*)qalloc((size_t)O*i3_groups(I)*sizeof(float)); }
else { t->fmt=3; t->q4=qalloc((int64_t)O*((I+3)/4)); t->s=qsalloc(O); }
}
static void qt_fill(QT *t, const float *w, int bits){
if(t->fmt==0) memcpy(t->qf, w, (int64_t)t->O*t->I*sizeof(float));
else if(t->fmt==1) quantize_rows(w, t->q8, t->s, t->O, t->I, bits);
else if(t->fmt==3) pack_int2(w, t->q4, t->s, t->O, t->I, bits);
+ else if(t->fmt==5) pack_int3_g64(w, t->q4, t->s, t->O, t->I);
else pack_int4(w, t->q4, t->s, t->O, t->I, bits);
}
@@ -840,16 +855,22 @@ static int detect_group_size(int O, int I, int64_t ns){
* diventava un int2 valido e il matmul leggeva oltre il buffer (O*I nibble a
* 4/byte). Qui i byte del peso devono corrispondere a un layout noto e i byte
* della scala alla cardinalita' attesa (O per-row, O*ng per-gruppo) — altrimenti
- * si termina invece di sforare. Ritorna fmt (1/2/3/4) e scrive *gs. */
+ * si termina invece di sforare. Ritorna fmt (1/2/3/4/5) e scrive *gs. */
static int qt_resolve_fmt(const char *name, int O, int I, int64_t nb, int64_t ns, int *gs){
int64_t exp_i8=(int64_t)O*I, exp_i4=(int64_t)O*((I+1)/2), exp_i2=(int64_t)O*((I+3)/4);
- int fmt = (nb==exp_i8)?1 : (nb==exp_i4)?2 : (nb==exp_i2)?3 : 0;
+ int64_t exp_i3=(int64_t)O*i3_rowbytes(I); /* int3-g64 (fmt=5): 24B per 64-input group */
+ /* Row formats take precedence: for tiny I the int3-g64 byte count can coincide with
+ * a row layout (e.g. [O,48]: ceil(48/2)=24=1*24). For real tensor shapes the counts
+ * are distinct, and the weight bytes — not the scale size — are the int3 tag, because
+ * int3-g64 and grouped-int4-at-gs=64 carry the SAME scale cardinality O*ceil(I/64). */
+ int fmt = (nb==exp_i8)?1 : (nb==exp_i4)?2 : (nb==exp_i2)?3 : (nb==exp_i3)?5 : 0;
if(!fmt){
- fprintf(stderr,"%s: quantized weight is %lld bytes — no int8/int4/int2 layout for [%d,%d], refusing (untrusted container)\n",
+ fprintf(stderr,"%s: quantized weight is %lld bytes — no int8/int4/int2/int3-g64 layout for [%d,%d], refusing (untrusted container)\n",
name,(long long)nb,O,I); exit(1); }
*gs=0;
if(fmt==2){ int g=detect_group_size(O,I,ns); if(g>0){ fmt=4; *gs=g; } }
- int64_t exp_scale = (fmt==4)? (int64_t)O*((I+*gs-1)/(*gs)) : (int64_t)O; /* in FLOAT */
+ int64_t exp_scale = (fmt==4)? (int64_t)O*((I+*gs-1)/(*gs))
+ : (fmt==5)? (int64_t)O*i3_groups(I) : (int64_t)O; /* in FLOAT */
if(ns != exp_scale*4){
fprintf(stderr,"%s: scale array is %lld bytes — expected %lld for [%d,%d] fmt=%d, refusing (untrusted container)\n",
name,(long long)ns,(long long)(exp_scale*4),O,I,fmt); exit(1); }
@@ -873,6 +894,9 @@ static void qt_from_disk(Model *m, const char *name, int O, int I, int bits, int
else if(fmt==4){ int ng=(I+gs-1)/gs;
if(t->fmt!=4||!t->q4){ t->fmt=4; t->O=O; t->I=I; t->gs=gs; t->q4=qalloc(nb); t->s=falloc((int64_t)O*ng); }
st_read_raw(&m->S,name,t->q4,drop); }
+ else if(fmt==5){ int64_t ng=i3_groups(I); /* int3-g64: 24B/group weights + O*ng group scales */
+ if(t->fmt!=5||!t->q4){ t->fmt=5; t->O=O; t->I=I; t->gs=0; t->q4=qalloc(nb); t->s=falloc((int64_t)O*ng); }
+ st_read_raw(&m->S,name,t->q4,drop); }
else { if(t->fmt!=fmt||!t->q4){ t->fmt=fmt; t->O=O; t->I=I; t->gs=0; t->q4=qalloc(nb); t->s=qsalloc(O); } st_read_raw(&m->S,name,t->q4,drop); }
st_read_f32(&m->S,sn,t->s,drop);
} else {
@@ -1104,6 +1128,13 @@ static void embed_row(Model *m, int tok, float *x){
for(int i=0;i>1]; x[i]=(float)((int)(byte&0xF)-8)*s;
if(i+1>4)-8)*s; }
return; }
+ if(e->fmt==5){ const uint8_t *q=e->q4+(int64_t)tok*i3_rowbytes(D); /* int3-g64 */
+ const float *sr=e->s+(int64_t)tok*i3_groups(D); int64_t ng=i3_groups(D);
+ for(int64_t g=0; g>2]>>((k&3)*2))&3)|(((hi[k>>3]>>(k&7))&1)<<2);
+ x[base+k]=(float)((int)u-4)*sr[g]; } }
+ return; }
const uint8_t *q=e->q4+(int64_t)tok*((D+3)/4); float s=e->s[tok]; /* int2 */
for(int i=0;i>2]; int sh=(i&3)*2; x[i]=(float)((int)((byte>>sh)&3)-2)*s; }
}
@@ -1598,8 +1629,11 @@ static int uring_finalize_load(UringBatch *b,int li,int publish_eid){
for(int k=0;k<3;k++){
fp[k]=s->fslab+fo; fo+=l->tq[k]->nbytes/4;
int64_t nb=l->tw[k]->nbytes;
- int fmt=(nb==(int64_t)OO[k]*II[k])?1:(nb==(int64_t)OO[k]*((II[k]+1)/2))?2:3;
- qt[k]->fmt=fmt; qt[k]->O=OO[k]; qt[k]->I=II[k]; qt[k]->qf=NULL;
+ /* qt_resolve_fmt like the other two expert paths: the raw ?1:?2:3 inference here
+ * missed grouped int4 (fmt=4, gs never set) and would mis-tag int3-g64 as int2. */
+ int gs=0;
+ int fmt=qt_resolve_fmt(l->tw[k]->name,OO[k],II[k],nb,l->tq[k]->nbytes,&gs);
+ qt[k]->fmt=fmt; qt[k]->O=OO[k]; qt[k]->I=II[k]; qt[k]->gs=gs; qt[k]->qf=NULL;
qt[k]->q8=(int8_t*)(s->slab+l->pos[k]); qt[k]->q4=s->slab+l->pos[k]; qt[k]->s=fp[k];
}
if(publish_eid) s->eid=l->eid;
@@ -1830,6 +1864,14 @@ static void qt_addrow(const QT *t, int row, float coef, float *acc){
acc[i] +=coef*scl[i/gs] *((int)(b&0xF)-8);
acc[i+1]+=coef*scl[(i+1)/gs]*((int)(b>>4)-8); }
if(I&1){ uint8_t b=w[I>>1]; acc[I-1]+=coef*scl[(I-1)/gs]*((int)(b&0xF)-8); } return; }
+ /* fmt=5 likewise before c: int3-g64 scales are per-GROUP [O,ng], not s[row] */
+ if(t->fmt==5){ const uint8_t *w=t->q4+(int64_t)row*i3_rowbytes(I);
+ const float *sr=t->s+(int64_t)row*i3_groups(I); int64_t ng=i3_groups(I);
+ for(int64_t g=0; g>2]>>((k&3)*2))&3)|(((hi[k>>3]>>(k&7))&1)<<2);
+ acc[base+k]+=cg*(float)((int)u-4); } }
+ return; }
float c=coef*t->s[row];
if(t->fmt==1){ const int8_t *w=t->q8+(int64_t)row*I; for(int i=0;ifmt==2){ const uint8_t *w=t->q4+(int64_t)row*((I+1)/2);
@@ -1855,6 +1897,13 @@ static void qt_matvec_rows(const QT *t, int r0, int n, const float *x, float *y)
for(int i=base;i>1];
acc+=(float)((i&1)?((int)(b>>4)-8):((int)(b&0xF)-8))*x[i]; }
a+=(double)acc*scl[g]; } }
+ else if(t->fmt==5){ const uint8_t *w=t->q4+(int64_t)row*i3_rowbytes(I);
+ const float *sr=t->s+(int64_t)row*i3_groups(I); int64_t ng=i3_groups(I);
+ for(int64_t g=0; g>2]>>((k&3)*2))&3)|(((hi[k>>3]>>(k&7))&1)<<2);
+ acc+=(float)((int)u-4)*x[base+k]; }
+ a+=(double)(acc*sr[g]); } }
else { const uint8_t *w=t->q4+(int64_t)row*((I+3)/4); float s=t->s[row]; float acc=0;
for(int i=0;i>2]; acc+=((int)((b>>((i&3)*2))&3)-2)*x[i]; } a=acc*s; }
y[j]=(float)a;
diff --git a/c/quant.h b/c/quant.h
index eb7dba2..b375538 100644
--- a/c/quant.h
+++ b/c/quant.h
@@ -268,6 +268,68 @@ static void matmul_i2(float *y, const float *x, const uint8_t *q2, const float *
y[(int64_t)s*O+o]=a*sc; } }
}
+/* ---- int3-g64 (fmt=5): 3-bit weights with ONE f32 scale per 64-input group -
+ * Per group: 16B low plane (2 bits/val, int2 layout) + 8B high plane (1 bit/val),
+ * values in [-4,3] stored v+4. 3.5 bits/weight effective — the quality/size point
+ * the #132 OLMoE ablation measured BEATING per-row int4. */
+#define I3_GROUP 64
+#define I3_GBYTES 24 /* 16B low plane + 8B high plane per group */
+static inline int64_t i3_groups(int I){ return ((int64_t)I + I3_GROUP - 1) / I3_GROUP; }
+static inline int64_t i3_rowbytes(int I){ return i3_groups(I) * I3_GBYTES; }
+
+/* Dequant-on-use with PER-GROUP scale. Exact f32 path only (no IDOT in v1: int8
+ * activations don't compose with per-group accumulation without a kernel
+ * restructure — follow-up). NEON: low plane = matmul_i2's unpack, high plane
+ * expanded via vtst on bit masks; x86 stays scalar for now (follow-up). */
+static void matmul_i3(float *y, const float *x, const uint8_t *q3, const float *scale, int S, int I, int O){
+ int64_t ng=i3_groups(I), rb=i3_rowbytes(I);
+ #pragma omp parallel for schedule(static)
+ for(int o=0;o>2), 4); /* 4 bytes = 16 low-plane values */
+ uint8x8_t by=vreinterpret_u8_u32(vdup_n_u32(wd));
+ uint8x8x2_t z01=vzip_u8(vand_u8(by,m2v), vand_u8(vshr_n_u8(by,2),m2v));
+ uint8x8x2_t z23=vzip_u8(vand_u8(vshr_n_u8(by,4),m2v), vshr_n_u8(by,6));
+ uint16x4x2_t zz=vzip_u16(vreinterpret_u16_u8(z01.val[0]), vreinterpret_u16_u8(z23.val[0]));
+ uint8x16_t lov=vcombine_u8(vreinterpret_u8_u16(zz.val[0]), vreinterpret_u8_u16(zz.val[1]));
+ uint8x16_t hv=vcombine_u8(vdup_n_u8(hi[k>>3]), vdup_n_u8(hi[(k>>3)+1]));
+ uint8x16_t hb=vandq_u8(vtstq_u8(hv,bitm), fourq); /* 4 where high bit set */
+ int8x16_t wq=vsubq_s8(vreinterpretq_s8_u8(vaddq_u8(lov,hb)), b4q); /* [-4,3] in order */
+ int16x8_t w0=vmovl_s8(vget_low_s8(wq)), w1=vmovl_s8(vget_high_s8(wq));
+ ac0=vfmaq_f32(ac0, vld1q_f32(xs+base+k), vcvtq_f32_s32(vmovl_s16(vget_low_s16(w0))));
+ ac1=vfmaq_f32(ac1, vld1q_f32(xs+base+k+4), vcvtq_f32_s32(vmovl_s16(vget_high_s16(w0))));
+ ac0=vfmaq_f32(ac0, vld1q_f32(xs+base+k+8), vcvtq_f32_s32(vmovl_s16(vget_low_s16(w1))));
+ ac1=vfmaq_f32(ac1, vld1q_f32(xs+base+k+12), vcvtq_f32_s32(vmovl_s16(vget_high_s16(w1))));
+ }
+ a=vaddvq_f32(vaddq_f32(ac0,ac1));
+ }
+#endif
+ for(;k>2]>>((k&3)*2))&3) | (((hi[k>>3]>>(k&7))&1)<<2);
+ a += xs[base+k]*(float)((int)u-4);
+ }
+ acc += a*srow[g];
+ }
+ y[(int64_t)s*O+o]=acc;
+ }
+ }
+}
+
/* ---- IDOT: integer dot kernels (int8-quantized activations) --------------- */
#if defined(__AVX512VNNI__) && defined(__AVX512BW__)
#define IDOT_KERNEL "avx512-vnni"
@@ -689,6 +751,34 @@ static void pack_int4(const float *w, uint8_t *q4, float *scale, int O, int I, i
}
}
}
+/* quantize w[O,I] f32 -> int3-g64 (fmt=5): per 64-input group, symmetric absmax
+ * (qmax=3, clamp [-4,3], stored v+4), 16B low plane + 8B high plane, ONE f32 scale
+ * per group. Same math as tools/quant_ablation.py `_quant_last_dim(bits=3, group=64)`
+ * (#132), here with real bit packing. */
+static void pack_int3_g64(const float *w, uint8_t *q3, float *scale, int O, int I){
+ int64_t ng=i3_groups(I), rb=i3_rowbytes(I);
+ #pragma omp parallel for schedule(static)
+ for(int o=0;oamax)amax=a; }
+ float s=amax/3.f; if(s<1e-8f)s=1e-8f; sr[g]=s;
+ uint8_t *lo=qr+g*I3_GBYTES, *hi=lo+16;
+ memset(lo,0,I3_GBYTES);
+ for(int k=0;k3)v=3; if(v<-4)v=-4;
+ unsigned u=(unsigned)(v+4); /* 0..7 */
+ lo[k>>2] |= (uint8_t)((u&3)<<((k&3)*2));
+ hi[k>>3] |= (uint8_t)(((u>>2)&1)<<(k&7));
+ }
+ }
+ }
+}
+
static void pack_int2(const float *w, uint8_t *q2, float *scale, int O, int I, int bits){
int qmax=(1<<(bits-1))-1, rb=(I+3)/4;
#pragma omp parallel for schedule(static)
diff --git a/c/tests/test_int3.c b/c/tests/test_int3.c
new file mode 100644
index 0000000..9fa7c73
--- /dev/null
+++ b/c/tests/test_int3.c
@@ -0,0 +1,147 @@
+/* int3-g64 (fmt=5) tests: pack layout, dequant round-trip vs plain-C reference,
+ * matmul_i3 (NEON + scalar tail) vs reference dequant-matmul, per-row helpers,
+ * the .qs-size format tag, and the quality claim in miniature (per-group int3
+ * beats per-row int4 on rows with outliers — the #132 result this format ships). */
+#define main coli_glm_main_unused
+#include "../colibri.c"
+#undef main
+
+#include
+#include
+#include
+
+static int fails = 0;
+#define CHECK(c) do{ if(!(c)){ printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #c); fails++; } }while(0)
+
+static uint64_t rng = 0x9E3779B97F4A7C15ull;
+static float rndf(void){ rng ^= rng << 13; rng ^= rng >> 7; rng ^= rng << 17;
+ return ((int64_t)(rng & 0xFFFFF) - 0x80000) / (float)0x80000; }
+
+/* reference: quantize like pack_int3_g64 but keep dequantized f32 (mirrors
+ * quant_ablation._quant_last_dim(bits=3, group=64)) */
+static void ref_i3_dequant(const float *w, float *dq, int O, int I){
+ int64_t ng=i3_groups(I);
+ for(int o=0;oamax)amax=a; }
+ float s=amax/3.f; if(s<1e-8f)s=1e-8f;
+ for(int k=0;k3)v=3; if(v<-4)v=-4;
+ dq[(int64_t)o*I+base+k]=(float)v*s;
+ }
+ }
+}
+static void unpack_i3(const uint8_t *q3, const float *s, float *dq, int O, int I){
+ int64_t ng=i3_groups(I), rb=i3_rowbytes(I);
+ for(int o=0;o>2]>>((k&3)*2))&3)|(((hi[k>>3]>>(k&7))&1)<<2);
+ dq[(int64_t)o*I+base+k]=(float)((int)u-4)*s[(int64_t)o*ng+g];
+ }
+ }
+}
+
+int main(void){
+ const int Is[]={64,128,192,100,65,7168}; /* incl. short tail groups and one real GLM dim */
+ enum { O=7, MAXI=7168 };
+ static float w[(int64_t)O*MAXI], dq_ref[(int64_t)O*MAXI], dq_pk[(int64_t)O*MAXI];
+ static float x[4*MAXI], y_ref[4*O], y_ker[4*O];
+ static uint8_t q3[(int64_t)O*(MAXI/64+1)*24];
+ static float sc[(int64_t)O*(MAXI/64+1)];
+
+ for(unsigned c=0;c unpack == reference quantize-dequantize, bit for bit */
+ pack_int3_g64(w, q3, sc, O, I);
+ ref_i3_dequant(w, dq_ref, O, I);
+ unpack_i3(q3, sc, dq_pk, O, I);
+ int bad=0;
+ for(int64_t i=0;i<(int64_t)O*I;i++) if(dq_pk[i]!=dq_ref[i]) bad++;
+ CHECK(bad==0);
+
+ /* 2. matmul_i3 == matmul over the dequantized reference (fp tolerance:
+ * NEON fma order differs from the scalar reference loop) */
+ for(int S=1;S<=4;S+=3){
+ for(int64_t i=0;i<(int64_t)S*I;i++) x[i]=rndf();
+ matmul_i3(y_ker, x, q3, sc, S, I, O);
+ for(int s=0;s1?fabsf(y_ref[i]):1;
+ if(d/m>2e-4f){ CHECK(!"matmul_i3 mismatch"); break; }
+ }
+ }
+
+ /* 3. QT plumbing: qt_alloc(bits=3) -> qt_fill -> matmul_qt & qt_bytes & helpers */
+ QT t; qt_alloc(&t, O, I, 3);
+ CHECK(t.fmt==5);
+ qt_fill(&t, w, 3);
+ CHECK(qt_bytes(&t)==(int64_t)O*i3_rowbytes(I)+(int64_t)O*i3_groups(I)*4);
+ matmul_qt(y_ker, x, &t, 1);
+ for(int o=0;o1?fabsf((float)a):1;
+ CHECK(d/m<=2e-4f);
+ }
+ float acc[MAXI]; memset(acc,0,I*sizeof(float));
+ qt_addrow(&t, 2, 0.5f, acc);
+ for(int i=0;i1?fabsf((float)a):1;
+ CHECK(d/m<=2e-4f);
+ }
+
+ /* 4. format resolution through the #413 gate: fmt=5 is tagged by its distinct
+ * WEIGHT byte count. int3-g64 and grouped-int4-at-gs=64 carry the SAME scale
+ * cardinality O*ceil(I/64), so the pair (weight bytes, scale bytes) must
+ * disambiguate: same scales, int4 weights -> fmt=4/gs=64; int3 weights -> fmt=5.
+ * Only well-posed for I > 256: below that, O row scales legitimately match a
+ * 1-group grouped layout too (detect_group_size probes gs up to 256), so
+ * per-row vs grouped is not distinguishable from byte counts alone. */
+ if(I>256){ int gs=-1;
+ int64_t ns_g64=(int64_t)O*i3_groups(I)*4, ns_row=(int64_t)O*4;
+ CHECK(qt_resolve_fmt("t.i3", O, I, (int64_t)O*i3_rowbytes(I), ns_g64, &gs)==5);
+ CHECK(gs==0);
+ CHECK(qt_resolve_fmt("t.i8", O, I, (int64_t)O*I, ns_row, &gs)==1);
+ CHECK(qt_resolve_fmt("t.i4", O, I, (int64_t)O*((I+1)/2), ns_row, &gs)==2);
+ CHECK(qt_resolve_fmt("t.i4g", O, I, (int64_t)O*((I+1)/2), ns_g64, &gs)==4);
+ CHECK(gs==64);
+ CHECK(qt_resolve_fmt("t.i2", O, I, (int64_t)O*((I+3)/4), ns_row, &gs)==3); }
+ free(t.q4); free(t.s);
+ }
+
+ /* 5. quality in miniature: on rows with outliers, per-group int3 must beat
+ * per-row int4 on reconstruction RMS (the #132 finding this format ships). */
+ {
+ int I=1024;
+ for(int64_t i=0;i<(int64_t)O*I;i++) w[i]=rndf()*0.02f;
+ for(int o=0;o>1];
+ int v=(i&1)?((int)(b>>4)-8):((int)(b&0xF)-8); w4=(float)v*t4.s[o]; }
+ double d3=w[(int64_t)o*I+i]-dq_ref[(int64_t)o*I+i], d4=w[(int64_t)o*I+i]-w4;
+ e3+=d3*d3; e4+=d4*d4;
+ }
+ CHECK(e3 < e4);
+ printf(" outlier-rows RMS: int3-g64 %.3e < int4-row %.3e (ratio %.2f)\n",
+ sqrt(e3/((double)O*I)), sqrt(e4/((double)O*I)), sqrt(e4/e3));
+ free(t4.q4); free(t4.s);
+ }
+
+ if(fails){ printf("int3-g64 tests: %d FAILED\n", fails); return 1; }
+ printf("int3-g64 tests: ok\n");
+ return 0;
+}
diff --git a/c/tests/test_int3_convert.py b/c/tests/test_int3_convert.py
new file mode 100644
index 0000000..2eaa7f7
--- /dev/null
+++ b/c/tests/test_int3_convert.py
@@ -0,0 +1,70 @@
+"""quant_int3_g64 (tools/convert_fp8_to_int4.py): pack layout + round-trip.
+
+Decodes the packed bytes with an independent NumPy decoder implementing the
+fmt=5 spec (16B low plane / 8B high plane per 64-group, v+4, per-group f32
+scale) and checks the dequantized result equals the reference
+quantize-dequantize (same math as quant_ablation._quant_last_dim(3, 64)).
+The C side of the same layout is covered by tests/test_int3.c.
+"""
+import os, sys, unittest
+try:
+ import numpy as np
+except ImportError:
+ raise unittest.SkipTest("numpy not installed")
+
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "tools"))
+from convert_fp8_to_int4 import quant_int3_g64
+
+
+def decode(packed, scales, O, I, group=64):
+ ng = (I + group - 1) // group
+ b = packed.reshape(O, ng, 24)
+ lo, hi = b[:, :, :16], b[:, :, 16:]
+ k = np.arange(group)
+ lov = (lo[:, :, k >> 2] >> ((k & 3) * 2)[None, None, :]) & 3
+ hiv = (hi[:, :, k >> 3] >> (k & 7)[None, None, :]) & 1
+ v = (lov | (hiv << 2)).astype(np.int64) - 4
+ dq = v.astype(np.float64) * scales.reshape(O, ng, 1).astype(np.float64)
+ return dq.reshape(O, ng * group)[:, :I]
+
+
+def reference(w, group=64):
+ """same math as quant_int3_g64 (which works in f32), replayed exactly, then
+ dequantized in f64 so it matches decode() bit for bit"""
+ O, I = w.shape
+ ng = (I + group - 1) // group
+ pad = ng * group - I
+ wp = np.pad(w, ((0, 0), (0, pad))) if pad else w
+ g = wp.reshape(O, ng, group)
+ s = np.maximum(np.abs(g).max(axis=2, keepdims=True) / 3.0, 1e-8).astype(np.float32)
+ q = np.clip(np.rint(g / s), -4, 3).astype(np.int64)
+ return (q.astype(np.float64) * s.astype(np.float64)).reshape(O, ng * group)[:, :I]
+
+
+class Int3ConvertTest(unittest.TestCase):
+ def test_round_trip(self):
+ rng = np.random.default_rng(7)
+ for I in (64, 128, 100, 65, 7168):
+ w = (rng.standard_normal((5, I)) * 0.05).astype(np.float32)
+ w[0, 3] = 1.7; w[2, min(5, I - 1)] = -2.2
+ packed, scales = quant_int3_g64(w)
+ ng = (I + 63) // 64
+ self.assertEqual(packed.size, 5 * ng * 24)
+ self.assertEqual(scales.size, 5 * ng)
+ np.testing.assert_allclose(decode(packed, scales, 5, I),
+ reference(w), rtol=0, atol=0)
+
+ def test_outliers_beat_row_int4(self):
+ rng = np.random.default_rng(11)
+ w = (rng.standard_normal((8, 1024)) * 0.02).astype(np.float32)
+ for o in range(8): w[o, (o * 37) % 1024] = 1.5
+ packed, scales = quant_int3_g64(w)
+ e3 = float(((decode(packed, scales, 8, 1024) - w) ** 2).mean())
+ s4 = np.maximum(np.abs(w).max(axis=1, keepdims=True) / 7.0, 1e-8)
+ w4 = np.clip(np.rint(w / s4), -8, 7) * s4
+ e4 = float(((w4 - w) ** 2).mean())
+ self.assertLess(e3, e4)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/c/tests/test_int3_load.c b/c/tests/test_int3_load.c
new file mode 100644
index 0000000..57634c0
--- /dev/null
+++ b/c/tests/test_int3_load.c
@@ -0,0 +1,103 @@
+/* Loader-seam test for fmt=5: writes a real .safetensors file containing an
+ * int3-g64 tensor (U8 payload + per-GROUP .qs) next to an int4 control tensor
+ * (per-row .qs), indexes it with st_init, loads both through qt_from_disk, and
+ * checks the byte-count/.qs-size format inference picks fmt=5 vs fmt=2 correctly
+ * and the loaded weights dequantize identically to pack_int3_g64's output. */
+#define main coli_glm_main_unused
+#include "../colibri.c"
+#undef main
+
+#include
+#include
+#include
+#include
+
+static int fails = 0;
+#define CHECK(c) do{ if(!(c)){ printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #c); fails++; } }while(0)
+
+static uint64_t rng = 0xA5A5A5A55A5A5A5Aull;
+static float rndf(void){ rng ^= rng << 13; rng ^= rng >> 7; rng ^= rng << 17;
+ return ((int64_t)(rng & 0xFFFFF) - 0x80000) / (float)0x80000; }
+
+static void deq4(const QT *t, float *dq){
+ for(int o=0;oO;o++) for(int i=0;i