3716e4006a
* docs: Metal expert-matmul backend design (Apple Silicon) Empirically-validated design for a batched MoE expert-matmul Metal backend. Microbenchmarks (scratchpad) establish: runtime-compiled Metal needs no Xcode; V3 (float4 + threadgroup reduction) kernel is correct and fast; synchronous per-matmul dispatch loses to CPU due to ~150us Metal launch latency, so the win is batched full-layer dispatch (854us/layer, 707 GFLOP/s) reading expert slabs zero-copy from unified memory. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * metal: backend infrastructure + kernel-correctness test (M1) Add backend_metal.{h,mm} — an opt-in Apple-GPU backend built with METAL=1 on macOS. Runtime-compiled shader (no Xcode needed), zero-copy over unified memory. Implements coli_metal_matmul (general quantized GEMV, f32/int8/int4/int2) via a threadgroup-reduction + float4 kernel; batched moe_block is stubbed (returns 0 -> CPU fallback) for M2. tests/test_backend_metal.mm validates all formats and edge shapes (odd S, non-mult-4 dims) against a CPU reference (nerr ~2e-6). Makefile gains a METAL=1 Darwin branch and a metal-test target. Default build unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * metal: batched moe_block + zero-copy slab registry (M2 backend) Implement coli_metal_moe_block: gate/up/silu/down for a whole expert block in ONE command buffer, with GPU memory barriers between stages and BINDLESS gpuAddress pointers so each expert is read zero-copy from its own RAM slab (exceeds Metal's ~31 buffer-binding limit). coli_metal_register/unregister wrap page-aligned slabs via newBufferWithBytesNoCopy and resolve interior pointers to GPU addresses. Per-row ragged expert routing supported; CPU does the final weighted scatter-add. test_backend_metal validates decode + ragged blocks vs a CPU reference (nerr ~2e-6). Still gated off in glm.c until the moe() wiring lands. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * metal: wire batched moe_block into glm.c, token-exact (M2 integration) moe() now dispatches each routed-expert block through the GPU in one command buffer when COLI_METAL=1, reading expert weights zero-copy from page-aligned RAM slabs (registered in expert_load). Falls back to CPU per-block on any unresolved slab or GPU fault. Default build byte-identical (all #ifdef COLI_METAL). Fixes a heap-corruption crash: expert_load registers slabs from parallel OpenMP threads, so the slab registry is now mutex-guarded (buffer creation stays outside the lock). Added command-buffer error checking (fall back to CPU on GPU fault) and a COLI_METAL_DEBUG one-shot trace. Validated token-exact vs the CPU path (greedy): identical 12-token output; expert-matmul time 29.9s -> 21.1s with pinned experts still on CPU. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * metal: instrument moe_block (GPU/CPU split, wall-vs-kernel time) Add diagnostics printed on the PROFILO line under COLI_METAL: GPU vs CPU-fallback block counts, experts-on-GPU, and a per-block time split (setup / gpu-wall / kernel / scatter). Reveals that with a warm cache all experts run on the GPU (0 fallback) and expert-matmul drops ~1.3x vs CPU, but ~62% of GPU wall-time is idle/scheduling latency (3.1s kernel of 8.3s wall over 396 sporadic submits) — the GPU powers down between blocks because attention runs on the CPU per layer. Points the next optimization at keeping the GPU hot (offload attention). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: Metal backend measured results + next levers Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: Phase 2 fused decode attention plan + absorption-core validated Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * metal: fused decode attention on GPU, token-exact (Phase 2) coli_metal_attn_decode runs a full S=1 decode attention layer in ONE command buffer: q_a -> rmsnorm -> q_b -> RoPE ; kv_a -> latent rmsnorm@pos + krot RoPE@pos (cache write) ; MLA absorption core (qabs/score/softmax/clat/ctx) ; o_proj. The absorption-core kernels were validated in isolation (nerr ~1e-6) before wiring. Projection matmuls reuse the mm_gemv kernel; attention weights are uploaded+cached (serial path, no lock); Lc/Rc caches are page-aligned + registered in kv_alloc for zero-copy GPU read/write. GLM-5.2 dims compiled in; falls back to CPU for S>1 (prefill/MTP verify), st0!=0, active DSA selection (context>topk), or mismatched dims. DSA index-key write stays on CPU so future selection still works. Validated token-exact vs CPU (identical greedy output); attention time 16.5s -> 10.5s (~1.57x), end-to-end 0.20 -> 0.28 tok/s. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: Phase 2 fused attention complete + known limits Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * metal: attention coverage/latency instrumentation + honest results Add per-layer fused-attention counters (METAL-ATTN line): GPU layer count, gpu-wall and true kernel time. Measurement (DRAFT=0, all-S=1 decode) shows the fused attention triggers on all decode layers but is submit-latency-bound: gpu-wall 3.70s vs kernel 0.63s (83% idle latency over 546 sporadic command buffers). Attention time is neutral vs CPU; the earlier MTP-on "16.5->10.5" was run-to-run variance. Design doc corrected with the honest result: both offloads are gated by Metal's ~5ms cold-GPU submit latency; reducing submit count (fuse attention+experts per layer) is the real lever. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * metal: fused attention handles S<=4 (covers MTP verify forwards) Extend coli_metal_attn_decode from S=1 to S<=4: the core kernels (qabs/score/ smax/clat/ctx) gain a query-row dimension with per-row causal masking (query s attends keys [0, pos_base+s]); rmsnorm/rope/copy became row-aware; projections run S rows via mm_gemv. This covers the default MTP config (draft=3 -> S=4 verify forwards), which previously fell back to CPU attention entirely. Token-exact vs CPU (identical greedy output, MTP on). Perf is inconclusive at short context: still submit-latency-bound (attn gpu-wall 5.5s vs kernel 0.9s) and the measurement is dominated by disk-streaming variance (+/-15s between runs). Next: measure with a fully-warm cache to isolate compute, then reduce submit count. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: clean warm A/B shows real ~1.4x (experts+S<=4 attention), token-exact Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * metal: interleave attention q/kv paths, 7->4 barriers (iter 2) The q-path (q_a->rmsnorm->q_b->rope) and kv-path (kv_a->copy->rmsnorm+rope) are independent until the absorption core, but were serialized by memory barriers. Interleave them into 4 barrier-separated stages so the GPU overlaps independent dispatches. Token-exact; attention gpu-wall 3.04s -> 2.73s (~10%). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * metal: zero-copy attention weights + fuse shared expert into GPU block (iter 2) Dense QT weights/scales now allocate page-aligned + registered (qalloc) under METAL, so the fused attention reads q_a/q_b/kv_a/kv_b/o zero-copy instead of uploading ~6 GB of duplicates (RSS -3 GB, upload copies gone). bind_gemv resolves registered pointers (buffer,offset) with a pre-check guard. Phase E's shared expert (identical shapes to a routed expert: gate/up [I,D], down [D,I], same int4 container) is appended to the first Metal moe_block as an extra expert with rw=1.0 over all S rows — removes 3 CPU matmuls per layer and fills the same GPU submit. CPU Phase E still runs on any fallback. Zero-copy validated token-exact: 35.1s -> 29.7s (0.34 tok/s) warm. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: iteration 2 findings Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: iter 2 final ~1.56x + iter 3 plan (disk/GPU overlap) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * metal: overlap disk loads with GPU compute inside the layer (iter 3) Split each MoE block into two GPU submits: the RESIDENT experts (pin/LRU hits, plus the fused shared expert) are encoded and committed BEFORE the missed experts' OMP pread loop, so the GPU computes while the disk reads; the missed subset follows in a second (sync) submit once loaded. New two-phase backend API (coli_metal_moe_block_begin/end) with handle-owned scratch so the async submit cannot collide with the sync path's static buffers; moe_submit/moe_finish are shared by both. Per-subset CPU fallback preserved (resident and missed fall back independently on unresolved slab or GPU fault). Token-exact. Warm 96GB: expert-matmul 8.96 -> 4.92s (resident compute now hidden inside the disk window; expert idle latency ~5.7s -> ~0.9s), total 28.97s (0.35 tok/s) vs CPU 50.2s = ~1.73x. Note: 'make glm METAL=1' after a default build does NOT rebuild (target looks up-to-date) — touch glm.c or clean when switching build flavors. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: iter 3 disk/GPU overlap results (~1.73x) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * metal: keep-alive spinner experiment (env-gated) + latency decomposition COLI_METAL_SPIN=1 keeps trivial GPU work in flight on a separate queue to probe whether inter-submit idle is clock ramp-down; thread is detached (a joinable global thread std::terminate'd the process at exit). First contended A/B was inconclusive but showed the spinner does NOT collapse attention wall per-call (~16ms both ways), so ramp-down is not the whole story. METAL-ATTN now decomposes latency: cpu-sched (commit->kernelStart) vs gpu-sched (kernelStart->GPUStart) vs kernel execution, to pinpoint where the ~13ms/call goes. Default behavior unchanged (spinner off). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * metal: standalone regression tests for fused decode attention run_attn builds full-size fake GLM-5.2 attention weights (int4, page-aligned, registered), replicates glm.c's absorb-branch math exactly on the CPU (q_a -> rmsnorm -> q_b -> rope; kv_a -> latent rmsnorm + krot rope -> cache; per-head qabs/score/softmax/clat/ctx; o_proj), and checks coli_metal_attn_decode against it at S=1/3/4 and pos_base 0/12/37 — including the Lc/Rc cache write-back, which end-to-end runs cannot isolate. All pass (nerr ~5e-6, cache ~1.4e-5). The whole Metal path (gemv, moe_block, fused attention) is now testable without the model. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * metal: route large row-batch matmul_qt GEMMs to the GPU (prefill) matmul_qt now dispatches to a new coli_metal_gemm when S >= COLI_METAL_GEMM_MIN (default 16), the weight is int8/int4 and registered (all dense QT allocs are, via qalloc), and we're not inside an OpenMP region (mirrors the CUDA guard). Decode-sized matmuls stay on the CPU where NEON wins vs submit latency; prefill's big GEMMs (kv_b reconstruction at S=Tk, o_proj, dense MLP, step_all's S x vocab logits) amortize it — microbench showed ~6x over the CPU idot at S=16. Standalone test: registered int4 GEMM S=64 vs cpu_ref (nerr 2.9e-6). Machine busy again; end-to-end token-exactness + threshold sweep pending idle. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * README: document the experimental Metal backend (Apple Silicon) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * metal: 1.5-2.1x faster moe_gemv (simdgroup-per-row + 8-value loads) Replace one-threadgroup-per-output-row (128 threads reducing via threadgroup memory) with one SIMDGROUP per output row, 4 rows per threadgroup, and uchar4 loads (8 nibbles / 8 int8 per lane-iteration). Removes the threadgroup barrier + shared-memory reduction entirely (simd_sum only) and doubles load width. Engine-like block-shape microbench (pure GPU time): S=4 block 2548->1739us, S=1 block 934->437us, big block 4582->3414us — 358-389 GB/s vs 182-264. Row-bound guard added (NT) since the grid rounds up to 4 rows/TG. All backend tests pass (moe_block nerr 2.4e-6, attention unchanged). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * metal: mm_gemv simdgroup-per-row + 8-value loads (attention projections, prefill GEMM) Apply the moe_gemv V2 transformation to the general quantized GEMV: one simdgroup per output element (4/threadgroup), 8-value loads for i8/i4/f32, no threadgroup reduction. Same measured 1.5-2.1x class of win; serves the fused-attention projections (q_a/q_b/kv_a/o), coli_metal_gemm (prefill), and coli_metal_matmul. All three dispatch sites updated (NT row-bound guard, grid ceil(NT/4) x 128). Full test suite green, incl. non-mult-of-8 tail paths (2050x6146) and all fmts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * metal: experimental COLI_MMAP=1 — experts as zero-copy views into mmap'd files Lazily mmap each safetensors file (PROT_READ, MAP_SHARED, mutex-guarded — expert loads are OMP-parallel), register the mapping with Metal, and make expert_load a pointer assignment into the map: no pread, no slab, no copy; the OS page cache is the cache. Alignment guards fall back to the slab path. Default OFF. First validation (machine at load 66 + 46GB swap): token-exact, RSS 58 -> 10.5 GB as designed, but GPU wall exploded (~130 MB/s effective) — the GPU demand-faults file-backed pages, catastrophic when memory pressure evicts them. Needs an idle-machine A/B to judge fairly (llama.cpp's identical technique relies on pages staying resident); possible fixes if slow even idle: CPU pre-touch of missed experts' pages before the GPU submit, or madvise/mlock windows. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * metal: CPU pre-touch for COLI_MMAP expert pages (fix GPU demand-faulting) In mmap mode, fault the missed expert's pages in on the CPU inside expert_load (madvise WILLNEED for async readahead + a page-stride touch): this is pread's I/O without the copy and without the slab, it runs inside the existing OMP loop that overlaps with the resident-experts GPU submit (iter 3), and it guarantees the GPU only ever reads resident pages — GPU demand-faulting of file-backed pages measured catastrophic (~130 MB/s). Read-only addition: outputs unchanged from the validated mmap run; perf pending the idle-machine A/B. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: idle-machine suite results (~1.33x same-session; mmap negative result) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * metal: COLI_METAL_UNTRACKED experiment (negative result, default off) Env-gated MTLResourceHazardTrackingModeUntracked on registered wraps + scratch to test whether cross-CB hazard tracking causes the ~10ms/CB gpu-sched delay. Idle A/B: no effect (gpu-sched 3.9 vs 3.4s, noise), token-exact. Together with the spinner negative, this pins the attention CB delay as inherent scheduler/wake overhead on an empty pipeline — removable only by eliminating the CB boundary, which CPU-side routing at ~58% hit-rate forces. Metal side is at its floor: kernel 3.5s+0.8s (near BW ceiling), sched ~3.2s, disk ~15s dominant (10 tok). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: loop conclusion — best config DIRECT=1+COLI_METAL=1, 0.42 tok/s (~1.4x) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * metal: refactor attention into encode_attention()+resolve_attn() (layer-CB prep) Behavior-preserving: attn_decode is now a thin wrapper; all attention tests byte-identical. Prepares embedding the chain in a full-layer command buffer. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * metal: full decode layer in ONE command buffer (token-exact) coli_metal_layer_decode runs the whole layer prelude on the GPU in a single submit: in_ln rmsnorm -> fused attention -> residual add -> post_ln rmsnorm -> shared expert (gate/up/silu/down) -> router (f32 simdgroup matvec + sigmoid) -> exact phase-A top-K selection (greedy argmax over sigmoid+bias with CPU tie order, --topp truncation, norm_topk, routed_scale) in a serial-per-row kernel. The CPU's per-layer work shrinks to: read 8 expert IDs, resolve/load, expert CBs (disk/GPU overlap unchanged), scatter. moe() consumes the precomputed routing (g_pre_*: skips phase A, keeps eusage/eheat/ereq counters for the learning cache) and adds the GPU shared-expert output instead of computing phase E. ld() tensors (norms/router/bias) now allocate registered so the GPU reads them zero-copy. DSA index keys still computed on CPU from the in_ln-normed x (new inrm output). Every missing condition falls back to the full CPU layer. Validated token-exact vs CPU (identical greedy output, MTP on). Profile: "altro" 3.8s -> 0.53s (12 tok); 0.42 tok/s despite disk-variance headwind. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: Phase 3 full-layer CB results — 0.43 tok/s record, token-exact Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * gitignore: Metal build artifacts, venv, bench datasets Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * remove internal design docs before PR Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
481 lines
36 KiB
Markdown
481 lines
36 KiB
Markdown
<p align="center">
|
||
<img src="assets/colibri.svg" width="500" alt="colibrì — tiny engine, immense model">
|
||
</p>
|
||
|
||
**Tiny engine, immense model.** Run **GLM-5.2 (744B-parameter MoE)** on a consumer machine with ~25 GB of RAM — in pure C, with zero dependencies, by streaming experts from disk.
|
||
|
||
```
|
||
$ ./coli chat
|
||
🐦 colibrì v1.0 — GLM-5.2 · 744B MoE · int4 · streaming CPU
|
||
✓ ready in 32s · resident 9.9 GB
|
||
› ciao!
|
||
◆ Ciao! 😊 Come posso aiutarti oggi?
|
||
```
|
||
|
||
## The idea
|
||
|
||
A 744B Mixture-of-Experts model activates only ~40B parameters per token — and only ~11 GB of those change from token to token (the routed experts). So:
|
||
|
||
- the **dense part** (attention, shared experts, embeddings — ~17B params) stays **resident in RAM at int4** (~9.9 GB);
|
||
- the **21,504 routed experts** (75 MoE layers × 256 experts + the MTP head, ~19 MB each at int4) live **on disk** (~370 GB) and are **streamed on demand**, with a per-layer LRU cache, an optional pinned hot-store, and the OS page cache as a free L2.
|
||
|
||
The engine is a single C file (`c/glm.c`, ~2,400 lines) plus small headers. No BLAS, no Python at runtime, no GPU required (an opt-in CUDA tier for pinned experts exists — see below).
|
||
|
||
## What's implemented
|
||
|
||
- **Faithful GLM-5.2 (`glm_moe_dsa`) forward** — validated token-exact against a `transformers` oracle (teacher-forcing 32/32, greedy 20/20 on a tiny-random model with the real architecture).
|
||
- **MLA attention** (q/kv-LoRA, interleaved partial RoPE) with **compressed KV-cache**: 576 floats/token instead of 32,768 (57× smaller — GLM-5.2 has 64 heads and no GQA).
|
||
- **DeepSeek-V3-style sigmoid router** (noaux_tc, routed_scaling_factor), shared expert, first-3-dense layers.
|
||
- **Native MTP speculative decoding** — GLM-5.2's own multi-token-prediction head (layer 78) drafts tokens that the main model verifies in one batched forward. **The head must be int8** (the converter does this by default): at int4 draft acceptance collapses to 0–4% and speculation never engages; at int8 it's 39–59% acceptance, **2.2–2.8 tokens/forward** (community-measured, [#8](https://github.com/JustVugg/colibri/issues/8)). Lossless *in exact arithmetic* — but **not byte-identical to non-speculative greedy in practice** ([#100](https://github.com/JustVugg/colibri/issues/100)). This isn't MTP-specific: colibrì's quantized integer kernels are shape-dependent, so any batched (S>1) or GPU forward rounds slightly differently from the single-token path, and int4 GLM-5.2 sits close enough to argmax ties that such a rounding change can flip a token. MTP, the CUDA expert tier, and batched prefill are three different ways to trip the same sensitivity (community-confirmed in #100: swapping only the kernel family forks greedy output on 3/5 prompts, with **zero speculation**). Every emitted token is still the argmax of a *valid* forward — the continuation stays correct — it just isn't the same stream. For byte-exact reproducibility: `DRAFT=0` (no speculation), plus `IDOT=0 COLI_CUDA=0` if you also want kernel-family/GPU independence. Under sampling, rejection sampling keeps the distribution correct. Honest caveat from the same measurement: on a **cold** cache each verified draft routes to extra experts (~660 → ~1100 expert-loads/token), so speculation can be a net *time* loss until the cache/pin warms up.
|
||
- **Grammar-forced speculative drafts** (`GRAMMAR=file.gbnf`, [#48](https://github.com/JustVugg/colibri/issues/48)) — on constrained-output workloads (JSON/NDJSON, function calling, structured extraction) the grammar itself is a third draft source: wherever it admits exactly **one** legal byte (braces, quotes, key names, enum bodies), that forced span is tokenized and injected as pre-accepted drafts with ~1.0 acceptance — no draft head, no lookup table, and it engages even with the int4 MTP head from [#8](https://github.com/JustVugg/colibri/issues/8). It never constrains sampling: forced spans are verified in the same batch-union forward as any draft, so a wrong or out-of-sync grammar cannot change the output — worst case is rejected drafts, and an adaptive guard turns the source off below 50% acceptance. Byte-level GBNF subset (literals, char classes, `| ( ) ? * +`, comments); `GRAMMAR_DRAFT=n` caps the forced span per forward (default 24). Composes with `DRAFT`/MTP, which fill the free-text gaps between forced spans.
|
||
- **True sampling** — temperature + nucleus, defaults tuned for int4 reality (0.7 / 0.90; the official 1.0 / 0.95 samples quantization noise from the tail).
|
||
- **Integer-dot kernels** (Q8_0-style int8 activations, AVX2 `maddubs`): int8 matmuls 1.4–2.5× faster (119 GFLOP/s measured), int4 1.8× in batch — routing decided per shape by measurement (int4 single-row stays f32: it measured slower).
|
||
- **MLA weight absorption** (DeepSeek trick) for decode: no per-token k/v reconstruction — the query absorbs `kv_b`, context is projected after attention. Validated exact: TF 32/32 and generation 20/20 with absorption forced everywhere.
|
||
- **Async expert readahead**: while one block of experts is being multiplied, the kernel is already reading the next (`WILLNEED`).
|
||
- **Quantization kernels**: int8 / packed int4 / packed int2, per-row scales, AVX2, dequant-on-use. Packing validated bit-identical to the int8 container.
|
||
- **DSA sparse attention** — GLM-5.2's lightning indexer, faithful to the reference `glm_moe_dsa` modeling: per-layer top-2048 causal key selection (full/shared indexer layers), auto-detected from the `out-idx-*` weights (`--indexer` converter mode, ~189 MB extracted from the FP8 repo). Validated exact: forcing the selection to keep every key reproduces dense attention token-for-token. `DSA=0` disables, `DSA_TOPK` overrides.
|
||
- **KV-cache persistence** — conversations reopen **warm** across engine restarts: serve mode appends the compressed MLA KV to `.coli_kv` after every turn (~182 KB/token, crash-safe) and resumes it at startup with zero re-prefill. Validated byte-identical to an uninterrupted session. `KVSAVE=0` disables.
|
||
- **Router-lookahead prefetch** (`PILOT=1`, experimental) — the next layer's routing is 71.6% predictable from the current layer's post-attention state (measured); a dedicated I/O thread prefetches those experts while the current layer computes.
|
||
- **Batch-union MoE**: in prefill (and MTP verification), each unique expert of the batch is read once and applied to every position that routes to it.
|
||
- **Byte-level BPE tokenizer in C** (GPT-2-style with Unicode-property regex, 320k merges).
|
||
- **RAM safety**: the expert cache is auto-sized from `MemAvailable` at startup — an honest peak projection (working set, KV, MTP row, reconstruction buffers) so the kernel OOM-killer never fires.
|
||
- **Offline FP8→int4 converter** (`c/tools/convert_fp8_to_int4.py`): downloads one shard at a time (~5 GB), dequants (128×128 block scales), requantizes to the engine's container, deletes the shard — the 756 GB FP8 checkpoint never needs to exist on disk at once. Resumable.
|
||
|
||
## Honest numbers (WSL2, 12 cores, 25 GB RAM, NVMe via VHDX)
|
||
|
||
Detailed GPU experiment: [GLM-5.2 on 6x RTX 5090](docs/experiments/glm52-6x5090-2026-07-12.md) — full expert residency across VRAM+RAM reaches 6.84 tok/s single-request decode.
|
||
|
||
| metric | value |
|
||
|---|---|
|
||
| model on disk (int4 container) | ~370 GB |
|
||
| resident RAM (dense, int4) | 9.9 GB |
|
||
| load time | ~30 s |
|
||
| peak RSS during chat | ~20 GB (auto-capped) |
|
||
| cold decode cost | ~11 GB disk reads/token (75 layers × 8 experts) |
|
||
| disk ceiling (this dev box's drive) | ~1 GB/s → ~0.05–0.1 tok/s cold |
|
||
| MTP speculation (int8 head) | 2.2–2.8 tok/forward measured ([#8](https://github.com/JustVugg/colibri/issues/8)) |
|
||
|
||
This is not fast. It is a 744B frontier-class model **answering correctly on a machine that costs less than one H100 fan**. Warm cache, pinned hot experts and MTP push the useful-response latency down considerably; the physics of the disk does the rest.
|
||
|
||
### SSD note
|
||
Cold starts are heavy on random reads (~11 GB/token), but reads don't meaningfully wear an SSD — colibrì's streaming is read-only. The real concerns under heavy use are (1) **swap traffic** if the system runs out of RAM (writes do wear the drive — keep a sane `--ram` budget; colibrì's auto-budget is designed to stay clear of swap) and (2) **sustained thermals**: hours at full read duty cycle will heat cheaper drives. Monitor drive temperature and health.
|
||
|
||
## Download the model
|
||
|
||
A pre-converted **GLM-5.2 int4** model for colibrì is available on Hugging Face — **use the version with the int8 MTP heads** (matey-0's clone):
|
||
|
||
**https://huggingface.co/mateogrgic/GLM-5.2-colibri-int4-with-int8-mtp**
|
||
|
||
> ⚠️ **The MTP head must be int8.** The original mirror ([jlnsrk/GLM-5.2-colibri-int4](https://huggingface.co/jlnsrk/GLM-5.2-colibri-int4)) ships **int4** MTP heads, which give **0% draft acceptance** — speculation silently never engages and you lose the ~2× MTP lever. This is the single most common "why is MTP stuck at 0%?" report ([#8](https://github.com/JustVugg/colibri/issues/8), [#102](https://github.com/JustVugg/colibri/issues/102)). The int8 head gives the measured **39–59% acceptance**. matey-0's clone above is the original int4 model with the three `out-mtp-*` files already swapped to int8 — download that one and you're done.
|
||
>
|
||
> Check what you have: `ls -l <model>/out-mtp-*`
|
||
> · **int8 (correct):** `3527131672 / 5366238584 / 1065950496`
|
||
> · **int4 (0% acceptance):** `1765523544 / 2686077736 / 536747200` — if you see these, replace just those three files from the int8 mirror.
|
||
|
||
Download the repository and point `COLI_MODEL` to its directory:
|
||
|
||
```bash
|
||
COLI_MODEL=/path/to/GLM-5.2-colibri-int4-with-int8-mtp ./coli chat
|
||
```
|
||
|
||
This skips the FP8 → int4 conversion step entirely. Thanks to DatPat for the original mirror and matey-0 for the int8-head clone.
|
||
|
||
### Quick start
|
||
|
||
```bash
|
||
cd c
|
||
./setup.sh # checks gcc/OpenMP, builds, self-tests
|
||
|
||
# ONE command does everything model-side: downloads GLM-5.2-FP8 shard by shard
|
||
# (never needs the full 756 GB at once), converts to the int4 container, then
|
||
# converts the MTP head for speculative decoding. Resumable at any point.
|
||
# Conversion (only) needs python with: pip install torch safetensors huggingface_hub numpy
|
||
./coli convert --model /nvme/glm52_i4 # ~400 GB free on a real ext4/NVMe path
|
||
|
||
# chat — RAM budget, expert cache and MTP are all detected automatically:
|
||
COLI_MODEL=/nvme/glm52_i4 ./coli chat
|
||
```
|
||
|
||
Inspect the planned storage hierarchy before loading the model:
|
||
|
||
```bash
|
||
COLI_MODEL=/nvme/glm52_i4 ./coli plan
|
||
COLI_MODEL=/nvme/glm52_i4 ./coli plan --gpu 0,1 --ram 128 --vram 48 --json
|
||
|
||
# apply the bounded plan to the normal runner
|
||
COLI_MODEL=/nvme/glm52_i4 ./coli chat --auto-tier
|
||
```
|
||
|
||
`coli plan` reads only safetensors headers and reports the model's exact dense/expert
|
||
footprint, runtime RAM reserve, safe expert-cache cap, and bounded VRAM hot tier. Its
|
||
versioned JSON output is intended to be shared by the CLI, API server, Web UI, and
|
||
desktop shell; it does not allocate model tensors or start inference.
|
||
`--auto-tier` applies the same plan to `chat`, `run`, `serve`, and benchmarks. It
|
||
sets the RAM budget and context immediately; the VRAM tier is enabled only when
|
||
the current `glm` binary is linked with CUDA. Explicit flags and environment
|
||
variables keep precedence over automatic values.
|
||
|
||
Before loading the model, `coli doctor` performs a read-only readiness check and
|
||
explains whether the selected Disk/RAM/VRAM placement is runnable:
|
||
|
||
```bash
|
||
COLI_MODEL=/nvme/glm52_i4 ./coli doctor
|
||
COLI_MODEL=/nvme/glm52_i4 ./coli doctor --gpu 0 --ram 128 --json
|
||
```
|
||
|
||
Doctor validates the model directory, config, tokenizer, safetensors headers,
|
||
engine executable, available RAM, requested NVIDIA devices, CUDA linkage, and the
|
||
same placement budget used by `coli plan`. It never starts `glm`, reads tensor
|
||
payloads, imports a model framework, or creates a CUDA context. The versioned JSON
|
||
report uses stable check IDs for automation. Warnings keep exit status 0; missing
|
||
requirements or an unsafe RAM projection return 1, while invalid CLI values return 2.
|
||
|
||
The engine at runtime is pure C — python is only used by the one-time converter.
|
||
|
||
### Windows 11 (native, no WSL)
|
||
|
||
colibrì builds and runs natively on Windows 11 x86-64 with MinGW-w64. The port adds
|
||
a `_WIN32` compatibility layer in `c/compat.h` that maps POSIX I/O to the Windows API
|
||
(pread → ReadFile+OVERLAPPED, posix_fadvise no-op, aligned allocation, MoveFileEx rename,
|
||
GlobalMemoryStatusEx RAM detection). All platform differences stay in `compat.h`; the
|
||
engine source is unchanged.
|
||
|
||
**Toolchain:** GCC via [winlibs](https://winlibs.com/) or MSYS2 MinGW-w64. Tested with
|
||
GCC 16.1.0 (x86_64-ucrt-posix-seh).
|
||
|
||
```powershell
|
||
# One-time toolchain install (pick one):
|
||
scoop install mingw-winlibs # portable, no shell needed
|
||
# or: pacman -S mingw-w64-x86_64-gcc make # via MSYS2
|
||
|
||
# Build (from c/ directory):
|
||
make glm.exe # GLM-5.2 engine (static, no DLL dependencies)
|
||
make olmoe.exe # OLMoE engine (same shims)
|
||
make iobench.exe # disk I/O benchmark
|
||
make test-c # run C tests
|
||
make test-python # run Python tests (requires python)
|
||
|
||
# Verify (tiny model, 2.4 MB):
|
||
pip install torch transformers safetensors huggingface_hub
|
||
python tools/make_glm_oracle.py # generate tiny oracle
|
||
SNAP=./glm_tiny TF=1 ./glm.exe 64 16 16 # expect "32/32 positions"
|
||
|
||
# Run with real model:
|
||
SNAP=D:\glm52_i4 ./glm.exe 64 4 16 # batch inference
|
||
python coli chat --model D:\glm52_i4 # interactive chat
|
||
python coli serve --model D:\glm52_i4 # OpenAI-compatible API
|
||
```
|
||
|
||
**Status:** Phase 1 complete (compiles, correct, static-linked). O_DIRECT (Phase 2),
|
||
GPU via `LoadLibrary` on `coli_cuda.dll` (Phases G0–G2), and full-model validation
|
||
are separate workstreams. See `PORT_WINDOWS_PLAN.md` for the full plan.
|
||
|
||
### OpenAI-compatible API
|
||
|
||
`coli serve` keeps one model process loaded and exposes a text-only OpenAI-compatible
|
||
HTTP API. The gateway uses only the Python standard library; inference still runs in
|
||
the same dependency-free C engine.
|
||
|
||
```bash
|
||
cd c
|
||
COLI_MODEL=/nvme/glm52_i4 COLI_API_KEY=local-secret ./coli serve \
|
||
--host 127.0.0.1 --port 8000 --model-id glm-5.2-colibri
|
||
|
||
curl http://127.0.0.1:8000/v1/chat/completions \
|
||
-H 'Authorization: Bearer local-secret' \
|
||
-H 'Content-Type: application/json' \
|
||
-d '{
|
||
"model": "glm-5.2-colibri",
|
||
"messages": [{"role": "user", "content": "Hello"}],
|
||
"stream": true
|
||
}'
|
||
```
|
||
|
||
Implemented endpoints are `GET /v1/models`, `GET /v1/models/{model}`,
|
||
`POST /v1/chat/completions`, and legacy `POST /v1/completions`. Chat and
|
||
completion requests support JSON responses, SSE streaming, usage counts,
|
||
`max_tokens`/`max_completion_tokens`, `temperature`, and `top_p`. The extension
|
||
`enable_thinking: true` enables GLM-5.2's reasoning block; the standard
|
||
`reasoning_effort` field also enables it unless set to `none`.
|
||
|
||
The first version is deliberately text-only and serves one generation at a time:
|
||
the 744B model stays in one persistent process, so concurrent HTTP requests queue
|
||
instead of loading duplicate model copies. Tools, image/audio input, custom stop
|
||
sequences, log probabilities, and token penalties return an explicit error rather
|
||
than being silently ignored. The default bind address is localhost; set
|
||
`COLI_API_KEY` before exposing the server beyond the machine.
|
||
|
||
Browser access from the Vite development server and Tauri local origins is enabled
|
||
by default. Repeat `--cors-origin https://your-ui.example` to allow another exact
|
||
origin, or use `--cors-origin '*'` only on a trusted local network.
|
||
|
||
The engine owns one mutable KV context, so HTTP generation uses a bounded FIFO
|
||
admission queue instead of pretending to run unsafe parallel sequences. Configure it
|
||
with `--max-queue N` (default 8) and `--queue-timeout SECONDS` (default 300), or the
|
||
`COLI_MAX_QUEUE` / `COLI_QUEUE_TIMEOUT` environment variables. Saturated and timed-out
|
||
requests receive OpenAI-shaped HTTP 429 errors before streaming headers are sent.
|
||
`GET /health` exposes active/queued/completed/rejected counters, and successful
|
||
generation responses include `x-colibri-queue-wait-ms`.
|
||
|
||
### Isolated KV contexts
|
||
|
||
`coli serve --kv-slots N` allocates up to 16 independent sequence contexts. Requests
|
||
select one with the optional integer `cache_slot` field; ordinary OpenAI clients omit
|
||
it and keep the original slot 0 behavior.
|
||
|
||
```json
|
||
{
|
||
"model": "glm-5.2-colibri",
|
||
"messages": [{"role": "user", "content": "Continue this conversation"}],
|
||
"cache_slot": 1
|
||
}
|
||
```
|
||
|
||
Each slot owns its token history, compressed MLA/DSA KV memory, MTP window, and
|
||
crash-safe persistence file (`.coli_kv`, `.coli_kv.1`, ...). The engine still executes
|
||
one sequence at a time; this establishes explicit KV ownership without pretending that
|
||
threaded HTTP is continuous batching. RAM admission accounts for every configured slot.
|
||
Use `COLI_KV_SLOTS=N` as the environment equivalent. Start with a small value: at the
|
||
default 4096-token context, every slot costs hundreds of MB.
|
||
|
||
### Experimental Metal backend (Apple Silicon)
|
||
|
||
On Apple Silicon the decode profile is matmul-bound, and unified memory removes the
|
||
PCIe copy tax that keeps CUDA's streaming experts on the CPU — so colibrì has an
|
||
opt-in Metal backend that runs the **routed-expert SwiGLU (batched, zero-copy from
|
||
the RAM slabs)**, the **fused decode attention** (full MLA layer in one command
|
||
buffer, S≤4), and **prefill's large GEMMs** on the GPU. Token-exact vs the CPU path.
|
||
|
||
```bash
|
||
cd c
|
||
make glm METAL=1 # macOS only; no Xcode needed (shader compiles at runtime)
|
||
make metal-test # standalone kernel/attention correctness vs CPU reference
|
||
COLI_METAL=1 COLI_MODEL=/path/glm52_i4 ./coli chat --ram 96
|
||
```
|
||
|
||
Measured on an M4 Max (128 GB, warm cache, MTP on): CPU 0.30 → Metal **0.42 tok/s (~1.4×)**
|
||
(best config adds `DIRECT=1`; ~3× vs this machine's first cold run).
|
||
Key design points: Metal's ~5 ms submit latency makes per-matmul dispatch a loss —
|
||
everything is batched into few command buffers per layer, and the resident experts'
|
||
GPU work is submitted *before* the missed experts' disk reads so I/O and compute
|
||
overlap. `COLI_METAL_GEMM_MIN` tunes the prefill GEMM row threshold (default 16).
|
||
Streaming, cache, MTP, DSA and the persistence formats are unchanged; every GPU
|
||
path falls back to the CPU per-block on any fault. Numerics are dequant→f32-MAC
|
||
(same as the CUDA tier); greedy outputs are byte-identical to the CPU engine.
|
||
|
||
### Experimental resident CUDA backend
|
||
|
||
colibrì includes an opt-in CUDA backend for model-resident tensors. Streaming
|
||
experts deliberately remain on the original CPU path for now: copying an expert
|
||
from NVMe to the GPU on every use would only replace the disk bottleneck with a
|
||
PCIe bottleneck. Resident quantized tensors are uploaded lazily once and reused.
|
||
|
||
```bash
|
||
cd c
|
||
make cuda-test CUDA=1 # q8/q4/q2/f32 kernel correctness
|
||
make CUDA=1
|
||
# optional dense-path experiment (hot experts are configured below)
|
||
COLI_CUDA=1 COLI_GPU=0 CUDA_DENSE=1 SNAP=/nvme/glm52_i4 ./glm 64 4 4
|
||
```
|
||
|
||
Requirements: Linux, an NVIDIA driver, and a CUDA Toolkit under
|
||
`/usr/local/cuda` (override with `CUDA_HOME=/path/to/cuda`). `CUDA_ARCH=native`
|
||
builds for the GPU in the current machine; set an explicit architecture when
|
||
cross-compiling. Requesting CUDA with a CPU-only binary, an invalid device, or
|
||
an unavailable runtime fails at startup instead of silently falling back.
|
||
|
||
The normal `make` build and runtime behavior are unchanged. CUDA defaults to an
|
||
expert-only accelerator: resident dense/attention tensors stay on CPU because
|
||
fixture measurements show that moving them does not help while expert I/O is
|
||
the bottleneck. `CUDA_DENSE=1` keeps the earlier all-resident experimental path.
|
||
A measured `PIN` profile can promote its hottest experts into the persistent
|
||
VRAM tier while keeping the rest in RAM:
|
||
|
||
```bash
|
||
STATS=stats.txt SNAP=/nvme/glm52_i4 ./glm 64 4 4 # collect routing frequencies first
|
||
COLI_CUDA=1 COLI_GPU=0 CUDA_EXPERT_GB=16 \
|
||
PIN=stats.txt PIN_GB=160 SNAP=/nvme/glm52_i4 ./glm 64 4 4
|
||
# multi-GPU expert tier, 96 GB total budget across six devices
|
||
COLI_CUDA=1 COLI_GPUS=0,1,2,3,4,5 CUDA_EXPERT_GB=96 \
|
||
PIN=stats.txt PIN_GB=160 SNAP=/nvme/glm52_i4 ./glm 64 4 4
|
||
```
|
||
|
||
Selected experts are uploaded during startup, so capacity failures occur before
|
||
inference and the log reports their exact tensor footprint. The budget is clamped
|
||
against free VRAM after reserving the projected dense resident set and 2 GB of
|
||
runtime headroom per selected device. With `COLI_GPUS`, `CUDA_EXPERT_GB` is a
|
||
total budget across the device set; experts are assigned whole to the
|
||
least-loaded device that can hold them. A NUMA-local RAM backing store is not
|
||
implemented yet.
|
||
|
||
Current limitations: devices use independent contexts and synchronous
|
||
host-staged activation copies—there is no P2P/NCCL dependency yet. The kernels
|
||
are correctness-first custom kernels rather than cuBLAS/Tensor Core kernels.
|
||
This draft intentionally makes no end-to-end speedup claim before the full model
|
||
is benchmarked.
|
||
|
||
For a reproducible backend A/B without the full checkpoint, generate the
|
||
deterministic 313M-parameter `glm_moe_dsa` fixture and run fixed-token replay:
|
||
|
||
```bash
|
||
cd c
|
||
python tools/make_glm_bench_model.py --output /nvme/colibri-bench-medium --device cuda
|
||
python tools/benchmark_cuda_fixture.py --model /nvme/colibri-bench-medium --gpu 0
|
||
```
|
||
|
||
The fixture has random weights and is not a language model. It exists only to
|
||
preserve the real MLA/MoE/streaming shapes and compare CPU streaming, dense-only
|
||
CUDA, CPU hot-store, and CUDA hot-expert execution with identical replay tokens.
|
||
|
||
### Web interface
|
||
|
||
`web/` contains a community-contributed browser UI (React + TypeScript, ~390
|
||
lines of source, a pure API client — it never touches the engine directly):
|
||
|
||
```bash
|
||
cd web
|
||
npm ci && npm run dev # then point it at an OpenAI-compatible endpoint
|
||
```
|
||
|
||
It speaks the standard OpenAI Chat Completions protocol with SSE streaming, so it
|
||
works against the colibrì OpenAI-compatible server (in review, #21) or any other
|
||
compatible endpoint. Nothing leaves the endpoint you configure. The terminal
|
||
`coli chat` remains the first-class interface.
|
||
|
||
Useful knobs (env or flags): `--temp T` token sampling temperature (default 0.7 + nucleus 0.90 — tuned for int4; 0 = greedy), `--topp 0.7` adaptive expert top-p (30–40% less disk), `--ngen N` max tokens per answer (`:more` in chat continues a truncated one), `--repin N` adapt RAM/VRAM hot experts every N emitted tokens, `AUTOPIN=0` disable the learning cache's auto-pin, `THINK=1` enable GLM-5.2's reasoning block, `DRAFT=n` MTP draft depth, `GRAMMAR=g.gbnf` grammar-forced drafts for constrained JSON/NDJSON output (`GRAMMAR_DRAFT=n` caps the forced span), `TF=1` teacher-forcing validation, `PILOT=1` router-lookahead disk prefetch (experimental — see below), `CAP_RAISE=0` don't auto-grow the expert cache.
|
||
|
||
**The expert cache auto-sizes to your RAM** (since 2026-07-10): the engine now *raises* the LRU cap to fill your `--ram` budget instead of only lowering it. Before this fix a 128 GB machine ran with the same 8-experts/layer cache as a 16 GB one (issue #12) — **if you benchmarked colibrì before this date, rerun: your numbers were capped.**
|
||
|
||
**Router-lookahead prefetch** (`PILOT=1`, experimental): GLM-5.2's expert routing is measurably predictable *ahead of time* — applying layer L+1's router to layer L's post-attention state recalls **71.6%** of the true top-8 (vs 41.3% for "same experts as last token"). `PILOT=1` uses this to issue next-layer expert readahead from a dedicated I/O thread while the current layer computes. On our dev box the disk is already ~80% saturated, so it measures neutral; on machines where compute and disk are balanced (like the Ryzen AI 9 in issue #12: 43% disk / 46% matmul) it should overlap real work — measurements welcome.
|
||
|
||
**The learning cache**: the engine records which experts your usage actually routes to (`.coli_usage` next to the model, updated every turn) and at startup automatically pins the hottest ones in spare RAM. colibrì literally gets faster the more you use it.
|
||
|
||
**Live tier adaptation** (`--repin N`, opt-in): at safe turn boundaries, a decaying
|
||
session heat map replaces cold pinned experts with hotter streamed experts. Replacement
|
||
loads the expert from disk into the existing RAM slot; GPU-backed slots immediately
|
||
refresh the same VRAM tier budget. A 25% hysteresis and a four-swap limit prevent tier
|
||
thrashing. Persistent `.coli_usage` remains the long-term signal and is not decayed.
|
||
|
||
**Conversations reopen warm** (`.coli_kv`, since 2026-07-10): `coli chat` persists the compressed MLA KV-cache to disk after every turn (~182 KB/token, appended incrementally, crash-safe). Close the chat, reopen it tomorrow — the model still remembers the whole conversation and **zero re-prefill happens**: validated byte-identical to an uninterrupted session. `:reset` clears it, `KVSAVE=0` disables it.
|
||
|
||
## Got a better machine? Try it — here's what to expect
|
||
|
||
colibrì was built on deliberately humble hardware (12 cores, 25 GB RAM, an older DRAM-less NVMe behind a WSL2 VHDX that measured ~1 GB/s random on *this* drive — note WSL2 VHDX is not inherently slow: a community 5090 box measured 10.5 GB/s O_DIRECT through one, [#101](https://github.com/JustVugg/colibri/issues/101)). **Every one of those constraints is a knob your machine can turn up.** The engine needs: Linux (or WSL2), macOS, or **Windows 11 natively (MinGW-w64)**; gcc with OpenMP, AVX2, ≥16 GB RAM, and the ~370 GB int4 model on a local NVMe (ext4/NTFS — never a network/9p mount).
|
||
|
||
**How to test it, in order:**
|
||
|
||
```bash
|
||
cd c && ./setup.sh # build + architecture self-test (expects 32/32)
|
||
|
||
# 1) measure YOUR disk the way the engine uses it (parallel 19 MB random reads):
|
||
gcc -O2 -fopenmp iobench.c -o iobench
|
||
./iobench /path/to/glm52_i4/out-00069.safetensors 19 64 8 0 # buffered, 8 threads
|
||
./iobench /path/to/glm52_i4/out-00069.safetensors 19 64 8 1 # O_DIRECT (bypass cache)
|
||
# Caveat (#86): iobench reads a bounded ~1 GB shard, so buffered reads on a big-RAM box
|
||
# report the PAGE CACHE, not the disk. Use the O_DIRECT run (arg 1) for a true number, and
|
||
# run it on a shard you haven't touched this session (a prior buffered run caches its pages).
|
||
# On macOS there is no O_DIRECT — iobench uses F_NOCACHE, which stops *new* caching but can't
|
||
# evict pages a prior buffered run already resident-mapped, so a macOS "O_DIRECT" figure right
|
||
# after a buffered run still reads cache. Reboot or use a fresh shard for a real cold read.
|
||
|
||
# 2) chat; watch the per-turn stats line (tok/s, expert hit-rate, RSS):
|
||
COLI_MODEL=/path/to/glm52_i4 ./coli chat
|
||
|
||
# 3) record expert usage, then pin the hottest experts in your spare RAM:
|
||
STATS=stats.txt ./coli chat
|
||
PIN=stats.txt PIN_GB=20 ./coli chat # scale PIN_GB to your free RAM
|
||
|
||
# 4) quality benchmarks (MMLU/HellaSwag/ARC):
|
||
./coli bench
|
||
```
|
||
|
||
**Back-of-envelope predictions** (decode is disk-bound: a cold token costs ~11.4 GB of expert reads; MTP speculation roughly halves the effective cost *once the cache is warm*; RAM turns cold reads into free cache hits):
|
||
|
||
| machine | expected |
|
||
|---|---|
|
||
| this dev box (WSL2 VHDX, ~1 GB/s, 25 GB RAM) | ~0.05–0.1 tok/s cold — proven baseline |
|
||
| native Linux, PCIe4 NVMe (~3–5 GB/s random), 32 GB | ~0.5–1 tok/s |
|
||
| PCIe5 NVMe or 2×NVMe RAID0 (~8–12 GB/s), 64 GB (PIN ~40 GB of hot experts) | ~2–4 tok/s |
|
||
| 128–256 GB RAM, 12 cores (hot experts cached) | ~2–4 tok/s — matmul-bound: ~80 GFLOP/token vs ~250 GFLOP/s of our AVX2 kernels |
|
||
| same RAM + 24–32 cores, or AVX-512/VNNI kernels | ~5–15 tok/s — interactive; kernel work is the multiplier |
|
||
|
||
These are estimates, not measurements — if you run colibrì on serious hardware, **please open an issue with your numbers**: real datapoints from better machines are exactly what this project needs next.
|
||
|
||
### Community benchmarks (measured)
|
||
|
||
Real numbers from real machines, stock build (`setup.sh`, gcc 13), greedy decoding, `--ngen 32`, MTP active:
|
||
|
||
| machine | disk (iobench, 19 MB × 64, 8 threads) | config | measured |
|
||
|---|---|---|---|
|
||
| Intel Core Ultra 7 270K Plus (24 threads) · WSL2 · 24 GB RAM · NVMe VHDX ([#2](https://github.com/JustVugg/colibri/issues/2)) | 1.96 GB/s buffered · 2.74 GB/s O_DIRECT | default | 0.07 tok/s · expert hit 3–4% · RSS 14.1 GB |
|
||
| 〃 | 〃 | `--topp 0.7` | **0.11 tok/s** · expert hit 11% · RSS 14.7 GB |
|
||
| Apple M5 Max (18 cores) · macOS · 128 GB unified · internal SSD ([#4](https://github.com/JustVugg/colibri/issues/4), [#5](https://github.com/JustVugg/colibri/issues/5)) | ~4 GB/s cold (the 14.2 GB/s reading was cache-influenced — see note) | default, MTP off | **1.06 tok/s** · expert hit 23% · RSS 21.8 GB |
|
||
| Apple M5 Max · macOS · 128 GB unified · 2 TB SSD · **Metal backend** ([#72](https://github.com/JustVugg/colibri/pull/72), [#87](https://github.com/JustVugg/colibri/issues/87)) | (macOS O_DIRECT figure unreliable — see note) | Metal on · `--ram 96` · 39.7 GB warm pin · MTP off | **1.83 tok/s** · expert hit 66% · warmed 1.11 → 1.83 over the run |
|
||
| 〃 · 46.9 GB pin (2.94M-selection history) · `--ram 110`, 1024-token run ([#103](https://github.com/JustVugg/colibri/issues/103)) | 〃 | Metal on (experts + attention) · MTP off | **2.06 tok/s** · hit 72.5% · coherent output · fastest datapoint yet (still on the pre-rebase Metal branch) |
|
||
| Epyc 9654 ES · Linux · 4x16GB DDR5-4800-rdimm · Samsung PCIe Gen3 x4 NVME SSD | — | `MTP=1 DIRECT=1` | 0.31 tok/s · expert hit 35% · RSS 21.52 GB |
|
||
| Ryzen AI 9 HX 370 (Framework 13) · Arch Linux · 128 GB · WD SN850X, BTRFS zstd ([#12](https://github.com/JustVugg/colibri/issues/12)) | — | int8 MTP head · `--cap 32` · 46.7 GB auto-learned PIN | **0.37 tok/s** · expert hit 66% · MTP acceptance 52% (2.59 tok/fw) · RSS 105 GB |
|
||
| Ryzen 9 9950X (32 threads) · Linux · 123 GB · Crucial P3 QLC Gen3 ([#31](https://github.com/JustVugg/colibri/issues/31)) | 1.51 GB/s buffered | default, 2 runs from cold | 0.10 tok/s · hit 53% · profile 66% disk |
|
||
| 〃 same machine, model moved to a Samsung 9100 PRO PCIe 5.0 ([#31](https://github.com/JustVugg/colibri/issues/31)) | **8.81 GB/s** O_DIRECT | 〃 (usage history retained) | **0.28 tok/s** · hit 57% · profile flips: 32% disk / **57% matmul** |
|
||
| Ryzen AI Max+ 395 (Framework Desktop) · Ubuntu · 128 GB LPDDR5x · Intel Optane 905p PCIe 3.0 ([#39](https://github.com/JustVugg/colibri/issues/39)) | 3.27 GB/s buffered | int8 MTP head · fresh history (pure LRU, auto-raised cap 65) | 0.16 tok/s · hit 57% · profile 49% disk / 47% matmul |
|
||
| 〃 five runs later — learned pin 47.6 GB ([#39](https://github.com/JustVugg/colibri/issues/39)) | 〃 | `--temp 0.7 --topp 0.7` | **0.40 tok/s** · hit 71% · fastest non-Apple datapoint |
|
||
| Ryzen 7 9800X3D (16T) · WSL2 · 70 GB RAM · Samsung 9100 PRO PCIe 5.0 · RTX 5090 ([#101](https://github.com/JustVugg/colibri/issues/101)) | **10.51 GB/s** O_DIRECT | MTP off · learned pin 24 GB · hit 54% · OMP hot-team on | **0.41 tok/s** · disk-bound (36.5 s disk vs 24.0 s matmul) · **CUDA expert tier ≈ 0%** (AVX-512 CPU matches the 5090) · `--topp 0.7` → **0.52 tok/s** |
|
||
| EPYC 7443 (24C/48T, Zen3 AVX2) · Linux · **430 GB RAM** · NVMe RAID-Z1 via TrueNAS VM ([#104](https://github.com/JustVugg/colibri/issues/104)) | ~1 GB/s (VM overhead) | 77.5 GB pin · cap auto-raised to 194/layer · MTP off | **1.00 tok/s** · **hit 98%** · disk eliminated → **RAM-bandwidth + matmul bound** (no AVX-512/VNNI on Zen3) |
|
||
|
||
Takeaways: with 24 GB of RAM the engine auto-caps the expert cache to 2 slots/layer, so decode stays cold even on a disk 2–2.7× faster than the dev box — **on small-RAM machines the RAM cap, not the disk, is the binding constraint**, exactly as the table above predicts; `--topp 0.7` alone bought a clean 1.6× end-to-end speedup. The M5 Max datapoint lands right on the table's second row: **~1 tok/s of a 744B model on a laptop SSD** — and its 14 GB/s disk shifts the bottleneck back to RAM budget and kernels. The Framework 13 rows are the cache thesis proven end-to-end on one machine: 0.29 → 0.37 tok/s (hit 28% → 66%, speculation finally engaging at 52% acceptance) just by giving the cache its RAM — int8 MTP head + a bigger cap + the learned pin. The cap part is now automatic (cap auto-raise, 2026-07-10). The 9950X pair is the cleanest bottleneck experiment yet — same machine, same history, only the disk swapped: ×5.8 disk bandwidth bought ×2.9 tokens, and the profile **flipped from 66% disk to 57% matmul**. But the crossover depends on the CPU kernel: the 9800X3D row ([#101](https://github.com/JustVugg/colibri/issues/101)) shows that with the OMP hot-team tuning on, the AVX-512 CPU matmul is fast enough that even a **10 GB/s NVMe stays disk-bound** — and there the **CUDA expert tier buys ≈ 0%**, because the CPU already matches the 5090 on expert matmul. The GPU tier earns its VRAM only when the CPU is the weak link, not by default. (Honest correction from #101: an earlier version of that report ran with the OMP tuning off, which manufactured a false matmul-bound crossover and a false +14% for CUDA — neither survived a clean re-run.)
|
||
|
||
## Quality benchmark — help wanted
|
||
|
||
We have never measured how much the int4 quantization costs in accuracy — the harness is built and wired, but scoring is one forward per answer option, and on the dev box's ~1 GB/s disk a full run takes the better part of a day. **This is the single most valuable thing a faster machine can contribute.** The code is here and ready; one command runs it end to end (it auto-downloads the datasets on first use):
|
||
|
||
```bash
|
||
cd c
|
||
./coli bench # hellaswag, arc_challenge, mmlu — 40 questions each
|
||
./coli bench hellaswag --limit 200 # one task, more questions
|
||
./coli bench mmlu arc_challenge --ram 100 # pick tasks, set a RAM budget
|
||
```
|
||
|
||
It prints per-task accuracy (log-likelihood scoring, EleutherAI-harness style). Published full-precision GLM-5.2 scores on these tasks sit around 85–95%; if our int4 container lands within a few points, the quantization is validated — if it doesn't, we know to invest in mixed / grouped-scale quantization. **If you have the hardware to run this, please open an issue with the numbers** — it's the measurement the project is missing.
|
||
|
||
## Supporting the project
|
||
|
||
colibrì is a one-person project, written and tested entirely on a 12-core laptop with 25 GB of RAM — the numbers above are the ceiling of what I can measure at home. If this project is useful or interesting to you and you'd like to support its development (better test hardware translates *directly* into a faster engine for everyone: real NVMe scaling data, bigger pinned caches, int2/int3 quality sweeps on real benchmarks), you can:
|
||
|
||
- ⭐ star the repo and share it;
|
||
- 🐛 open issues with benchmark numbers from your hardware;
|
||
- 💬 reach out via GitHub issues if you'd like to sponsor development or donate hardware.
|
||
|
||
Every contribution, from a datapoint to a disk, moves the ceiling.
|
||
|
||
## Repo layout
|
||
|
||
```
|
||
Makefile root build/check entry point
|
||
c/
|
||
├── glm.c single-file GLM engine
|
||
├── st.h, tok.h, json.h runtime headers
|
||
├── backend_cuda.* optional CUDA tier
|
||
├── Makefile build and local checks
|
||
├── coli user-facing CLI
|
||
├── openai_server.py OpenAI-compatible HTTP gateway
|
||
├── setup.sh one-command local setup
|
||
├── tools/ offline conversion, fixtures and benchmarks
|
||
├── scripts/ long-running conversion helpers
|
||
└── tests/ dependency-free C and Python tests
|
||
web/ browser UI (pure OpenAI-API client, community-maintained)
|
||
```
|
||
|
||
The runtime path intentionally stays flat and readable: `glm.c` plus its small
|
||
headers. Auxiliary Python and shell tooling is grouped separately and is never a
|
||
runtime dependency of the engine.
|
||
|
||
From the repository root, `make`, `make check`, and `make clean` delegate to the
|
||
engine Makefile. Existing commands run from `c/` continue to work unchanged.
|
||
|
||
## Why "colibrì"
|
||
|
||
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.
|
||
|
||
## License
|
||
|
||
Apache 2.0. GLM-5.2 weights are released by Z.ai under MIT.
|