diff --git a/.github/workflows/site.yml b/.github/workflows/site.yml
new file mode 100644
index 0000000..160cd61
--- /dev/null
+++ b/.github/workflows/site.yml
@@ -0,0 +1,36 @@
+name: Deploy website
+
+# Publishes site/ to GitHub Pages. One-time repo setup:
+# Settings → Pages → Build and deployment → Source: "GitHub Actions".
+# Custom domain later: add site/CNAME with the bare domain, point DNS
+# (A/AAAA to GitHub Pages IPs or CNAME to .github.io), done.
+
+on:
+ push:
+ branches: [main]
+ paths: ['site/**', '.github/workflows/site.yml']
+ workflow_dispatch:
+
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+concurrency:
+ group: pages
+ cancel-in-progress: true
+
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/configure-pages@v5
+ - uses: actions/upload-pages-artifact@v3
+ with:
+ path: site
+ - id: deployment
+ uses: actions/deploy-pages@v4
diff --git a/.gitignore b/.gitignore
index b3e1193..61ab52e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -37,6 +37,7 @@ c/tests/test_schema_gbnf
c/tests/test_schema_gbnf.exe
c/tests/test_compat_direct
c/tests/test_compat_direct.exe
+result
# oracoli tiny generati (make_glm_oracle.py) e dati benchmark scaricati
c/glm_tiny/
diff --git a/README.md b/README.md
index 9e33501..aaf7463 100644
--- a/README.md
+++ b/README.md
@@ -3,6 +3,12 @@
+
+
+
+
+
+ Website ·
English · 简体中文 · 繁體中文 · Italiano
@@ -44,6 +50,16 @@ brightness is routing heat, and every expert routed in a turn flashes white. Hov
as a 3-D galaxy — 13,260 characterised experts, 1,041 replicated specialists clustering by topic
(poetry, law, Chinese, SQL…). Position is measured routing affinity, not a learned embedding. Drag to spin.
+## 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 +77,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.
@@ -203,6 +231,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).
@@ -220,6 +255,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;
@@ -260,6 +307,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.
diff --git a/SUMMARY.md b/SUMMARY.md
new file mode 100644
index 0000000..f4de311
--- /dev/null
+++ b/SUMMARY.md
@@ -0,0 +1,356 @@
+# 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
+
+All mechanism code is confined to `c/backend_metal.mm` —
+`coli_metal_register`/`coli_metal_unregister`'s existing signatures and every call site in
+`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
+`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,
+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
+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`) 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). 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
+ 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_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
+ `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 `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:
+
+`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 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 `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
+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 (`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
+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 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 `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.
+
+## 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. **Pre-existing fslab OOM-unwind bug — now CARRIED ON THIS BRANCH** (follow-up commit,
+ 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
+ some later realloc of the same slot unregisters by pointer, a strictly longer-lived
+ 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
+
+| Seam | E4 (`e4/metal-heap`) | E5 (this branch) |
+|---|---|---|
+| Allocation | New: `MTLHeap` sub-buffers via `coli_metal_heap_alloc` | Unchanged: same `posix_memalign` + `newBufferWithBytesNoCopy` |
+| 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 |
+| 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. 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.
+
+## 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
+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
+`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.** 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
+ 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. **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 — 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
+ 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.
+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/Makefile b/c/Makefile
index bb49499..4ad515f 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),)
@@ -166,7 +171,7 @@ else
PYTHON ?= python3
endif
CUDA_OBJ =
-TEST_BINS = tests/test_json$(EXE) tests/test_st$(EXE) tests/test_st_mirror$(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_kv_alloc$(EXE) tests/test_i4_acc512$(EXE) tests/test_compat_direct$(EXE) tests/test_dsa_select$(EXE) tests/test_logit_nan$(EXE)
+TEST_BINS = tests/test_json$(EXE) tests/test_st$(EXE) tests/test_st_mirror$(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
@@ -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))
@@ -294,6 +300,8 @@ iobench$(EXE): iobench.c compat.h
tests/test_json$(EXE): tests/test_json.c json.h
$(CC) $(CFLAGS) $< -o $@ $(LDFLAGS)
+tests/test_tok_o200k$(EXE): tests/test_tok_o200k.c tok.h tok_unicode.h tok_unicode_o200k.h json.h
+ $(CC) $(CFLAGS) $< -o $@ $(LDFLAGS)
tests/test_st_pread$(EXE): tests/test_st_pread.c st.h json.h compat.h
$(CC) $(CFLAGS) -DST_PREAD_CHUNK=7 $< -o $@ $(LDFLAGS)
@@ -338,6 +346,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)
@@ -355,9 +369,17 @@ 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)
+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)
$(PYTHON) tools/run_tests.py $(TEST_BINS)
@@ -366,6 +388,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/backend_cuda.cu b/c/backend_cuda.cu
index 413e53b..327915e 100644
--- a/c/backend_cuda.cu
+++ b/c/backend_cuda.cu
@@ -20,6 +20,9 @@ struct ColiCudaTensor {
float *scales;
size_t weight_bytes;
int fmt, I, O, device;
+ int gs; /* quant group size; 0 = per-row scales (#334) */
+ int ng; /* number of scale groups per row = ceil(I/gs) for fmt=4 */
+ size_t scale_count; /* floats in `scales`: O per-row, O*ng grouped */
int tracked;
RaggedKVEntry ragged[512];
int ragged_count;
@@ -34,15 +37,17 @@ 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;
+ int group_pending; size_t group_pending_bytes; /* async expert-group in flight (Inc.4) */
} DeviceContext;
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];
@@ -54,6 +59,8 @@ static std::mutex g_group_stats_mu;
static int cuda_ok(cudaError_t err, const char *what) {
if (err == cudaSuccess) return 1;
std::fprintf(stderr, "[CUDA] %s: %s\n", what, cudaGetErrorString(err));
+ (void)cudaGetLastError(); /* consume the sticky error: a failed call must
+ not poison the next launch's error check */
return 0;
}
@@ -79,8 +86,9 @@ static int select_ctx(DeviceContext *ctx) {
__host__ __device__ static size_t row_bytes(int fmt, int I) {
if (fmt == 0) return (size_t)I * sizeof(float);
if (fmt == 1) return (size_t)I;
- if (fmt == 2) return (size_t)(I + 1) / 2;
+ if (fmt == 2 || fmt == 4) return (size_t)(I + 1) / 2; /* fmt=4: same packed int4 */
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;
}
@@ -89,7 +97,7 @@ __device__ static float weight_at(const void *weights, int fmt, size_t row, int
if (fmt == 0) return reinterpret_cast(base)[i];
if (fmt == 1) return static_cast(reinterpret_cast(base)[i]);
const uint8_t *q = base;
- if (fmt == 2) {
+ if (fmt == 2 || fmt == 4) { /* fmt=4: same nibble layout */
uint8_t v = q[i >> 1];
int n=(i&1)?(v>>4):(v&15); return static_cast(n&8?n-16:n);
}
@@ -97,20 +105,45 @@ __device__ static float weight_at(const void *weights, int fmt, size_t row, int
return static_cast(((v >> ((i & 3) * 2)) & 3) - 2);
}
+/* Scale for output `row`, input element `k`. fmt=4 (grouped int4) stores ng
+ * scales per row at scales[row*ng + k/gs]; every other quantized format has
+ * one scale per row at scales[row]. Mirrors quant_matmul's fmt==4 branch so the
+ * attention absorb kernels apply per-group scales instead of the per-row
+ * (fmt=2) semantic that crashed #298's g64 kv_b. */
+__device__ static float absorb_scale(const float *wscale, int fmt, int gs, int ng, int row, int k) {
+ if (!fmt) return 1.f;
+ if (fmt != 4) return wscale[row];
+ int g = k / gs; if (g >= ng) g = ng - 1; /* tail of the last (partial) group */
+ return wscale[(size_t)row * ng + g];
+}
+
__global__ static void offset_to_signed_s4(uint8_t *q,size_t n){
size_t i=(size_t)blockIdx.x*blockDim.x+threadIdx.x;if(i= ng) g = ng - 1; /* tail elements in the last (partial) group */
+ sum += xs[i] * weight_at(weights, fmt, row, i) * scl[g];
+ }
+ } else {
+ for (int i = threadIdx.x; i < I; i += blockDim.x)
+ sum += xs[i] * weight_at(weights, fmt, row, i);
+ }
__shared__ float partial[256];
partial[threadIdx.x] = sum;
@@ -120,7 +153,7 @@ __global__ static void quant_matmul(float *y, const float *x, const void *weight
__syncthreads();
}
if (!threadIdx.x)
- y[(size_t)s * O + o] = partial[0] * (fmt ? scales[o] : 1.0f);
+ y[(size_t)s * O + o] = (fmt && fmt != 4) ? partial[0] * scales[o] : partial[0];
}
__global__ static void silu_mul(float *gate, const float *up, size_t n) {
@@ -296,13 +329,50 @@ __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=S||nt<1)return;
extern __shared__ float sm[];float *qa=sm,*cl=qa+K,*scores=cl+K,*red=scores+T;
const float *qs=q+((size_t)s*H+h)*(Q+R);
for(int k=tid;kqx) 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);
@@ -503,40 +574,66 @@ extern "C" void coli_cuda_group_stats(uint64_t *calls, uint64_t *experts, uint64
if(d2h_ms) *d2h_ms=g_group_d2h_ms;
}
+/* group size for the NEXT upload on this thread (fmt=4): routed through a
+ * thread_local so the widely-wired upload signature (and the Windows DLL ABI)
+ * stays untouched. pin_load uploads in parallel, hence thread_local. */
+static thread_local int g_upload_gs = 0;
+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);
extern "C" int coli_cuda_tensor_upload(ColiCudaTensor **tensor,
const void *weights, const float *scales,
int fmt, int I, int O, int device) {
+ if (!tensor) return 0;
+ if (*tensor) {
+ /* Cached device copy: usable even when the caller's host pointers are
+ * gone. CUDA_RELEASE_HOST slots null their host pointers after upload,
+ * and with the old order (!weights checked first) every later matmul
+ * on such a slot failed here — the GPU tier silently never computed
+ * for host-released slab experts. */
+ ColiCudaTensor *t = *tensor;
+ int want_gs = (fmt==4 && g_upload_gs>0) ? g_upload_gs : 0;
+ return t->fmt == fmt && t->I == I && t->O == O && t->device == device && t->gs == want_gs;
+ }
DeviceContext *ctx = find_ctx(device);
- if (!tensor || !weights || I < 1 || O < 1 || !select_ctx(ctx)) return 0;
+ if (!weights || I < 1 || O < 1 || !select_ctx(ctx)) return 0;
size_t rb = row_bytes(fmt, I);
if (!rb || (fmt && !scales)) return 0;
- if (*tensor) {
- ColiCudaTensor *t = *tensor;
- return t->fmt == fmt && t->I == I && t->O == O && t->device == device;
- }
ColiCudaTensor *t = static_cast(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->ng = t->gs ? (I + t->gs - 1) / t->gs : 1;
+ t->scale_count = t->gs ? (size_t)O * (size_t)t->ng : (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,20 +643,35 @@ 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;
}
+ int ng = tensor->ng > 0 ? tensor->ng : 1;
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");
+}
+
+/* Test hook: COLI_GPU_FAIL_AFTER=N makes every GPU COMPUTE entry point report
+ * failure after N successful calls (N=0: every call fails), exercising the
+ * engine's CPU fallbacks and host-rematerialization end-to-end without real
+ * hardware faults. Uploads/queries are not gated. Unset: no effect. */
+static long g_gpu_calls;
+static int fault_injected(void) {
+ const char *fa = std::getenv("COLI_GPU_FAIL_AFTER");
+ return fa && g_gpu_calls++ >= std::atol(fa);
}
extern "C" 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) {
- if (S < 1 || !coli_cuda_tensor_upload(tensor, weights, scales, fmt, I, O, device)) return 0;
+ int fmt, int S, int I, int O, int device, int gs) {
+ if (fault_injected()) return 0;
+ if (S < 1) return 0;
+ if (gs > 0) { if (!coli_cuda_tensor_upload_g(tensor, weights, scales, fmt, I, O, device, gs)) return 0; }
+ else { if (!coli_cuda_tensor_upload(tensor, weights, scales, fmt, I, O, device)) return 0; }
ColiCudaTensor *t = *tensor;
DeviceContext *ctx = find_ctx(t->device);
if (!select_ctx(ctx)) return 0;
@@ -568,7 +680,7 @@ extern "C" int coli_cuda_matmul(ColiCudaTensor **tensor,
if (!reserve(&ctx->x, &ctx->x_cap, xb) || !reserve(&ctx->y, &ctx->y_cap, yb)) return 0;
if (!cuda_ok(cudaMemcpy(ctx->x, x, xb, cudaMemcpyHostToDevice), "input upload")) return 0;
dim3 grid((unsigned)O, (unsigned)S);
- quant_matmul<<>>(ctx->y, ctx->x, t->weights, t->scales, fmt, S, I, O, rb);
+ quant_matmul<<>>(ctx->y, ctx->x, t->weights, t->scales, fmt, S, I, O, rb, t->gs, t->ng);
if (!cuda_ok(cudaGetLastError(), "matmul launch") ||
!cuda_ok(cudaMemcpy(y, ctx->y, yb, cudaMemcpyDeviceToHost), "output download")) return 0;
return 1;
@@ -577,6 +689,7 @@ extern "C" int coli_cuda_matmul(ColiCudaTensor **tensor,
extern "C" int coli_cuda_expert_mlp(ColiCudaTensor *gate, ColiCudaTensor *up,
ColiCudaTensor *down, float *y,
const float *x, int S) {
+ if (fault_injected()) return 0;
if (!gate || !up || !down || !x || !y || S < 1 ||
gate->device != up->device || gate->device != down->device ||
gate->I != up->I || gate->O != up->O ||
@@ -591,13 +704,13 @@ extern "C" int coli_cuda_expert_mlp(ColiCudaTensor *gate, ColiCudaTensor *up,
if (!cuda_ok(cudaMemcpy(ctx->x,x,xb,cudaMemcpyHostToDevice),"expert input upload")) return 0;
dim3 hidden_grid((unsigned)I,(unsigned)S), output_grid((unsigned)D,(unsigned)S);
quant_matmul<<>>(ctx->gate,ctx->x,gate->weights,gate->scales,
- gate->fmt,S,D,I,row_bytes(gate->fmt,D));
+ gate->fmt,S,D,I,row_bytes(gate->fmt,D),gate->gs,gate->ng);
quant_matmul<<>>(ctx->up,ctx->x,up->weights,up->scales,
- up->fmt,S,D,I,row_bytes(up->fmt,D));
+ up->fmt,S,D,I,row_bytes(up->fmt,D),up->gs,up->ng);
size_t n=(size_t)S*I;
silu_mul<<<(unsigned)((n+255)/256),256>>>(ctx->gate,ctx->up,n);
quant_matmul<<>>(ctx->y,ctx->gate,down->weights,down->scales,
- down->fmt,S,I,D,row_bytes(down->fmt,I));
+ down->fmt,S,I,D,row_bytes(down->fmt,I),down->gs,down->ng);
if (!cuda_ok(cudaGetLastError(),"expert MLP launch") ||
!cuda_ok(cudaMemcpy(y,ctx->y,yb,cudaMemcpyDeviceToHost),"expert output download")) return 0;
return 1;
@@ -605,6 +718,7 @@ extern "C" int coli_cuda_expert_mlp(ColiCudaTensor *gate, ColiCudaTensor *up,
extern "C" int coli_cuda_shared_mlp_w4a16(ColiCudaTensor *gate,ColiCudaTensor *up,
ColiCudaTensor *down,float *y,const float *x,int S){
+ if (fault_injected()) return 0;
if(!gate||!up||!down||!x||!y||S<1||gate->fmt!=2||up->fmt!=2||down->fmt!=2||
gate->device!=up->device||gate->device!=down->device||gate->I!=up->I||
gate->O!=up->O||down->I!=gate->O||down->O!=gate->I)return 0;
@@ -636,19 +750,24 @@ extern "C" int coli_cuda_expert_group(ColiCudaTensor *const *gates,
ColiCudaTensor *const *downs,
const int *rows, int count,
float *y, const float *x) {
+ if (fault_injected()) return 0;
if (!gates || !ups || !downs || !rows || !x || !y || count < 1) return 0;
ColiCudaTensor *first=gates[0];
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;
@@ -689,7 +808,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. */
@@ -711,12 +838,12 @@ extern "C" int coli_cuda_expert_group(ColiCudaTensor *const *gates,
/* piccoli batch: tile TC quasi vuoti + overhead di lancio — il
* kernel naive per-elemento resta piu' veloce (misurato in decode) */
quant_matmul<<stream>>>(g16,x16,
- host[c].g,host[c].gs,host[c].gf,r,D,I,row_bytes(host[c].gf,D));
+ host[c].g,host[c].gs,host[c].gf,r,D,I,row_bytes(host[c].gf,D),0,1);
quant_matmul<<stream>>>(u16,x16,
- host[c].u,host[c].us,host[c].uf,r,D,I,row_bytes(host[c].uf,D));
+ host[c].u,host[c].us,host[c].uf,r,D,I,row_bytes(host[c].uf,D),0,1);
silu_mul<<<(unsigned)(((size_t)r*I+255)/256),256,0,ctx->stream>>>(g16,u16,(size_t)r*I);
quant_matmul<<stream>>>(y16,g16,
- host[c].d,host[c].ds,host[c].df,r,I,D,row_bytes(host[c].df,I));
+ host[c].d,host[c].ds,host[c].df,r,I,D,row_bytes(host[c].df,I),0,1);
}
off16+=r;
}
@@ -730,7 +857,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);
@@ -757,10 +895,79 @@ extern "C" int coli_cuda_expert_group(ColiCudaTensor *const *gates,
return 1;
}
+/* ---- Async expert group (Inc.4): issue/take split of coli_cuda_expert_group ----
+ * The measured cost of the sync call at decode is ~0.45 ms/call of HOST-side wait
+ * (stream sync + staging), vs ~0.18 ms of actual GPU work — 70% tax, paid ~5x per
+ * layer because a token's 8 experts scatter across devices. issue() stages and
+ * launches on the device stream and returns immediately; take() syncs and hands
+ * back the pinned result rows. One issue may be outstanding per device; moe()
+ * takes at each layer end, which also orders the next layer's reuse of the ctx
+ * scratch buffers. Small batches only (decode/spec): bigger totals keep the sync
+ * path with its TC variants. Numerics are the sync path's small-batch kernels,
+ * so greedy output is byte-identical by construction. */
+extern "C" int coli_cuda_expert_group_issue(ColiCudaTensor *const *gates,
+ ColiCudaTensor *const *ups,
+ ColiCudaTensor *const *downs,
+ const int *rows, int count,
+ const float *x) {
+ if (!gates || !ups || !downs || !rows || !x || count < 1 || count > 64) return 0;
+ ColiCudaTensor *first=gates[0];
+ if (!first) return 0;
+ int device=first->device,D=first->I,I=first->O,total=0;
+ GroupDesc host[64];
+ 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};
+ total+=rows[c];
+ }
+ if(total>8) return 0; /* decode-scale only */
+ DeviceContext *ctx=find_ctx(device); if(!ctx||ctx->group_pending||!select_ctx(ctx)) return 0;
+ size_t xb=(size_t)total*D*sizeof(float), ib=(size_t)total*I*sizeof(float);
+ if(!reserve(&ctx->x,&ctx->x_cap,xb)||!reserve(&ctx->y,&ctx->y_cap,xb)||
+ !reserve(&ctx->gate,&ctx->gate_cap,ib)||!reserve(&ctx->up,&ctx->up_cap,ib)||
+ !reserve_pinned(&ctx->host_x,&ctx->host_x_cap,xb)||
+ !reserve_pinned(&ctx->host_y,&ctx->host_y_cap,xb)) return 0;
+ std::memcpy(ctx->host_x,x,xb);
+ if(!cuda_ok(cudaMemcpyAsync(ctx->x,ctx->host_x,xb,cudaMemcpyHostToDevice,ctx->stream),
+ "expert group issue upload")) return 0;
+ for(int c=0;cgate+(size_t)host[c].offset*I,*u16=ctx->up+(size_t)host[c].offset*I;
+ float *x16=ctx->x+(size_t)host[c].offset*D,*y16=ctx->y+(size_t)host[c].offset*D;
+ quant_matmul<<stream>>>(g16,x16,
+ host[c].g,host[c].gs,host[c].gf,r,D,I,row_bytes(host[c].gf,D),0,1);
+ quant_matmul<<stream>>>(u16,x16,
+ host[c].u,host[c].us,host[c].uf,r,D,I,row_bytes(host[c].uf,D),0,1);
+ silu_mul<<<(unsigned)(((size_t)r*I+255)/256),256,0,ctx->stream>>>(g16,u16,(size_t)r*I);
+ quant_matmul<<stream>>>(y16,g16,
+ host[c].d,host[c].ds,host[c].df,r,I,D,row_bytes(host[c].df,I),0,1);
+ }
+ if(!cuda_ok(cudaGetLastError(),"expert group issue launch")||
+ !cuda_ok(cudaMemcpyAsync(ctx->host_y,ctx->y,xb,cudaMemcpyDeviceToHost,ctx->stream),
+ "expert group issue download")) return 0;
+ ctx->group_pending=1; ctx->group_pending_bytes=xb;
+ { std::lock_guard lock(g_group_stats_mu);
+ g_group_calls++; g_group_experts+=(uint64_t)count; g_group_rows+=(uint64_t)total; }
+ return 1;
+}
+
+extern "C" const float *coli_cuda_expert_group_take(int device) {
+ DeviceContext *ctx=find_ctx(device);
+ if(!ctx||!ctx->group_pending) return nullptr;
+ ctx->group_pending=0;
+ if(!select_ctx(ctx)) return nullptr;
+ if(!cuda_ok(cudaStreamSynchronize(ctx->stream),"expert group take")) return nullptr;
+ return ctx->host_y;
+}
+
extern "C" int coli_cuda_attention_absorb(ColiCudaTensor *w,float *ctx,const float *q,
const float *latent,const float *rope,int H,int Q,
int R,int V,int K,int T,float scale){
+ if (fault_injected()) return 0;
if(!w||!ctx||!q||!latent||!rope||H<1||Q<1||R<1||V<1||K<1||K>512||T<1||T>4096||
w->I!=K||w->O!=H*(Q+V))return 0;
DeviceContext *dc=find_ctx(w->device);if(!select_ctx(dc))return 0;
@@ -773,7 +980,7 @@ extern "C" int coli_cuda_attention_absorb(ColiCudaTensor *w,float *ctx,const flo
!cuda_ok(cudaMemcpyAsync(dc->ar,rope,rb,cudaMemcpyHostToDevice,dc->stream),"attention rope upload"))return 0;
size_t shared=(size_t)(2*K+T)*sizeof(float);
attention_absorb_kernel<<stream>>>(dc->ac,dc->aq,dc->al,dc->ar,w->weights,w->scales,
- w->fmt,H,Q,R,V,K,T,scale);
+ w->fmt,H,Q,R,V,K,T,scale,w->gs,w->ng);
if(!cuda_ok(cudaGetLastError(),"attention absorb launch")||
!cuda_ok(cudaMemcpyAsync(ctx,dc->ac,cb,cudaMemcpyDeviceToHost,dc->stream),"attention context download")||
!cuda_ok(cudaStreamSynchronize(dc->stream),"attention synchronize"))return 0;
@@ -796,13 +1003,13 @@ static int attention_absorb_batch_run(ColiCudaTensor *w,ColiCudaTensor *proj,flo
!cuda_ok(cudaMemcpyAsync(dc->ar,rope,rb,cudaMemcpyHostToDevice,dc->stream),"attention batch rope upload"))return 0;
size_t shared=(size_t)(2*K+T+256)*sizeof(float);
attention_absorb_batch_kernel<<stream>>>(dc->ac,dc->aq,dc->al,
- dc->ar,w->weights,w->scales,w->fmt,S,H,Q,R,V,K,T,scale);
+ dc->ar,w->weights,w->scales,w->fmt,S,H,Q,R,V,K,T,scale,w->gs,w->ng);
if(!cuda_ok(cudaGetLastError(),"attention batch launch"))return 0;
const float *src=dc->ac;size_t ob=cb;
if(proj){
ob=(size_t)S*proj->O*sizeof(float);if(!reserve(&dc->y,&dc->y_cap,ob))return 0;
quant_matmul<<O,S),256,0,dc->stream>>>(dc->y,dc->ac,proj->weights,
- proj->scales,proj->fmt,S,proj->I,proj->O,row_bytes(proj->fmt,proj->I));
+ proj->scales,proj->fmt,S,proj->I,proj->O,row_bytes(proj->fmt,proj->I),proj->gs,proj->ng);
if(!cuda_ok(cudaGetLastError(),"attention o_proj launch"))return 0;src=dc->y;
}
if(!cuda_ok(cudaMemcpyAsync(out,src,ob,cudaMemcpyDeviceToHost,dc->stream),
@@ -814,12 +1021,14 @@ static int attention_absorb_batch_run(ColiCudaTensor *w,ColiCudaTensor *proj,flo
extern "C" int coli_cuda_attention_absorb_batch(ColiCudaTensor *w,float *ctx,const float *q,
const float *latent,const float *rope,int S,int H,int Q,int R,int V,int K,int T,
float scale){
+ if (fault_injected()) return 0;
return attention_absorb_batch_run(w,nullptr,ctx,q,latent,rope,S,H,Q,R,V,K,T,scale);
}
extern "C" int coli_cuda_attention_project_batch(ColiCudaTensor *w,ColiCudaTensor *proj,
float *out,const float *q,const float *latent,const float *rope,int S,int H,int Q,
int R,int V,int K,int T,float scale){
+ if (fault_injected()) return 0;
return attention_absorb_batch_run(w,proj,out,q,latent,rope,S,H,Q,R,V,K,T,scale);
}
@@ -900,7 +1109,7 @@ extern "C" int coli_cuda_attention_project_ragged(ColiCudaTensor *w,ColiCudaTens
attention_absorb_ragged_kernel<<stream>>>(dc->ac,dc->aq,ddl,ddr,
dn,w->weights,w->scales,w->fmt,S,H,Q,R,V,K,T,scale);
quant_matmul<<O,S),256,0,dc->stream>>>(dc->y,dc->ac,proj->weights,
- proj->scales,proj->fmt,S,proj->I,proj->O,row_bytes(proj->fmt,proj->I));
+ proj->scales,proj->fmt,S,proj->I,proj->O,row_bytes(proj->fmt,proj->I),proj->gs,proj->ng);
return cuda_ok(cudaGetLastError(),"ragged attention launch")&&
cuda_ok(cudaMemcpyAsync(out,dc->y,ob,cudaMemcpyDeviceToHost,dc->stream),"ragged output download")&&
cuda_ok(cudaStreamSynchronize(dc->stream),"ragged attention synchronize");
@@ -911,7 +1120,8 @@ extern "C" void coli_cuda_tensor_free(ColiCudaTensor *tensor) {
DeviceContext *ctx = find_ctx(tensor->device);
if (ctx) select_ctx(ctx);
if (tensor->tracked && ctx) {
- size_t bytes = tensor->weight_bytes + (tensor->fmt ? (size_t)tensor->O * sizeof(float) : 0);
+ int ng = tensor->ng > 0 ? tensor->ng : 1;
+ size_t bytes = tensor->weight_bytes + (tensor->fmt ? (size_t)tensor->O * ng * sizeof(float) : 0);
if (ctx->tensor_count) ctx->tensor_count--;
if (ctx->tensor_bytes >= bytes) ctx->tensor_bytes -= bytes;
}
@@ -925,7 +1135,9 @@ extern "C" void coli_cuda_tensor_free(ColiCudaTensor *tensor) {
}
extern "C" size_t coli_cuda_tensor_bytes(const ColiCudaTensor *tensor) {
- return tensor ? tensor->weight_bytes + (tensor->fmt ? (size_t)tensor->O * sizeof(float) : 0) : 0;
+ if (!tensor) return 0;
+ int ng = tensor->ng > 0 ? tensor->ng : 1;
+ return tensor->weight_bytes + (tensor->fmt ? (size_t)tensor->O * ng * sizeof(float) : 0);
}
extern "C" int coli_cuda_tensor_device(const ColiCudaTensor *tensor) {
@@ -986,7 +1198,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];
}
@@ -1010,6 +1222,7 @@ extern "C" int coli_cuda_pipe_download(int device,const void *src,void *dst,size
}
extern "C" int coli_cuda_pipe_rmsnorm(int device,float *y_dev,const float *x_dev,
const float *w_dev,int S,int D,float eps){
+ if (fault_injected()) return 0;
DeviceContext *ctx=find_ctx(device);
if(S<1||D<1||!select_ctx(ctx)) return 0;
pipe_rmsnorm_rows<<>>(y_dev,x_dev,w_dev,D,eps,D,D);
@@ -1018,6 +1231,7 @@ extern "C" int coli_cuda_pipe_rmsnorm(int device,float *y_dev,const float *x_dev
extern "C" 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 (fault_injected()) return 0;
DeviceContext *ctx=find_ctx(device);
if(S<1||D<1||xstride>>(y_dev,x_dev,w_dev,D,eps,xstride,ystride);
@@ -1026,6 +1240,7 @@ extern "C" int coli_cuda_pipe_rmsnorm_s(int device,float *y_dev,const float *x_d
extern "C" int coli_cuda_pipe_rope(int device,float *v_dev,const int *pos_dev,
int rows,int stride,int offset,int R,int heads,
float theta){
+ if (fault_injected()) return 0;
DeviceContext *ctx=find_ctx(device);
if(rows<1||R<2||R>256||heads<1||!select_ctx(ctx)) return 0;
pipe_rope_rows<<>>(v_dev,pos_dev,0,stride,offset,R,heads,theta);
@@ -1033,11 +1248,88 @@ extern "C" int coli_cuda_pipe_rope(int device,float *v_dev,const int *pos_dev,
}
extern "C" 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){
+ if (fault_injected()) return 0;
DeviceContext *ctx=find_ctx(device);
if(rows<1||R<2||R>256||heads<1||!select_ctx(ctx)) return 0;
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;
@@ -1050,6 +1342,7 @@ extern "C" int coli_cuda_pipe_copy2d(int device,float *dst,int dpitch,const floa
extern "C" int coli_cuda_attention_project_batch_dev(ColiCudaTensor *w,ColiCudaTensor *proj,
float *out,const float *q_dev,const float *latent_dev,const float *rope_dev,
int S,int H,int Q,int R,int V,int K,int T,float scale){
+ if (fault_injected()) return 0;
if(!w||!proj||!out||!q_dev||!latent_dev||!rope_dev||S<1||H<1||Q<1||R<1||V<1||
K<1||K>512||T8192||w->I!=K||w->O!=H*(Q+V)||
proj->device!=w->device||proj->I!=H*V)return 0;
@@ -1058,12 +1351,12 @@ extern "C" int coli_cuda_attention_project_batch_dev(ColiCudaTensor *w,ColiCudaT
if(!reserve(&dc->ac,&dc->ac_cap,cb))return 0;
size_t shared=(size_t)(2*K+T+256)*sizeof(float);
attention_absorb_batch_kernel<<stream>>>(dc->ac,q_dev,latent_dev,
- rope_dev,w->weights,w->scales,w->fmt,S,H,Q,R,V,K,T,scale);
+ rope_dev,w->weights,w->scales,w->fmt,S,H,Q,R,V,K,T,scale,w->gs,w->ng);
if(!cuda_ok(cudaGetLastError(),"pipe attention launch"))return 0;
size_t ob=(size_t)S*proj->O*sizeof(float);
if(!reserve(&dc->y,&dc->y_cap,ob))return 0;
quant_matmul<<O,S),256,0,dc->stream>>>(dc->y,dc->ac,proj->weights,
- proj->scales,proj->fmt,S,proj->I,proj->O,row_bytes(proj->fmt,proj->I));
+ proj->scales,proj->fmt,S,proj->I,proj->O,row_bytes(proj->fmt,proj->I),proj->gs,proj->ng);
if(!cuda_ok(cudaGetLastError(),"pipe o_proj launch"))return 0;
if(!cuda_ok(cudaMemcpyAsync(out,dc->y,ob,cudaMemcpyDeviceToHost,dc->stream),"pipe attention download")||
!cuda_ok(cudaStreamSynchronize(dc->stream),"pipe attention sync"))return 0;
@@ -1071,17 +1364,20 @@ extern "C" int coli_cuda_attention_project_batch_dev(ColiCudaTensor *w,ColiCudaT
}
extern "C" int coli_cuda_pipe_silu_mul(int device,float *gate_dev,const float *up_dev,
size_t n){
+ if (fault_injected()) return 0;
DeviceContext *ctx=find_ctx(device); if(!n||!select_ctx(ctx)) return 0;
silu_mul<<<(unsigned)((n+255)/256),256>>>(gate_dev,up_dev,n);
return cuda_ok(cudaGetLastError(),"pipe silu mul");
}
extern "C" int coli_cuda_pipe_add(int device,float *x_dev,const float *t_dev,size_t n){
+ if (fault_injected()) return 0;
DeviceContext *ctx=find_ctx(device); if(!n||!select_ctx(ctx)) return 0;
pipe_add_n<<<(unsigned)((n+255)/256),256>>>(x_dev,t_dev,n);
return cuda_ok(cudaGetLastError(),"pipe add");
}
extern "C" int coli_cuda_pipe_rows_add(int device,float *x_dev,const float *partial_dev,
const int *rows_dev,int nrows,int D){
+ if (fault_injected()) return 0;
DeviceContext *ctx=find_ctx(device); if(nrows<1||D<1||!select_ctx(ctx)) return 0;
pipe_rows_add<<>>(x_dev,partial_dev,rows_dev,D);
return cuda_ok(cudaGetLastError(),"pipe rows add");
@@ -1090,11 +1386,12 @@ extern "C" int coli_cuda_pipe_rows_add(int device,float *x_dev,const float *part
* coli_cuda_matmul, zero host transfers. */
extern "C" int coli_cuda_pipe_gemm(ColiCudaTensor *t,float *y_dev,const float *x_dev,
int S){
+ if (fault_injected()) return 0;
if(!t||S<1) return 0;
DeviceContext *ctx=find_ctx(t->device); if(!select_ctx(ctx)) return 0;
dim3 grid((unsigned)t->O,(unsigned)S);
quant_matmul<<>>(y_dev,x_dev,t->weights,t->scales,t->fmt,S,t->I,t->O,
- row_bytes(t->fmt,t->I));
+ row_bytes(t->fmt,t->I),t->gs,t->ng);
return cuda_ok(cudaGetLastError(),"pipe gemm");
}
/* copia diretta scheda->scheda (P2P se disponibile, altrimenti staging driver) */
@@ -1109,6 +1406,7 @@ extern "C" int coli_cuda_pipe_peer_copy(int dst_dev,float *dst,int src_dev,
extern "C" int coli_cuda_attention_project_batch_dev_out(ColiCudaTensor *w,ColiCudaTensor *proj,
float *out_dev,const float *q_dev,const float *latent_dev,const float *rope_dev,
int S,int H,int Q,int R,int V,int K,int T,float scale){
+ if (fault_injected()) return 0;
if(!w||!proj||!out_dev||!q_dev||!latent_dev||!rope_dev||S<1||H<1||Q<1||R<1||V<1||
K<1||K>512||T8192||w->I!=K||w->O!=H*(Q+V)||
proj->device!=w->device||proj->I!=H*V)return 0;
@@ -1117,10 +1415,10 @@ extern "C" int coli_cuda_attention_project_batch_dev_out(ColiCudaTensor *w,ColiC
if(!reserve(&dc->ac,&dc->ac_cap,cb))return 0;
size_t shared=(size_t)(2*K+T+256)*sizeof(float);
attention_absorb_batch_kernel<<stream>>>(dc->ac,q_dev,latent_dev,
- rope_dev,w->weights,w->scales,w->fmt,S,H,Q,R,V,K,T,scale);
+ rope_dev,w->weights,w->scales,w->fmt,S,H,Q,R,V,K,T,scale,w->gs,w->ng);
if(!cuda_ok(cudaGetLastError(),"pipe attention launch (dev out)"))return 0;
quant_matmul<<O,S),256,0,dc->stream>>>(out_dev,dc->ac,proj->weights,
- proj->scales,proj->fmt,S,proj->I,proj->O,row_bytes(proj->fmt,proj->I));
+ proj->scales,proj->fmt,S,proj->I,proj->O,row_bytes(proj->fmt,proj->I),proj->gs,proj->ng);
if(!cuda_ok(cudaGetLastError(),"pipe o_proj launch (dev out)"))return 0;
return cuda_ok(cudaStreamSynchronize(dc->stream),"pipe attention sync (dev out)");
}
@@ -1130,12 +1428,13 @@ extern "C" int coli_cuda_attention_project_batch_dev_out(ColiCudaTensor *w,ColiC
extern "C" int coli_cuda_attention_absorb_batch_dev(ColiCudaTensor *w,float *ctx_dev,
const float *q_dev,const float *latent_dev,const float *rope_dev,
int S,int H,int Q,int R,int V,int K,int T,float scale){
+ if (fault_injected()) return 0;
if(!w||!ctx_dev||!q_dev||!latent_dev||!rope_dev||S<1||H<1||Q<1||R<1||V<1||
K<1||K>512||T8192||w->I!=K||w->O!=H*(Q+V))return 0;
DeviceContext *dc=find_ctx(w->device);if(!select_ctx(dc))return 0;
size_t shared=(size_t)(2*K+T+256)*sizeof(float);
attention_absorb_batch_kernel<<stream>>>(ctx_dev,q_dev,latent_dev,
- rope_dev,w->weights,w->scales,w->fmt,S,H,Q,R,V,K,T,scale);
+ rope_dev,w->weights,w->scales,w->fmt,S,H,Q,R,V,K,T,scale,w->gs,w->ng);
if(!cuda_ok(cudaGetLastError(),"pipe shard attention launch"))return 0;
return cuda_ok(cudaStreamSynchronize(dc->stream),"pipe shard attention sync");
}
@@ -1144,6 +1443,7 @@ extern "C" int coli_cuda_attention_absorb_batch_dev(ColiCudaTensor *w,float *ctx
extern "C" int coli_cuda_attention_absorb_kvdev(ColiCudaTensor *w,float *ctx,const float *q,
const float *latent_dev,const float *rope_dev,int H,int Q,int R,int V,int K,int T,
float scale){
+ if (fault_injected()) return 0;
if(!w||!ctx||!q||!latent_dev||!rope_dev||H<1||Q<1||R<1||V<1||K<1||K>512||T<1||T>8192||
w->I!=K||w->O!=H*(Q+V))return 0;
DeviceContext *dc=find_ctx(w->device);if(!select_ctx(dc))return 0;
@@ -1152,7 +1452,7 @@ extern "C" int coli_cuda_attention_absorb_kvdev(ColiCudaTensor *w,float *ctx,con
if(!cuda_ok(cudaMemcpyAsync(dc->aq,q,qb,cudaMemcpyHostToDevice,dc->stream),"kvdev q upload"))return 0;
size_t shared=(size_t)(2*K+T+256)*sizeof(float);
attention_absorb_batch_kernel<<stream>>>(dc->ac,dc->aq,latent_dev,
- rope_dev,w->weights,w->scales,w->fmt,1,H,Q,R,V,K,T,scale);
+ rope_dev,w->weights,w->scales,w->fmt,1,H,Q,R,V,K,T,scale,w->gs,w->ng);
if(!cuda_ok(cudaGetLastError(),"kvdev absorb launch")||
!cuda_ok(cudaMemcpyAsync(ctx,dc->ac,cb,cudaMemcpyDeviceToHost,dc->stream),"kvdev ctx download")||
!cuda_ok(cudaStreamSynchronize(dc->stream),"kvdev absorb sync"))return 0;
diff --git a/c/backend_cuda.h b/c/backend_cuda.h
index 63c1f11..5196a13 100644
--- a/c/backend_cuda.h
+++ b/c/backend_cuda.h
@@ -36,20 +36,24 @@ 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);
/*
* y[S,O] = x[S,I] @ W[O,I]^T.
- * fmt matches QT in glm.c: 0=f32, 1=int8, 2=int4, 3=int2.
- * The first successful call uploads W and its row scales; later calls reuse it.
+ * fmt matches QT in glm.c: 0=f32, 1=int8, 2=int4, 3=int2, 4=grouped int4.
+ * gs is the group size for fmt=4 (0 for all other formats).
+ * The first successful call uploads W and its scales; later calls reuse it.
* Returns 1 on success and 0 when CUDA is not initialized or the format is invalid.
*/
COLI_CUDA_DLLEXPORT 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);
+ int fmt, int S, int I, int O, int device, int gs);
/* Fused expert pipeline: y = down(silu(gate(x)) * up(x)). All three tensors
* must already be resident on one device. Activations cross PCIe once in
@@ -67,6 +71,16 @@ COLI_CUDA_DLLEXPORT int coli_cuda_shared_mlp_w4a16(ColiCudaTensor *gate, ColiCud
/* Packed group of same-shaped experts. Inputs and outputs contain sum(rows)
* consecutive [D] rows in call order. */
+/* Async issue/take split of the group call below (Inc.4): issue launches on the
+ * device stream and returns; take syncs and returns the pinned result rows (valid
+ * until the next issue on that device). Small totals only (<=8 rows); one
+ * outstanding issue per device. */
+COLI_CUDA_DLLEXPORT int coli_cuda_expert_group_issue(ColiCudaTensor *const *gates,
+ ColiCudaTensor *const *ups,
+ ColiCudaTensor *const *downs,
+ const int *rows, int count, const float *x);
+COLI_CUDA_DLLEXPORT const float *coli_cuda_expert_group_take(int device);
+
COLI_CUDA_DLLEXPORT int coli_cuda_expert_group(ColiCudaTensor *const *gates,
ColiCudaTensor *const *ups,
ColiCudaTensor *const *downs,
@@ -126,6 +140,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..e65f641 100644
--- a/c/backend_loader.c
+++ b/c/backend_loader.c
@@ -41,14 +41,20 @@ typedef int (*fn_expert_mlp)(ColiCudaTensor *gate, ColiCudaTensor *up
typedef int (*fn_expert_group)(ColiCudaTensor *const *gates, ColiCudaTensor *const *ups,
ColiCudaTensor *const *downs, const int *rows, int count,
float *y, const float *x);
+typedef int (*fn_expert_group_issue)(ColiCudaTensor *const *gates,
+ ColiCudaTensor *const *ups,
+ ColiCudaTensor *const *downs,
+ const int *rows, int count, const float *x);
+typedef const float * (*fn_expert_group_take)(int device);
typedef int (*fn_attention_absorb)(ColiCudaTensor *kv_b, float *ctx, const float *q,
const float *latent, const float *rope, int H, int Q,
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);
+ int fmt, int S, int I, int O, int device, int gs);
typedef void (*fn_tensor_free)(ColiCudaTensor *tensor);
typedef size_t (*fn_tensor_bytes)(const ColiCudaTensor *tensor);
typedef int (*fn_tensor_device)(const ColiCudaTensor *tensor);
@@ -76,6 +82,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);
@@ -100,8 +107,11 @@ static struct {
fn_group_stats group_stats;
fn_expert_mlp expert_mlp;
fn_expert_group expert_group;
+ fn_expert_group_issue expert_group_issue;
+ fn_expert_group_take expert_group_take;
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;
@@ -123,6 +133,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;
@@ -194,8 +205,11 @@ static int coli_cuda_load(void){
RESOLVE(group_stats, fn_group_stats)
RESOLVE(expert_mlp, fn_expert_mlp)
RESOLVE(expert_group, fn_expert_group)
+ RESOLVE(expert_group_issue, fn_expert_group_issue)
+ RESOLVE(expert_group_take, fn_expert_group_take)
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)
@@ -217,6 +231,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)
@@ -289,6 +304,19 @@ int coli_cuda_expert_group(ColiCudaTensor *const *gates, ColiCudaTensor *const *
return g_cuda.expert_group(gates, ups, downs, rows, count, y, x);
}
+int coli_cuda_expert_group_issue(ColiCudaTensor *const *gates,
+ ColiCudaTensor *const *ups,
+ ColiCudaTensor *const *downs,
+ const int *rows, int count, const float *x){
+ if(!g_cuda.available) return 0;
+ return g_cuda.expert_group_issue(gates, ups, downs, rows, count, x);
+}
+
+const float *coli_cuda_expert_group_take(int device){
+ if(!g_cuda.available) return NULL;
+ return g_cuda.expert_group_take(device);
+}
+
int coli_cuda_attention_absorb(ColiCudaTensor *kv_b, float *ctx, const float *q,
const float *latent, const float *rope, int H, int Q,
int R, int V, int K, int T, float attention_scale){
@@ -302,11 +330,16 @@ 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){
+ int fmt, int S, int I, int O, int device, int gs){
if(!g_cuda.available) return 0;
- return g_cuda.matmul(tensor, y, x, weights, scales, fmt, S, I, O, device);
+ return g_cuda.matmul(tensor, y, x, weights, scales, fmt, S, I, O, device, gs);
}
void coli_cuda_tensor_free(ColiCudaTensor *tensor){
@@ -407,6 +440,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/backend_metal.h b/c/backend_metal.h
index 4383b3b..35c4931 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,
@@ -98,6 +115,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 51ed8a0..6ec3fb4 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)
@@ -258,6 +323,73 @@ 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_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; 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 colibri.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. 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];
+ }
+ 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). 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_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 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;
+ 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;
static id ensure(id b, size_t *cap, size_t need) {
@@ -284,6 +416,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,11 +433,45 @@ 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; }
+ // 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;
}
@@ -313,13 +481,29 @@ 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; return; }
- g_slabs.push_back({base, len, b});
+ 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) { old = s.buf; s.len = len; s.buf = b; found = true; break; }
+ if (!found) g_slabs.push_back({base, len, b});
+ }
+ // 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) {
- 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) {
@@ -357,7 +541,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) {
@@ -597,12 +788,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 +852,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;
@@ -668,6 +907,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;
+ 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);
@@ -689,7 +931,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];
diff --git a/c/coli b/c/coli
index 6378ce5..5cef2fc 100755
--- a/c/coli
+++ b/c/coli
@@ -240,6 +240,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/colibri.c b/c/colibri.c
index e5e08f3..4a4b2e0 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
@@ -92,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
@@ -108,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 */
}
@@ -125,6 +137,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;
@@ -133,7 +149,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;
@@ -163,11 +184,34 @@ typedef struct {
KVState *kv;
ESlot **ecache; int *ecn; int ecap; /* LRU expert per-layer */
float **kv_dev_L, **kv_dev_R; int *kv_dev_valid; /* ombra KV su device (decode) */
+ float **ln_dev; /* in_ln/post_ln cached on device: [layer*2+{0,1}] (Inc.4) */
ESlot ws[64]; /* working set del layer corrente (load paralleli) */
ESlot **pin; int *npin; /* HOT-STORE: expert pinnati in RAM (mai evicted) */
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 */
@@ -212,9 +256,17 @@ static inline int spec_pinned(void){ return g_spec_pin && g_spec_live; }
static void matmul_qt_ex(float *y, const float *x, QT *w, int S, int allow_idot);
static void matmul_qt(float *y, const float *x, QT *w, int S){ matmul_qt_ex(y,x,w,S,1); }
+/* fmt=4 fused gate+up (defined later, after the quant kernels) */
+static void matmul_i4_grouped_pair(float *yg, float *yu, const float *x,
+ const uint8_t *qg, const float *sg,
+ const uint8_t *qu, const float *su,
+ int S, int I, int O, int gs);
+
static void expert_gate_up(float *g,float *u,const float *x,QT *wg,QT *wu,int S){
if(!g_no_fused_pair&&!spec_pinned()&&S==1&&wg->fmt==2&&wu->fmt==2&&wg->I==wu->I&&wg->O==wu->O)
matmul_i4_pair(g,u,x,wg->q4,wg->s,wu->q4,wu->s,wg->I,wg->O);
+ else if(!g_no_fused_pair&&S==1&&wg->fmt==4&&wu->fmt==4&&wg->I==wu->I&&wg->O==wu->O&&wg->gs==wu->gs)
+ matmul_i4_grouped_pair(g,u,x,wg->q4,wg->s,wu->q4,wu->s,S,wg->I,wg->O,wg->gs);
else { matmul_qt(g,x,wg,S); matmul_qt(u,x,wu,S); }
}
@@ -233,8 +285,14 @@ 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
+ * 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){
@@ -242,6 +300,7 @@ static int qt_cuda_update(QT *t){
t->fmt==1?(const void*)t->q8:(const void*)t->q4;
return coli_cuda_tensor_update(t->cuda,weights,t->s);
}
+static double g_ovl_issue,g_ovl_cpu,g_ovl_take,g_ovl_mark; /* Inc.4 overlap-window split (OVL report) */
static void cuda_stats_print(void){
size_t n=0,b=0; coli_cuda_stats(-1,&n,&b);
fprintf(stderr,"[CUDA] resident set: %zu tensors, %.2f GB VRAM\n",n,b/1e9);
@@ -257,6 +316,9 @@ static void cuda_stats_print(void){
getenv("COLI_CUDA_PROFILE")?"; timing sotto":"");
if(calls&&getenv("COLI_CUDA_PROFILE")) fprintf(stderr,
"[CUDA] expert groups timing: H2D %.1f ms | kernel %.1f ms | D2H %.1f ms\n",h2d,kernel,d2h);
+ if(g_ovl_issue+g_ovl_cpu+g_ovl_take>0) fprintf(stderr,
+ "[CUDA] overlap window: pack+issue %.2fs | cpu-rows %.2fs | take(sync+acc) %.2fs\n",
+ g_ovl_issue,g_ovl_cpu,g_ovl_take);
}
static int parse_cuda_devices(const char *list, int *out){
if(!list||!*list) return 0;
@@ -298,6 +360,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 +428,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 +440,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){
@@ -326,6 +456,70 @@ static float *falloc(int64_t n){
float *p=malloc((size_t)n*sizeof(float)); if(!p){fprintf(stderr,"OOM\n");exit(1);} return p; }
+
+/* Fused gate+up for grouped int4 (fmt=4): computes both yg[S,O] and yu[S,O] from
+ * the same x[S,I], reading x once instead of twice — saves ~33% of expert-matmul time at decode.
+ * The per-group scale logic matches matmul_i4_grouped exactly. */
+static void matmul_i4_grouped_pair(float *yg, float *yu, const float *x,
+ const uint8_t *qg, const float *sg,
+ const uint8_t *qu, const float *su,
+ int S, int I, int O, int gs){
+ int rb=(I+1)/2; int ng=(I+gs-1)/gs;
+ #pragma omp parallel for schedule(static)
+ for(int o=0;oI) glen=I-base;
+ float scg=sgl[g], scu=sul[g];
+ int i=base;
+#ifdef __AVX2__
+ const __m128i m4=_mm_set1_epi8(0x0F); const __m256i b8=_mm256_set1_epi32(8);
+ __m256 accg=_mm256_setzero_ps(), accu=_mm256_setzero_ps();
+ for(; i+16<=base+glen; i+=16){
+ __m128i byg=_mm_loadl_epi64((const __m128i*)(wg+(i>>1)));
+ __m128i log=_mm_and_si128(byg,m4),hig=_mm_and_si128(_mm_srli_epi16(byg,4),m4);
+ __m128i nibg=_mm_unpacklo_epi8(log,hig);
+ __m256 w0g=_mm256_cvtepi32_ps(_mm256_sub_epi32(_mm256_cvtepu8_epi32(nibg),b8));
+ __m256 w1g=_mm256_cvtepi32_ps(_mm256_sub_epi32(_mm256_cvtepu8_epi32(_mm_srli_si128(nibg,8)),b8));
+ accg=_mm256_fmadd_ps(_mm256_loadu_ps(xs+i), w0g, accg);
+ accg=_mm256_fmadd_ps(_mm256_loadu_ps(xs+i+8), w1g, accg);
+ __m128i byu=_mm_loadl_epi64((const __m128i*)(wu2+(i>>1)));
+ __m128i lou=_mm_and_si128(byu,m4),hiu=_mm_and_si128(_mm_srli_epi16(byu,4),m4);
+ __m128i nibu=_mm_unpacklo_epi8(lou,hiu);
+ __m256 w0u=_mm256_cvtepi32_ps(_mm256_sub_epi32(_mm256_cvtepu8_epi32(nibu),b8));
+ __m256 w1u=_mm256_cvtepi32_ps(_mm256_sub_epi32(_mm256_cvtepu8_epi32(_mm_srli_si128(nibu,8)),b8));
+ accu=_mm256_fmadd_ps(_mm256_loadu_ps(xs+i), w0u, accu);
+ accu=_mm256_fmadd_ps(_mm256_loadu_ps(xs+i+8), w1u, accu);
+ }
+ ag+=hsum256(accg)*scg; au+=hsum256(accu)*scu;
+#endif
+ for(; i+1>1], bu=wu2[i>>1];
+ ag+=(xs[i]*(float)((int)(bg&0xF)-8)+xs[i+1]*(float)((int)(bg>>4)-8))*scg;
+ au+=(xs[i]*(float)((int)(bu&0xF)-8)+xs[i+1]*(float)((int)(bu>>4)-8))*scu;
+ }
+ if(i>1], bu=wu2[i>>1];
+ ag+=xs[i]*(float)((int)(bg&0xF)-8)*scg;
+ au+=xs[i]*(float)((int)(bu&0xF)-8)*scu;
+ }
+ }
+ yg[(int64_t)s*O+o]=ag; yu[(int64_t)s*O+o]=au;
+ }
+ }
+}
+
+/* allow_idot=0: forza il kernel int4/int8 ESATTO (attivazioni f32). Serve alle proiezioni di
+ * attenzione: sono sensibili alla quantizzazione int8 delle attivazioni dell'IDOT. Misurato su
+ * GLM-5.2 int4, 1023 token, log-lik -5040.33 (esatto) -> -5160.47 (IDOT) = +0.117 nat/token,
+ * ~+12% perplexity. Gli altri matmul del prefill (o_proj, kv_b, expert) tengono l'IDOT.
+ * EN: allow_idot=0 forces the EXACT int4/int8 kernel (f32 activations). The attention
+ * projections need it: IDOT's int8 activation quantization costs +0.117 nats/token there
+ * (~+12% perplexity), measured. Every other prefill matmul keeps IDOT as before. */
static void matmul_qt_ex(float *y, const float *x, QT *w, int S, int allow_idot){
#ifdef COLI_METAL
if(g_metal_enabled && S>=g_metal_gemm_min && !spec_pinned() && (w->fmt==1||w->fmt==2) && !omp_in_parallel()){
@@ -334,10 +528,10 @@ 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;
+ if(coli_cuda_matmul(&w->cuda,y,x,weights,w->s,w->fmt,S,w->I,w->O,w->cuda_device,w->gs)) return;
w->cuda_failed=1;
fprintf(stderr,"[CUDA] tensor [%d,%d] on device %d disabled after an error; falling back to CPU\n",
w->O,w->I,w->cuda_device);
@@ -356,6 +550,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);
}
@@ -401,12 +596,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. */
@@ -426,6 +615,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
@@ -459,19 +661,31 @@ 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 */
+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<=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<=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);
}
@@ -700,16 +934,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); }
@@ -733,6 +973,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 {
@@ -770,13 +1013,14 @@ static void qt_cuda_colocate(QT *dst,const QT *src){
}
static void layer_cuda_shard_kvb(Layer *l,int H,int Q,int V){
if(!g_cuda_enabled||!g_cuda_dense||g_cuda_ndev<2||l->kv_b.fmt==0)return;
- int rb=l->kv_b.fmt==1?l->kv_b.I:(l->kv_b.fmt==2?(l->kv_b.I+1)/2:(l->kv_b.I+3)/4);
+ int rb=l->kv_b.fmt==1?l->kv_b.I:
+ (l->kv_b.fmt==2||l->kv_b.fmt==4)?(l->kv_b.I+1)/2:(l->kv_b.I+3)/4;
const uint8_t *weights=l->kv_b.fmt==1?(const uint8_t*)l->kv_b.q8:l->kv_b.q4;
for(int d=0,h0=0;dkv_b.s+(int64_t)h0*(Q+V);
- if(!coli_cuda_tensor_upload(&l->kv_b_shard[d],part,scale,l->kv_b.fmt,l->kv_b.I,rows,g_cuda_devices[d]))return;
+ const float *scale=l->kv_b.s+(int64_t)h0*(Q+V)*(l->kv_b.gs>0?(l->kv_b.I+l->kv_b.gs-1)/l->kv_b.gs:1);
+ if(!coli_cuda_tensor_upload_g(&l->kv_b_shard[d],part,scale,l->kv_b.fmt,l->kv_b.I,rows,g_cuda_devices[d],l->kv_b.gs))return;
l->shard_h0[d]=h0;l->shard_hn[d]=hn;l->n_kv_b_shard++;h0+=hn;
}
int old=-1;for(int i=0;ikv_b.cuda_device)old=i;
@@ -804,6 +1048,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 +1093,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
}
@@ -860,12 +1107,17 @@ static void model_init(Model *m, const char *snap, int cap, int ebits, int dbits
"self_attn.q_a_proj.weight","self_attn.q_b_proj.weight","self_attn.kv_a_proj_with_mqa.weight",
"self_attn.kv_b_proj.weight","self_attn.o_proj.weight","mlp.gate.weight",
"mlp.shared_experts.gate_proj.weight","mlp.shared_experts.down_proj.weight",
- "mlp.experts.0.gate_proj.weight","mlp.experts.255.down_proj.weight"};
+ "mlp.experts.0.gate_proj.weight"};
char mn[256]; m->has_mtp=1;
for(unsigned q=0;qn_layers,req[q]);
if(!st_has(&m->S,mn)){ m->has_mtp=0; break; }
}
+ /* probe the LAST expert by index, not a fixed 255: REAP-pruned
+ * checkpoints have n_routed_experts < 256 and the MTP set stays complete,
+ * so a hardcoded expert.255 would spuriously report has_mtp=0 on them. */
+ snprintf(mn,sizeof(mn),"model.layers.%d.mlp.experts.%d.down_proj.weight",c->n_layers,c->n_experts-1);
+ if(!st_has(&m->S,mn)) m->has_mtp=0;
if(getenv("MTP") && atoi(getenv("MTP"))==0) m->has_mtp=0;
if(m->has_mtp){
int i=c->n_layers; Layer *l=&m->mtpL;
@@ -894,6 +1146,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
}
@@ -948,12 +1202,27 @@ static void model_init(Model *m, const char *snap, int cap, int ebits, int dbits
static void embed_row(Model *m, int tok, float *x){
int D=m->c.hidden; QT *e=&m->embed;
if(e->fmt==0){ memcpy(x, e->qf+(int64_t)tok*D, D*sizeof(float)); return; }
+ if(e->fmt==4){ /* grouped int4: per-group scale (embed/lm_head at io_bits, usually fmt 0/1) */
+ const uint8_t *q=e->q4+(int64_t)tok*((D+1)/2); int gs=e->gs,ng=(D+gs-1)/gs;
+ const float *scl=e->s+(int64_t)tok*ng;
+ for(int g=0;g*gsD)glen=D-base; float s=scl[g];
+ for(int i=base;i+1>1]; x[i]=(float)((int)(byte&0xF)-8)*s;
+ x[i+1]=(float)((int)(byte>>4)-8)*s; }
+ if(glen&1){ uint8_t byte=q[(base+glen-1)>>1]; x[base+glen-1]=(float)((int)(byte&0xF)-8)*s; } }
+ return; }
if(e->fmt==1){ const int8_t *q=e->q8+(int64_t)tok*D; float s=e->s[tok];
for(int i=0;ifmt==2){ const uint8_t *q=e->q4+(int64_t)tok*((D+1)/2); float s=e->s[tok]; /* int4 */
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; }
}
@@ -1079,7 +1348,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. */
@@ -1189,6 +1486,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;
}
@@ -1210,12 +1512,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_rep(&m->S, tw[ord[0]]->fd, rep) : -1;
@@ -1225,27 +1537,43 @@ 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;
atomic_fetch_add_explicit(&g_mir_bytes[rep],(int64_t)r,memory_order_relaxed);
atomic_fetch_add_explicit(&g_mir_nread[rep],1,memory_order_relaxed);
}
}
if(!done){ /* fallback bufferizzato */
- if(mir_pread(&m->S, tw[ord[0]]->fd, rep, s->slab, wtot, off0, "pread expert")){ if(fatal) exit(1); return -1; }
+ if(mir_pread(&m->S, tw[ord[0]]->fd, rep, 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(mir_pread(&m->S, tw[k]->fd, rep, s->slab+o, tw[k]->nbytes, tw[k]->off, "pread expert")){ if(fatal) exit(1); return -1; }
+ if(mir_pread(&m->S, tw[k]->fd, rep, 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(mir_pread(&m->S, tq[k]->fd, rep, (char*)(s->fslab+fo), tq[k]->nbytes, tq[k]->off, "pread qs")){ if(fatal) exit(1); return -1; }
+ if(mir_pread(&m->S, tq[k]->fd, rep, (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.
* The drop targets the fd of the replica READ. */
@@ -1264,9 +1592,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;
}
@@ -1441,8 +1775,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;
@@ -1487,6 +1824,22 @@ static int g_pipe=0; /* PIPE=1: async expert-load pipeline. Default ON for
* the matmul. PIPE=0 opts back into the blocking serial path. */
static int g_pipe_nw=8; /* PIPE_WORKERS=n: I/O worker threads (disk-parallel reads) */
static int g_uring=0; /* URING=1: Linux io_uring load/completion backend; implies PIPE */
+static int g_pipe_block=0;/* COLI_PIPE_BLOCK=1: pipe_wait blocca su una condvar invece dello
+ * spin sched_yield (default OFF = spin byte-identico). EN: a yield
+ * storm on the main thread fights the OpenMP team for cycles during
+ * multi-ms loads; the condvar wake costs ~5us against reads that
+ * cost 0.5-3ms (#159). Pthread pool only: the URING backend has no
+ * waiter spin to replace. */
+/* PIPE_WORKERS>0 esplicito nell'env implica PIPE=1: dimensionare il pool
+ * dichiara l'intento di usarlo (una campagna intera l'ha impostato con la
+ * pipe spenta senza accorgersene). EN: fires ONLY when PIPE is unset in the
+ * env AND the platform default left the pipe off (on _WIN32 it already
+ * defaults to 1) AND PIPE_WORKERS parses positive — the internal default of
+ * 8 does not count, PIPE_WORKERS=0/empty does not, and an explicit PIPE=0
+ * always wins. */
+static int pipe_workers_imply_pipe(const char *pipe_env, const char *pw_env, int pipe_now){
+ return !pipe_now && !pipe_env && pw_env && atoi(pw_env)>0;
+}
typedef struct {
_Atomic uint64_t cur; /* (gen<<8)|index; gen main-only, index 0..njobs (≤64) */
_Atomic int njobs; /* current batch job count */
@@ -1494,6 +1847,7 @@ typedef struct {
_Atomic int layer; /* current batch layer */
_Atomic int ready[64]; /* per-slot load-done flag */
pthread_mutex_t mx; pthread_cond_t cv; /* ONLY for parking/waking idle workers */
+ pthread_cond_t cv_done; /* COLI_PIPE_BLOCK: signals ready[] transitions */
Model *m;
pthread_t th[16]; int nw; int started;
} PipePool;
@@ -1516,8 +1870,13 @@ 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);
+ if(g_pipe_block){ /* wake a main thread parked in pipe_wait */
+ pthread_mutex_lock(&p->mx);
+ pthread_cond_broadcast(&p->cv_done);
+ pthread_mutex_unlock(&p->mx);
+ }
}
/* CAS failed → another worker advanced index (or gen advanced): re-loop */
}
@@ -1535,6 +1894,7 @@ static void pipe_init(Model *m){
g_pp.m=m; g_pp.nw=g_pipe_nw; if(g_pp.nw>16) g_pp.nw=16; if(g_pp.nw<1) g_pp.nw=1;
atomic_store(&g_pp.cur,0); atomic_store(&g_pp.njobs,0);
pthread_mutex_init(&g_pp.mx,NULL); pthread_cond_init(&g_pp.cv,NULL);
+ pthread_cond_init(&g_pp.cv_done,NULL);
for(int i=0;islab); 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);
+ 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
/* 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. The readahead
- * targets the SAME replica that will serve the pread (expert_route is deterministic). */
+ * 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.
+ * The readahead targets the SAME replica that will serve the pread (expert_route
+ * is deterministic).
+ * 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]; int rep=expert_route(layer,eid);
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_rep(&m->S,nm,rep);
+ snprintf(nm,sizeof(nm),"model.layers.%d.mlp.experts.%d.%s",layer,eid,suf[k]);
+ if(!g_direct) st_prefetch_rep(&m->S,nm,rep);
char qs[320]; snprintf(qs,sizeof(qs),"%s.qs",nm); st_prefetch_rep(&m->S,qs,rep);
}
}
@@ -1627,6 +2012,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);
@@ -1640,6 +2033,13 @@ static void qt_matvec_rows(const QT *t, int r0, int n, const float *x, float *y)
int I=t->I;
for(int j=0;jfmt==0){ const float *w=t->qf+(int64_t)row*I; for(int i=0;ifmt==4){ /* grouped int4: per-group scale */
+ const uint8_t *w=t->q4+(int64_t)row*((I+1)/2); int gs=t->gs,ng=(I+gs-1)/gs;
+ const float *scl=t->s+(int64_t)row*ng;
+ for(int g=0;g*gsI)glen=I-base; float sc=scl[g]; float acc=0;
+ for(int i=base;i+1>1]; acc+=((int)(b&0xF)-8)*x[i]+((int)(b>>4)-8)*x[i+1]; }
+ if(glen&1){ uint8_t b=w[(base+glen-1)>>1]; acc+=((int)(b&0xF)-8)*x[base+glen-1]; }
+ a+=acc*sc; } }
else if(t->fmt==1){ const int8_t *w=t->q8+(int64_t)row*I; float s=t->s[row];
float acc=0; for(int i=0;ifmt==2){ const uint8_t *w=t->q4+(int64_t)row*((I+1)/2); float s=t->s[row]; float acc=0;
@@ -1652,6 +2052,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;
@@ -1660,6 +2067,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){
@@ -2203,6 +2611,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). */
@@ -2222,7 +2639,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));
@@ -2232,6 +2650,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
@@ -2243,7 +2670,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;seusage[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;
}
@@ -2558,7 +2989,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
@@ -2575,6 +3006,11 @@ static void moe(Model *m, Layer *l, int layer, float *x, int S, float *out, int
}
#ifdef COLI_CUDA
ESlot *group_e[64]; int group_n[64]; int ngroup=0;
+ /* Inc.4 overlap stash: pass-1 packing kept for the take phase after the CPU loop */
+ ESlot *eg_e[64]; int eg_n[64], eg_row[64][4], eg_npg=0; float eg_w[64][4];
+ int dev_nc0[COLI_CUDA_MAX_DEVICES], dev_off0[COLI_CUDA_MAX_DEVICES],
+ dev_total0[COLI_CUDA_MAX_DEVICES], dev_which0[COLI_CUDA_MAX_DEVICES][64];
+ memset(dev_nc0,0,sizeof(dev_nc0)); (void)eg_npg; (void)dev_total0; (void)dev_off0;
#endif
#ifdef COLI_METAL
if(g_metal_enabled){
@@ -2603,9 +3039,83 @@ static void moe(Model *m, Layer *l, int layer, float *x, int S, float *out, int
free(mxg); free(mrows); free(mrw);
}
#undef MB_BUILD
+#endif
+#ifdef COLI_CUDA
+ /* Inc.4 pass 1: collect the VRAM-resident experts' groups and ISSUE them async
+ * BEFORE the CPU loop below, so the GPU computes its share while the CPU works
+ * through the RAM-tier/miss rows — t_emm becomes max(cpu, gpu) instead of the
+ * sum. Only resident experts are collected (misses are never cuda_eligible), so
+ * no pipe_wait is needed here; the CPU loop keeps its own waits. Any issue
+ * failure drops the layer back to the collect-in-loop + sync-group path. */
+ int early_issued=0, done_j[64]={0};
+ {
+ static int g_group_async2=-1;
+ if(g_group_async2<0) g_group_async2=getenv("COLI_GROUP_ASYNC")?atoi(getenv("COLI_GROUP_ASYNC")):0;
+ if(!metal_done && g_group_async2 && group_enabled && S<=4 && g_cuda_enabled &&
+ g_cuda_ndev>0 && !omp_in_parallel()){
+ ESlot *pg_e[64]; int pg_n[64], pg_j[64], npg=0;
+ int prow[64][4]; float pw[64][4];
+ for(int j=0;jg.cuda_eligible&&e->u.cuda_eligible&&e->d.cuda_eligible)) continue;
+ int nr=0;
+ for(int s=0;sg.cuda_device==g_cuda_devices[di]) pd_total[di]+=pg_n[q];
+ for(int di=1;dig.cuda_device==device){
+ int nc=pd_nc[di]++; ESlot *e=pg_e[q];
+ pd_g[di][nc]=e->g.cuda; pd_u[di][nc]=e->u.cuda; pd_d[di][nc]=e->d.cuda;
+ pd_rows[di][nc]=pg_n[q]; pd_which[di][nc]=q;
+ for(int r=0;rt_emm+=now_s()-tg0;
+ for(int q=0;qgpu_expert_calls++;
+ }
+ } else {
+ for(int di=0;di=1 routing invariant.
@@ -2652,6 +3162,36 @@ static void moe(Model *m, Layer *l, int layer, float *x, int S, float *out, int
m->cpu_expert_rows+=(uint64_t)nr;}
}
#ifdef COLI_CUDA
+ /* Inc.4 take phase: the CPU loop above ran while the GPU computed the issued
+ * groups — collect them now. A failed device recomputes its experts on the CPU
+ * (expert_host_ensure reloads slabs released by CUDA_RELEASE_HOST). */
+ if(early_issued){
+ double tg1=now_s();
+ g_ovl_cpu+=tg1-g_ovl_mark; /* CPU-row window between issue and take */
+ for(int di=0;dig,&e->u,nr);
+ for(int64_t z=0;z<(int64_t)nr*I;z++) gg[z]=siluf(gg[z])*uu[z];
+ matmul_qt(hh,gg,&e->d,nr);
+ for(int r=0;rt_emm+=now_s()-tg1; g_ovl_take+=now_s()-tg1;
+ }
ColiCudaTensor *dev_g[COLI_CUDA_MAX_DEVICES][64],*dev_u[COLI_CUDA_MAX_DEVICES][64];
ColiCudaTensor *dev_d[COLI_CUDA_MAX_DEVICES][64];
int dev_rows[COLI_CUDA_MAX_DEVICES][64],dev_which[COLI_CUDA_MAX_DEVICES][64];
@@ -2673,6 +3213,33 @@ static void moe(Model *m, Layer *l, int layer, float *x, int S, float *out, int
}
}
double tg=now_s();
+ /* Inc.4: at decode scale, issue every device's group WITHOUT syncing, then take
+ * them all — one stream sync per device per layer instead of a full staged
+ * round-trip per call (measured: ~70% of the sync call is host-side wait).
+ * Any issue failure drains what was issued and the whole layer falls back to
+ * the sync path below, which recomputes from group_x (idempotent). */
+ int async_done=0;
+ static int g_group_async=-1;
+ if(g_group_async<0) g_group_async=getenv("COLI_GROUP_ASYNC")?atoi(getenv("COLI_GROUP_ASYNC")):0;
+ if(g_group_async && S<=4 && g_cuda_ndev>0){
+ int issued[COLI_CUDA_MAX_DEVICES]={0}, all=1;
+ for(int di=0;di1) schedule(static)
for(int di=0;dish_gate.cuda_eligible||!l->sh_up.cuda_eligible||!l->sh_down.cuda_eligible||
!qt_cuda_upload(&l->sh_gate)||!qt_cuda_upload(&l->sh_up)||!qt_cuda_upload(&l->sh_down)||
l->sh_gate.cuda_device!=dev||l->sh_up.cuda_device!=dev||l->sh_down.cuda_device!=dev) return 0;
- float *w_in =coli_cuda_pipe_scratch(dev,8,(size_t)D*4);
- float *w_post=coli_cuda_pipe_scratch(dev,9,(size_t)D*4);
+ /* Inc.4: the layernorm weights are constants — upload once per layer and keep them
+ * on the layer's device, instead of two synchronous 24 KB uploads per layer per
+ * token (152 sync H2D/token measured on the profile). */
+ if(!m->ln_dev) m->ln_dev=calloc((size_t)(c->n_layers+1)*2,sizeof(float*));
+ float *w_in=m->ln_dev[(size_t)li*2], *w_post=m->ln_dev[(size_t)li*2+1];
+ if(!w_in){
+ w_in=coli_cuda_pipe_alloc(dev,(size_t)D*4);
+ if(!w_in||!coli_cuda_pipe_upload(dev,w_in,l->in_ln,(size_t)D*4)) return 0;
+ m->ln_dev[(size_t)li*2]=w_in;
+ }
+ if(!w_post){
+ w_post=coli_cuda_pipe_alloc(dev,(size_t)D*4);
+ if(!w_post||!coli_cuda_pipe_upload(dev,w_post,l->post_ln,(size_t)D*4)) return 0;
+ m->ln_dev[(size_t)li*2+1]=w_post;
+ }
float *nrm_d=coli_cuda_pipe_scratch(dev,10,xb);
float *y_d =coli_cuda_pipe_scratch(dev,11,xb);
float *sg_d =coli_cuda_pipe_scratch(dev,12,(size_t)S*sI*4);
float *su_d =coli_cuda_pipe_scratch(dev,13,(size_t)S*sI*4);
float *snap =coli_cuda_pipe_scratch(dev,14,xb);
- if(!w_in||!w_post||!nrm_d||!y_d||!sg_d||!su_d||!snap) return 0;
+ if(!nrm_d||!y_d||!sg_d||!su_d||!snap) return 0;
if(!coli_cuda_pipe_peer_copy(dev,snap,dev,x_dev,xb)) return 0; /* snapshot per il fallback */
- if(!coli_cuda_pipe_upload(dev,w_in,l->in_ln,(size_t)D*4)||
- !coli_cuda_pipe_upload(dev,w_post,l->post_ln,(size_t)D*4)) return 0;
double ta=now_s();
if(!coli_cuda_pipe_rmsnorm(dev,nrm_d,x_dev,w_in,S,D,c->eps)) return 0;
/* DSA: i layer con indexer FULL cachano k_idx dalla nrm pre-attention (CPU, piccolo) */
@@ -3147,6 +3725,36 @@ static int pipe_layer_sparse(Model *m, Layer *l, int li, float *x_dev, int S, in
if(!attn_pipe_prefill(m,l,li,nrm_d,1,S,pos_base,NULL,y_d)) return 0;
if(!coli_cuda_pipe_add(dev,x_dev,y_d,(size_t)S*D)) return 0; /* prima mutazione */
if(!coli_cuda_pipe_rmsnorm(dev,nrm_d,x_dev,w_post,S,D,c->eps)) 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.
@@ -3177,7 +3785,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) */
@@ -3549,10 +4159,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). */
-static void grammar_setup(Tok *T){
+/* 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;
@@ -3564,59 +4200,45 @@ static void grammar_setup(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_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);
+ if(getenv("GRAMMAR_DRAFT")) g->max=atoi(getenv("GRAMMAR_DRAFT"));
+ grammar_setup_text(g,T,txt,path);
}
/* 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 ----
@@ -3674,12 +4296,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){
@@ -3701,7 +4323,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++;
@@ -3717,9 +4339,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 */
@@ -3868,7 +4490,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
}
@@ -3919,6 +4543,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;
@@ -3993,12 +4653,24 @@ 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));
+ int cap=(int)strlen(prompt)+16; int *pids=malloc((cap+2)*sizeof(int));
int np=tok_encode(&T,prompt,(int)strlen(prompt),pids,cap);
if(np<1){ fprintf(stderr,"prompt is empty after tokenization\n"); return; }
+ /* GLM prefix (#108): every GLM training sequence starts [gMASK]; without it the
+ * model is out-of-distribution and generates garbage. Serve/SCORE modes prepend it;
+ * run_text must too. CHAT_TEMPLATE=0 disables (raw prompt, for debugging/non-GLM).
+ * A prompt that already starts with the prefix passes through intact. */
+ int templ=getenv("CHAT_TEMPLATE")?atoi(getenv("CHAT_TEMPLATE")):1;
+ if(templ){
+ int gmask=tok_id_of(&T,"[gMASK]"), sop=tok_id_of(&T,"");
+ if(gmask>=0 && sop>=0 && (np<2 || pids[0]!=gmask || pids[1]!=sop)){
+ memmove(pids+2,pids,np*sizeof(int));
+ pids[0]=gmask; pids[1]=sop; np+=2;
+ }
+ }
printf("prompt: %d tokens | generating up to %d (EOS stop=%d) | n-gram draft=%d\n", np, ngen, eos, g_draft);
fputs(prompt,stdout); fflush(stdout);
kv_alloc(m, np+ngen+g_draft+2);
@@ -4022,7 +4694,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;
@@ -4049,8 +4721,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,
@@ -4181,16 +4853,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 */
@@ -4258,7 +4935,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 */
@@ -4365,8 +5042,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;
@@ -4387,21 +5064,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); }
@@ -4427,6 +5114,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;
@@ -4435,14 +5123,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++;
@@ -4508,13 +5233,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;
}
@@ -4538,7 +5265,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;
@@ -4657,7 +5384,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;
@@ -4870,6 +5597,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; }
@@ -4908,6 +5685,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 */
@@ -4918,26 +5718,26 @@ 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
+#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. */
+#ifdef __linux__
+ if(gpu_prefix>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);
+ 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){
@@ -4981,7 +5781,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",
@@ -5317,9 +6117,16 @@ int main(int argc, char **argv){
0
#endif
;
+ if(pipe_workers_imply_pipe(getenv("PIPE"),getenv("PIPE_WORKERS"),g_pipe)){
+ g_pipe=1;
+ fprintf(stderr,"[PIPE] PIPE_WORKERS is set — enabling the async pipe (PIPE=1 implied; set PIPE=0 to override)\n");
+ }
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_pipe_block = getenv("COLI_PIPE_BLOCK")?atoi(getenv("COLI_PIPE_BLOCK")):0; /* blocking pipe_wait (default: spin) */
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__
@@ -5382,6 +6189,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;
@@ -5421,6 +6229,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
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 3900ba6..ae1032c 100644
--- a/c/openai_server.py
+++ b/c/openai_server.py
@@ -370,10 +370,46 @@ 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")
# `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):
@@ -403,10 +439,38 @@ 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")
+ 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"
@@ -433,7 +497,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):
@@ -597,12 +661,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):
@@ -622,12 +689,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:
@@ -863,7 +931,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.
@@ -874,9 +942,9 @@ 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)
- tools = (body.get("tools") or body.get("functions") or None) if chat else None
- if body.get("tool_choice") == "none":
+ 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
cache_slot = body.get("cache_slot")
if (cache_slot is not None and
@@ -903,7 +971,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:
@@ -1010,7 +1078,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)
@@ -1027,7 +1095,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)
@@ -1078,9 +1146,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")
diff --git a/c/quant.h b/c/quant.h
index 928de96..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"
@@ -314,12 +376,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 +467,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));
@@ -655,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/resource_plan.py b/c/resource_plan.py
index 5f17a6a..1374a3b 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,69 @@ 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:
+ # 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 = {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) < 2:
+ continue
+ try:
+ core, socket = int(fields[-2]), int(fields[-1])
+ except ValueError:
+ continue # "-" for an offline core/socket
+ 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():
@@ -219,6 +289,54 @@ def cpu_socket_count():
return 1
+def _auto_tune(bottleneck_class, projected_hit, gpus, cpu_sockets, plan_has_metal):
+ """Derive tuning knobs from the bottleneck classification."""
+ tune = {}
+ has_gpu = bool(gpus)
+ n_gpu = len(gpus)
+
+ # MTP: costs more than it saves when compute-bound (#389 measured 42% loss)
+ if bottleneck_class == "compute":
+ tune["DRAFT"] = {"value": "0",
+ "reason": "compute-bound: MTP batch overhead exceeds yield"}
+ elif bottleneck_class == "disk" and projected_hit < 0.90:
+ tune["DRAFT"] = {"value": "0",
+ "reason": "low hit rate: MTP widens expert union, adds disk reads"}
+ # otherwise leave DRAFT unset (engine default: auto)
+
+ # PIPE: resident pipeline mode depends on GPU count
+ if has_gpu and n_gpu == 1:
+ tune["COLI_CUDA_PIPE"] = {"value": "1",
+ "reason": "single GPU: S=1 pipeline gate"}
+ elif has_gpu and n_gpu > 1:
+ tune["COLI_CUDA_PIPE"] = {"value": "2",
+ "reason": "multi-GPU: residual stays on-device across layers"}
+ elif not has_gpu and bottleneck_class == "disk":
+ tune["PIPE"] = {"value": "1",
+ "reason": "overlap disk reads with resident expert compute"}
+
+ # NUMA: selective interleave for GPU hosts, blanket hint for CPU-only
+ if cpu_sockets > 1 and has_gpu:
+ tune["COLI_NUMA"] = {"value": "1",
+ "reason": "multi-socket + GPU: interleave expert slabs, protect DMA buffers"}
+ elif cpu_sockets > 1 and not has_gpu:
+ tune["COLI_NUMA"] = {"value": "1",
+ "reason": "multi-socket CPU-only: interleave expert slabs across nodes"}
+ tune["_numa_hint"] = "numactl --interleave=all may perform better on CPU-only hosts"
+
+ # OMP: kill hot-thread spin when GPU/Metal owns the power budget
+ if plan_has_metal:
+ tune["COLI_NO_OMP_TUNE"] = {"value": "1",
+ "reason": "Metal: OMP spin-wait steals GPU power budget"}
+
+ # PIN: fully resident if RAM allows and no GPU tier competes
+ if projected_hit >= 0.99 and not has_gpu:
+ tune["PIN_GB"] = {"value": "all",
+ "reason": "enough RAM for full expert residency"}
+
+ return tune
+
+
POLICIES = {
"quality": {"preserve_quantization": True, "preserve_router": True},
"balanced": {"preserve_quantization": True, "preserve_router": True},
@@ -290,19 +408,35 @@ def build_plan(model, ram_gb=0, context=4096, gpu_indices=None, vram_gb=0,
if cold_bytes:
warnings.append("cold expert misses may reach disk; normal decode speed depends on hit rate")
+ total_expert = info["expert_bytes"]
+ resident_expert = hot_bytes + warm_bytes
+ projected_hit = resident_expert / total_expert if total_expert else 1.0
+
if cold_bytes:
bottleneck = "disk expert misses"
- elif warm_bytes:
- bottleneck = "CPU expert compute and RAM bandwidth"
+ bottleneck_class = "disk"
+ elif warm_bytes and gpus:
+ bottleneck = "CPU expert tail and GPU compute"
+ bottleneck_class = "mixed"
+ elif projected_hit >= 0.99:
+ if gpus:
+ bottleneck = "GPU compute and interconnect"
+ else:
+ bottleneck = "CPU expert compute (fully resident)"
+ bottleneck_class = "compute"
else:
- bottleneck = "GPU compute and interconnect"
+ bottleneck = "CPU expert compute and RAM bandwidth"
+ bottleneck_class = "memory"
+
+ tune = _auto_tune(bottleneck_class, projected_hit, gpus, cpu_sockets,
+ plan_has_metal=False)
return {
"version": 2,
"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": {
@@ -317,6 +451,9 @@ def build_plan(model, ram_gb=0, context=4096, gpu_indices=None, vram_gb=0,
"expert_capacity": vram_experts, "requires_host_backing": False},
},
"expected_bottleneck": bottleneck,
+ "bottleneck_class": bottleneck_class,
+ "projected_hit_rate": round(projected_hit, 4),
+ "tune": tune,
"decisions": [
{"target": "VRAM", "reason": "profile-ranked hot experts"},
{"target": "RAM", "reason": "warm experts execute on CPU without quality loss"},
@@ -331,15 +468,23 @@ 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")
- if sys.platform.startswith("linux") and plan["cpu"].get("sockets", 1) > 1:
- # Selectively interleave large expert/dense slabs across memory controllers.
- # Unlike blanket numactl interleave, this leaves CUDA staging buffers local.
- result.setdefault("COLI_NUMA", "1")
+ # 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("_"):
+ continue
+ result.setdefault(key, entry["value"])
if plan["policy"]["name"] == "balanced":
result.setdefault("REPIN", "64")
ram = plan["tiers"]["ram"]
@@ -386,5 +531,18 @@ def format_plan(plan):
else:
lines.append("VRAM no NVIDIA device detected · CPU path")
lines.append(f"limit {plan['expected_bottleneck']}")
+ hit = plan.get("projected_hit_rate", 0)
+ lines.append(f"hit {hit:.0%} projected expert residency")
+ tune = plan.get("tune", {})
+ if tune:
+ lines.append("")
+ lines.append("auto-tune:")
+ for key, entry in tune.items():
+ if key.startswith("_"):
+ continue
+ lines.append(f" {key}={entry['value']:12s} {entry['reason']}")
+ hint = tune.get("_numa_hint")
+ if hint:
+ lines.append(f" hint: {hint}")
lines.extend(f"warn {warning}" for warning in plan["warnings"])
return "\n".join(lines)
diff --git a/c/sample.h b/c/sample.h
index 26db529..414c621 100644
--- a/c/sample.h
+++ b/c/sample.h
@@ -116,6 +116,16 @@ static void stops_arm_tok(const Cfg *c, int tok_eos, Tok *T){
int nsp = 0;
if (T) for (int id = 0; id < T->n_ids && g_nstop < 64; id++)
if (T->id_special[id] && !is_stop(id)) { g_stop[g_nstop++] = id; nsp++; }
+ /* #401: in serve mode keep ONLY <|endoftext|>. Role markers <|user|>/<|observation|>
+ * (config stops + tokenizer special set) are boundaries the Python server owns; as
+ * hard stops they cut generation the moment the model opens a block,
+ * because int4 argmax noise picks a stop-token ID over the correct '<' token. */
+ if (getenv("SERVE") && tok_eos >= 0) {
+ int kept = 0;
+ for (int i = 0; i < g_nstop; i++) if (g_stop[i] == tok_eos) g_stop[kept++] = g_stop[i];
+ if (kept < g_nstop) fprintf(stderr, "[stop] serve mode: filtered %d non-EOS stop tokens (tool-call safety, #401)\n", g_nstop - kept);
+ g_nstop = kept; nsp = 0;
+ }
fprintf(stderr, "[stop] %d stop tokens:", g_nstop);
for (int i = 0; i < g_nstop; i++) fprintf(stderr, " %d", g_stop[i]);
if (nsp) fprintf(stderr, " (%d from the tokenizer's special set)", nsp);
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/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 1 && coli_cuda_tensor_upload(&t8, q8, s8, 1, 4, 2, d1)) return 1;
if (!coli_cuda_matmul(&t8, got, x, q8, s8, 1, 2, 4, 2, d0) || !close_enough(got, want8, 4)) return 1;
+ /* Cached tensor must stay callable without live host pointers
+ * (CUDA_RELEASE_HOST slots null theirs after upload) — including
+ * SUSTAINED reuse, not just the first call. */
+ for (int rep = 0; rep < 64; rep++)
+ if (!coli_cuda_matmul(&t8, got, x, nullptr, nullptr, 1, 2, 4, 2, d0) ||
+ !close_enough(got, want8, 4)) return 1;
+ /* A tensor uploaded from a TEMPORARY host buffer must survive the buffer
+ * being scribbled and freed (the release-host lifecycle). */
+ {
+ int8_t *tmpw = static_cast(std::malloc(8));
+ float *tmps = static_cast(std::malloc(2 * sizeof(float)));
+ if (!tmpw || !tmps) return 2;
+ for (int i = 0; i < 8; i++) tmpw[i] = q8[i];
+ tmps[0] = s8[0]; tmps[1] = s8[1];
+ ColiCudaTensor *tt = nullptr;
+ if (!coli_cuda_tensor_upload(&tt, tmpw, tmps, 1, 4, 2, d0)) return 1;
+ for (int i = 0; i < 8; i++) tmpw[i] = 99;
+ std::free(tmpw); std::free(tmps);
+ if (!coli_cuda_matmul(&tt, got, x, nullptr, nullptr, 1, 2, 4, 2, d0) ||
+ !close_enough(got, want8, 4)) return 1;
+ coli_cuda_tensor_free(tt);
+ }
+ /* Upload failures must be graceful and must not corrupt accounting —
+ * and must not poison LATER healthy launches (sticky-error regression). */
+ {
+ size_t c0 = 0, b0 = 0, c1 = 0, b1 = 0;
+ coli_cuda_stats(-1, &c0, &b0);
+ ColiCudaTensor *bad = nullptr;
+ if (coli_cuda_tensor_upload(&bad, q8, s8, 1, 4, 2, 9999)) return 1;
+ if (coli_cuda_tensor_upload(&bad, q8, s8, 7, 4, 2, d0)) return 1;
+ if (coli_cuda_tensor_upload(&bad, q8, nullptr, 1, 4, 2, d0)) return 1;
+ if (coli_cuda_tensor_upload(&bad, nullptr, s8, 1, 4, 2, d0)) return 1;
+ if (coli_cuda_tensor_upload(&bad, q8, s8, 1, 1 << 20, 1 << 24, d0)) return 1; /* ~16 TB */
+ if (bad) return 1;
+ coli_cuda_stats(-1, &c1, &b1);
+ if (c0 != c1 || b0 != b1) return 1;
+ /* healthy launch immediately after the failed allocation */
+ if (!coli_cuda_matmul(&t8, got, x, nullptr, nullptr, 1, 2, 4, 2, d0) ||
+ !close_enough(got, want8, 4)) return 1;
+ }
+ /* Fault injection hook: on/off, restores cleanly. */
+ if (setenv("COLI_GPU_FAIL_AFTER", "0", 1)) return 2;
+ if (coli_cuda_matmul(&t8, got, x, nullptr, nullptr, 1, 2, 4, 2, d0)) return 1;
+ if (unsetenv("COLI_GPU_FAIL_AFTER")) return 2;
+ if (!coli_cuda_matmul(&t8, got, x, nullptr, nullptr, 1, 2, 4, 2, d0) ||
+ !close_enough(got, want8, 4)) return 1;
const int8_t q8b[8]={-1,-2,-3,-4, 1,-2,3,-4};
const float s8b[2]={1.f,.5f},want8b[4]={10.f,15.f,-3.f,-2.5f};
if(!coli_cuda_tensor_update(t8,q8b,s8b)||
- !coli_cuda_matmul(&t8,got,x,q8b,s8b,1,2,4,2,d0)||
+ !coli_cuda_matmul(&t8,got,x,q8b,s8b,1,2,4,2,d0,0)||
!close_enough(got,want8b,4))return 1;
/* Rows [-8,-1,0,7] and [1,2,3,4], packed low nibble first. */
@@ -61,26 +107,26 @@ int main(int argc, char **argv) {
const float s4[2] = {1.0f, 0.25f};
const float want4[2] = {-34.0f, -2.5f};
ColiCudaTensor *t4 = nullptr;
- if (!coli_cuda_matmul(&t4, got, x, q4, s4, 2, 1, 4, 2, d1) || !close_enough(got, want4, 2)) return 1;
+ if (!coli_cuda_matmul(&t4, got, x, q4, s4, 2, 1, 4, 2, d1, 0) || !close_enough(got, want4, 2)) return 1;
const uint8_t q2[2] = {0xe4, 0x1b};
const float s2[2] = {0.5f, 2.0f};
const float want2[2] = {-2.0f, 12.0f};
ColiCudaTensor *t2 = nullptr;
- if (!coli_cuda_matmul(&t2, got, x, q2, s2, 3, 1, 4, 2, d1) || !close_enough(got, want2, 2)) return 1;
+ if (!coli_cuda_matmul(&t2, got, x, q2, s2, 3, 1, 4, 2, d1, 0) || !close_enough(got, want2, 2)) return 1;
const float wf[8] = {1, 0, -1, 2, 0.5f, 0.5f, 0.5f, 0.5f};
const float wantf[2] = {-10.0f, -1.0f};
ColiCudaTensor *tf = nullptr;
- if (!coli_cuda_matmul(&tf, got, x, wf, nullptr, 0, 1, 4, 2, d0) || !close_enough(got, wantf, 2)) return 1;
+ if (!coli_cuda_matmul(&tf, got, x, wf, nullptr, 0, 1, 4, 2, d0, 0) || !close_enough(got, wantf, 2)) return 1;
const float eg[8] = {1,0,0,0, 0,1,0,0};
const float eu[8] = {1,0,0,0, 0,1,0,0};
const float ed[8] = {1,0, 0,1, 1,1, 1,-1};
ColiCudaTensor *tg=nullptr,*tu=nullptr,*td=nullptr;
- if (!coli_cuda_tensor_upload(&tg,eg,nullptr,0,4,2,d0) ||
- !coli_cuda_tensor_upload(&tu,eu,nullptr,0,4,2,d0) ||
- !coli_cuda_tensor_upload(&td,ed,nullptr,0,2,4,d0)) return 1;
+ if (!coli_cuda_tensor_upload(&tg,eg,nullptr,0,4,2,d0,0) ||
+ !coli_cuda_tensor_upload(&tu,eu,nullptr,0,4,2,d0,0) ||
+ !coli_cuda_tensor_upload(&td,ed,nullptr,0,2,4,d0,0)) return 1;
float expert[8], want_expert[8];
for(int s=0;s<2;s++){
float a=x[s*4], b=x[s*4+1];
@@ -98,7 +144,7 @@ int main(int argc, char **argv) {
const float aw[16]={1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1};
const float aq[4]={1,2,.5f,-.5f},al[12]={1,0,0,0, 0,1,0,0, 0,0,1,0};
const float ar[6]={1,0, 0,1, 1,1};float actx[2],aref[2];
- ColiCudaTensor *at=nullptr;if(!coli_cuda_tensor_upload(&at,aw,nullptr,0,4,4,d0))return 1;
+ ColiCudaTensor *at=nullptr;if(!coli_cuda_tensor_upload(&at,aw,nullptr,0,4,4,d0,0))return 1;
float score[3];for(int t=0;t<3;t++)score[t]=aq[0]*al[t*4]+aq[1]*al[t*4+1]+aq[2]*ar[t*2]+aq[3]*ar[t*2+1];
float mx=score[0],z=0;for(int t=1;t<3;t++)mx=score[t]>mx?score[t]:mx;
for(int t=0;t<3;t++){score[t]=std::exp(score[t]-mx);z+=score[t];}for(int t=0;t<3;t++)score[t]/=z;
@@ -117,9 +163,9 @@ int main(int argc, char **argv) {
for(int i=0;i<32;i++)ws4[i]=0.01f+(i%5)*0.002f;
for(int i=0;i<64;i++)gx4[i]=std::sin((float)(i+1)*0.17f)*2.f;
ColiCudaTensor *g4=nullptr,*u4=nullptr,*d4=nullptr;
- if(!coli_cuda_tensor_upload(&g4,w4,ws4,2,32,32,d0)||
- !coli_cuda_tensor_upload(&u4,w4,ws4,2,32,32,d0)||
- !coli_cuda_tensor_upload(&d4,w4,ws4,2,32,32,d0))return 1;
+ if(!coli_cuda_tensor_upload(&g4,w4,ws4,2,32,32,d0,0)||
+ !coli_cuda_tensor_upload(&u4,w4,ws4,2,32,32,d0,0)||
+ !coli_cuda_tensor_upload(&d4,w4,ws4,2,32,32,d0,0))return 1;
ColiCudaTensor *gg4[2]={g4,g4},*ug4[2]={u4,u4},*dg4[2]={d4,d4};
if(!coli_cuda_expert_group(gg4,ug4,dg4,group_rows,2,scalar4,gx4))return 1;
setenv("COLI_CUDA_TC_INT4","1",1);
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;
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_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_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/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 bool:
+ """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:
+ """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(), _skip_reason() or "glm.exe + glm_tiny required")
+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(),
+ _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.
+
+ 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/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;iI;i++){
+ if(t->fmt==5){
+ int64_t g=i/I3_GROUP; const uint8_t *lo=t->q4+(int64_t)o*i3_rowbytes(t->I)+g*I3_GBYTES, *hi=lo+16;
+ int k=i%I3_GROUP;
+ unsigned u=((lo[k>>2]>>((k&3)*2))&3)|(((hi[k>>3]>>(k&7))&1)<<2);
+ dq[(int64_t)o*t->I+i]=(float)((int)u-4)*t->s[(int64_t)o*i3_groups(t->I)+g];
+ } else { /* fmt2 */
+ uint8_t b=t->q4[(int64_t)o*((t->I+1)/2)+(i>>1)];
+ int v=(i&1)?((int)(b>>4)-8):((int)(b&0xF)-8);
+ dq[(int64_t)o*t->I+i]=(float)v*t->s[o];
+ }
+ }
+}
+
+int main(void){
+ enum { O=5, I=320 }; /* 5 groups per row; I > 256 so the per-row int4
+ * control stays fmt=2 (detect_group_size probes
+ * gs up to 256: any smaller I would make O row
+ * scales match a legitimate 1-group layout) */
+ int64_t ng=i3_groups(I), rb=i3_rowbytes(I);
+ static float w[O*I];
+ for(int i=0;i> 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
+
+
+@unittest.skipIf(P is None, "numpy not available (offline-tooling test)")
+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/tests/test_openai_server.py b/c/tests/test_openai_server.py
index 4cda9ef..8000b40 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,31 @@ 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)
+ 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/c/tests/test_pipe_block.c b/c/tests/test_pipe_block.c
new file mode 100644
index 0000000..8571695
--- /dev/null
+++ b/c/tests/test_pipe_block.c
@@ -0,0 +1,169 @@
+/* COLI_PIPE_BLOCK: the pipe pool's condvar waiter must be observably
+ * equivalent to the sched_yield spin it replaces — same bytes land in the
+ * same ws[] slots, and no interleaving loses a wakeup (the worker RELEASE-
+ * stores ready[] BEFORE taking mx to broadcast; the waiter re-checks under
+ * the lock, so a flag set between its fast-path check and the wait cannot
+ * be missed). Both waiters are exercised against the same on-disk fixture,
+ * alternating parked waits (wait issued before the load finishes) with
+ * fast-path waits (load already done), across enough generations to cycle
+ * the pool's gen-tagged cursor.
+ *
+ * Also pins the PIPE_WORKERS => PIPE implication table: fires ONLY when
+ * PIPE is unset in the env AND the platform default left the pipe off AND
+ * PIPE_WORKERS parses positive (PIPE_WORKERS=0/empty/negative must NOT
+ * silently enable a clamped 1-worker pipe). */
+#include
+#include
+#include
+#include
+#include
+#include
+#define main coli_glm_main_unused
+#include "../colibri.c"
+#undef main
+
+static int fail(const char *s){ fprintf(stderr,"FAIL: %s\n",s); return 1; }
+
+enum { NE=8, LAYER=1 }; /* experts 0..NE-1 on one MoE layer */
+/* per-expert file image: [gate 12][up 12][down 12][gate.qs 12][up.qs 12][down.qs 16] */
+enum { WB=12, QS_G=12, QS_U=12, QS_D=16, ESZ=3*WB+QS_G+QS_U+QS_D };
+
+static unsigned char wbyte(int e,int j){ return (unsigned char)(e*31+j+1); }
+static float scale(int e,int i){ return (float)(e*8+i)+0.5f; }
+
+#define TMPF "test_pipe_block.tmp"
+
+static int write_fixture(void){
+ FILE *w=fopen(TMPF,"wb"); if(!w) return fail("create temp");
+ for(int e=0;ec.hidden=4; m->c.moe_inter=3; m->ebits=8;
+ m->S.n=NE*6; m->S.cap=NE*6; m->S.t=calloc(NE*6,sizeof(st_tensor));
+ if(!m->S.t) return fail("tensor metadata allocation");
+ const char *proj[3]={"gate_proj","up_proj","down_proj"};
+ int sbytes[3]={QS_G,QS_U,QS_D};
+ for(int e=0;eS.t[e*6+k]=(st_tensor){strdup(name),fd,wo,WB,3,WB}; wo+=WB;
+ size_t n=strlen(name); memcpy(name+n,".qs",4);
+ m->S.t[e*6+3+k]=(st_tensor){strdup(name),fd,so,sbytes[k],2,sbytes[k]/4}; so+=sbytes[k];
+ }
+ }
+ return 0;
+}
+
+static int check_slot(ESlot *s,int e){
+ if(s->eid!=e || s->g.fmt!=1 || s->u.fmt!=1 || s->d.fmt!=1){
+ fprintf(stderr," slot: eid=%d (want %d) fmt g/u/d=%d/%d/%d (want 1/1/1)\n",
+ s->eid,e,s->g.fmt,s->u.fmt,s->d.fmt);
+ return 1;
+ }
+ const unsigned char *g=(const unsigned char*)s->g.q8,
+ *u=(const unsigned char*)s->u.q8,
+ *d=(const unsigned char*)s->d.q8; /* q8 is int8_t; compare raw bytes */
+ for(int j=0;jg.s[i]!=scale(e,i) || s->u.s[i]!=scale(e,3+i)){
+ fprintf(stderr," slot e=%d scale %d: g=%g/%g u=%g/%g (got/want)\n",e,i,
+ (double)s->g.s[i],(double)scale(e,i),(double)s->u.s[i],(double)scale(e,3+i));
+ return 1;
+ }
+ for(int i=0;i<4;i++)
+ if(s->d.s[i]!=scale(e,6+i)){
+ fprintf(stderr," slot e=%d scale %d: d=%g/%g (got/want)\n",e,i,
+ (double)s->d.s[i],(double)scale(e,6+i));
+ return 1;
+ }
+ return 0;
+}
+
+static int run_generations(Model *m,int block,int gens){
+ g_pipe_block=block;
+ for(int gen=0;genws[q],eids[q])) return fail(block?"slot contents (block)":"slot contents (spin)");
+ }
+ }
+ return 0;
+}
+
+static int test_implication_table(void){
+ struct { const char *pipe_env,*pw_env; int pipe_now,want; } T[]={
+ {NULL,"4",0,1}, /* pool sized, pipe off, PIPE unset → imply */
+ {NULL,"16",0,1},
+ {NULL,"0",0,0}, /* PIPE_WORKERS=0 must NOT enable a clamped pipe */
+ {NULL,"",0,0},
+ {NULL,"-3",0,0},
+ {"0","4",0,0}, /* explicit PIPE=0 always wins */
+ {"1","4",1,0}, /* explicit PIPE=1: nothing to imply */
+ {NULL,"4",1,0}, /* platform default already ON (win32) */
+ {NULL,NULL,0,0},
+ };
+ for(size_t i=0;i",T[i].pw_env?T[i].pw_env:"",T[i].pipe_now);
+ return 1;
+ }
+ return 0;
+}
+
+int main(void){
+ if(test_implication_table()) return 1;
+
+ /* Relative to the CWD, like test_compat_direct's TMPF — NOT "/tmp/...":
+ * the windows job builds native .exe files and "/tmp" is not a Windows
+ * path. fwrite then reopen read-only: Windows compat has pread, not pwrite. */
+ if(write_fixture()) return 1;
+ int fd=open(TMPF,COMPAT_O_RDONLY);
+ if(fd<0) return fail("open temp");
+
+ static Model m; /* zeroed: buffered pread path, no mmap/cuda */
+ if(build_fixture(&m,fd)){ close(fd); remove(TMPF); return 1; }
+
+ g_pipe=1; g_pipe_nw=4;
+ pipe_init(&m);
+
+ /* spin waiter first (control), then the condvar waiter under the same
+ * dispatch pattern; 200 generations each cycles the gen-tagged cursor
+ * and alternates parked/fast-path waits. */
+ if(run_generations(&m,0,200) || run_generations(&m,1,200)){ close(fd); remove(TMPF); return 1; }
+
+ for(int q=0;q 24 threads, 12 physical cores.
+
+ # 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 +
+ # 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
+ # (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,
@@ -117,13 +192,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",
@@ -140,6 +215,81 @@ class ResourcePlanTest(unittest.TestCase):
plan = build_plan(self.model, available_memory=16 * GB, available_disk=1,
gpus=[], physical_cpus=8, cpu_sockets=1)
self.assertNotIn("COLI_NUMA", environment_for_plan(plan))
+
+ def test_auto_tune_mtp_off_when_compute_bound(self):
+ # Tiny model with 64 GB RAM and no GPU: all experts fit in RAM with no
+ # warm tier, so the plan classifies as compute-bound.
+ plan = build_plan(self.model, ram_gb=64, available_memory=64 * GB,
+ available_disk=100 * GB, gpus=[], physical_cpus=24,
+ cpu_sockets=2)
+ # With such a small model fully in RAM and no GPU, bottleneck is compute
+ self.assertEqual(plan["bottleneck_class"], "compute")
+ self.assertIn("DRAFT", plan["tune"])
+ self.assertEqual(plan["tune"]["DRAFT"]["value"], "0")
+ env = environment_for_plan(plan)
+ self.assertEqual(env["DRAFT"], "0")
+ explicit = environment_for_plan(plan, {"DRAFT": "3"})
+ self.assertEqual(explicit["DRAFT"], "3")
+
+ def test_auto_tune_mtp_off_when_disk_low_hit(self):
+ # Use a model large enough that 8 GB RAM can't hold all experts.
+ big = tempfile.TemporaryDirectory()
+ bigmodel = Path(big.name)
+ (bigmodel / "config.json").write_text(json.dumps({
+ "num_hidden_layers": 2, "n_routed_experts": 4,
+ "kv_lora_rank": 4, "qk_rope_head_dim": 2,
+ "qk_nope_head_dim": 3, "v_head_dim": 5, "num_attention_heads": 2,
+ }))
+ expert_size = 3 * GB # each expert 3 GB → 12 GB total, won't fit in 8 GB budget
+ write_shard(bigmodel / "out-00000.safetensors", [
+ ("model.embed_tokens.weight", 100),
+ ("model.layers.0.self_attn.q_a_proj.weight", 200),
+ ])
+ for i in range(4):
+ write_shard(bigmodel / f"out-{i+1:05d}.safetensors", [
+ (f"model.layers.1.mlp.experts.{i}.gate_proj.weight", expert_size),
+ ])
+ plan = build_plan(bigmodel, ram_gb=0, available_memory=4 * GB,
+ available_disk=100 * GB, gpus=[], physical_cpus=8,
+ cpu_sockets=1)
+ big.cleanup()
+ self.assertEqual(plan["bottleneck_class"], "disk")
+ self.assertLess(plan["projected_hit_rate"], 0.90)
+ self.assertEqual(plan["tune"]["DRAFT"]["value"], "0")
+
+ def test_auto_tune_pipe_multi_gpu(self):
+ gpus = [
+ {"index": 0, "name": "a", "total_bytes": 32 * GB, "free_bytes": 30 * GB},
+ {"index": 1, "name": "b", "total_bytes": 32 * GB, "free_bytes": 30 * GB},
+ ]
+ plan = build_plan(self.model, ram_gb=16, available_memory=32 * GB,
+ available_disk=1, gpus=gpus, cpu_sockets=2)
+ self.assertEqual(plan["tune"]["COLI_CUDA_PIPE"]["value"], "2")
+ env = environment_for_plan(plan)
+ self.assertEqual(env["COLI_CUDA_PIPE"], "2")
+
+ def test_auto_tune_pipe_single_gpu(self):
+ gpus = [{"index": 0, "name": "a", "total_bytes": 12 * GB, "free_bytes": 10 * GB}]
+ plan = build_plan(self.model, ram_gb=16, available_memory=32 * GB,
+ available_disk=1, gpus=gpus, cpu_sockets=1)
+ self.assertEqual(plan["tune"]["COLI_CUDA_PIPE"]["value"], "1")
+
+ def test_auto_tune_numa_hint_for_cpu_only(self):
+ plan = build_plan(self.model, ram_gb=64, available_memory=64 * GB,
+ available_disk=1, gpus=[], physical_cpus=64, cpu_sockets=2)
+ self.assertIn("_numa_hint", plan["tune"])
+ self.assertIn("numactl", plan["tune"]["_numa_hint"])
+ self.assertIn("auto-tune", format_plan(plan))
+
+ def test_format_plan_shows_tune_and_hit_rate(self):
+ plan = build_plan(self.model, ram_gb=64, available_memory=64 * GB,
+ available_disk=100 * GB, gpus=[], physical_cpus=24,
+ cpu_sockets=1)
+ text = format_plan(plan)
+ self.assertIn("hit", text)
+ self.assertIn("auto-tune", text)
+ self.assertIn("DRAFT", text)
+
def test_cpu_binary_does_not_apply_gpu_tier(self):
plan = build_plan(self.model, available_memory=16 * GB, available_disk=1,
gpus=[{"index": 0, "name": "a", "total_bytes": 8 * GB,
@@ -178,5 +328,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()
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;e