diff --git a/.clang-format b/.clang-format
new file mode 100644
index 00000000..f9116525
--- /dev/null
+++ b/.clang-format
@@ -0,0 +1,10 @@
+BasedOnStyle: LLVM
+IndentWidth: 4
+ColumnLimit: 120
+AllowShortFunctionsOnASingleLine: All
+AllowShortIfStatementsOnASingleLine: AllIfsAndElse
+AllowShortLoopsOnASingleLine: true
+BreakBeforeBraces: Attach
+PointerAlignment: Right
+SpaceAfterCStyleCast: false
+SortIncludes: false
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 00000000..e0acabb3
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,24 @@
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+indent_style = space
+indent_size = 4
+
+[*.{c,h,cu}]
+indent_size = 4
+
+[*.{ts,tsx,js,json,css}]
+indent_size = 2
+
+[Makefile]
+indent_style = tab
+
+[*.yml]
+indent_size = 2
+
+[*.md]
+trim_trailing_whitespace = false
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 72b7149e..426ea413 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -12,8 +12,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- - name: Build glm
- run: cd c && make glm
+ - name: Build colibri
+ run: cd c && make colibri
- name: C test suite
run: cd c && make test-c
@@ -41,6 +41,27 @@ jobs:
-Xcompiler=-Wall,-Wextra
echo "CUDA syntax check passed"
+ engine-hip-syntax:
+ name: HIP syntax check
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Free runner disk
+ # rocm/dev images are large; hosted runners need the headroom freed
+ # BEFORE the pull (which is why this is not a container: job).
+ run: sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL
+ - name: Compile backend_cuda.cu + test binary with hipcc (no GPU)
+ run: |
+ # Tag pinned: `latest` outgrew the runner disk once already. The
+ # gcc-side engine link is skipped: containerized gcc/ld cannot link
+ # ROCm runtime libs (both 22.04 and 24.04 bases); the hipcc-driver
+ # link of the test binary below covers the single-source claim, and
+ # the engine link is exercised on real ROCm hardware (GPU_BACKENDS.md).
+ docker run --rm -v "$PWD":/src -w /src rocm/dev-ubuntu-24.04:6.2 bash -c '
+ apt-get update && apt-get install -y --no-install-recommends make gcc g++ &&
+ make -C c gpu-compile HIP=1 HIP_ARCH=gfx1100'
+ echo "HIP syntax check passed"
+
windows-cuda-build:
# The Windows CUDA path has no coverage anywhere: `engine-cuda-syntax` above
# compiles with nvcc's *GCC* host on Linux, and check.yml's windows job is
@@ -105,13 +126,13 @@ jobs:
make cuda-dll CUDA_ARCH=sm_80
test -f coli_cuda.dll || { echo "cuda-dll reported success but produced no DLL" >&2; exit 1; }
echo "coli_cuda.dll built (MSVC host)"
- - name: make glm CUDA_DLL=1 (host links backend_loader, not cudart)
+ - name: make colibri CUDA_DLL=1 (host links backend_loader, not cudart)
shell: msys2 {0}
run: |
cd c
- make glm CUDA_DLL=1
- test -f glm.exe || { echo "glm CUDA_DLL=1 reported success but produced no exe" >&2; exit 1; }
- echo "glm.exe built against the DLL loader"
+ make colibri CUDA_DLL=1
+ test -f colibri.exe || { echo "colibri CUDA_DLL=1 reported success but produced no exe" >&2; exit 1; }
+ echo "colibri.exe built against the DLL loader"
web:
name: Web UI
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index e471fb99..a556b80b 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -59,7 +59,12 @@ jobs:
run: |
TAG=${GITHUB_REF#refs/tags/}
mkdir -p dist
- cp c/glm${{ matrix.ext }} dist/colibri-${TAG}-${{ matrix.name }}${{ matrix.ext }}
+ # The engine goes in under its PLAIN name. `coli` locates the engine by looking for
+ # "colibri"/"colibri.exe" (then "glm") next to itself, so shipping it as
+ # colibri--.exe made every downloaded archive fail with
+ # "engine is not built. Run: coli build" -- with the engine sitting right there.
+ # The version lives in the ARCHIVE name, which is where a downloader looks for it.
+ cp c/glm${{ matrix.ext }} dist/colibri${{ matrix.ext }}
cp c/coli dist/
cp c/version.py dist/
cp c/openai_server.py dist/
@@ -73,10 +78,36 @@ jobs:
tar czf colibri-${TAG}-${{ matrix.name }}.tar.gz *
fi
+ # Unpack what we are about to publish, in a clean directory, and prove `coli` finds the
+ # engine. A packaging mistake is invisible to every other job in this repo: the build is
+ # green, the tests are green, and the artifact is still unusable. Assert on behaviour.
+ - name: Verify the packaged archive actually runs
+ run: |
+ TAG=${GITHUB_REF#refs/tags/}
+ mkdir -p verify && cd verify
+ if [ "${{ matrix.ext }}" = ".exe" ]; then
+ 7z x ../dist/colibri-${TAG}-${{ matrix.name }}.zip > /dev/null
+ else
+ tar xzf ../dist/colibri-${TAG}-${{ matrix.name }}.tar.gz
+ fi
+ test -x colibri${{ matrix.ext }} || { echo "engine missing from archive"; ls -la; exit 1; }
+ out=$(python3 coli info 2>&1 || true)
+ echo "$out"
+ case "$out" in
+ *"engine is not built"*) echo "FAIL: coli cannot find the packaged engine"; exit 1 ;;
+ esac
+ echo "$out" | grep -q "colibri" || { echo "FAIL: coli did not run"; exit 1; }
+ echo "OK: coli locates the engine in the published archive"
+
+ # Only the archives -- never the loose files. `dist/colibri-*.*` used to work by accident
+ # (the engine was versioned, so it did not match); now that the engine is plainly named,
+ # be explicit about what gets published.
- uses: actions/upload-artifact@v4
with:
name: colibri-${{ matrix.name }}
- path: dist/colibri-*.*
+ path: |
+ dist/colibri-*.zip
+ dist/colibri-*.tar.gz
release:
needs: build
diff --git a/.gitignore b/.gitignore
index 981e7400..f62b3792 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,6 +10,8 @@ desktop/src-tauri/target/
desktop/src-tauri/gen/
# binari compilati (si rigenerano con make / coli build)
+c/colibri
+c/colibri.exe
c/glm
c/glm.exe
c/olmoe
@@ -23,18 +25,12 @@ c/backend_loader.o
c/coli_cuda.dll
c/coli_cuda.lib
c/coli_cuda.exp
-c/tests/test_json
-c/tests/test_json.exe
-c/tests/test_st
-c/tests/test_st.exe
-c/tests/test_tier
-c/tests/test_tier.exe
-c/tests/test_grammar
-c/tests/test_grammar.exe
-c/tests/test_schema_gbnf
-c/tests/test_schema_gbnf.exe
-c/tests/test_compat_direct
-c/tests/test_compat_direct.exe
+c/tests/test_*
+!c/tests/test_*.c
+!c/tests/test_*.cu
+!c/tests/test_*.mm
+!c/tests/test_*.py
+result
# oracoli tiny generati (make_glm_oracle.py) e dati benchmark scaricati
c/glm_tiny/
@@ -68,3 +64,7 @@ c/tests/test_decode_batch
c/tests/test_i4_acc512
c/tests/test_idot
c/tests/test_uring
+olmoe_merged/
+olmoe_i4/
+c/olmoe_merged/
+c/olmoe_i4/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 61779660..1b98abed 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,83 @@
All notable changes to colibrì are documented here.
Format follows [Keep a Changelog](https://keepachangelog.com/).
+## [1.1.0] — 2026-07-22
+
+A community release. 27 pull requests from more than 20 contributors, 216 commits since
+v1.0.0. Most of what follows was found, measured, or fixed by people who do not work on
+this project and had nothing to gain from it.
+
+### Added
+
+- **AMD GPU support (HIP/ROCm)** (#339) — single-source `backend_gpu_compat.h` with a WMMA
+ dispatch gate, so one codebase builds for CUDA and HIP. Validated on an RX 9070 XT
+ (RDNA4, ROCm 7.2): token-exact against CPU on a real fmt=4 gs64 container, with resident
+ dense *and* with routed experts in VRAM, plus a fail-injection control proving the GPU
+ actually executed the work.
+- **Dual-SSD streaming** (`COLI_MODEL_MIRROR`, #421) — read the model from two drives at
+ once, roughly doubling streaming bandwidth on a disk-bound host.
+- **N-drive shard split** (`COLI_MODEL_DIRS`, #469) — capacity aggregation: run a container
+ no single drive can hold, spread across several with no duplication.
+- **fmt=5 (int3-g64)** (#168) — 3-bit weights with per-64 group scales: measured 3.3x lower
+ outlier-row error than per-row int4 at 25% fewer bytes.
+- **fmt=6 (E8/IQ3 lattice)** (#465) — CPU decode kernel and dispatch; index codec tooling (#458).
+- **`tools/try_tool_calling.py`** — dependency-free two-turn tool-calling probe that doubles
+ as a smoke test.
+
+### Fixed
+
+- **Tool calling in coding clients** (#401), root cause found, in two parts:
+ - **#506** — the engine capped prompt encoding at `CTX-2`, and the tokenizer stops dead at
+ its limit *without reporting anything*. A prompt longer than the context was therefore
+ silently truncated to its first `CTX-2` tokens and answered anyway. With the 4096 default
+ that is 4094 — exactly the `prefill 4094` in the field report. The dropped tail was the
+ tool instructions and the user's actual turn, so the model emitted a bare `<` and stopped;
+ and because clients append to the *end* while truncation keeps the *head*, every retry
+ re-sent a byte-identical prompt. Now refused with a 400 `context_length_exceeded`.
+ - **#505** — a tool call whose closing `` never arrived was dropped whole,
+ because the parser required both tags. Now recovered when unambiguous, on both the
+ streamed and non-streamed paths.
+ - **#437** — non-EOS role markers were armed as hard stops in serve mode and cut generation
+ the instant a tool block started.
+- **Grouped-int4 (fmt=4) produced garbage output on CUDA** with `CUDA_DENSE=1` (#298) — the
+ dense and attention kernels applied per-group scales as if they were per-row. Hardware-verified.
+- **OpenMP tuning re-exec preserved the CPU affinity mask** (#476), jailing every thread onto
+ one core when `OMP_PROC_BIND`/`OMP_PLACES` were set: roughly a 20x slowdown.
+- **Pilot eviction guard dropped ~100% of speculations** once the cache filled (#497),
+ collapsing `PILOT_REAL` to a hint-only path.
+- **Silent budget clamp** capped the CUDA expert tier at ~109 experts regardless of
+ `CUDA_EXPERT_GB` (#495).
+- fmt=4 guard at the per-row-only CUDA entry points (#464/#470); `COLI_CUDA_MTP=1` and
+ `COLI_CUDA=0` are now honoured over implicit defaults (#468).
+
+### Security
+
+Threat model: model files come from mirrors that are not trusted.
+
+- **#368** — server hardening, JSON and tokenizer parser hardening, build flags, downloader
+ and dependency pinning.
+- **#413** — the quant layout is resolved *and* validated against the on-disk byte counts
+ (unknown layouts are refused rather than falling through to int2), shape-product overflow
+ is rejected, and the olmoe dtype-3 path no longer trusts a crafted `nbytes` (heap overflow).
+
+### Performance — all byte-identical
+
+- **#481** +4.7x on the MLA-absorb score and value-mix reductions
+- **#477** +13% decode on AVX-512 (`qt_addrow` / `qt_matvec_rows`)
+- **#475** +11.6% with opt-in `XEXP=1` (one OpenMP region per expert block at S=1 full residency)
+- **#473** +5.5% int4 IDOT at S=1 on AVX-512 VNNI
+
+### Changed
+
+- `glm.c` is now `colibri.c` plus header modules (#391); `make glm` remains as an alias.
+- Serve stage 2 (#192): `response_format`, per-request grammars, grammar-forced drafts.
+
+### Upgrade notes
+
+- **`CTX` still defaults to 4096.** Coding clients send far more than that in a single system
+ prompt. Use `CTX=32768`. Before this release an over-long prompt was silently truncated;
+ now you get a clear 400 instead.
+
## [1.0.0] — 2026-07-19
First tagged release. The engine has been running in production since late June
diff --git a/GPU_BACKENDS.md b/GPU_BACKENDS.md
new file mode 100644
index 00000000..be421bfb
--- /dev/null
+++ b/GPU_BACKENDS.md
@@ -0,0 +1,85 @@
+# GPU backends: CUDA and HIP/ROCm
+
+colibrì's GPU expert backend is **one source file** (`c/backend_cuda.cu`) compiled
+for either vendor through `c/backend_gpu_compat.h` — the same one-shim-header
+pattern `compat.h` uses for the Windows port. Compiled by nvcc the shim is a
+pass-through to `cuda_runtime.h` (the NVIDIA path is byte-identical to the
+pre-HIP tree); compiled by hipcc it maps the 14-symbol CUDA runtime surface the
+backend uses onto HIP 1:1. The kernels use only shared syntax
+(`__global__`, `__shared__`, `__syncthreads__`, `<<<>>>`), no vendor intrinsics.
+
+**Rule for contributors:** vendor differences go in `backend_gpu_compat.h`
+only — never `#ifdef __HIP__` (or CUDA-specific code) in `backend_cuda.cu`.
+
+## Supported environments
+
+| backend | platform | toolchain | build |
+|---|---|---|---|
+| CUDA (`CUDA=1`) | Linux x86-64 | CUDA toolkit (nvcc), `CUDA_HOME=/usr/local/cuda` default | `make -C c glm CUDA=1 [CUDA_ARCH=native\|sm_XX]` |
+| HIP (`HIP=1`) | Linux x86-64 | ROCm (hipcc), `ROCM_HOME=/opt/rocm` default; tested on ROCm 7.2 | `make -C c glm HIP=1 [HIP_ARCH=native\|gfxXXXX]` |
+
+`CUDA=1` and `HIP=1` are mutually exclusive and both opt-in: the default build
+remains pure, dependency-free CPU. Both are refused on non-Linux with an early
+`$(error)`. `*_ARCH=native` targets the local GPU; pass an explicit arch when
+distributing or on machines with an unsupported iGPU visible to the runtime
+(and mask iGPUs at runtime with `HIP_VISIBLE_DEVICES=` on ROCm).
+
+## Runtime configuration (identical for both vendors)
+
+- `COLI_CUDA=1` + `COLI_GPU=N` (or `COLI_GPUS=0,1,...`) — enable, select devices
+- `CUDA_EXPERT_GB=G` — VRAM budget for the expert tier (clamped to free VRAM
+ minus projected dense set and 2 GB headroom per device)
+- `CUDA_RELEASE_HOST=1` — GPU-tier experts drop their host backing after
+ upload (default on multi-GPU); combined with `PIN=auto`/`PIN_FILL`, VRAM
+ becomes additional pinned capacity at zero RAM cost. The engine
+ rematerializes an expert from disk (`expert_host_ensure`) whenever the CPU
+ path needs one whose host copy was released — validated under total GPU
+ failure below.
+- `CUDA_DENSE=1` — experimental resident-dense path (unchanged)
+- `COLI_CUDA_TC_W4A16=1` — opt-in W4A16 tensor-core path. **NVIDIA-only**:
+ the WMMA kernels are compile-gated (`COLI_GPU_HAS_WMMA` in the compat
+ header) because gfx GPUs report `compute_major >= 7` and a runtime check
+ alone would select empty kernel bodies under HIP. On AMD, all compute uses
+ the portable kernels; rocWMMA matrix-core support is a possible follow-up.
+
+## Validation
+
+### Unit tests (run on GPU hardware)
+
+```sh
+make -C c cuda-test [CUDA_ARCH=...] # NVIDIA
+make -C c hip-test [HIP_ARCH=...] # AMD (same test source)
+```
+
+Covers q8/q4/q2/f32 matmul correctness, multi-device placement/stats, and
+`tensor_update` — the standard upstream suite, unchanged, compiled by hipcc.
+(A companion PR adds failure-path tests for the backend; they are
+vendor-neutral and run under `hip-test` identically.)
+
+### CI (no GPU required)
+
+The `engine-hip-syntax` job in `.github/workflows/ci.yml` compiles the
+backend and its test binary with hipcc (`rocm/dev` container pinned to
+`6.2`, `gfx1100`) on every PR, mirroring `engine-cuda-syntax`. Kernel
+*execution* is not possible on hosted runners; that is what `hip-test`
+on real hardware is for (matrix below).
+
+### Hardware test matrix (documented results)
+
+| environment | result |
+|---|---|
+| AMD RX 9070 XT (gfx1201), ROCm 7.2.4, Linux 7.0 | `hip-test` **pass** (all cases above); GLM-5.2 end-to-end runs (0.32 tok/s @ 61% expert hit with CUDA_RELEASE_HOST=1); benchmark series in PR #112 |
+| NVIDIA | compile-verified in CI (`sm_80`); nvcc path is a pass-through include — **runtime run of `make cuda-test` on NVIDIA hardware welcomed**, the test source is vendor-neutral |
+
+## Known behavior notes
+
+- GPU float matmuls round differently than the CPU int8-dot (IDOT) kernels:
+ greedy output is **not token-identical** across backends (consistent with
+ the shape-dependence documented in #100), and MTP draft acceptance measures
+ lower on GPU-heavy configs (~40% → ~31% on the PR #112 machine). A
+ numerics-matched integer GPU kernel is the planned follow-up.
+- An earlier revision of this branch carried `CUDA_EXTEND=1` (VRAM tier
+ holding experts beyond the RAM pin). It was superseded by upstream's
+ `PIN=auto` + `PIN_FILL` + `CUDA_RELEASE_HOST`, which achieve the same
+ capacity extension with deeper engine integration; this branch's safety
+ and validation work now targets that mechanism.
diff --git a/README.it.md b/README.it.md
new file mode 100644
index 00000000..76e3c21c
--- /dev/null
+++ b/README.it.md
@@ -0,0 +1,260 @@
+
+
+
+
+
+ English · 简体中文 · 繁體中文 · Italiano
+
+
+**Motore piccolo, modello immenso.** Esegui **GLM-5.2 (744 miliardi di parametri, MoE)** su un computer consumer con ~25 GB di RAM — in C puro, zero dipendenze, caricando gli expert dal disco in streaming.
+
+Colibrì è un runtime MoE leggero e che preserva la qualità: tratta VRAM, RAM e
+disco come un'unica gerarchia di memoria gestita. Se la memoria veloce non basta
+il modello rallenta, ma la policy predefinita **non cambia mai silenziosamente la
+precisione del modello né la semantica del router**.
+
+```
+$ ./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?
+```
+
+## Guardalo in azione
+
+
+
+
+La dashboard web (./coli web): un modello da 744B a 4 tok/s, TTFT 1.6 s, disco 0 —
+residenza completa degli expert su 6× RTX 5090, con metriche token in tempo reale, breakdown dei tempi per turno,
+la barra dei livelli VRAM/RAM/disco e il mini-cervello live nell'angolo.
+
+
+
+
+La pagina Brain: tutti i 19.456 expert come una corteccia vivente — il colore indica
+il livello di archiviazione, la luminosità il calore di routing, e ogni expert instradato in un turno
+lampeggia bianco. Passando il cursore si vede l'affinità
+tematica misurata dell'expert.
+
+
+
+
+La pagina Atlas: l'atlante
+misurato degli expert come una galassia 3D — 13.260 expert caratterizzati, 1.041 specialisti
+replicabili che si raggruppano per argomento (poesia, legge, cinese, SQL…). La posizione deriva
+dall'affinità di routing misurata, non da un embedding appreso. Trascinare per ruotare.
+
+## L'idea
+
+Un modello Mixture-of-Experts da 744B attiva solo ~40B parametri per token — e
+solo ~11 GB di quelli cambiano da un token all'altro (gli expert instradati):
+
+
+
+
+
+Il modello non ha bisogno di *stare* in memoria veloce — ha bisogno di essere
+**piazzato**:
+
+- la **parte densa** (attenzione, expert condivisi, embedding — ~17B parametri)
+ resta **residente in RAM a int4** (~9.9 GB);
+- i **19.456 expert instradati** (75 layer MoE × 256 + la testa MTP, ~19 MB
+ ciascuno a int4) stanno **su disco** (~370 GB) e vengono **caricati on demand
+ in streaming**, con una cache LRU per layer, un hot-store pinnato che impara,
+ e un livello VRAM opzionale.
+
+Il motore è un singolo file C (`c/colibri.c`) più header piccoli. Niente BLAS,
+niente Python a runtime, niente GPU obbligatoria.
+
+## Come funziona
+
+### Il percorso di ogni token
+
+
+
+
+
+Ogni layer di ogni token percorre gli stessi cinque passi. L'obiettivo
+progettuale è che **il piazzamento decide solo la velocità** — le decisioni
+del router e la precisione dei pesi sono identiche sia che un expert risponda
+dalla VRAM sia dal disco.
+
+### Una gerarchia di memoria, non un requisito di memoria
+
+
+
+
+
+Lo stesso motore copre l'intero spettro: su un portatile da 25 GB tutto viene
+caricato dal disco in streaming (lento, ma corretto); su un host grande l'intero
+set di expert diventa residente (`CUDA_EXPERT_GB=auto PIN_GB=all`) e il disco
+esce completamente dal percorso di decode. Tra i livelli c'è una **cache che
+impara**: il motore registra quali expert il *tuo* carico di lavoro instrada
+(`.coli_usage`, aggiornato a ogni turno) e fissa automaticamente i più caldi —
+colibrì diventa letteralmente più veloce man mano che lo usi. Sugli host
+multi-socket, `COLI_NUMA=1` interlaccia i pesi residenti tra i controller di
+memoria ([#82](https://github.com/JustVugg/colibri/issues/82)).
+
+### Mai aspettare il disco due volte
+
+I miss nella cache costano caro, quindi il motore investe la maggior parte
+della sua astuzia per evitarli e sovrapporli: le tre matrici di ogni expert sono
+memorizzate contigue e lette con un unico `pread`; un pool I/O asincrono
+limitato (`PIPE=1`, attivo per default) carica gli expert mancanti mentre quelli
+residenti calcolano; le posizioni in batch leggono ogni expert unico una sola
+volta (**batch-union**); un thread di lookahead del router (`PILOT=1`) fa il
+prefetch degli expert del layer successivo — il routing è misurabilmente
+**prevedibile al 71.6% un layer in anticipo**. Sulle GPU, la pipeline residente
+(`COLI_CUDA_PIPE=2`) mantiene il flusso residuo on-device tra i layer, così il
+loop CPU degli expert procede senza interruzioni; su Apple Silicon un backend
+[Metal](docs/metal.md) sperimentale esegue la matmul batch degli expert sulla
+GPU a memoria unificata.
+
+### Modello fedele, stato compresso
+
+Il forward pass è validato **token-esatto contro un oracle `transformers`**
+(teacher-forcing 32/32). L'attenzione MLA memorizza uno stato KV compresso — 576
+float/token invece di 32.768 (**57× più piccolo**) — e lo persiste tra i
+riavvii (`.coli_kv`): le conversazioni riaprono "calde", senza alcun re-prefill,
+byte-identiche a una sessione ininterrotta. L'attenzione sparsa DSA (il
+lightning indexer di GLM-5.2) è implementata fedelmente e validata forzando la
+selezione di tutte le chiavi per riprodurre esattamente l'attenzione densa.
+
+### Decodifica speculativa, onestamente
+
+La testa MTP nativa di GLM-5.2 propone token che il modello principale verifica
+in un unico forward batch — 2.2–2.8 token/forward quando conviene. Due regole
+conquistate a caro prezzo sono i default: la testa MTP deve essere **int8** (le
+teste int4 crollano al 0–4% di accettazione,
+[#8](https://github.com/JustVugg/colibri/issues/8)), e draft e verifica devono
+calcolare **la stessa funzione** — `SPEC_PIN=1` fissa entrambi sulla stessa
+famiglia di kernel ([#163](https://github.com/JustVugg/colibri/issues/163)
+contiene l'intera indagine forense). I draft forzati da grammatica
+([`GRAMMAR=file.gbnf`](docs/grammar-draft.md)) aggiungono accettazione quasi
+gratuita sull'output JSON vincolato. Se la speculazione conviene dipende dalla
+temperatura della cache — misura, e usa `DRAFT=0` quando non paga.
+
+## Cosa ottiene
+
+
+
+
+
+Stesso motore, stesso container int4 — cambia solo dove risiedono gli expert.
+Punti salienti dalle [tabelle benchmark complete](docs/benchmarks.md):
+
+- **6× RTX 5090, residenza completa:** 5.8–6.8 tok/s in decode, TTFT ~13 s
+ ([log dell'esperimento](docs/experiments/glm52-6x5090-2026-07-12.md));
+- **desktop solo-CPU da 128 GB:** ~1.8 tok/s a cache calda
+ ([#200](https://github.com/JustVugg/colibri/issues/200));
+- **singola RTX 5070 Ti, classe laptop:** 1.07 tok/s tramite la pipeline
+ GPU-residente ([#273](https://github.com/JustVugg/colibri/issues/273));
+- **macchina di sviluppo da 25 GB:** 0.05–0.1 tok/s a freddo — il punto di
+ partenza dimostrato da cui è nato il progetto, e ancora oggi la baseline onesta.
+
+La qualità è misurata, non presunta: il costo di quantizzazione del container
+int4 e le ablazioni su granularità delle scale e rotazione sono in
+[docs/benchmarks.md](docs/benchmarks.md#quality-benchmark) e
+[#108](https://github.com/JustVugg/colibri/issues/108)/[#81](https://github.com/JustVugg/colibri/issues/81).
+
+## Per iniziare
+
+### 1. Scarica il modello
+
+Un container **GLM-5.2 int4** pre-convertito è su Hugging Face — **usa la
+versione con le teste MTP int8**:
+
+**https://huggingface.co/mateogrgic/GLM-5.2-colibri-int4-with-int8-mtp**
+
+> ⚠️ Il mirror originale contiene teste MTP int4 → accettazione dei draft allo 0%
+> ([#8](https://github.com/JustVugg/colibri/issues/8)). Verifica la tua versione:
+> `ls -l /out-mtp-*` — int8 (corretto) è `3527131672 / 5366238584 / 1065950496`.
+
+Oppure converti tu stesso dalla sorgente FP8 — un unico comando riprendibile che
+non richiede mai i 756 GB completi su disco contemporaneamente:
+
+```bash
+cd c && ./setup.sh # verifica gcc/OpenMP, compila, autotest
+./coli convert --model /nvme/glm52_i4 # scarica e converti shard per shard (python, una tantum)
+```
+
+### 2. Esegui
+
+```bash
+COLI_MODEL=/nvme/glm52_i4 ./coli chat # budget RAM, cache e MTP rilevati automaticamente
+COLI_MODEL=/nvme/glm52_i4 ./coli plan # mostra il piazzamento pianificato VRAM/RAM/disco
+COLI_MODEL=/nvme/glm52_i4 ./coli doctor # controllo di idoneità (sola lettura)
+./coli web --model /nvme/glm52_i4 # API + dashboard web sulla stessa porta
+./coli serve --model /nvme/glm52_i4 # solo API compatibile OpenAI
+```
+
+Il motore a runtime è puro C — python si usa solo per il convertitore (una tantum)
+e per il gateway API opzionale.
+
+### 3. Approfondisci
+
+| argomento | documento |
+|---|---|
+| Benchmark, dati dalla comunità, misurazioni di qualità | [docs/benchmarks.md](docs/benchmarks.md) |
+| Parametri di tuning, policy, cache che impara, prefetch | [docs/tuning.md](docs/tuning.md) |
+| Build nativa su Windows 11 (con CUDA DLL) | [docs/windows.md](docs/windows.md) |
+| Backend CUDA, livello expert in VRAM, residenza completa | [docs/cuda.md](docs/cuda.md) |
+| Backend Metal per Apple Silicon | [docs/metal.md](docs/metal.md) |
+| API compatibile OpenAI, KV slot, dashboard web | [docs/api.md](docs/api.md) |
+| Draft forzati da grammatica (output strutturato) | [docs/grammar-draft.md](docs/grammar-draft.md) |
+| Inventario delle variabili d'ambiente | [docs/ENVIRONMENT.md](docs/ENVIRONMENT.md) |
+
+## Sostenere il progetto
+
+colibrì è nato come progetto di una sola persona su un portatile con 12 core
+e 25 GB di RAM; oggi i suoi numeri arrivano da una comunità di macchine reali.
+Se ti è utile:
+
+- ⭐ metti una stella al repository e condividilo;
+- 🐛 apri issue con i numeri di benchmark del tuo hardware — i datapoint
+ fanno avanzare questo progetto più di qualsiasi altra cosa;
+- 💬 contattaci via GitHub issues per sponsorizzare lo sviluppo o donare hardware.
+
+## Struttura del repository
+
+```
+Makefile punto d'ingresso root per build/check
+c/
+├── colibri.c motore principale
+├── quant.h kernel matmul quantizzati (SIMD multi-architettura)
+├── sample.h campionamento, RNG, set di stop
+├── kv_persist.h persistenza KV su disco (.coli_kv)
+├── telemetry.h protocollo dashboard, statistiche, usage
+├── st.h, tok.h, json.h header di runtime
+├── backend_cuda.* livello CUDA opzionale
+├── Makefile build e check locali
+├── coli CLI utente
+├── openai_server.py gateway HTTP compatibile OpenAI
+├── setup.sh setup locale in un solo comando
+├── tools/ conversione offline, fixture e benchmark
+├── scripts/ helper per conversioni lunghe
+└── tests/ test C e Python senza dipendenze
+web/ UI browser (puro client API OpenAI)
+desktop/ shell desktop Tauri v2 che racchiude la web UI
+docs/ documentazione di riferimento, esperimenti, media
+```
+
+Il percorso a runtime resta intenzionalmente piatto e leggibile: `colibri.c`
+più i suoi header. Dalla radice del repository, `make`, `make check` e
+`make clean` delegano al Makefile del motore.
+
+## Perché "colibrì"
+
+Il colibrì pesa pochi grammi, sta sospeso nel vuoto e visita un migliaio di
+fiori al giorno. Questo motore tiene in vita un gigante da 744 miliardi di
+parametri con le razioni di un colibrì: 25 GB di RAM, dodici core CPU e
+tanta pazienza col disco.
+
+Il nome è rimasto in italiano perché questa è la lingua in cui è stato scritto
+il primo prototipo — i commenti nel codice lo testimoniano ancora.
+
+## Licenza
+
+Apache 2.0. I pesi di GLM-5.2 sono rilasciati da Z.ai sotto licenza MIT.
diff --git a/README.md b/README.md
index d4fe5217..8b48ee59 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@
Website ·
- English · 繁體中文
+ English · 简体中文 · 繁體中文 · Italiano
**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.
@@ -50,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
@@ -67,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.
@@ -88,6 +110,22 @@ precision are the same whether an expert answered from VRAM or from disk.
+### Dual-SSD: two copies of the model, twice the read bandwidth
+
+Decode is disk-bound on most machines, and expert reads are read-only — so if you have a **second SSD**, put a full copy of the model on it and let the engine stream from both drives at once:
+
+```bash
+COLI_MODEL=/fast/glm52_i4 COLI_MODEL_MIRROR=/second/glm52_i4 ./coli chat
+COLI_DISK_WEIGHTS=9,3 ... # optional: primary,mirror bandwidth ratio (else measured at startup)
+```
+
+Each expert is routed to one drive by a deterministic hash, weighted by the two drives' measured (or declared) bandwidth, so readahead/PILOT prefetch and the demand read always hit the same drive and nothing is cached twice. The aggregate bandwidth is the sum of both drives — a 9 GB/s + 3 GB/s pair reads experts ~33% faster than the fast drive alone, and the OMP-parallel pin/warmup load streams from both. Details worth knowing:
+
+- the mirror is **validated at startup** (per-file size + safetensors header must be byte-identical to the primary); divergent or missing files silently stay on the primary, so a **partial mirror is fine** — a smaller second SSD holding only some shards still helps;
+- the mirror is **never written**: `.coli_usage`, `.coli_kv` and all sidecars stay on the primary;
+- a read error on the mirror falls back to the primary (one warning, no crash), so unplugging the second drive mid-run degrades instead of killing the server;
+- routing never changes tokens — both copies are byte-identical, and the per-run `MIRROR:` stats line shows GB served per drive.
+
The same engine spans the whole range: on a 25 GB laptop everything streams from
disk (slow but correct); on a large host the entire expert set becomes resident
(`CUDA_EXPERT_GB=auto PIN_GB=all`) and disk drops out of the decode path
@@ -110,6 +148,13 @@ on-device across layers so the CPU expert loop runs uninterrupted; on Apple
Silicon an experimental [Metal backend](docs/metal.md) does the batched expert
math on the unified-memory GPU.
+> **On real NVMe, measure `DIRECT=1`.** O_DIRECT bypasses the page cache and is
+> often a large win on drives with DRAM cache and bandwidth headroom (+34%
+> decode measured with `PIPE=1` on a Blackwell/Windows box; 4.25→9.69 GB/s in
+> iobench on a GB10) — but it is drive-dependent: QLC/DRAM-less or virtualised
+> disks can be neutral to negative. Try it first; keep what your hardware
+> rewards.
+
### Faithful model, compressed state
The forward pass is validated **token-exact against a `transformers` oracle**
@@ -198,6 +243,17 @@ 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).
+
### 3. Go deeper
| topic | doc |
@@ -211,6 +267,18 @@ and the optional API gateway.
| 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;
@@ -251,6 +319,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/README.zh-CN.md b/README.zh-CN.md
new file mode 100644
index 00000000..352de33c
--- /dev/null
+++ b/README.zh-CN.md
@@ -0,0 +1,237 @@
+
+
+
+
+
+ English · 简体中文 · 繁體中文 · Italiano
+
+
+**小巧引擎,庞大模型。**只需约 25 GB 内存,就能在消费级电脑上运行 **GLM-5.2(744B 参数的 MoE)**——以零依赖的纯 C 实现,从磁盘流式加载专家。
+
+Colibrì 是一套轻量、保持模型质量的 MoE 运行时,将 VRAM、RAM
+与存储设备视为统一管理的内存层级。高速内存不足可能降低速度,
+但默认策略**绝不会在未告知的情况下改变模型精度或路由语义**。
+
+```
+$ ./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?
+```
+
+## 实际运行效果
+
+
+
+
+网页仪表盘(./coli web):744B 模型达到 4 tok/s、TTFT 1.6 秒、磁盘读取 0——
+在 6× RTX 5090 上让所有专家常驻,并实时显示 token 指标、每轮耗时明细、
+VRAM/RAM/磁盘层级条,以及角落的实时迷你大脑。
+
+
+
+
+大脑(Brain)页面:将全部 19,456 个专家呈现为活的皮层——颜色代表存储层级,
+亮度代表路由热度,每轮被路由到的专家都会闪白。将光标停在专家上,即可查看其
+实测主题亲和度。
+
+
+
+
+图谱(Atlas)页面:将实测专家图谱
+呈现为 3D 星系——共 13,260 个已分析专家,其中 1,041 个可复现的专门专家会按主题聚集
+(诗歌、法律、中文、SQL……)。位置取自实测路由亲和度,而非学习出的嵌入向量。拖拽即可旋转。
+
+## 核心概念
+
+744B 的专家混合(Mixture-of-Experts)模型,每个 token 只会激活约 40B 参数——
+其中每个 token 之间会变动的只有约 11 GB(被路由到的专家):
+
+
+
+
+
+所以模型不必完整**装进**高速内存,而是需要正确**放置**:
+
+- **稠密部分**(注意力、共享专家、嵌入——约 17B 参数)以 int4
+ **常驻 RAM**(约 9.9 GB);
+- **19,456 个路由专家**(75 个 MoE 层 × 256,加上 MTP head;每个在 int4 下约 19 MB)
+ **存放在磁盘**(约 370 GB),并**按需流式加载**,配合逐层 LRU 缓存、
+ 会学习的热门专家固定存储区,以及可选的 VRAM 层级。
+
+引擎是一个 C 主文件(`c/colibri.c`)加上若干头文件。不需要 BLAS,
+运行时不需要 Python,也不需要 GPU。
+
+## 工作原理
+
+### 每个 token 的处理路径
+
+
+
+
+
+每个 token 的每一层都会经过相同的五个步骤。设计目标是让
+**放置只决定速度**——无论专家是从 VRAM 还是磁盘响应,路由器的决策与权重精度都完全相同。
+
+### 统一内存层级,取代单一内存门槛
+
+
+
+
+
+同一套引擎覆盖完整硬件范围:在 25 GB 笔记本上,一切都从磁盘流式加载
+(慢,但结果正确);在大内存主机上,则可让整组专家常驻
+(`CUDA_EXPERT_GB=auto PIN_GB=all`),让磁盘完全退出解码路径。
+两端之间有一层**学习型缓存**:引擎会记录*你的*工作负载路由到哪些专家
+(`.coli_usage`,每轮更新),并自动固定最热门的专家——colibrì 确实会越用越快。
+在多路主机上,`COLI_NUMA=1` 会将常驻权重交错分配到各内存控制器
+([#82](https://github.com/JustVugg/colibri/issues/82))。
+
+### 绝不为同一次磁盘读取等待两遍
+
+缓存未命中的代价很高,因此引擎大部分的巧思都用来避免或重叠这些读取:
+每个专家的三个矩阵相邻存储,并以一次 `pread` 读取;有界异步 I/O 池
+(`PIPE=1`,默认启用)会在常驻专家计算时加载缺失的专家;批量位置只读取每个
+不重复专家一次(**批量并集**);路由前瞻线程(`PILOT=1`)则预取下一层专家——
+实测显示,路由结果提前一层时有 **71.6% 的可预测性**。
+在 GPU 上,常驻管线(`COLI_CUDA_PIPE=2`)让残差流跨层保留在设备端,
+使 CPU 专家循环不中断;在 Apple Silicon 上,实验性的
+[Metal 后端](docs/metal.md)会用统一内存 GPU 执行批量专家运算。
+
+### 忠实模型,压缩状态
+
+前向传播已通过 `transformers` oracle 验证为**逐 token 完全一致**
+(teacher-forcing 32/32)。MLA 注意力存储压缩后的 KV 状态——每个 token 为 576 个
+浮点数,而非 32,768 个(**缩小 57×**)——并跨重启持久保存
+(`.coli_kv`):对话可暖启恢复,不需重新 prefill,结果与不中断的会话
+逐字节相同。DSA 稀疏注意力(GLM-5.2 的 lightning indexer)已忠实实现,
+并通过强制选取所有 key,验证可精确复现稠密注意力。
+
+### 诚实的推测解码
+
+GLM-5.2 原生 MTP head 会起草 token,再由主模型以一次批量前向传播验证——
+条件合适时每次 forward 可产生 2.2–2.8 个 token。两条来之不易的规则已成为默认值:
+MTP head 必须是 **int8**(int4 head 的接受率会崩塌到 0–4%,见
+[#8](https://github.com/JustVugg/colibri/issues/8)),且草稿与验证必须计算
+**相同函数**——`SPEC_PIN=1` 会把两者固定在同一 kernel family
+(完整取证过程见 [#163](https://github.com/JustVugg/colibri/issues/163))。
+语法强制草稿([`GRAMMAR=file.gbnf`](docs/grammar-draft.md))可在受限 JSON 输出中,
+以近乎免费的代价提高接受率。推测解码是否带来净收益取决于缓存热度——请实测,
+若不划算就使用 `DRAFT=0`。
+
+## 实际成果
+
+
+
+
+
+同一套引擎、同一个 int4 容器——硬件只会改变专家的存放位置。
+[完整 benchmark 表格](docs/benchmarks.md)中的重点如下:
+
+- **6× RTX 5090,全部常驻:**解码 5.8–6.8 tok/s,TTFT 约 13 秒
+ ([实验记录](docs/experiments/glm52-6x5090-2026-07-12.md));
+- **128 GB、仅使用 CPU 的台式机:**热缓存后约 1.8 tok/s
+ ([#200](https://github.com/JustVugg/colibri/issues/200));
+- **单张 RTX 5070 Ti 的笔记本级主机:**通过 GPU 常驻管线达到 1.07 tok/s
+ ([#273](https://github.com/JustVugg/colibri/issues/273));
+- **25 GB 开发机:**冷启动 0.05–0.1 tok/s——这是项目起步时已证实的下限,
+ 也仍是诚实的基准。
+
+质量来自测量,而非假设:int4 容器的量化损失,以及 scale granularity/rotation
+消融实验,收录于 [docs/benchmarks.md](docs/benchmarks.md#quality-benchmark)、
+[#108](https://github.com/JustVugg/colibri/issues/108) 与
+[#81](https://github.com/JustVugg/colibri/issues/81)。
+
+## 开始使用
+
+### 1. 获取模型
+
+Hugging Face 上已有预转换的 **GLM-5.2 int4** 容器——请务必使用
+**含 int8 MTP head 的版本**:
+
+**https://huggingface.co/mateogrgic/GLM-5.2-colibri-int4-with-int8-mtp**
+
+> ⚠️ 原始镜像使用 int4 MTP head → 草稿接受率为 0%
+>([#8](https://github.com/JustVugg/colibri/issues/8))。请检查你的版本:
+> `ls -l /out-mtp-*`——正确的 int8 大小为 `3527131672 / 5366238584 / 1065950496`。
+
+你也可以自行从 FP8 源转换——只需一条可断点续传的命令,且任何时候都不需要
+在磁盘上同时存放完整的 756 GB:
+
+```bash
+cd c && ./setup.sh # 检查 gcc/OpenMP、构建并运行自测
+./coli convert --model /nvme/glm52_i4 # 逐 shard 下载并转换(仅此一次需要 python)
+```
+
+### 2. 运行
+
+```bash
+COLI_MODEL=/nvme/glm52_i4 ./coli chat # 自动检测 RAM 预算、缓存与 MTP
+COLI_MODEL=/nvme/glm52_i4 ./coli plan # 查看规划的 VRAM/RAM/磁盘配置
+COLI_MODEL=/nvme/glm52_i4 ./coli doctor # 只读就绪检查
+./coli web --model /nvme/glm52_i4 # 在同一端口提供 API 与网页仪表盘
+./coli serve --model /nvme/glm52_i4 # 仅提供 OpenAI 兼容 API
+```
+
+引擎运行时是纯 C——python 只供一次性转换工具与可选的 API gateway 使用。
+
+### 3. 深入了解
+
+| 主题 | 文档 |
+|---|---|
+| Benchmark、社区实测数据、质量测量 | [docs/benchmarks.md](docs/benchmarks.md) |
+| 调优选项、策略、学习型缓存、预取 | [docs/tuning.md](docs/tuning.md) |
+| Windows 11 原生构建(含 CUDA DLL) | [docs/windows.md](docs/windows.md) |
+| CUDA 后端、VRAM 专家层级、全部常驻 | [docs/cuda.md](docs/cuda.md) |
+| Apple Silicon Metal 后端 | [docs/metal.md](docs/metal.md) |
+| OpenAI 兼容 API、KV slots、网页仪表盘 | [docs/api.md](docs/api.md) |
+| 语法强制草稿(结构化输出) | [docs/grammar-draft.md](docs/grammar-draft.md) |
+| 环境变量完整清单 | [docs/ENVIRONMENT.md](docs/ENVIRONMENT.md) |
+
+## 支持项目
+
+colibrì 最初由一人使用 12 核心、25 GB RAM 的笔记本开发;
+如今它的数据来自社区中各种真实机器。如果这个项目对你有用:
+
+- ⭐ 为仓库加星并分享;
+- 🐛 以 issue 提交你的硬件 benchmark 数据——实测数据比任何其他事都更能推动项目;
+- 💬 若想赞助开发或捐赠硬件,请通过 GitHub issues 联系。
+
+## 仓库结构
+
+```
+Makefile 根目录构建/检查入口
+c/
+├── colibri.c 引擎主文件
+├── quant.h 量化 matmul 内核(SIMD 多架构)
+├── sample.h 采样与 stop-set 管理
+├── kv_persist.h .coli_kv 磁盘持久化
+├── telemetry.h 仪表盘协议、统计与用量持久化
+├── st.h, tok.h, json.h 运行时头文件
+├── backend_cuda.* 可选的 CUDA 层级
+├── Makefile 构建与本地检查
+├── coli 用户界面 CLI
+├── openai_server.py OpenAI 兼容 HTTP gateway
+├── setup.sh 一条命令完成本地设置
+├── tools/ 离线转换、fixtures 与 benchmarks
+├── scripts/ 长时间转换辅助工具
+└── tests/ 零依赖的 C 与 Python 测试
+web/ 浏览器 UI(纯 OpenAI API client)
+desktop/ 封装网页 UI 的 Tauri v2 桌面 shell
+docs/ 参考文档、实验与媒体文件
+```
+
+运行时路径刻意保持扁平、易读:`colibri.c` 加上若干头文件。
+在仓库根目录执行 `make`、`make check` 与 `make clean`,
+都会转发给引擎的 Makefile。
+
+## 为什么叫"colibrì"
+
+蜂鸟只有几克重,能在原地悬停,并在一天内造访上千朵花。
+这套引擎只用蜂鸟般的配给,就能让 744B 参数的巨人运转:
+25 GB RAM、十二个 CPU 核心,以及对磁盘的大量耐心。
+
+## 许可证
+
+Apache 2.0。GLM-5.2 权重由 Z.ai 以 MIT 许可发布。
diff --git a/README.zh-TW.md b/README.zh-TW.md
index 1662617c..2576eba1 100644
--- a/README.zh-TW.md
+++ b/README.zh-TW.md
@@ -3,7 +3,7 @@
- English · 繁體中文
+ English · 简体中文 · 繁體中文 · Italiano
**小巧引擎,龐大模型。**只要約 25 GB 記憶體,就能在消費級電腦上執行 **GLM-5.2(744B 參數的 MoE)**——以零相依套件的純 C 實作,從硬碟串流載入專家。
@@ -60,7 +60,7 @@ VRAM/RAM/硬碟層級長條,以及角落的即時迷你大腦。
**存放在硬碟**(約 370 GB),並**隨需串流載入**,搭配逐層 LRU 快取、
會學習的熱門專家固定儲存區,以及選用的 VRAM 層級。
-引擎由單一 C 檔(`c/glm.c`)與少量標頭檔組成。不需要 BLAS,
+引擎由主 C 檔(`c/colibri.c`)與多個標頭檔模組組成。不需要 BLAS,
執行階段不需要 Python,也不需要 GPU。
## 運作方式
@@ -203,7 +203,11 @@ colibrì 最初是由一人使用 12 核心、25 GB RAM 的筆電開發;
```
Makefile 根目錄建置/檢查入口
c/
-├── glm.c 單檔 GLM 引擎
+├── colibri.c GLM 引擎主檔
+├── quant.h 量化 matmul kernel
+├── sample.h 取樣與 stop-set
+├── kv_persist.h .coli_kv 磁碟持久化
+├── telemetry.h 儀表板協定、統計
├── st.h, tok.h, json.h 執行階段標頭檔
├── backend_cuda.* 選用的 CUDA 層級
├── Makefile 建置與本機檢查
@@ -218,7 +222,7 @@ desktop/ 包裝網頁 UI 的 Tauri v2 桌面 shell
docs/ 參考文件、實驗與媒體檔
```
-執行階段路徑刻意維持扁平、易讀:`glm.c` 加上少量標頭檔。
+執行階段路徑刻意維持扁平、易讀:`colibri.c` 加上模組化標頭檔。
在儲存庫根目錄執行 `make`、`make check` 與 `make clean`,
都會轉交給引擎的 Makefile。
diff --git a/SUMMARY.md b/SUMMARY.md
new file mode 100644
index 00000000..f4de3116
--- /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/.gitignore b/c/.gitignore
index d8e1f882..51695f90 100644
--- a/c/.gitignore
+++ b/c/.gitignore
@@ -3,3 +3,5 @@ glm_tiny/
olmoe_hf/
olmoe_i4/
.build-config
+tests/test_st_mirror
+tests/test_st_pread
diff --git a/c/Makefile b/c/Makefile
index 1f8583b7..1799b6b5 100644
--- a/c/Makefile
+++ b/c/Makefile
@@ -56,11 +56,16 @@ OMPL =
endif
CFLAGS = -O3 $(OMPC) -Wall -Wextra -Wno-unused-parameter -Wno-misleading-indentation -Wno-unused-function
# Opt-in: ARCH=native appends -mcpu=native (arm64 clang uses -mcpu, not -march),
-# which unlocks the i8mm SMMLA int8/int4 dot kernels in glm.c. ARCH unset ->
+# 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),)
@@ -70,7 +75,7 @@ else ifneq ($(IS_WIN),)
# ARCH default = x86-64-v3 (portable binary with AVX2). For max speed on THIS
# machine use ARCH=native: on AVX-VNNI CPUs (Intel Alder Lake+, Meteor Lake+)
# it also unlocks the 128-bit VPDPBUSD int8/int4 dot kernel (dot_i8i8/dot_i4i8),
-# which the x86-64-v3 baseline does not define. The #ifdef guards in glm.c mean
+# which the x86-64-v3 baseline does not define. The #ifdef guards in colibri.c mean
# a v3 build simply compiles out the VNNI path - safe on any x86-64.
CC = gcc
ARCH ?= x86-64-v3
@@ -143,17 +148,45 @@ else
CUDA_HOME ?= /usr/local/cuda
NVCC ?= $(CUDA_HOME)/bin/nvcc
endif
+# CUDA target selection. Default `native` builds SASS for the build machine's GPU
+# only (fast local dev). A headless CI/Docker build has no GPU, so `-arch=native`
+# there detects nothing and the shipped coli_cuda.dll fails at first kernel dispatch.
+# For a portable/release DLL that runs on any Ampere..Blackwell card AND JITs on
+# newer archs, build with CUDA_ARCH=portable: it emits SASS for sm_80/86/89/90/120
+# plus a compute_120 PTX fallback (sm_120 / RTX 50-series needs CUDA >= 12.8).
CUDA_ARCH ?= native
+ifeq ($(CUDA_ARCH),portable)
+CUDA_GENCODE = -gencode arch=compute_80,code=sm_80 \
+ -gencode arch=compute_86,code=sm_86 \
+ -gencode arch=compute_89,code=sm_89 \
+ -gencode arch=compute_90,code=sm_90 \
+ -gencode arch=compute_120,code=sm_120 \
+ -gencode arch=compute_120,code=compute_120
+else
+CUDA_GENCODE = -arch=$(CUDA_ARCH)
+endif
ifneq ($(IS_WIN),)
# nvcc's host compiler on Windows is MSVC cl.exe: the GCC-style
# -Xcompiler=-Wall,-Wextra is rejected ("D8021 invalid numeric argument
# '/Wextra'"), which made `make cuda-dll` unbuildable as shipped. -W3 is
# the MSVC warning level (dash form, NOT /W3: MSYS make would mangle the
# slash-form into a filesystem path).
-NVCCFLAGS ?= -O3 -std=c++17 -arch=$(CUDA_ARCH) -Xcompiler=-W3
+NVCCFLAGS ?= -O3 -std=c++17 $(CUDA_GENCODE) -Xcompiler=-W3
else
-NVCCFLAGS ?= -O3 -std=c++17 -arch=$(CUDA_ARCH) -Xcompiler=-Wall,-Wextra
+NVCCFLAGS ?= -O3 -std=c++17 $(CUDA_GENCODE) -Xcompiler=-Wall,-Wextra
endif
+# HIP=1 builds the SAME backend for AMD GPUs via ROCm: backend_cuda.cu is
+# compiled unchanged through backend_gpu_compat.h (one source, two vendors,
+# like compat.h does for Windows). HIP_ARCH=native targets the GPU in this
+# machine; set an explicit arch (e.g. HIP_ARCH=gfx1201) when distributing.
+HIP ?= 0
+ROCM_HOME ?= /opt/rocm
+HIPCC ?= $(ROCM_HOME)/bin/hipcc
+HIP_ARCH ?= native
+HIPCCFLAGS ?= -O3 -std=c++17 -x hip --offload-arch=$(HIP_ARCH) -Wall -Wextra
+GPUCC = $(NVCC)
+GPUFLAGS = $(NVCCFLAGS)
+GPUCC_NAME = nvcc (set CUDA_HOME or NVCC)
# PYTHON is a HOST tool (clean/test-c/test-python run it on the build machine),
# so key it off the host, NOT $(IS_WIN) — which is derived from the *target*
# triple ($(CC) -dumpmachine). Otherwise a cross build (make CC=x86_64-w64-
@@ -166,7 +199,7 @@ else
PYTHON ?= python3
endif
CUDA_OBJ =
-TEST_BINS = tests/test_json$(EXE) tests/test_st$(EXE) tests/test_st_pread$(EXE) tests/test_tier$(EXE) tests/test_grammar$(EXE) tests/test_schema_gbnf$(EXE) tests/test_decode_batch$(EXE) tests/test_idot$(EXE) tests/test_i4_grouped$(EXE) tests/test_stops$(EXE) tests/test_topp$(EXE) tests/test_sample_nan$(EXE) tests/test_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) tests/test_e8_kernel$(EXE)
ifneq (,$(LINUX))
TEST_BINS += tests/test_uring$(EXE)
endif
@@ -188,11 +221,26 @@ ifneq ($(IS_WIN),)
# On Windows use CUDA_DLL=1 (runtime DLL), not CUDA=1 (direct link).
$(error On Windows use: make CUDA_DLL=1 cuda-dll (see backend_loader.c))
endif
+ifeq ($(HIP),1)
+$(error choose CUDA=1 or HIP=1, not both)
+endif
CFLAGS += -DCOLI_CUDA
LDFLAGS += -L$(CUDA_HOME)/lib64 -Wl,-rpath,$(CUDA_HOME)/lib64 -lcudart -lstdc++
CUDA_OBJ = backend_cuda.o
endif
+ifeq ($(HIP),1)
+ifeq (,$(LINUX))
+$(error HIP=1 is supported only on Linux)
+endif
+CFLAGS += -DCOLI_CUDA
+LDFLAGS += -L$(ROCM_HOME)/lib -Wl,-rpath,$(ROCM_HOME)/lib -lamdhip64 -lstdc++
+CUDA_OBJ = backend_cuda.o
+GPUCC = $(HIPCC)
+GPUFLAGS = $(HIPCCFLAGS)
+GPUCC_NAME = hipcc (set ROCM_HOME or HIPCC)
+endif
+
# METAL=1 adds an opt-in Apple-GPU backend (macOS only). The shader is compiled at
# runtime, so no Xcode / offline metal compiler is required. Default build unchanged.
METAL ?= 0
@@ -207,27 +255,34 @@ LDFLAGS += -framework Metal -framework Foundation -lc++
METAL_OBJ = backend_metal.o
endif
-all: glm$(EXE)
+all: colibri$(EXE)
-# phony 'glm' → 'glm.exe' on Windows (so 'make glm' and 'coli build' work on every platform)
-glm: glm$(EXE)
+# phony targets — 'glm' kept for backward compatibility
+colibri: colibri$(EXE)
+glm: colibri$(EXE)
# Config stamp: make only tracks file timestamps, not flag changes. Without this,
-# `make glm.exe CUDA_DLL=1` after a prior CPU-only build reports "up to date" and
-# silently keeps the CPU-only binary (no CUDA loader) — a build that looks like it
-# worked but isn't. We record the build-affecting flags in .build-config and rewrite
-# it ONLY when they change (evaluated here at parse time, so the file's timestamp
-# moves exactly when the config moves). glm.exe and the CUDA/loader objects depend
+# `make colibri.exe CUDA_DLL=1` after a prior CPU-only build reports "up to date"
+# and silently keeps the CPU-only binary (no CUDA loader) — a build that looks like
+# it worked but isn't. We record the build-affecting flags in .build-config and
+# rewrite it ONLY when they change (evaluated here at parse time, so the file's
+# timestamp moves exactly when the config moves). The binary and CUDA/loader objects depend
# on it, so they relink on a config change and stay put otherwise. (#306)
-BUILD_CONFIG := $(CC)|$(CFLAGS)|$(LDFLAGS)|CUDA=$(CUDA)|CUDA_DLL=$(CUDA_DLL)|ARCH=$(ARCH)|CUDA_ARCH=$(CUDA_ARCH)|METAL=$(METAL)
+BUILD_CONFIG := $(CC)|$(CFLAGS)|$(LDFLAGS)|CUDA=$(CUDA)|CUDA_DLL=$(CUDA_DLL)|ARCH=$(ARCH)|CUDA_ARCH=$(CUDA_ARCH)|METAL=$(METAL)|HIP=$(HIP)|HIP_ARCH=$(HIP_ARCH)
BUILD_CONFIG_OLD := $(shell cat .build-config 2>/dev/null)
ifneq "$(BUILD_CONFIG)" "$(BUILD_CONFIG_OLD)"
-$(shell printf '%s' '$(BUILD_CONFIG)' > .build-config)
+# $(file ...) writes via make's own primitive (GNU Make >= 4.0, 2013), NOT by
+# spawning a shell. The earlier `$(shell printf '%s' ... > .build-config)`
+# required a POSIX `printf` on PATH — present under MSYS2/Git-Bash but NOT under
+# cmd.exe, so `make glm.exe` from the VS Native Tools prompt or with scoop MinGW
+# (neither ships sh.exe) failed with "'printf' is not recognized" and left the
+# stamp stale. $(file ...) works regardless of the shell make falls back to. (#478)
+$(file >.build-config,$(BUILD_CONFIG))
endif
.build-config: ;
-glm$(EXE): glm.c st.h uring.h json.h tok.h tok_unicode.h compat.h grammar.h $(CUDA_OBJ) $(METAL_OBJ) .build-config
- $(CC) $(CFLAGS) glm.c $(CUDA_OBJ) $(METAL_OBJ) -o glm$(EXE) $(LDFLAGS)
+colibri$(EXE): colibri.c st.h uring.h json.h tok.h tok_unicode.h compat.h grammar.h quant.h sample.h kv_persist.h telemetry.h $(CUDA_OBJ) $(METAL_OBJ) .build-config
+ $(CC) $(CFLAGS) colibri.c $(CUDA_OBJ) $(METAL_OBJ) -o colibri$(EXE) $(LDFLAGS)
# Windows runtime loader object: resolves coli_cuda_* from coli_cuda.dll.
backend_loader.o: backend_loader.c backend_cuda.h compat.h .build-config
@@ -245,9 +300,9 @@ cuda-dll: backend_cuda.cu backend_cuda.h
-L"$(CUDA_HOME)/lib/x64" -lcudart \
backend_cuda.cu -o coli_cuda.dll
-backend_cuda.o: backend_cuda.cu backend_cuda.h .build-config
- @command -v "$(NVCC)" >/dev/null 2>&1 || { echo "nvcc not found: set CUDA_HOME or NVCC" >&2; exit 1; }
- "$(NVCC)" $(NVCCFLAGS) -c backend_cuda.cu -o $@
+backend_cuda.o: backend_cuda.cu backend_cuda.h backend_gpu_compat.h .build-config
+ @command -v "$(GPUCC)" >/dev/null 2>&1 || { echo "$(GPUCC_NAME) not found" >&2; exit 1; }
+ "$(GPUCC)" $(GPUFLAGS) -c backend_cuda.cu -o $@
backend_metal.o: backend_metal.mm backend_metal.h
$(METALXX) -c backend_metal.mm -o $@
@@ -256,14 +311,23 @@ metal-test: tests/test_backend_metal.mm backend_metal.mm backend_metal.h
$(METALXX) tests/test_backend_metal.mm backend_metal.mm -framework Metal -framework Foundation -o backend_metal_test
./backend_metal_test
-cuda-test: backend_cuda.cu backend_cuda.h tests/test_backend_cuda.cu
- @command -v "$(NVCC)" >/dev/null 2>&1 || { echo "nvcc not found: set CUDA_HOME or NVCC" >&2; exit 1; }
- "$(NVCC)" $(NVCCFLAGS) backend_cuda.cu tests/test_backend_cuda.cu -o backend_cuda_test$(EXE)
+cuda-test: backend_cuda.cu backend_cuda.h backend_gpu_compat.h tests/test_backend_cuda.cu
+ @command -v "$(GPUCC)" >/dev/null 2>&1 || { echo "$(GPUCC_NAME) not found" >&2; exit 1; }
+ "$(GPUCC)" $(GPUFLAGS) backend_cuda.cu tests/test_backend_cuda.cu -o backend_cuda_test$(EXE)
./backend_cuda_test$(EXE)
-cuda-bench: backend_cuda.cu backend_cuda.h tests/bench_tensor_core.cu
- @command -v "$(NVCC)" >/dev/null 2>&1 || { echo "nvcc not found: set CUDA_HOME or NVCC" >&2; exit 1; }
- "$(NVCC)" $(NVCCFLAGS) backend_cuda.cu tests/bench_tensor_core.cu -o backend_cuda_bench$(EXE)
+# convenience alias: kernel correctness on AMD (same test, hipcc toolchain)
+hip-test:
+ $(MAKE) cuda-test HIP=1
+
+# CI: compile the backend and its test binary WITHOUT executing them (for
+# runners with the toolchain but no GPU). Pass CUDA_ARCH/HIP=1+HIP_ARCH.
+gpu-compile: backend_cuda.o
+ "$(GPUCC)" $(GPUFLAGS) backend_cuda.cu tests/test_backend_cuda.cu -o backend_cuda_test$(EXE)
+
+cuda-bench: backend_cuda.cu backend_cuda.h backend_gpu_compat.h tests/bench_tensor_core.cu
+ @command -v "$(GPUCC)" >/dev/null 2>&1 || { echo "$(GPUCC_NAME) not found" >&2; exit 1; }
+ "$(GPUCC)" $(GPUFLAGS) backend_cuda.cu tests/bench_tensor_core.cu -o backend_cuda_bench$(EXE)
./backend_cuda_bench$(EXE)
olmoe$(EXE): olmoe.c st.h json.h compat.h
@@ -272,8 +336,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))
@@ -285,7 +350,7 @@ PORTABLE_ARCH = native
endif
portable:
- $(MAKE) glm$(EXE) ARCH=$(PORTABLE_ARCH)
+ $(MAKE) colibri$(EXE) ARCH=$(PORTABLE_ARCH)
iobench$(EXE): iobench.c compat.h
$(CC) $(CFLAGS) iobench.c -o iobench$(EXE) $(LDFLAGS)
@@ -293,12 +358,17 @@ 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)
tests/test_st$(EXE): tests/test_st.c st.h json.h compat.h
$(CC) $(CFLAGS) $< -o $@ $(LDFLAGS)
+tests/test_st_mirror$(EXE): tests/test_st_mirror.c st.h json.h compat.h
+ $(CC) $(CFLAGS) $< -o $@ $(LDFLAGS)
+
tests/test_tier$(EXE): tests/test_tier.c tier.h
$(CC) $(CFLAGS) $< -o $@ $(LDFLAGS)
@@ -311,30 +381,41 @@ tests/test_schema_gbnf$(EXE): tests/test_schema_gbnf.c schema_gbnf.h grammar.h j
tests/test_decode_batch$(EXE): tests/test_decode_batch.c decode_batch.h
$(CC) $(CFLAGS) $< -o $@ $(LDFLAGS)
-tests/test_idot$(EXE): tests/test_idot.c glm.c st.h uring.h json.h tok.h tok_unicode.h compat.h grammar.h tier.h
+tests/test_idot$(EXE): tests/test_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_i4_grouped$(EXE): tests/test_i4_grouped.c glm.c st.h uring.h json.h tok.h tok_unicode.h compat.h grammar.h tier.h
+tests/test_i4_grouped$(EXE): tests/test_i4_grouped.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_stops$(EXE): tests/test_stops.c glm.c st.h uring.h json.h tok.h tok_unicode.h compat.h grammar.h tier.h
+tests/test_stops$(EXE): tests/test_stops.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_topp$(EXE): tests/test_topp.c glm.c st.h uring.h json.h tok.h tok_unicode.h compat.h grammar.h tier.h
+tests/test_topp$(EXE): tests/test_topp.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_topp is a microbenchmark (old qsort vs new heap partial-select, #335), NOT a test
# gate -- intentionally absent from TEST_BINS. Build on demand: make tests/bench_topp
-tests/bench_topp$(EXE): tests/bench_topp.c glm.c st.h uring.h json.h tok.h tok_unicode.h compat.h grammar.h tier.h
+tests/bench_topp$(EXE): tests/bench_topp.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_sample_nan$(EXE): tests/test_sample_nan.c glm.c st.h uring.h json.h tok.h tok_unicode.h compat.h grammar.h tier.h
+tests/test_sample_nan$(EXE): tests/test_sample_nan.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_kv_alloc$(EXE): tests/test_kv_alloc.c glm.c st.h json.h tok.h tok_unicode.h compat.h grammar.h tier.h
+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_logit_nan$(EXE): tests/test_logit_nan.c glm.c st.h uring.h json.h tok.h tok_unicode.h compat.h grammar.h tier.h
+# fmt=6 kernel oracle: needs the generated grid table, and its fixture comes from
+# the reference codec (tools/make_e8_fixture.py) — regenerate if the layout moves.
+tests/test_e8_kernel$(EXE): tests/test_e8_kernel.c quant.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)
tests/test_i4_acc512$(EXE): tests/test_i4_acc512.c
@@ -343,15 +424,28 @@ tests/test_i4_acc512$(EXE): tests/test_i4_acc512.c
tests/test_compat_direct$(EXE): tests/test_compat_direct.c compat.h
$(CC) $(CFLAGS) $< -o $@ $(LDFLAGS)
-tests/test_dsa_select$(EXE): tests/test_dsa_select.c glm.c st.h uring.h json.h tok.h tok_unicode.h compat.h grammar.h tier.h
+tests/test_dsa_select$(EXE): tests/test_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_dsa_select is a microbenchmark (old qsort vs new quickselect partial-select, #356),
# NOT a test gate -- intentionally absent from TEST_BINS. Build on demand: make tests/bench_dsa_select
-tests/bench_dsa_select$(EXE): tests/bench_dsa_select.c glm.c st.h uring.h json.h tok.h tok_unicode.h compat.h grammar.h tier.h
+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)
+
+# bench_mla_simd: microbenchmark (scalar vs AVX2/NEON MLA-absorb reductions, #442),
+# NOT a test gate. Build on demand: make tests/bench_mla_simd
+tests/bench_mla_simd$(EXE): tests/bench_mla_simd.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_uring$(EXE): tests/test_uring.c glm.c st.h uring.h json.h tok.h tok_unicode.h compat.h grammar.h tier.h
+tests/test_pipe_block$(EXE): tests/test_pipe_block.c colibri.c st.h uring.h json.h tok.h tok_unicode.h compat.h grammar.h tier.h
$(CC) $(CFLAGS) $< -o $@ $(LDFLAGS)
test-c: $(TEST_BINS)
@@ -362,18 +456,41 @@ 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
$(MAKE) portable
$(MAKE) test
-install: glm$(EXE) olmoe$(EXE)
+install: colibri$(EXE) olmoe$(EXE)
$(INSTALL) -d $(DESTDIR)$(BINDIR)
$(INSTALL) -d $(DESTDIR)$(LIBEXECDIR)
$(INSTALL) -d $(DESTDIR)$(LIBEXECDIR)/tools
$(INSTALL) -m 755 coli $(DESTDIR)$(BINDIR)/coli
- $(INSTALL) -m 755 glm$(EXE) $(DESTDIR)$(LIBEXECDIR)/glm$(EXE)
+ $(INSTALL) -m 755 colibri$(EXE) $(DESTDIR)$(LIBEXECDIR)/colibri$(EXE)
$(INSTALL) -m 755 olmoe$(EXE) $(DESTDIR)$(LIBEXECDIR)/olmoe$(EXE)
$(INSTALL) -m 644 resource_plan.py doctor.py openai_server.py $(DESTDIR)$(LIBEXECDIR)/
$(INSTALL) -m 644 tools/*.py $(DESTDIR)$(LIBEXECDIR)/tools/
@@ -387,4 +504,4 @@ clean:
bench: iobench$(EXE)
@if [ -n "$(ARGS)" ]; then ./iobench$(EXE) $(ARGS); else echo "built iobench$(EXE) — run: ./iobench$(EXE) "; fi
-.PHONY: all glm cuda-test cuda-bench cuda-dll portable test-c test-python test check clean install uninstall bench
+.PHONY: all colibri glm cuda-test hip-test gpu-compile cuda-bench cuda-dll portable test-c test-python test check clean install uninstall bench
diff --git a/c/backend_cuda.cu b/c/backend_cuda.cu
index 413e53b4..5c98b1fb 100644
--- a/c/backend_cuda.cu
+++ b/c/backend_cuda.cu
@@ -1,7 +1,6 @@
#include "backend_cuda.h"
-#include
-#include
+#include "backend_gpu_compat.h"
#include
#include
@@ -20,6 +19,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 +36,18 @@ 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;
+ cudaEvent_t ev_done; int ev_done_ok; /* resident-group issue completion (#431 PR-C0) */
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) {
- DeviceContext *ctx = find_ctx(device);
- if (!tensor || !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) 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;
- return t->fmt == fmt && t->I == I && t->O == O && t->device == device;
+ 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 (!weights || I < 1 || O < 1 || !select_ctx(ctx)) return 0;
+ size_t rb = row_bytes(fmt, I);
+ if (!rb || (fmt && !scales)) return 0;
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,40 @@ 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;
+ /* fmt=4 carries [O, ceil(I/gs)] scales: without the group size the plain
+ * upload truncates the buffer to O floats and quant_matmul divides by
+ * gs==0. Callers must come through the gs>0 path (upload_g) or stay on
+ * the CPU (#298, #334). */
+ if (fmt == 4 && gs <= 0) 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 +685,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 +694,12 @@ 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;
+ /* same reason as coli_cuda_matmul: fmt=4 without recorded group info would
+ * misread the scales (and divide by gs==0 in the kernel). */
+ if (gate && ((gate->fmt == 4 && gate->gs <= 0) ||
+ (up && up->fmt == 4 && up->gs <= 0) ||
+ (down && down->fmt == 4 && down->gs <= 0))) 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 +714,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,10 +728,11 @@ 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;
- DeviceContext *ctx=find_ctx(gate->device);if(!select_ctx(ctx)||ctx->compute_major<7)return 0;
+ DeviceContext *ctx=find_ctx(gate->device);if(!select_ctx(ctx)||!COLI_GPU_HAS_WMMA||ctx->compute_major<7)return 0;
int D=gate->I,I=gate->O;size_t xb=(size_t)S*D*sizeof(float),ib=(size_t)S*I*sizeof(float);
if(!reserve(&ctx->x,&ctx->x_cap,xb)||!reserve(&ctx->gate,&ctx->gate_cap,ib)||
!reserve(&ctx->up,&ctx->up_cap,ib)||!reserve(&ctx->y,&ctx->y_cap,xb)||
@@ -636,19 +760,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;
@@ -665,7 +794,8 @@ extern "C" int coli_cuda_expert_group(ColiCudaTensor *const *gates,
if(!cuda_ok(copy_desc,"expert group descriptors"))return 0;
int profile=getenv("COLI_CUDA_PROFILE")&&atoi(getenv("COLI_CUDA_PROFILE"));
cudaEvent_t ev[4]={};
- if(profile) for(int i=0;i<4;i++) if(!cuda_ok(cudaEventCreate(&ev[i]),"profile event")) profile=0;
+ if(profile) for(int i=0;i<4;i++) if(!cuda_ok(cudaEventCreate(&ev[i]),"profile event")){
+ for(int j=0;jstream);
if(async)std::memcpy(ctx->host_x,x,xb);
cudaError_t copy_x=async?cudaMemcpyAsync(ctx->x,ctx->host_x,xb,cudaMemcpyHostToDevice,ctx->stream)
@@ -688,8 +818,16 @@ 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);
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"))){
+ }else if(all_s4&&COLI_GPU_HAS_WMMA&&ctx->compute_major>=7&&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 +849,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 +868,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 +906,80 @@ 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,
+ g->gs,u->gs,d->gs};
+ 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 +992,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 +1015,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 +1033,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 +1121,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 +1132,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 +1147,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 +1210,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 +1234,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 +1243,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 +1252,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 +1260,189 @@ 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;
+}
+/* ---- resident expert-group accumulation (#431 PR-C0) ----------------------
+ * Decode-time (S=1) expert groups without the host round-trip: the input row
+ * is P2P'd from the layer's home device, the group runs through the grouped-W4
+ * kernels on its own stream, the down-projection outputs are weighted and
+ * reduced ON DEVICE (fixed expert order), and the device's partial sum is
+ * peer-pushed into a per-issue slot on the home device. take() makes the home
+ * legacy stream wait on every issue event and reduces the slots in issue order
+ * — deterministic, no atomics, no host bytes. The CPU tier overlaps with all
+ * of it exactly as before. */
+__global__ static void bcast_row(float *dst,const float *src,int count,int D){
+ for(int i=blockIdx.x*blockDim.x+threadIdx.x;i64||!x_src_dev||!partial_slot_dev) return 0;
+ ColiCudaTensor *first=gates[0]; if(!first) return 0;
+ int device=first->device,D=first->I,I=first->O;
+ GroupDesc host[64];
+ int total=0,all_s4=1;
+ 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,1,total,
+ g->gs,u->gs,d->gs};
+ all_s4&=g->fmt==2&&u->fmt==2&&d->fmt==2;
+ total++;
+ }
+ if(!all_s4) return 0; /* resident path: per-row int4 only */
+ DeviceContext *ctx=find_ctx(device); if(!select_ctx(ctx)) return 0;
+ if(!ctx->ev_done_ok){
+ if(!cuda_ok(cudaEventCreateWithFlags(&ctx->ev_done,cudaEventDisableTiming),
+ "resident group event")) return 0;
+ ctx->ev_done_ok=1;
+ }
+ /* size for the 64-expert cap, not for `count`: reserve() reallocs on growth,
+ * and a realloc here could free a buffer the PREVIOUS layer's still-queued
+ * async work on this stream reads. Fixed caps make re-issue realloc-free. */
+ size_t xb=(size_t)64*D*sizeof(float), ib=(size_t)64*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(&ctx->ac,&ctx->ac_cap,(size_t)(D+64)*sizeof(float))||
+ !reserve_bytes(&ctx->group_desc,&ctx->group_desc_cap,(size_t)64*sizeof(GroupDesc)))
+ return 0;
+ float *w_dev=ctx->ac+D, *partial_local=ctx->ac;
+ if(!cuda_ok(cudaMemcpyAsync(ctx->group_desc,host,(size_t)count*sizeof(GroupDesc),
+ cudaMemcpyHostToDevice,ctx->stream),"resident group desc")||
+ !cuda_ok(cudaMemcpyAsync(w_dev,weights,(size_t)count*sizeof(float),
+ cudaMemcpyHostToDevice,ctx->stream),"resident group weights"))
+ return 0;
+ /* input row: P2P from the home device. The caller guarantees x_src_dev is
+ * materialized (the pre-moe nrm download already synced the home stream). */
+ if(!cuda_ok(cudaMemcpyPeerAsync(ctx->x,device,x_src_dev,home_device,
+ (size_t)D*sizeof(float),ctx->stream),"resident group x p2p"))
+ return 0;
+ bcast_row<<<64,256,0,ctx->stream>>>(ctx->x,ctx->x,count,D); /* row 0 -> rows 1..count-1 (in-place safe: row 0 rewritten with itself) */
+ GroupDesc *dev=(GroupDesc*)ctx->group_desc;
+ dim3 hg((unsigned)I,1,(unsigned)count),og((unsigned)D,1,(unsigned)count);
+ grouped_hidden_w4_dual<<stream>>>(ctx->gate,ctx->up,ctx->x,dev,I,D);
+ silu_mul<<<(unsigned)(((size_t)count*I+255)/256),256,0,ctx->stream>>>(ctx->gate,ctx->up,(size_t)count*I);
+ grouped_down_w4<<stream>>>(ctx->y,ctx->gate,dev,D,I);
+ weighted_sum_rows<<<48,256,0,ctx->stream>>>(partial_local,ctx->y,w_dev,count,D);
+ if(!cuda_ok(cudaMemcpyPeerAsync(partial_slot_dev,home_device,partial_local,device,
+ (size_t)D*sizeof(float),ctx->stream),"resident partial p2p"))
+ return 0;
+ if(!cuda_ok(cudaEventRecord(ctx->ev_done,ctx->stream),"resident event record")) return 0;
+ return cuda_ok(cudaGetLastError(),"resident group launch");
+}
+extern "C" int coli_cuda_expert_group_resident_take(int home_device,const int *devices,int n_issued,
+ float *slots_dev,float *acc_dev,int D){
+ if(n_issued<1||!slots_dev||!acc_dev||D<1) return 0;
+ DeviceContext *home=find_ctx(home_device); if(!select_ctx(home)) return 0;
+ for(int i=0;iev_done_ok) return 0;
+ if(!cuda_ok(cudaStreamWaitEvent(0,src->ev_done,0),"resident take wait")) return 0;
+ }
+ sum_slots<<<48,256>>>(acc_dev,slots_dev,n_issued,D); /* legacy stream: ordered with pipe_* */
+ return cuda_ok(cudaGetLastError(),"resident take reduce");
+}
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 +1455,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 +1464,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 +1477,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 +1499,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 +1519,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 +1528,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 +1541,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 +1556,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 +1565,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 63c1f113..b83c58a9 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,16 @@ 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_expert_group_resident_issue(ColiCudaTensor *const *gates,
+ ColiCudaTensor *const *ups, ColiCudaTensor *const *downs,
+ const float *weights, int count,
+ int home_device, const float *x_src_dev, float *partial_slot_dev);
+COLI_CUDA_DLLEXPORT int coli_cuda_expert_group_resident_take(int home_device,const int *devices,
+ int n_issued,float *slots_dev,float *acc_dev,int D);
+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_gpu_compat.h b/c/backend_gpu_compat.h
new file mode 100644
index 00000000..990462af
--- /dev/null
+++ b/c/backend_gpu_compat.h
@@ -0,0 +1,66 @@
+/* backend_gpu_compat.h — one GPU backend source, two vendors.
+ * Same pattern as compat.h for Windows: every platform difference lives in
+ * this header and backend_cuda.cu stays untouched. Compiled by nvcc this is
+ * a pass-through to the CUDA runtime (and mma.h for the tensor-core paths);
+ * compiled by hipcc (ROCm, HIP=1) it maps the CUDA runtime surface
+ * backend_cuda.cu uses onto HIP 1:1. The kernel language (__global__,
+ * __shared__, <<<>>>) is shared syntax.
+ *
+ * COLI_GPU_HAS_WMMA: the WMMA tensor-core kernels are guarded by
+ * __CUDA_ARCH__ >= 700 (device side) and by this flag at the host dispatch
+ * sites. Under HIP the flag is 0: gfx GPUs report compute_major >= 7, so a
+ * runtime-only check would select empty kernel bodies. Matrix-core support
+ * via rocWMMA is a possible follow-up; until then HIP always uses the
+ * portable kernels. */
+#ifndef COLIBRI_BACKEND_GPU_COMPAT_H
+#define COLIBRI_BACKEND_GPU_COMPAT_H
+
+#if defined(__HIP_PLATFORM_AMD__) || defined(__HIP__)
+#include
+#include
+#define COLI_GPU_HAS_WMMA 0
+#define cudaError_t hipError_t
+#define cudaSuccess hipSuccess
+#define cudaGetErrorString hipGetErrorString
+#define cudaGetLastError hipGetLastError
+#define cudaSetDevice hipSetDevice
+#define cudaGetDeviceCount hipGetDeviceCount
+#define cudaDeviceProp hipDeviceProp_t
+#define cudaGetDeviceProperties hipGetDeviceProperties
+#define cudaMalloc hipMalloc
+#define cudaFree hipFree
+#define cudaMemcpy hipMemcpy
+#define cudaMemcpy2D hipMemcpy2D
+#define cudaMemcpyAsync hipMemcpyAsync
+#define cudaMemcpyHostToDevice hipMemcpyHostToDevice
+#define cudaMemcpyDeviceToHost hipMemcpyDeviceToHost
+#define cudaMemGetInfo hipMemGetInfo
+#define cudaStream_t hipStream_t
+#define cudaStreamCreate hipStreamCreate
+#define cudaStreamCreateWithFlags hipStreamCreateWithFlags
+#define cudaStreamNonBlocking hipStreamNonBlocking
+#define cudaStreamDestroy hipStreamDestroy
+#define cudaStreamSynchronize hipStreamSynchronize
+#define cudaStreamWaitEvent hipStreamWaitEvent
+#define cudaDeviceSynchronize hipDeviceSynchronize
+#define cudaEvent_t hipEvent_t
+#define cudaEventCreate hipEventCreate
+#define cudaEventCreateWithFlags hipEventCreateWithFlags
+#define cudaEventDisableTiming hipEventDisableTiming
+#define cudaEventDestroy hipEventDestroy
+#define cudaEventRecord hipEventRecord
+#define cudaEventSynchronize hipEventSynchronize
+#define cudaEventElapsedTime hipEventElapsedTime
+#define cudaMallocHost hipHostMalloc
+#define cudaFreeHost hipHostFree
+#define cudaMemcpyDeviceToDevice hipMemcpyDeviceToDevice
+#define cudaMemcpyPeer hipMemcpyPeer
+#define cudaMemcpyPeerAsync hipMemcpyPeerAsync
+#define cudaMemsetAsync hipMemsetAsync
+#else
+#include
+#include
+#define COLI_GPU_HAS_WMMA 1
+#endif
+
+#endif
diff --git a/c/backend_loader.c b/c/backend_loader.c
index eedbd50b..21be5484 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,9 @@ 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_group_resident_issue)(ColiCudaTensor *const *gates,ColiCudaTensor *const *ups,ColiCudaTensor *const *downs,const float *weights,int count,int home_device,const float *x_src_dev,float *partial_slot_dev);
+typedef int (*fn_group_resident_take)(int home_device,const int *devices,int n_issued,float *slots_dev,float *acc_dev,int D);
+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 +109,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 +135,9 @@ static struct {
fn_pipe_peer_copy pipe_peer_copy;
fn_pipe_rmsnorm pipe_rmsnorm;
fn_pipe_rmsnorm_s pipe_rmsnorm_s;
+ fn_group_resident_issue expert_group_resident_issue;
+ fn_group_resident_take expert_group_resident_take;
+ 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 +209,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 +235,9 @@ 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(expert_group_resident_issue, fn_group_resident_issue)
+ RESOLVE(expert_group_resident_take, fn_group_resident_take)
+ 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 +310,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 +336,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 +446,21 @@ 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_expert_group_resident_issue(ColiCudaTensor *const *gates,ColiCudaTensor *const *ups,ColiCudaTensor *const *downs,const float *weights,int count,int home_device,const float *x_src_dev,float *partial_slot_dev){
+ if(!g_cuda.available || !g_cuda.expert_group_resident_issue){ return 0; }
+ return g_cuda.expert_group_resident_issue(gates, ups, downs, weights, count, home_device, x_src_dev, partial_slot_dev);
+}
+
+int coli_cuda_expert_group_resident_take(int home_device,const int *devices,int n_issued,float *slots_dev,float *acc_dev,int D){
+ if(!g_cuda.available || !g_cuda.expert_group_resident_take){ return 0; }
+ return g_cuda.expert_group_resident_take(home_device, devices, n_issued, slots_dev, acc_dev, D);
+}
+
+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 4383b3b6..35c4931c 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 51ed8a09..6ec3fb4f 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_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 @@ kernel void r_top8(device const float* sig [[buffer(0)]], device const float* bi
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 @@ static size_t fmt_bytes(int fmt, int I, int O) {
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 @@ static size_t fmt_bytes(int fmt, int I, int O) {
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 @@ static size_t fmt_bytes(int fmt, int I, int O) {
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 @@ static size_t fmt_bytes(int fmt, int I, int O) {
}
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 @@ static bool resolve_attn(const AttnW *W, float *Lc, float *Rc,
// 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 @@ static bool resolve_attn(const AttnW *W, float *Lc, float *Rc,
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 bool resolve_attn(const AttnW *W, float *Lc, float *Rc,
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 bool resolve_attn(const AttnW *W, float *Lc, float *Rc,
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 803c22b5..133d21e6 100755
--- a/c/coli
+++ b/c/coli
@@ -15,6 +15,9 @@ Run GLM-5.2 (744B) locally on CPU with roughly 15-26 GB of RAM.
Configuration through environment variables or flags (also valid after the subcommand):
COLI_MODEL= model directory (default /home/vincenzo/glm52_i4)
+ COLI_MODEL_MIRROR= second copy of the model on another drive: expert reads
+ are split across both SSDs (COLI_DISK_WEIGHTS=9,3 sets the
+ primary,mirror bandwidth ratio; default: measured at startup)
--ram N RAM budget in GB (automatically sizes the expert cache)
--repin N adapt RAM/VRAM experts every N tokens
--topp P adaptive expert top-p --topk N fixed top-k
@@ -54,16 +57,20 @@ from version import __version__ as _version
# guess is right (e.g. a custom packaging layout).
_EXE = ".exe" if sys.platform == "win32" else ""
_LIBEXEC = os.path.join(os.path.dirname(HERE), "libexec", "colibri")
+_here_colibri = os.path.join(HERE, "colibri" + _EXE)
_here_glm = os.path.join(HERE, "glm" + _EXE)
if os.environ.get("COLI_ENGINE"):
GLM = os.environ["COLI_ENGINE"]
TOOLS = os.path.join(os.path.dirname(GLM), "tools")
+elif os.path.exists(_here_colibri):
+ GLM = _here_colibri
+ TOOLS = os.path.join(HERE, "tools")
elif os.path.exists(_here_glm):
GLM = _here_glm
TOOLS = os.path.join(HERE, "tools")
else:
- GLM = os.path.join(_LIBEXEC, "glm" + _EXE)
+ GLM = os.path.join(_LIBEXEC, "colibri" + _EXE)
TOOLS = os.path.join(_LIBEXEC, "tools")
sys.path.insert(0, _LIBEXEC) # so `import resource_plan`, `doctor`, `openai_server` still resolve
@@ -233,6 +240,55 @@ 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).
+ # An explicit COLI_CUDA=0 in the environment must win over the implicit
+ # auto-enable: before this check, a Windows user setting COLI_CUDA=0 for
+ # a CPU baseline silently got a ~12.6 GB VRAM expert tier anyway (the
+ # engine's "CPU" rows were GPU-assisted). --gpu none remains the
+ # canonical hard off-switch (works on every platform, also clears the
+ # CUDA_* sizing vars).
+ if (sys.platform == "win32" and a.gpu is None and not a.vram
+ and e.get("COLI_CUDA") != "0"):
+ 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.
+ elif e.get("COLI_CUDA") == "0":
+ # honoured off-switch: also drop stale device/sizing vars so the
+ # engine can't be re-enabled by leftovers (same as --gpu none).
+ e.pop("COLI_GPU",None); e.pop("COLI_GPUS",None)
+ e.pop("CUDA_EXPERT_GB",None); e.pop("CUDA_DENSE",None)
# --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:
@@ -241,13 +297,13 @@ def env_for(a):
e["COLI_CUDA"]="0"; e.pop("CUDA_EXPERT_GB",None); e.pop("CUDA_DENSE",None)
else:
if not cuda_binary():
- sys.exit(f"{C.yel}--gpu needs the CUDA build:{C.r} make glm CUDA=1 (this binary is CPU-only)")
+ sys.exit(f"{C.yel}--gpu needs the CUDA build:{C.r} make colibri CUDA=1 (this binary is CPU-only)")
e["COLI_CUDA"]="1"
if a.gpu!="auto": e["COLI_GPUS"]=a.gpu
e.setdefault("CUDA_DENSE","1")
if a.vram and a.gpu!="none":
if not cuda_binary():
- sys.exit(f"{C.yel}--vram needs the CUDA build:{C.r} make glm CUDA=1 (this binary is CPU-only)")
+ sys.exit(f"{C.yel}--vram needs the CUDA build:{C.r} make colibri CUDA=1 (this binary is CPU-only)")
e["COLI_CUDA"]="1"; e["CUDA_EXPERT_GB"]=str(a.vram)
return e
@@ -421,8 +477,8 @@ def cmd_build(a):
banner("build")
if not os.path.exists(os.path.join(HERE, "Makefile")):
sys.exit(f"{C.yel}coli build{C.r} only works from a source checkout (this is an installed copy).\n"
- f" Clone https://github.com/JustVugg/colibri and run ./setup.sh, or make -C c glm.")
- sys.exit(subprocess.call(["make","-C",HERE,"glm"]))
+ f" Clone https://github.com/JustVugg/colibri and run ./setup.sh, or make -C c colibri.")
+ sys.exit(subprocess.call(["make","-C",HERE,"colibri"]))
def cmd_info(a):
banner("info")
@@ -802,7 +858,7 @@ def cmd_stop(a):
if "coli" in cmd and " serve" in cmd and pid!=os.getpid():
if not any(p==pid for p,_ in targets): targets.append((pid,"coli serve (cmdline)"))
comm=open(f"/proc/{pd}/comm").read().strip()
- if comm in ("glm","exe","olmoe"):
+ if comm in ("colibri","glm","exe","olmoe"):
env=open(f"/proc/{pd}/environ","rb").read().replace(b"\0",b"\n").decode("utf-8","replace")
if "SERVE=1" in env: targets.append((pid,f"engine `{comm}` (SERVE=1)"))
except (OSError,PermissionError): continue
diff --git a/c/glm.c b/c/colibri.c
similarity index 76%
rename from c/glm.c
rename to c/colibri.c
index a3e7ab74..fef41369 100644
--- a/c/glm.c
+++ b/c/colibri.c
@@ -69,25 +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 */
-#endif
-#ifdef __AVX2__
-#include
-static inline float hsum256(__m256 v){ /* somma orizzontale di 8 float */
- __m128 lo=_mm256_castps256_ps128(v), hi=_mm256_extractf128_ps(v,1);
- lo=_mm_add_ps(lo,hi); __m128 sh=_mm_movehl_ps(lo,lo); lo=_mm_add_ps(lo,sh);
- sh=_mm_shuffle_ps(lo,lo,1); lo=_mm_add_ss(lo,sh); return _mm_cvtss_f32(lo);
-}
-#elif defined(__ARM_NEON)
-#include /* Apple Silicon / aarch64: kernel NEON */
-#elif defined(__VSX__)
-#include /* POWER8+ (ppc64le): kernel VSX */
-#undef vector /* igiene: si usano __vector/__bool espliciti */
-#undef pixel
-#undef bool
+/* 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
@@ -107,7 +94,16 @@ 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=6 (E8/IQ3 lattice, #452): 98B per 256 weights = 3.0625 bits/weight, grid
+ * indices + parity-packed signs + sub-scales + fp16 super-scale, ALL inside q4 —
+ * `s` is unused for this format (see quant.h E8_* and tools/iq3_pack.py).
+ * 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
@@ -123,6 +119,12 @@ 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; }
+ if(t->fmt==6) /* E8/IQ3: 98B per 256 weights, scales in-block, .qs is a 4-byte tag */
+ return (int64_t)t->O*(((int64_t)t->I+255)/256)*98 + 4;
return (int64_t)t->O*((t->I+1)/2) + (int64_t)t->O*4; /* fmt=2 int4 per-row */
}
@@ -140,6 +142,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;
@@ -148,7 +154,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;
@@ -178,11 +189,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 */
@@ -218,13 +252,29 @@ typedef struct {
uint64_t bytes_mtp, bytes_main; /* byte letti da disco per tipo layer */
} Model;
-static void usage_save(Model *m); /* cache che impara: definita accanto a stats_dump */
-static void tiers_emit(Model *m);
-static void ehit_mark(Model *m, int layer, int eid);
-static void emap_emit(Model *m);
-static void hits_emit(Model *m);
-static void hwinfo_emit(Model *m);
-static int64_t expert_bytes_probe(Model *m, int ebits); /* PROF: tier sizes in the report */
+#include "quant.h"
+static int g_no_fused_pair=0;
+static int g_spec_pin=1;
+static int g_spec_live=0;
+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); }
+}
+
static int g_repin;
static uint64_t g_last_repin;
#ifdef COLI_CUDA
@@ -233,6 +283,7 @@ static double g_cuda_expert_gb;
static int g_cuda_expert_auto;
static int g_cuda_dense;
static int g_cuda_release_host;
+static double g_cuda_reserve_gb; /* CUDA_RESERVE_GB: VRAM headroom kept free of expert tier (default 2 GB) */
static int g_cuda_devices[COLI_CUDA_MAX_DEVICES], g_cuda_ndev, g_cuda_rr;
static int64_t g_cuda_dense_projected[COLI_CUDA_MAX_DEVICES];
static void qt_cuda_reset(QT *t){
@@ -240,8 +291,14 @@ static void qt_cuda_reset(QT *t){
t->cuda_failed=0;
}
static int qt_cuda_upload(QT *t){
+ if(t->fmt==5||t->fmt==6) return 0; /* int3-g64 / E8: 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){
@@ -249,6 +306,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);
@@ -264,6 +322,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;
@@ -305,6 +366,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) */
@@ -314,6 +434,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;
@@ -324,6 +446,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){
@@ -332,687 +461,62 @@ static float *falloc(int64_t n){
if(n<0 || (uint64_t)n > SIZE_MAX/sizeof(float)){ fprintf(stderr,"falloc: n=%lld is out of range\n",(long long)n); exit(1); }
float *p=malloc((size_t)n*sizeof(float)); if(!p){fprintf(stderr,"OOM\n");exit(1);} return p; }
-/* ---- Accumulatore int4->float a 512 bit / 512-bit int4->float accumulator ----
- * Stessa matematica lossless di matmul_i4 (nibble->f32, FMA), ma 32 pesi/iter su
- * due catene FMA indipendenti. NON bit-identico al vecchio ordine: la riduzione
- * ad albero accumula MENO errore della somma sequenziale (misurato 2-4x più
- * vicino all'oracolo double sulle forme reali; perplexity invariata, +4-7% sul
- * decode con routing CPU-heavy — vedi docs/experiments/glm52-6x5090-2026-07-12.md).
- * EN: same lossless math as matmul_i4, 32 weights/iter on two independent FMA
- * chains. Not bit-identical to the old order: tree reduction accumulates LESS
- * rounding than sequential summation. I4_ACC512=0 restores the old order (A/B). */
-#if defined(__AVX512F__) && defined(__AVX512BW__)
-static int g_i4_acc512=1;
-static inline float dot_i4f_avx512(const uint8_t *w,const float *x,int I){
- const __m128i m4=_mm_set1_epi8(0x0F); const __m512i b8=_mm512_set1_epi32(8);
- __m512 acc0=_mm512_setzero_ps(),acc1=_mm512_setzero_ps(); int i=0;
- for(;i+32<=I;i+=32){ __m128i by=_mm_loadu_si128((const __m128i*)(w+(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);
- __m512 w0=_mm512_cvtepi32_ps(_mm512_sub_epi32(_mm512_cvtepu8_epi32(n0),b8));
- __m512 w1=_mm512_cvtepi32_ps(_mm512_sub_epi32(_mm512_cvtepu8_epi32(n1),b8));
- acc0=_mm512_fmadd_ps(_mm512_loadu_ps(x+i),w0,acc0);
- acc1=_mm512_fmadd_ps(_mm512_loadu_ps(x+i+16),w1,acc1);
- }
- return _mm512_reduce_add_ps(_mm512_add_ps(acc0,acc1));
-}
-/* selftest contro il riferimento scalare (I4_ACC512_TEST=1): copre l'ordine dei
- * nibble e ogni multiplo di 32. / selftest vs the scalar reference. */
-static int i4_acc512_selftest(void){
- enum { N=224 }; uint8_t w[(N+1)/2]; float x[N];
- for(int i=0;i>1]=(uint8_t)(q+8);
- else w[i>>1]|=(uint8_t)((q+8)<<4);
- x[i]=(float)(((i*29+7)%101)-50)/37.f;
- }
- for(int n=32;n<=N;n+=32){
- float ref=0; for(int i=0;i>1]>>((i&1)*4))&15)-8);
- float got=dot_i4f_avx512(w,x,n),tol=2e-5f*(1.f+fabsf(ref));
- if(fabsf(got-ref)>tol){ fprintf(stderr,"AVX512 i4 selftest n=%d: %.9g != %.9g\n",n,got,ref); return 0; }
- }
- return 1;
-}
-#endif
-/* y[S,O] = x[S,I] @ W^T, W[O,I] f32 */
-static void matmul(float *y, const float *x, const float *W, int S, int I, int O){
- #pragma omp parallel for schedule(static)
- for (int o=0;o>1))); /* 8 byte=16 nibble */
- __m128i lo=_mm_and_si128(by,m4), hi=_mm_and_si128(_mm_srli_epi16(by,4),m4);
- __m128i nib=_mm_unpacklo_epi8(lo,hi); /* nibble in ordine */
- __m256 w0=_mm256_cvtepi32_ps(_mm256_sub_epi32(_mm256_cvtepu8_epi32(nib),b8));
- __m256 w1=_mm256_cvtepi32_ps(_mm256_sub_epi32(_mm256_cvtepu8_epi32(_mm_srli_si128(nib,8)),b8));
- acc=_mm256_fmadd_ps(_mm256_loadu_ps(xs+i), w0, acc);
- acc=_mm256_fmadd_ps(_mm256_loadu_ps(xs+i+8), w1, acc); }
- a=hsum256(acc);
-#elif defined(__ARM_NEON)
- const uint8x8_t m4=vdup_n_u8(0x0F); const int8x8_t b8=vdup_n_s8(8);
- float32x4_t ac0=vdupq_n_f32(0), ac1=vdupq_n_f32(0);
- for(;i+16<=I;i+=16){ uint8x8_t by=vld1_u8(w+(i>>1)); /* 8 byte=16 nibble */
- uint8x8x2_t z=vzip_u8(vand_u8(by,m4), vshr_n_u8(by,4)); /* nibble in ordine */
- int16x8_t w0=vmovl_s8(vsub_s8(vreinterpret_s8_u8(z.val[0]),b8));
- int16x8_t w1=vmovl_s8(vsub_s8(vreinterpret_s8_u8(z.val[1]),b8));
- ac0=vfmaq_f32(ac0, vld1q_f32(xs+i), vcvtq_f32_s32(vmovl_s16(vget_low_s16(w0))));
- ac1=vfmaq_f32(ac1, vld1q_f32(xs+i+4), vcvtq_f32_s32(vmovl_s16(vget_high_s16(w0))));
- ac0=vfmaq_f32(ac0, vld1q_f32(xs+i+8), vcvtq_f32_s32(vmovl_s16(vget_low_s16(w1))));
- ac1=vfmaq_f32(ac1, vld1q_f32(xs+i+12), vcvtq_f32_s32(vmovl_s16(vget_high_s16(w1)))); }
- a=vaddvq_f32(vaddq_f32(ac0,ac1));
-#endif
-#if defined(__AVX512F__) && defined(__AVX512BW__)
- }
-#endif
- for(;i+1>1]; int lo=(int)(byte&0xF)-8, hi=(int)(byte>>4)-8;
- a += xs[i]*(float)lo + xs[i+1]*(float)hi; }
- if(i>1]; int lo=(int)(byte&0xF)-8; a += xs[i]*(float)lo; }
- y[(int64_t)s*O+o]=a*sc; } }
-}
-/* y[S,O] = x[S,I] @ W^T with W int4 packed (2/byte) + per-GROUP scales (fmt=4).
- * Same nibble math as matmul_i4, but the scale changes every `gs` elements along I.
- * The accumulator resets at each group boundary: dot(x[grp], w[grp]) * scale[grp].
- * gs MUST be a multiple of 16 (the AVX2 vector width). */
-static void matmul_i4_grouped(float *y, const float *x, const uint8_t *q4, const float *scale,
- int S, int I, int O, int gs){
+
+/* 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 sc=scl[g];
+ 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 acc=_mm256_setzero_ps();
- for(; i+16<=base+glen; i+=16){ __m128i by=_mm_loadl_epi64((const __m128i*)(w+(i>>1)));
- __m128i lo=_mm_and_si128(by,m4),hi=_mm_and_si128(_mm_srli_epi16(by,4),m4);
- __m128i nib=_mm_unpacklo_epi8(lo,hi);
- __m256 w0=_mm256_cvtepi32_ps(_mm256_sub_epi32(_mm256_cvtepu8_epi32(nib),b8));
- __m256 w1=_mm256_cvtepi32_ps(_mm256_sub_epi32(_mm256_cvtepu8_epi32(_mm_srli_si128(nib,8)),b8));
- acc=_mm256_fmadd_ps(_mm256_loadu_ps(xs+i), w0, acc);
- acc=_mm256_fmadd_ps(_mm256_loadu_ps(xs+i+8), w1, acc); }
- a+=hsum256(acc)*sc;
-#endif
- /* scalar tail for the group remainder */
- for(; i>1];
- a+=(xs[i]*(float)((int)(byte&0xF)-8)+xs[i+1]*(float)((int)(byte>>4)-8))*sc; }
- else { uint8_t byte=w[i>>1]; a+=xs[i]*(float)((int)(byte&0xF)-8)*sc; }
+ __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);
}
- }
- y[(int64_t)s*O+o]=a;
- }
- }
-}
-/* Decode hot path for gate+up: same exact q4 dot products as matmul_i4, but one
- * OpenMP dispatch covers both matrices. KTransformers uses persistent pools;
- * this keeps colibri dependency-free while removing one team launch/expert. */
-static void matmul_i4_pair(float *yg, float *yu, const float *x,
- const uint8_t *qg, const float *sg,
- const uint8_t *qu, const float *su, int I, int O){
- int rb=(I+1)/2;
- #pragma omp parallel for schedule(static)
- for(int z=0;z<2*O;z++){
- int o=z>1)));
- __m128i lo=_mm_and_si128(by,m4),hi=_mm_and_si128(_mm_srli_epi16(by,4),m4);
- __m128i nib=_mm_unpacklo_epi8(lo,hi);
- __m256 w0=_mm256_cvtepi32_ps(_mm256_sub_epi32(_mm256_cvtepu8_epi32(nib),b8));
- __m256 w1=_mm256_cvtepi32_ps(_mm256_sub_epi32(_mm256_cvtepu8_epi32(_mm_srli_si128(nib,8)),b8));
- acc=_mm256_fmadd_ps(_mm256_loadu_ps(x+i),w0,acc);
- acc=_mm256_fmadd_ps(_mm256_loadu_ps(x+i+8),w1,acc); }
- a=hsum256(acc);
-#elif defined(__ARM_NEON)
- const uint8x8_t m4=vdup_n_u8(0x0F); const int8x8_t b8=vdup_n_s8(8);
- float32x4_t ac0=vdupq_n_f32(0),ac1=vdupq_n_f32(0);
- for(;i+16<=I;i+=16){ uint8x8_t by=vld1_u8(w+(i>>1));
- uint8x8x2_t n=vzip_u8(vand_u8(by,m4),vshr_n_u8(by,4));
- int16x8_t w0=vmovl_s8(vsub_s8(vreinterpret_s8_u8(n.val[0]),b8));
- int16x8_t w1=vmovl_s8(vsub_s8(vreinterpret_s8_u8(n.val[1]),b8));
- ac0=vfmaq_f32(ac0,vld1q_f32(x+i),vcvtq_f32_s32(vmovl_s16(vget_low_s16(w0))));
- ac1=vfmaq_f32(ac1,vld1q_f32(x+i+4),vcvtq_f32_s32(vmovl_s16(vget_high_s16(w0))));
- ac0=vfmaq_f32(ac0,vld1q_f32(x+i+8),vcvtq_f32_s32(vmovl_s16(vget_low_s16(w1))));
- ac1=vfmaq_f32(ac1,vld1q_f32(x+i+12),vcvtq_f32_s32(vmovl_s16(vget_high_s16(w1)))); }
- a=vaddvq_f32(vaddq_f32(ac0,ac1));
-#endif
-#if defined(__AVX512F__) && defined(__AVX512BW__)
- }
-#endif
- for(;i+1>1]; a+=x[i]*(float)((b&15)-8)+x[i+1]*(float)((b>>4)-8); }
- if(i>1]&15)-8);
- (z=2) non calcolano la STESSA funzione. Tre interruttori dipendono da S: il gate
- * int4-IDOT (S>=g_i4s — asimmetrico proprio dove g_i4s>1), la fusione gate+up solo-S==1,
- * e la soglia righe del GEMM Metal. Con SPEC_PIN=1 (default) ogni forward emesso mentre
- * i draft del modello sono attivi resta sulla famiglia di kernel di S=1: draft e verifica
- * coincidono per costruzione. Prefill e decode non speculativo sono intoccati.
- * EN: MTP acceptance collapses when the draft (S=1) and verify (S>=2) forwards do not
- * compute the SAME function. Three switches are S-dependent: the int4 IDOT gate
- * (S>=g_i4s — asymmetric exactly on ISAs where g_i4s>1), the S==1-only gate+up fusion,
- * and the Metal GEMM row threshold. SPEC_PIN=1 (default) pins every forward issued
- * while model drafts are live to the platform's S=1 kernel family, so draft and verify
- * agree by construction; prefill and non-speculative decode are untouched.
- * SPEC_PIN=0 restores the S-dependent gates (A/B). */
-static int g_spec_pin=1;
-static int g_spec_live=0; /* set by spec_decode while drafts are live */
-static inline int spec_pinned(void){ return g_spec_pin && g_spec_live; }
-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 { matmul_qt(g,x,wg,S); matmul_qt(u,x,wu,S); }
-}
-/* y[S,O] = x[S,I] @ W^T con W int2 impacchettato (4 valori/byte) + scala[O]. nibble 2-bit -> [-2,1]. */
-static void matmul_i2(float *y, const float *x, const uint8_t *q2, const float *scale, int S, int I, int O){
- int rb=(I+3)/4;
- #pragma omp parallel for schedule(static)
- for (int o=0;o>2))); /* 4 byte=16 valori */
- __m128i p0=_mm_and_si128(by,m2), p1=_mm_and_si128(_mm_srli_epi16(by,2),m2);
- __m128i p2=_mm_and_si128(_mm_srli_epi16(by,4),m2), p3=_mm_and_si128(_mm_srli_epi16(by,6),m2);
- __m128i lo=_mm_unpacklo_epi8(p0,p1), hi=_mm_unpacklo_epi8(p2,p3);
- __m128i nib=_mm_unpacklo_epi16(lo,hi); /* 16 valori in ordine */
- __m256 w0=_mm256_cvtepi32_ps(_mm256_sub_epi32(_mm256_cvtepu8_epi32(nib),b2));
- __m256 w1=_mm256_cvtepi32_ps(_mm256_sub_epi32(_mm256_cvtepu8_epi32(_mm_srli_si128(nib,8)),b2));
- acc=_mm256_fmadd_ps(_mm256_loadu_ps(xs+i), w0, acc);
- acc=_mm256_fmadd_ps(_mm256_loadu_ps(xs+i+8), w1, acc); }
- a=hsum256(acc);
-#elif defined(__ARM_NEON)
- const uint8x8_t m2v=vdup_n_u8(3); const int8x8_t b2v=vdup_n_s8(2);
- float32x4_t ac0=vdupq_n_f32(0), ac1=vdupq_n_f32(0);
- for(;i+16<=I;i+=16){ uint32_t wd; memcpy(&wd, w+(i>>2), 4); /* 4 byte=16 valori */
- 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]));
- int16x8_t w0=vmovl_s8(vsub_s8(vreinterpret_s8_u16(zz.val[0]),b2v)); /* 16 valori in ordine */
- int16x8_t w1=vmovl_s8(vsub_s8(vreinterpret_s8_u16(zz.val[1]),b2v));
- ac0=vfmaq_f32(ac0, vld1q_f32(xs+i), vcvtq_f32_s32(vmovl_s16(vget_low_s16(w0))));
- ac1=vfmaq_f32(ac1, vld1q_f32(xs+i+4), vcvtq_f32_s32(vmovl_s16(vget_high_s16(w0))));
- ac0=vfmaq_f32(ac0, vld1q_f32(xs+i+8), vcvtq_f32_s32(vmovl_s16(vget_low_s16(w1))));
- ac1=vfmaq_f32(ac1, vld1q_f32(xs+i+12), vcvtq_f32_s32(vmovl_s16(vget_high_s16(w1)))); }
- a=vaddvq_f32(vaddq_f32(ac0,ac1));
-#endif
- for(;i>2]; int sh=(i&3)*2; a += xs[i]*(float)((int)((byte>>sh)&3)-2); }
- y[(int64_t)s*O+o]=a*sc; } }
-}
-/* ---- KERNEL INTERI (IDOT): attivazioni quantizzate a int8 per riga (absmax/127,
- * stile Q8_0), prodotto scalare INTERO via maddubs/madd AVX2 — niente conversione
- * f32 dei pesi nel ciclo caldo. ~2-3x sui matmul quantizzati; errore aggiunto ~0.3%
- * RMS per matmul (attivazione int8), IDOT=0 torna al percorso f32 esatto. */
-#if defined(__AVX512VNNI__) && defined(__AVX512BW__)
-#define IDOT_KERNEL "avx512-vnni"
-#elif defined(__AVXVNNI__) && defined(__AVX2__)
-#define IDOT_KERNEL "avx-vnni"
-#elif defined(__AVX2__)
-#define IDOT_KERNEL "avx2"
-#elif defined(__ARM_NEON) && defined(__ARM_FEATURE_MATMUL_INT8)
-#define IDOT_KERNEL "neon-i8mm"
-#elif defined(__ARM_NEON)
-#define IDOT_KERNEL "neon"
-#elif defined(__VSX__)
-#define IDOT_KERNEL "vsx"
-#else
-#define IDOT_KERNEL "scalar"
-#endif
-static int g_idot=1;
-#if defined(__ARM_NEON) && defined(__ARM_FEATURE_DOTPROD)
-static int g_i4s=1; /* SDOT presente: int4 IDOT conviene anche a S=1 (decode). Misurato
- * su Apple M-series: +14%%, expert-matmul -16%%. EN: with SDOT, int4
- * IDOT pays even at S=1 (decode); measured on Apple M-series. */
-#elif defined(__VSX__)
-static int g_i4s=1; /* POWER8 vec_msum: qui il fallback f32 e' SCALARE, quindi l'IDOT
- * int4 conviene anche a S=1. Misurato su POWER8 S824 (vedi PR).
- * EN: on VSX the f32 fallback is plain scalar C, so int4 IDOT
- * pays even at S=1. Measured on a POWER8 S824 (see PR). */
-#else
-static int g_i4s=2; /* senza SDOT / altrove: soglia originale (misura AVX2 dell'autore).
- * EN: without SDOT / elsewhere: original threshold (author's AVX2). */
-#endif
-static inline float qrow_i8(const float *x, int8_t *q, int I){
- float amax=0; for(int i=0;iamax)amax=a; }
- float s=amax/127.f; if(s<1e-12f) s=1e-12f; float inv=1.f/s;
- for(int i=0;i s32 directly, 64 bytes/iter, no 16-bit intermediate.
- * AVX-512 has no vpsignb: |w| via abs, sign folded into x with a mask-negate
- * (w==0 -> product 0 either way). |x|<=127 (qrow_i8), |w|<=128 as u8: each
- * s32 lane adds <= 4*128*127, safe up to I=16384 like the AVX2 bound. */
- __m512i acc=_mm512_setzero_si512();
- for(;i+64<=I;i+=64){
- __m512i wv=_mm512_loadu_si512((const void*)(w+i));
- __m512i xv=_mm512_loadu_si512((const void*)(x+i));
- __mmask64 neg=_mm512_movepi8_mask(wv);
- __m512i xs=_mm512_mask_sub_epi8(xv,neg,_mm512_setzero_si512(),xv);
- acc=_mm512_dpbusd_epi32(acc,_mm512_abs_epi8(wv),xs);
- }
- sum=_mm512_reduce_add_epi32(acc);
-#elif defined(__AVXVNNI__) && defined(__AVX2__)
- /* AVX-VNNI 128-bit: vpdpbusd u8*s8 -> s32, 16 byte/iter. Stesso trucco del
- * segno della variante 512-bit: |w| via abs, segno piegato in x con maschera
- * (w==0 -> product 0). __AVX2__ serve per _mm_sign_epi8 / abs. */
- __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); /* x * sign(w); _mm_sign zona __AVX2__ */
- acc=_mm_dpbusd_epi32(acc,_mm_abs_epi8(wv),xs);
- }
- sum=hsum128_i32(acc);
-#elif defined(__AVX2__)
- __m256i acc=_mm256_setzero_si256(); const __m256i ones=_mm256_set1_epi16(1);
- for(;i+32<=I;i+=32){
- __m256i wv=_mm256_loadu_si256((const __m256i*)(w+i));
- __m256i xv=_mm256_loadu_si256((const __m256i*)(x+i));
- __m256i p=_mm256_maddubs_epi16(_mm256_sign_epi8(wv,wv),_mm256_sign_epi8(xv,wv));
- acc=_mm256_add_epi32(acc,_mm256_madd_epi16(p,ones));
- }
- sum=hsum256_i32(acc);
-#elif defined(__ARM_NEON)
- /* ARM: SDOT nativo se disponibile (Apple Silicon: sempre); altrimenti vmull/vpadal.
- * Stesso bound anti-overflow del trucco AVX2: coppie <= 128*127*2 = 32512 < 32767. */
-#if defined(__ARM_FEATURE_DOTPROD)
- /* 4 accumulatori indipendenti: SDOT ha latenza ~3-4 cicli, con un solo acc la
- * catena seriale strozza il core a ~26 GB/s di pesi; con 4 lane indipendenti il
- * dot diventa memory-bound (misurato su M4: 26 -> 63 GB/s per core, 2.4x). */
- int32x4_t a0=vdupq_n_s32(0),a1=vdupq_n_s32(0),a2=vdupq_n_s32(0),a3=vdupq_n_s32(0);
- for(;i+64<=I;i+=64){
- a0=vdotq_s32(a0,vld1q_s8(w+i), vld1q_s8(x+i));
- a1=vdotq_s32(a1,vld1q_s8(w+i+16),vld1q_s8(x+i+16));
- a2=vdotq_s32(a2,vld1q_s8(w+i+32),vld1q_s8(x+i+32));
- a3=vdotq_s32(a3,vld1q_s8(w+i+48),vld1q_s8(x+i+48));
- }
- int32x4_t acc=vaddq_s32(vaddq_s32(a0,a1),vaddq_s32(a2,a3));
- for(;i+16<=I;i+=16) acc=vdotq_s32(acc,vld1q_s8(w+i),vld1q_s8(x+i));
- sum=vaddvq_s32(acc);
-#else
- int32x4_t acc=vdupq_n_s32(0);
- for(;i+16<=I;i+=16){
- int8x16_t wv=vld1q_s8(w+i), xv=vld1q_s8(x+i);
- int16x8_t p=vmull_s8(vget_low_s8(wv),vget_low_s8(xv));
- p=vmlal_s8(p,vget_high_s8(wv),vget_high_s8(xv));
- acc=vpadalq_s16(acc,p);
- }
- sum=vaddvq_s32(acc);
-#endif
-#elif defined(__VSX__)
- /* POWER8: vec_msum (s8 x u8 -> s32) somma i prodotti byte DIRETTAMENTE in lane
- * s32, 16 byte/iter: il bound anti-saturazione a 16 bit di maddubs qui non serve.
- * Stesso trucco del segno (|w| u8 per x*sign(w) s8), ma |w| via select+sub MODULO
- * e non vec_abs: -128 deve diventare 128 u8, non saturare a 127.
- * EN: vec_msum accumulates byte products straight into s32 lanes; |w| is built
- * with a modulo subtract select instead of vec_abs so w=-128 wraps to 128 (u8)
- * rather than saturating to 127. |x|<=127 from qrow_i8, so x negation is safe. */
- __vector signed int acc=vec_splats(0);
- const __vector signed char vz=vec_splats((signed char)0);
- for(;i+16<=I;i+=16){
- __vector signed char wv=vec_xl(0,(const signed char*)(w+i));
- __vector signed char xv=vec_xl(0,(const signed char*)(x+i));
- __vector __bool char neg=vec_cmplt(wv,vz);
- __vector signed char xs=vec_sel(xv,vec_sub(vz,xv),neg);
- __vector unsigned char wa=(__vector unsigned char)vec_sel(wv,vec_sub(vz,wv),neg);
- acc=vec_msum(xs,wa,acc);
- }
- sum=vec_extract(acc,0)+vec_extract(acc,1)+vec_extract(acc,2)+vec_extract(acc,3);
-#endif
- for(;i int8 [-8,7] al volo, poi stesso trucco */
-static inline int32_t dot_i4i8(const uint8_t *w4, const int8_t *x, int I){
- int32_t sum=0; int i=0;
-#if defined(__AVX512VNNI__) && defined(__AVX512BW__)
- /* 32 bytes = 64 nibbles -> int8 in [-8,7], one vpdpbusd per 64 values.
- * 256-bit unpack leaves values in per-128-lane order [0-15][32-47]/[16-31][48-63];
- * dot pairing is order-invariant, so permute x's 128-bit blocks to match
- * instead of re-ordering w (one vpermq per iter, off the critical unpack path). */
- const __m256i m4v=_mm256_set1_epi8(0x0F);
- const __m512i b8v=_mm512_set1_epi8(8);
- const __m512i xidx=_mm512_setr_epi64(0,1,4,5,2,3,6,7);
- __m512i acc=_mm512_setzero_si512();
- for(;i+64<=I;i+=64){
- __m256i by=_mm256_loadu_si256((const __m256i*)(w4+(i>>1)));
- __m256i lo=_mm256_and_si256(by,m4v), hi=_mm256_and_si256(_mm256_srli_epi16(by,4),m4v);
- __m256i z0=_mm256_unpacklo_epi8(lo,hi), z1=_mm256_unpackhi_epi8(lo,hi);
- __m512i wv=_mm512_sub_epi8(_mm512_inserti64x4(_mm512_castsi256_si512(z0),z1,1),b8v);
- __m512i xv=_mm512_permutexvar_epi64(xidx,_mm512_loadu_si512((const void*)(x+i)));
- __mmask64 neg=_mm512_movepi8_mask(wv);
- __m512i xs=_mm512_mask_sub_epi8(xv,neg,_mm512_setzero_si512(),xv);
- acc=_mm512_dpbusd_epi32(acc,_mm512_abs_epi8(wv),xs);
- }
- sum=_mm512_reduce_add_epi32(acc);
-#elif defined(__AVXVNNI__) && defined(__AVX2__)
- /* AVX-VNNI 128-bit, int4: 16 byte = 32 nibble -> int8 [-8,7] in due half
- * (n0/n1), ciascuno alimentato a un vpdpbusd da 16 byte. Stesso unpack
- * 128-bit del ramo AVX2 sotto; 32 elementi/iter come li. */
- 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 by=_mm_loadu_si128((const __m128i*)(w4+(i>>1))); /* 16 byte = 32 nibble */
- __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); /* nibble in ordine */
- __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);
-#elif defined(__AVX2__)
- const __m128i m4=_mm_set1_epi8(0x0F); const __m256i b8=_mm256_set1_epi8(8);
- const __m256i ones=_mm256_set1_epi16(1);
- __m256i acc=_mm256_setzero_si256();
- for(;i+32<=I;i+=32){
- __m128i by=_mm_loadu_si128((const __m128i*)(w4+(i>>1))); /* 16 byte = 32 nibble */
- __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); /* in ordine */
- __m256i wv=_mm256_sub_epi8(_mm256_set_m128i(n1,n0),b8);
- __m256i xv=_mm256_loadu_si256((const __m256i*)(x+i));
- __m256i p=_mm256_maddubs_epi16(_mm256_sign_epi8(wv,wv),_mm256_sign_epi8(xv,wv));
- acc=_mm256_add_epi32(acc,_mm256_madd_epi16(p,ones));
- }
- sum=hsum256_i32(acc);
-#elif defined(__ARM_NEON)
- const uint8x16_t m4q=vdupq_n_u8(0x0F); const int8x16_t b8q=vdupq_n_s8(8);
-#if defined(__ARM_FEATURE_DOTPROD)
- /* 4 accumulatori indipendenti (vedi dot_i8i8): spezza la catena seriale su acc.
- * Misurato su M4: 12.4 -> 29.9 GB/s di pesi per core (2.4x). */
- int32x4_t a0=vdupq_n_s32(0),a1=vdupq_n_s32(0),a2=vdupq_n_s32(0),a3=vdupq_n_s32(0);
- for(;i+64<=I;i+=64){
- uint8x16_t byA=vld1q_u8(w4+(i>>1)), byB=vld1q_u8(w4+(i>>1)+16);
- uint8x16x2_t zA=vzipq_u8(vandq_u8(byA,m4q), vshrq_n_u8(byA,4)); /* nibble in ordine */
- uint8x16x2_t zB=vzipq_u8(vandq_u8(byB,m4q), vshrq_n_u8(byB,4));
- a0=vdotq_s32(a0,vsubq_s8(vreinterpretq_s8_u8(zA.val[0]),b8q),vld1q_s8(x+i));
- a1=vdotq_s32(a1,vsubq_s8(vreinterpretq_s8_u8(zA.val[1]),b8q),vld1q_s8(x+i+16));
- a2=vdotq_s32(a2,vsubq_s8(vreinterpretq_s8_u8(zB.val[0]),b8q),vld1q_s8(x+i+32));
- a3=vdotq_s32(a3,vsubq_s8(vreinterpretq_s8_u8(zB.val[1]),b8q),vld1q_s8(x+i+48));
- }
- int32x4_t acc=vaddq_s32(vaddq_s32(a0,a1),vaddq_s32(a2,a3));
- for(;i+32<=I;i+=32){
- uint8x16_t by=vld1q_u8(w4+(i>>1)); /* 16 byte = 32 nibble */
- uint8x16x2_t z=vzipq_u8(vandq_u8(by,m4q), vshrq_n_u8(by,4)); /* nibble in ordine */
- acc=vdotq_s32(acc,vsubq_s8(vreinterpretq_s8_u8(z.val[0]),b8q),vld1q_s8(x+i));
- acc=vdotq_s32(acc,vsubq_s8(vreinterpretq_s8_u8(z.val[1]),b8q),vld1q_s8(x+i+16));
- }
- sum=vaddvq_s32(acc);
-#else
- int32x4_t acc=vdupq_n_s32(0);
- for(;i+32<=I;i+=32){
- uint8x16_t by=vld1q_u8(w4+(i>>1)); /* 16 byte = 32 nibble */
- uint8x16x2_t z=vzipq_u8(vandq_u8(by,m4q), vshrq_n_u8(by,4)); /* nibble in ordine */
- int8x16_t w0=vsubq_s8(vreinterpretq_s8_u8(z.val[0]),b8q);
- int8x16_t w1=vsubq_s8(vreinterpretq_s8_u8(z.val[1]),b8q);
- int8x16_t x0=vld1q_s8(x+i), x1=vld1q_s8(x+i+16);
- int16x8_t p=vmull_s8(vget_low_s8(w0),vget_low_s8(x0)); /* |w|<=8: nessun overflow */
- p=vmlal_s8(p,vget_high_s8(w0),vget_high_s8(x0));
- acc=vpadalq_s16(acc,p);
- p=vmull_s8(vget_low_s8(w1),vget_low_s8(x1));
- p=vmlal_s8(p,vget_high_s8(w1),vget_high_s8(x1));
- acc=vpadalq_s16(acc,p);
- }
- sum=vaddvq_s32(acc);
-#endif
-#elif defined(__VSX__)
- /* 16 byte = 32 nibble. vec_mergeh/vec_mergel su ppc64le (GCC) interallacciano come
- * unpacklo/unpackhi x86 (verificato empiricamente su POWER8): i nibble escono in
- * ordine di memoria. |w|<=8 dopo il -8, quindi stesso trucco del segno di dot_i8i8.
- * EN: vec_mergeh/l on ppc64le interleave like x86 unpacklo/hi (verified on POWER8),
- * so nibbles come out in memory order; then the same sign trick as dot_i8i8. */
- const __vector unsigned char m4v=vec_splats((unsigned char)0x0F);
- const __vector unsigned char sh4=vec_splats((unsigned char)4);
- const __vector signed char b8v=vec_splats((signed char)8);
- const __vector signed char vz=vec_splats((signed char)0);
- __vector signed int acc=vec_splats(0);
- for(;i+32<=I;i+=32){
- __vector unsigned char by=vec_xl(0,w4+(i>>1)); /* 16 byte = 32 nibble */
- __vector unsigned char lo=vec_and(by,m4v), hi=vec_sr(by,sh4);
- __vector signed char w0=vec_sub((__vector signed char)vec_mergeh(lo,hi),b8v);
- __vector signed char w1=vec_sub((__vector signed char)vec_mergel(lo,hi),b8v);
- __vector signed char x0=vec_xl(0,(const signed char*)(x+i));
- __vector signed char x1=vec_xl(0,(const signed char*)(x+i+16));
- __vector __bool char n0=vec_cmplt(w0,vz), n1=vec_cmplt(w1,vz);
- acc=vec_msum(vec_sel(x0,vec_sub(vz,x0),n0),
- (__vector unsigned char)vec_sel(w0,vec_sub(vz,w0),n0),acc);
- acc=vec_msum(vec_sel(x1,vec_sub(vz,x1),n1),
- (__vector unsigned char)vec_sel(w1,vec_sub(vz,w1),n1),acc);
- }
- sum=vec_extract(acc,0)+vec_extract(acc,1)+vec_extract(acc,2)+vec_extract(acc,3);
-#endif
- for(;i+1>1]; sum+=((int)(b&0xF)-8)*x[i]+((int)(b>>4)-8)*x[i+1]; }
- if(i>1]; sum+=((int)(b&0xF)-8)*x[i]; }
- return sum;
-}
-#if defined(__ARM_NEON) && defined(__ARM_FEATURE_MATMUL_INT8)
-/* SMMLA (i8mm): vmmlaq_s32 vede ogni int8x16_t come matrice 2x8 row-major (byte 0-7 =
- * riga 0, byte 8-15 = riga 1) e accumula C += A*B^T nel 2x2 int32: lane0=a0.b0,
- * lane1=a0.b1, lane2=a1.b0, lane3=a1.b1. vcombine di due mezze-righe costruisce la
- * matrice: A = due righe di peso (o,o+1), B = due righe di attivazione (s,s+1), quindi
- * meta' traffico pesi e doppio lavoro per istruzione a S>=2. EN: vmmlaq_s32 treats each
- * int8x16_t as a 2x8 row-major matrix and does C += A*B^T on a 2x2 int32 tile; vcombine
- * of vget_low/high halves builds the 2-row register from two weight/activation rows. */
-static inline int32x4_t mm_tile16(int32x4_t acc, int8x16_t wo, int8x16_t wo1,
- int8x16_t xs, int8x16_t xs1){
- acc=vmmlaq_s32(acc, vcombine_s8(vget_low_s8(wo), vget_low_s8(wo1)),
- vcombine_s8(vget_low_s8(xs), vget_low_s8(xs1)));
- return vmmlaq_s32(acc, vcombine_s8(vget_high_s8(wo), vget_high_s8(wo1)),
- vcombine_s8(vget_high_s8(xs), vget_high_s8(xs1)));
-}
-static void matmul_q_idot_mm(float *y, const int8_t *xq, const float *sx, const int8_t *q,
- const float *scale, int S, int I, int O){
- #pragma omp parallel for schedule(static)
- for(int o=0;o<(O&~1);o+=2){
- const int8_t *wo=q+(int64_t)o*I, *wo1=q+(int64_t)(o+1)*I;
- float sc0=scale[o], sc1=scale[o+1];
- for(int s=0;s<(S&~1);s+=2){
- const int8_t *xs=xq+(int64_t)s*I, *xs1=xq+(int64_t)(s+1)*I;
- /* 4 accumulatori indipendenti: una sola catena vmmla e' latency-bound.
- * EN: 4 independent accumulators; a single vmmla chain is latency-bound. */
- int32x4_t a0=vdupq_n_s32(0),a1=vdupq_n_s32(0),a2=vdupq_n_s32(0),a3=vdupq_n_s32(0); int i=0;
- for(;i+64<=I;i+=64){
- a0=mm_tile16(a0,vld1q_s8(wo+i), vld1q_s8(wo1+i), vld1q_s8(xs+i), vld1q_s8(xs1+i));
- a1=mm_tile16(a1,vld1q_s8(wo+i+16),vld1q_s8(wo1+i+16),vld1q_s8(xs+i+16),vld1q_s8(xs1+i+16));
- a2=mm_tile16(a2,vld1q_s8(wo+i+32),vld1q_s8(wo1+i+32),vld1q_s8(xs+i+32),vld1q_s8(xs1+i+32));
- a3=mm_tile16(a3,vld1q_s8(wo+i+48),vld1q_s8(wo1+i+48),vld1q_s8(xs+i+48),vld1q_s8(xs1+i+48));
- }
- for(;i+16<=I;i+=16)
- a0=mm_tile16(a0,vld1q_s8(wo+i),vld1q_s8(wo1+i),vld1q_s8(xs+i),vld1q_s8(xs1+i));
- int32x4_t acc=vaddq_s32(vaddq_s32(a0,a1),vaddq_s32(a2,a3));
- int32_t d00=vgetq_lane_s32(acc,0), d01=vgetq_lane_s32(acc,1);
- int32_t d10=vgetq_lane_s32(acc,2), d11=vgetq_lane_s32(acc,3);
- for(;i>1)), byo1=vld1q_u8(wo1+(i>>1));
- uint8x16_t cyo=vld1q_u8(wo+(i>>1)+16), cyo1=vld1q_u8(wo1+(i>>1)+16);
- uint8x16x2_t zo =vzipq_u8(vandq_u8(byo, m4q), vshrq_n_u8(byo, 4));
- uint8x16x2_t zo1=vzipq_u8(vandq_u8(byo1,m4q), vshrq_n_u8(byo1,4));
- uint8x16x2_t ko =vzipq_u8(vandq_u8(cyo, m4q), vshrq_n_u8(cyo, 4));
- uint8x16x2_t ko1=vzipq_u8(vandq_u8(cyo1,m4q), vshrq_n_u8(cyo1,4));
- a0=mm_tile16(a0, vsubq_s8(vreinterpretq_s8_u8(zo.val[0]),b8q),
- vsubq_s8(vreinterpretq_s8_u8(zo1.val[0]),b8q),
- vld1q_s8(xs+i), vld1q_s8(xs1+i));
- a1=mm_tile16(a1, vsubq_s8(vreinterpretq_s8_u8(zo.val[1]),b8q),
- vsubq_s8(vreinterpretq_s8_u8(zo1.val[1]),b8q),
- vld1q_s8(xs+i+16), vld1q_s8(xs1+i+16));
- a2=mm_tile16(a2, vsubq_s8(vreinterpretq_s8_u8(ko.val[0]),b8q),
- vsubq_s8(vreinterpretq_s8_u8(ko1.val[0]),b8q),
- vld1q_s8(xs+i+32), vld1q_s8(xs1+i+32));
- a3=mm_tile16(a3, vsubq_s8(vreinterpretq_s8_u8(ko.val[1]),b8q),
- vsubq_s8(vreinterpretq_s8_u8(ko1.val[1]),b8q),
- vld1q_s8(xs+i+48), vld1q_s8(xs1+i+48));
- }
- for(;i+32<=I;i+=32){
- uint8x16_t byo=vld1q_u8(wo+(i>>1)), byo1=vld1q_u8(wo1+(i>>1));
- uint8x16x2_t zo =vzipq_u8(vandq_u8(byo, m4q), vshrq_n_u8(byo, 4));
- uint8x16x2_t zo1=vzipq_u8(vandq_u8(byo1,m4q), vshrq_n_u8(byo1,4));
- a0=mm_tile16(a0, vsubq_s8(vreinterpretq_s8_u8(zo.val[0]),b8q),
- vsubq_s8(vreinterpretq_s8_u8(zo1.val[0]),b8q),
- vld1q_s8(xs+i), vld1q_s8(xs1+i));
- a1=mm_tile16(a1, vsubq_s8(vreinterpretq_s8_u8(zo.val[1]),b8q),
- vsubq_s8(vreinterpretq_s8_u8(zo1.val[1]),b8q),
- vld1q_s8(xs+i+16), vld1q_s8(xs1+i+16));
+ 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;
+ }
}
- int32x4_t acc=vaddq_s32(vaddq_s32(a0,a1),vaddq_s32(a2,a3));
- int32_t d00=vgetq_lane_s32(acc,0), d01=vgetq_lane_s32(acc,1);
- int32_t d10=vgetq_lane_s32(acc,2), d11=vgetq_lane_s32(acc,3);
- for(;i+1>1], bo1=wo1[i>>1];
- int a0=(int)(bo&0xF)-8, a1=(int)(bo>>4)-8, b0=(int)(bo1&0xF)-8, b1=(int)(bo1>>4)-8;
- int u0=xs[i],u1=xs[i+1],v0=xs1[i],v1=xs1[i+1];
- d00+=a0*u0+a1*u1; d01+=a0*v0+a1*v1; d10+=b0*u0+b1*u1; d11+=b0*v0+b1*v1; }
- if(i>1], bo1=wo1[i>>1];
- int a0=(int)(bo&0xF)-8, b0=(int)(bo1&0xF)-8;
- d00+=a0*xs[i]; d01+=a0*xs1[i]; d10+=b0*xs[i]; d11+=b0*xs1[i]; }
- y[(int64_t)s*O+o] =(float)d00*sc0*sx[s];
- y[(int64_t)s*O+(o+1)] =(float)d10*sc1*sx[s];
- y[(int64_t)(s+1)*O+o] =(float)d01*sc0*sx[s+1];
- y[(int64_t)(s+1)*O+(o+1)]=(float)d11*sc1*sx[s+1];
+ yg[(int64_t)s*O+o]=ag; yu[(int64_t)s*O+o]=au;
}
- if(S&1){ int s=S-1; const int8_t *xs=xq+(int64_t)s*I;
- y[(int64_t)s*O+o] =(float)dot_i4i8(wo, xs,I)*sc0*sx[s];
- y[(int64_t)s*O+(o+1)]=(float)dot_i4i8(wo1,xs,I)*sc1*sx[s]; }
- }
- if(O&1){ int o=O-1; const uint8_t *w=q4+(int64_t)o*rb; float sc=scale[o];
- #pragma omp parallel for schedule(static)
- for(int s=0;s=2){ matmul_q_idot_mm(y,xq,sx,q,scale,S,I,O); return; }
-#endif
- #pragma omp parallel for schedule(static)
- for(int o=0;o=2){ matmul_i4_idot_mm(y,xq,sx,q4,scale,S,I,O); return; }
-#endif
- #pragma omp parallel for schedule(static)
- for(int o=0;og_qscratch.xq_cap){
- int8_t *p=realloc(g_qscratch.xq,xn);
- if(!p){ fprintf(stderr,"OOM quant scratch\n"); exit(1); }
- g_qscratch.xq=p; g_qscratch.xq_cap=xn;
- }
- if(sn>g_qscratch.sx_cap){
- float *p=realloc(g_qscratch.sx,sn*sizeof(float));
- if(!p){ fprintf(stderr,"OOM quant scales\n"); exit(1); }
- g_qscratch.sx=p; g_qscratch.sx_cap=sn;
}
- *xq=g_qscratch.xq; *sx=g_qscratch.sx;
}
/* allow_idot=0: forza il kernel int4/int8 ESATTO (attivazioni f32). Serve alle proiezioni di
@@ -1022,48 +526,26 @@ static void quant_scratch(size_t xn, size_t sn, int8_t **xq, float **sx){
* 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);
-static void matmul_qt(float *y, const float *x, QT *w, int S){ matmul_qt_ex(y,x,w,S,1); }
static void matmul_qt_ex(float *y, const float *x, QT *w, int S, int allow_idot){
#ifdef COLI_METAL
- /* Large row-batches (prefill: kv_b reconstruction, o_proj, dense MLP, step_all logits)
- * amortize Metal's ~5ms submit latency; small-S decode matmuls stay on CPU (NEON wins).
- * Weights must be registered (all dense QT allocs are, via qalloc). */
if(g_metal_enabled && S>=g_metal_gemm_min && !spec_pinned() && (w->fmt==1||w->fmt==2) && !omp_in_parallel()){
const void *wp = w->fmt==1 ? (const void*)w->q8 : (const void*)w->q4;
if(coli_metal_gemm(y,x,wp,w->s,w->fmt,S,w->I,w->O)) return;
}
#endif
#ifdef COLI_CUDA
- /* The CUDA backend owns persistent copies only for model-resident tensors.
- * Streaming expert slots are reused for different IDs and must never enter
- * this cache. Nested OpenMP calls stay on CPU because each device context
- * intentionally owns one synchronous scratch stream in this stage. */
- 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);
}
#endif
if(w->fmt==0){ matmul(y,x,w->qf,S,w->I,w->O); return; }
- /* fmt=4: grouped int4 — always use the exact grouped kernel (no IDOT approximation,
- * since the whole point of grouped scales is better quality). */
if(w->fmt==4){ matmul_i4_grouped(y,x,w->q4,w->s,S,w->I,w->O,w->gs); return; }
- /* int8 IDOT vince sempre (1.4-2.5x). int4 IDOT: l'autore su AVX2 trovo' che a S=1
- * non ripaga (soglia S>=2); ma su ARM/SDOT il singolo token CONVIENE (vedi g_i4s /
- * PR #9 per il gemello VNNI). Soglia configurabile con I4S.
- * EN: int8 IDOT always wins (1.4-2.5x). int4 IDOT: on AVX2 the author found S=1 didn't
- * pay (S>=2 gate); on ARM/SDOT single-token DOES pay (see g_i4s / PR #9 for the VNNI
- * twin). Threshold configurable via I4S. */
- /* #163: sotto SPEC_PIN il gate int4-IDOT usa la decisione di S=1 per OGNI S, cosi'
- * draft e verifica restano sulla stessa famiglia. (CUDA non e' toccato: la sua
- * condizione non dipende da S, quindi e' gia' coerente tra draft e verifica.)
- * EN: under SPEC_PIN the int4 IDOT gate uses the S=1 decision for EVERY S, so draft
- * and verify stay in one family. (CUDA untouched: its condition is S-independent,
- * hence already draft/verify-consistent.) */
+ if(w->fmt==6){ matmul_e8(y,x,w->q4,NULL,S,w->I,w->O); return; } /* scales live in-block */
if(allow_idot && g_idot && (w->fmt==1 || (w->fmt==2 && (spec_pinned() ? g_i4s<=1 : S>=g_i4s)))){
int I=w->I; int8_t *xq; float *sx;
if(S<0 || I<0 || (size_t)S>SIZE_MAX/(size_t)(I?I:1)){ fprintf(stderr,"matmul_qt: shape overflow\n"); exit(1); }
@@ -1075,52 +557,10 @@ 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);
}
-/* quantizza w[O,I] f32 -> int8 q[O,I] + scala[O] simmetrica per riga */
-static void quantize_rows(const float *w, int8_t *q, float *scale, int O, int I, int bits){
- int qmax=(1<<(bits-1))-1;
- #pragma omp parallel for schedule(static)
- for(int o=0;oamax)amax=a; }
- float s=amax/qmax; if(s<1e-8f)s=1e-8f; scale[o]=s;
- int8_t *qr=q+(int64_t)o*I;
- for(int i=0;iqmax)v=qmax; if(v<-qmax-1)v=-qmax-1; qr[i]=(int8_t)v; }
- }
-}
-/* quantizza w[O,I] f32 -> int4 impacchettato (2/byte) + scala[O].
- * bits<=4: valori in [-qmax-1,qmax] stanno in un nibble [-8,7]; memorizzati come v+8 (0..15). */
-static void pack_int4(const float *w, uint8_t *q4, float *scale, int O, int I, int bits){
- int qmax=(1<<(bits-1))-1, rb=(I+1)/2;
- #pragma omp parallel for schedule(static)
- for(int o=0;oamax)amax=a; }
- float s=amax/qmax; if(s<1e-8f)s=1e-8f; scale[o]=s;
- uint8_t *qr=q4+(int64_t)o*rb;
- for(int i=0;iqmax)v0=qmax; if(v0<-8)v0=-8;
- int v1=0; if(i+1qmax)v1=qmax; if(v1<-8)v1=-8; }
- qr[i>>1] = (uint8_t)((v0+8) | ((v1+8)<<4));
- }
- }
-}
-
-/* quantizza w[O,I] f32 -> int2 impacchettato (4/byte) + scala[O]. valori nibble 2-bit in [-2,1]. */
-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)
- for(int o=0;oamax)amax=a; }
- float s=amax/qmax; if(s<1e-8f)s=1e-8f; scale[o]=s;
- uint8_t *qr=q2+(int64_t)o*rb;
- for(int i=0;iqmax)v=qmax; if(v<-2)v=-2; byte|=(uint8_t)((v+2)<<(k*2)); }
- qr[i>>2]=byte;
- }
- }
-}
-
static int g_nopack=0; /* NOPACK=1 -> tiene i valori <=4bit in contenitore int8 (per validare il packing) */
static int g_drop=0; /* DROP=1 -> scarta le pagine expart dopo l'uso. Default 0: le lascia in
* page-cache (buff/cache, NON RSS) come L2 gratuito -> sfrutta lo
@@ -1163,12 +603,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. */
@@ -1188,6 +622,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
@@ -1206,6 +653,11 @@ static int g_disk_split=0; /* DISK_SPLIT=1: contatori che spezzano i DISK LOAD (
* (int4), con i byte letti. Default OFF: a flag spento gli atomic
* non vengono MAI toccati (zero overhead), le righe extra di stats
* non vengono stampate. Solo misura: nessun effetto sull'output. */
+
+#include "sample.h"
+#include "kv_persist.h"
+#include "telemetry.h"
+
/* Aligned allocator for dense QT weights/scales: under METAL, page-align + register so the
* GPU reads them zero-copy (no upload duplicate). Plain malloc otherwise. */
/* ---- COLI_NUMA=1 (#82): interleave the expert slabs across NUMA nodes ----
@@ -1216,19 +668,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< old behavior (a speculation evicts the plain LRU).
+ * Default ON: protect a RESIDENT demand-loaded expert from a speculation only when
+ * it is genuinely WARM (>=2 accesses) AND clearly hotter than the predicted expert
+ * by tier_pick_lfru's 25%+4-freq hysteresis; otherwise the speculation may evict the
+ * plain-LRU victim as before. Cache placement only -> output byte-identical. (#441, #490) */
/* Handshake main<->pilota per il load-vero cross-layer. Invariante di sicurezza in DUE parti:
* 1) Percorso MATMUL (moe): il pilota scrive SOLO ecache[layer] con layer > g_cur_moe_layer;
* il matmul in moe() legge SOLO ecache[layer]==g_cur_moe_layer, e la barriera a inizio moe()
@@ -1276,18 +762,21 @@ static _Atomic int g_cur_moe_layer=-1; /* massimo layer moe in cui il MAIN e'
static int g_pilot_inflight[256]; /* protected by g_pilot_mx; URING can load a layer concurrently */
static _Atomic long g_pilot_loads=0; /* load cross-layer VERI completati (banda spesa) */
static _Atomic long g_pilot_drops=0; /* predizioni scartate perche' il main possiede gia' il layer */
-/* sceglie il formato da `bits`: >=16 f32, 5..8 int8, <=4 int4-packed */
+/* format from `bits`: >=16 f32, 5..8 int8, 4 int4-packed, 3 int3-g64 (group scales), <=2 int2 */
static void qt_alloc(QT *t, int O, int I, int bits){
t->O=O; t->I=I; t->qf=NULL; t->q8=NULL; t->q4=NULL; t->s=NULL;
if(bits>=16){ t->fmt=0; t->qf=falloc((int64_t)O*I); }
else if(bits>=5 || g_nopack){ t->fmt=1; t->q8=qalloc((int64_t)O*I); t->s=qsalloc(O); }
- else if(bits>=3){ t->fmt=2; t->q4=qalloc((int64_t)O*((I+1)/2)); t->s=qsalloc(O); }
+ else if(bits>=4){ t->fmt=2; t->q4=qalloc((int64_t)O*((I+1)/2)); t->s=qsalloc(O); }
+ else if(bits==3){ t->fmt=5; t->q4=qalloc((int64_t)O*i3_rowbytes(I));
+ t->s=(float*)qalloc((size_t)O*i3_groups(I)*sizeof(float)); }
else { t->fmt=3; t->q4=qalloc((int64_t)O*((I+3)/4)); t->s=qsalloc(O); }
}
static void qt_fill(QT *t, const float *w, int bits){
if(t->fmt==0) memcpy(t->qf, w, (int64_t)t->O*t->I*sizeof(float));
else if(t->fmt==1) quantize_rows(w, t->q8, t->s, t->O, t->I, bits);
else if(t->fmt==3) pack_int2(w, t->q4, t->s, t->O, t->I, bits);
+ else if(t->fmt==5) pack_int3_g64(w, t->q4, t->s, t->O, t->I);
else pack_int4(w, t->q4, t->s, t->O, t->I, bits);
}
@@ -1332,11 +821,31 @@ static void rope_interleave(float *v, int pos, const Cfg *c){
}
/* ---------- config ---------- */
+/* SEC-9: bounded slurp for untrusted config/oracle JSON. config.json arrives from
+ * unverified mirrors (see qt_check_fmt threat model); an unbounded ftell->malloc
+ * gave a hostile file a load-time OOM or, on malloc failure, a NULL deref via
+ * b[got]=0. Cap the size, NULL-check the alloc, require a full read. Returns a
+ * malloc'd NUL-terminated buffer, or NULL on any failure. Mirrors tok.h tk_read_file. */
+#define CFG_MAX_BYTES (256ll<<20) /* config/oracle JSON is KB-MB in practice */
+static char* cfg_slurp(const char *path){
+ FILE *f=fopen(path,"rb"); if(!f) return NULL;
+ fseek(f,0,SEEK_END); long n=ftell(f); fseek(f,0,SEEK_SET);
+ if(n<0 || (long long)n>CFG_MAX_BYTES){ fclose(f); return NULL; }
+ char *b=malloc((size_t)n+1); if(!b){ fclose(f); return NULL; }
+ size_t got=fread(b,1,(size_t)n,f); fclose(f);
+ if((long)got!=n){ free(b); return NULL; }
+ b[got]=0; return b;
+}
static jval* cfg_root(const char *snap, char **arena){
char p[2048]; snprintf(p,sizeof(p),"%s/config.json",snap);
FILE *f=fopen(p,"rb"); if(!f){perror(p);exit(1);}
fseek(f,0,SEEK_END); long n=ftell(f); fseek(f,0,SEEK_SET);
- char *b=malloc(n+1); size_t got=fread(b,1,n,f); b[got]=0; fclose(f);
+ /* SEC: config.json arriva dalla dir modello non fidata. Limita la dimensione
+ * (un file ostile enorme = OOM al load) e controlla la malloc: senza il NULL
+ * check, b[got]=0 su malloc fallita era un NULL-deref. */
+ if(n<0 || n>(256L<<20)){ fprintf(stderr,"%s: size %ld out of range (0..256 MB)\n",p,n); exit(1); }
+ char *b=malloc((size_t)n+1); if(!b){ fprintf(stderr,"OOM reading %s (%ld bytes)\n",p,n); exit(1); }
+ size_t got=fread(b,1,(size_t)n,f); b[got]=0; fclose(f);
if((long)got!=n) fprintf(stderr,"warning: short read on %s (%ld of %ld)\n",p,(long)got,n);
return json_parse(b,arena);
}
@@ -1375,8 +884,9 @@ static void load_cfg(Cfg *c, const char *snap){
FILE *gf=fopen(gp,"rb"); /* assente = nessun problema: e' opzionale */
if(gf){
fseek(gf,0,SEEK_END); long gn=ftell(gf); fseek(gf,0,SEEK_SET);
- if(gn>0){
- char *gb=malloc(gn+1); size_t gg=fread(gb,1,gn,gf); gb[gg]=0;
+ char *gb = (gn>0 && gn<=(256L<<20)) ? malloc((size_t)gn+1) : NULL; /* SEC: cap + NULL check */
+ if(gb){
+ size_t gg=fread(gb,1,(size_t)gn,gf); gb[gg]=0;
char *ga=NULL; jval *gr=json_parse(gb,&ga);
jval *ge=gr?json_get(gr,"eos_token_id"):NULL;
if(ge){
@@ -1445,6 +955,37 @@ static int detect_group_size(int O, int I, int64_t ns){
return 0;
}
+/* SEC: risolve e VALIDA il formato quantizzato di un tensore [O,I] letto da un
+ * container non fidato (mirror). L'inferenza precedente (`?1:?2:3`) cadeva su
+ * int2 per QUALSIASI conteggio byte non riconosciuto: un peso troppo corto
+ * 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/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);
+ int64_t exp_i3=(int64_t)O*i3_rowbytes(I); /* int3-g64 (fmt=5): 24B per 64-input group */
+ /* fmt=6 (E8/IQ3, #452): scales live inside the 98B super-blocks, so the .qs
+ * convention is kept with a single-float tag — ns==4 is the discriminator
+ * (every other format carries at least O floats of real scales). */
+ if(ns==4 && nb==(int64_t)O*e8_rowbytes(I)){ *gs=0; return 6; }
+ /* 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/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))
+ : (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); }
+ return fmt;
+}
/* costruisce un QT [O,I] dal disco in `t` (buffer riusabili tra chiamate).
* - se esiste `name.qs`: pesi GIA' quantizzati nel container (U8 qdata + F32 scala) -> letti diretti
* - altrimenti: tensore pieno (f32/bf16) -> quantizzato a runtime a `bits` (oracolo tiny / pesi pieni)
@@ -1454,22 +995,35 @@ static void qt_from_disk(Model *m, const char *name, int O, int I, int bits, int
if(st_has(&m->S,sn)){
int64_t nb=st_nbytes(&m->S,name);
int64_t ns=st_nbytes(&m->S,sn); /* scale bytes (F32) */
- /* Detect int4-grouped (fmt=4): packed int4 weight bytes BUT scale array is
- * larger than O*4 — the group size is derived from the scale-array size. */
- int fmt = (nb==(int64_t)O*I)?1 : (nb==(int64_t)O*((I+1)/2))?2 : 3;
+ /* fmt=4 int4-grouped: byte int4 ma scala > O*4 — gs deriva dalla scala.
+ * qt_resolve_fmt valida entrambi i conteggi contro [O,I] e termina se
+ * non fidati (SEC). */
int gs=0;
- if(fmt==2) gs=detect_group_size(O,I,ns);
- if(gs>0) fmt=4;
+ int fmt = qt_resolve_fmt(name,O,I,nb,ns,&gs);
if(fmt==1){ if(t->fmt!=1||!t->q8){ t->fmt=1; t->O=O; t->I=I; t->gs=0; t->q8=qalloc(nb); t->s=qsalloc(O); } st_read_raw(&m->S,name,t->q8,drop); }
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(fmt==6){ /* E8/IQ3: everything in-block, .qs is the 4-byte tag */
+ if(t->fmt!=6||!t->q4){ t->fmt=6; t->O=O; t->I=I; t->gs=0; t->q4=qalloc(nb); t->s=qsalloc(1); }
+ 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);
+ /* cap MUST match the scale cardinality qt_resolve_fmt already validated and
+ * the falloc above actually reserved, per format: grouped-int4 (fmt=4) keeps
+ * O*ceil(I/gs) scales and int3-g64 (fmt=5) keeps O*i3_groups(I); everything
+ * else is per-row O. Using the per-row bound for a grouped format would
+ * reject a legitimate container (fmt=5 regressed exactly that way). */
+ st_read_f32_cap(&m->S,sn,t->s,
+ fmt==4 ? (int64_t)O*((I+gs-1)/gs) :
+ fmt==5 ? (int64_t)O*i3_groups(I) :
+ fmt==6 ? (int64_t)1 : (int64_t)O, drop);
} else {
if(!t->qf && !t->q8 && !t->q4) qt_alloc(t,O,I,bits);
- if(t->fmt==0) st_read_f32(&m->S,name,t->qf,drop);
- else { float *tmp=falloc((int64_t)O*I); st_read_f32(&m->S,name,tmp,drop); qt_fill(t,tmp,bits); free(tmp); }
+ if(t->fmt==0) st_read_f32_cap(&m->S,name,t->qf,(int64_t)O*I,drop);
+ else { float *tmp=falloc((int64_t)O*I); st_read_f32_cap(&m->S,name,tmp,(int64_t)O*I,drop); qt_fill(t,tmp,bits); free(tmp); }
}
}
static QT qt_load(Model *m, const char *name, int O, int I, int bits){
@@ -1501,13 +1055,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;
@@ -1518,7 +1073,9 @@ static void layer_cuda_shard_kvb(Layer *l,int H,int Q,int V){
static void model_init(Model *m, const char *snap, int cap, int ebits, int dbits){
memset(m,0,sizeof(*m)); m->ebits=ebits; m->dbits=dbits;
- load_cfg(&m->c,snap); st_init(&m->S,snap);
+ load_cfg(&m->c,snap);
+ { const char *xd=getenv("COLI_MODEL_DIRS"); /* SPLIT: model shards spread across N drives */
+ st_init_multi(&m->S,snap,(xd&&*xd)?xd:NULL); }
Cfg *c=&m->c; char nm[256]; int H=c->n_heads, D=c->hidden;
/* embed e lm_head sono il confine I/O: tenerli ad alta precisione (come i quant dynamic
* reali). A bf16 ~1.9GB su GLM reale: trascurabile. dbits>=8 -> qui f32; piu' basso -> dbits. */
@@ -1535,6 +1092,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++){
@@ -1579,6 +1137,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
}
@@ -1591,12 +1151,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;
@@ -1625,6 +1190,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
}
@@ -1678,13 +1245,29 @@ static void model_init(Model *m, const char *snap, int cap, int ebits, int dbits
/* embed: dequantizza la riga del token (scala per-riga) in x[hidden] */
static void embed_row(Model *m, int tok, float *x){
int D=m->c.hidden; QT *e=&m->embed;
+ if(tok<0 || tok>=e->O){ memset(x,0,(size_t)D*sizeof(float)); return; } /* #SEC-5: out-of-range token id -> zero row, never OOB */
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; }
}
@@ -1725,6 +1308,57 @@ static void *map_of_fd(int fd){
return base;
}
+/* ==================== DUAL-SSD: two model copies, two drives ====================
+ * COLI_MODEL_MIRROR= registers a SECOND (read-only) copy of the model on
+ * another drive; expert reads are split between the two according to
+ * COLI_DISK_WEIGHTS=, (relative bandwidth; without the env it
+ * is measured at startup with the engine's own access pattern). Cold decode is
+ * disk-bound (~11 GB/token): two NVMe drives reading in parallel add up. */
+static const char *g_mirror_dir=NULL; /* COLI_MODEL_MIRROR / SNAP_MIRROR */
+static int g_mirror=0; /* 1 = mirror active (at least one shard accepted) */
+static int g_mir_share=64; /* expert share routed to the mirror, out of 256 */
+static _Atomic int64_t g_mir_bytes[2]; /* bytes served per drive: [0] primary, [1] mirror */
+static _Atomic int64_t g_mir_nread[2];
+
+/* replica of one expert: DETERMINISTIC hash of (layer,eid). Determinism is a
+ * requirement, not a style choice: the readahead/PILOT WILLNEED and the demand
+ * pread must hit the same fd/page-cache, and in buffered mode an expert must
+ * never be cached twice (one copy per drive). */
+static inline int expert_route(int layer,int eid){
+ if(!g_mirror) return 0;
+ uint32_t h=(uint32_t)layer*2654435761u ^ (uint32_t)eid*0x9E3779B9u;
+ h^=h>>16; h*=0x45d9f3bu; h^=h>>16;
+ return (int)(h&255) < g_mir_share;
+}
+
+/* buffered fd of the replica, falling back to the primary if the file is not mirrored */
+static inline int rep_bfd(shards *S,int fd,int rep){
+ int r=st_fd_rep(S,fd,rep); return r<0?fd:r;
+}
+
+static int pread_full(int fd, void *buf, int64_t n, int64_t off, const char *tag);
+
+/* pread on the chosen replica with fallback to the primary on error/short-read:
+ * an unreadable sector (or an unmount) of the mirror must never kill the process
+ * when the primary can serve the same bytes. Delegates to pread_full so the
+ * mirror path inherits the short-read/EINTR loop and honest reporting (#236).
+ * Accounts bytes per drive. Returns 0 = ok, -1 = real error/EOF (like pread_full). */
+static ssize_t mir_pread(shards *S,int fd,int rep,void *buf,int64_t n,int64_t off,const char *tag){
+ int rfd = st_fd_rep(S,fd,rep);
+ int used = rep && rfd>=0;
+ if(rfd<0) rfd=fd;
+ int rc=pread_full(rfd,buf,n,off,tag);
+ if(rc && used){
+ static _Atomic int warned;
+ if(!atomic_exchange(&warned,1))
+ fprintf(stderr,"[MIRROR] read error on the mirror copy — falling back to the primary drive\n");
+ used=0; rc=pread_full(fd,buf,n,off,tag);
+ }
+ if(!rc){ atomic_fetch_add_explicit(&g_mir_bytes[used],n,memory_order_relaxed);
+ atomic_fetch_add_explicit(&g_mir_nread[used],1,memory_order_relaxed); }
+ return rc;
+}
+
/* carica un expert nello slot. Container pre-quantizzato: le 3 matrici sono contigue nel
* file -> UNA pread coalescente da ~19 MB dentro `slab` (+ le scale in fslab); i QT sono
* viste dentro lo slab (zero copie). Fallback per modelli non quantizzati (oracolo tiny).
@@ -1759,16 +1393,47 @@ 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. */
if(s->eid!=eid){ qt_cuda_reset(&s->g); qt_cuda_reset(&s->u); qt_cuda_reset(&s->d); }
#endif
Cfg *c=&m->c; int I=c->moe_inter, D=c->hidden, b=m->ebits;
- char nm[3][288]; const char *suf[3]={"gate_proj","up_proj","down_proj"};
+ /* suf as a bounded char[][16] (not const char*) lets GCC prove the %s in the
+ * nm[k]/qn snprintfs can't overflow: worst key is "model.layers..mlp.experts..down_proj.weight"
+ * = 66 bytes incl NUL, well under nm[288] and qn[320]. See #484. */
+ char nm[3][288]; const char suf[3][16]={"gate_proj","up_proj","down_proj"};
for(int k=0;k<3;k++) snprintf(nm[k],sizeof(nm[k]),"model.layers.%d.mlp.experts.%d.%s.weight",layer,eid,suf[k]);
- char qn[300]; snprintf(qn,sizeof(qn),"%s.qs",nm[0]);
+ char qn[320]; snprintf(qn,sizeof(qn),"%s.qs",nm[0]);
if(!st_has(&m->S,qn)){ /* fallback: tensori pieni, quantizza a runtime.
* Reachable ONLY for unquantized models (no .qs);
* GLM always has .qs, so the pilot never hits it. */
@@ -1792,21 +1457,20 @@ static int expert_load_impl(Model *m, int layer, int eid, ESlot *s, int fatal){
else { __atomic_add_fetch(&m->ld_main,1,__ATOMIC_RELAXED);
__atomic_add_fetch(&m->bytes_main,(uint64_t)tb,__ATOMIC_RELAXED); }
}
+ int rep=expert_route(layer,eid); /* DUAL-SSD: this expert's replica */
+ if(rep && st_fd_rep(&m->S,tw[0]->fd,1)<0) rep=0; /* shard not in the mirror (partial) */
if(g_mmap){
void *bw[3],*bq[3]; int okm=1;
for(int k=0;k<3;k++){
- bw[k]=map_of_fd(tw[k]->fd); bq[k]=map_of_fd(tq[k]->fd);
+ bw[k]=map_of_fd(rep_bfd(&m->S,tw[k]->fd,rep)); bq[k]=map_of_fd(rep_bfd(&m->S,tq[k]->fd,rep));
if(!bw[k]||!bq[k]||((tw[k]->off)&3)||((tq[k]->off)&3)) okm=0;
}
if(okm){
QT *qt[3]={&s->g,&s->u,&s->d}; int OO[3]={I,I,D}, II[3]={D,D,I};
for(int k=0;k<3;k++){
int64_t nb=tw[k]->nbytes;
- int fmt=(nb==(int64_t)OO[k]*II[k])?1:(nb==(int64_t)OO[k]*((II[k]+1)/2))?2:3;
- /* detect grouped int4 (fmt=4): int4 weight bytes + larger scale array */
int gs=0;
- if(fmt==2) gs=detect_group_size(OO[k],II[k],tq[k]->nbytes);
- if(gs>0) fmt=4;
+ int fmt=qt_resolve_fmt(tw[k]->name,OO[k],II[k],nb,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*)((char*)bw[k]+tw[k]->off); qt[k]->q4=(uint8_t*)((char*)bw[k]+tw[k]->off);
qt[k]->s=(float*)((char*)bq[k]+tq[k]->off);
@@ -1833,7 +1497,9 @@ static int expert_load_impl(Model *m, int layer, int eid, ESlot *s, int fatal){
* the final resident set only, after GPU release has already nulled out the
* pointers for anything that isn't genuinely RAM-tier. */
atomic_fetch_add_explicit(&g_prof_io,(int64_t)(n+nq),memory_order_relaxed);
+ atomic_fetch_add_explicit(&g_mir_bytes[rep],tw[k]->nbytes+tq[k]->nbytes,memory_order_relaxed);
}
+ atomic_fetch_add_explicit(&g_mir_nread[rep],1,memory_order_relaxed);
s->eid=eid; return 0;
}
}
@@ -1868,6 +1534,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;
}
@@ -1889,52 +1560,79 @@ static int expert_load_impl(Model *m, int layer, int eid, ESlot *s, int fatal){
numa_slab_bind(s->fslab,(size_t)ftot*sizeof(float));
#endif
}
+ /* DISK-CLASS: classify before the reads; computed unconditionally at dc_on sites so
+ * the timer (dc_t0) brackets exactly the read work, matching what the GB/s in the
+ * DISK-CLASS line describes. dc_on gates ALL of it off demand=0 call sites
+ * (pilot/repin/pin -- never classified, see the call sites) and off PROF=0 runs
+ * (dc_needed()) -- zero cost, zero behavior change there. The fd choice below is
+ * NOT influenced by the verdict: this is measurement only. */
+ int dc_on = demand && dc_needed();
+ int dc_cls = dc_on ? expert_classify(m,layer,eid) : DC_WARM;
+ double dc_t0 = dc_on ? now_s() : 0;
+ if(dc_on) dc_wall_enter(dc_cls,dc_t0); /* busy-wall open; EVERY exit path below must pair it */
int ord[3]={0,1,2}; /* ordina per offset nel file */
for(int a=0;a<3;a++) for(int bb=a+1;bb<3;bb++) if(tw[ord[bb]]->offoff){ int t=ord[a]; ord[a]=ord[bb]; ord[bb]=t; }
int contig = tw[ord[0]]->fd==tw[ord[1]]->fd && tw[ord[1]]->fd==tw[ord[2]]->fd
&& tw[ord[0]]->off+tw[ord[0]]->nbytes==tw[ord[1]]->off
&& tw[ord[1]]->off+tw[ord[1]]->nbytes==tw[ord[2]]->off;
- int64_t pos[3]; int done=0;
+ int64_t pos[3]; int done=0, dc_direct=0;
if(contig){
int64_t off0=tw[ord[0]]->off;
- int dfd = g_direct ? st_direct_fd(&m->S, tw[ord[0]]->fd) : -1;
+ int dfd = g_direct ? st_direct_fd_rep(&m->S, tw[ord[0]]->fd, rep) : -1;
if(dfd>=0){ /* O_DIRECT: offset/len allineati a 4K */
int64_t base=off0 & ~4095LL, need=(off0-base)+wtot;
int64_t len=(need+4095)&~4095LL;
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(pread_full(tw[ord[0]]->fd, 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(pread_full(tw[k]->fd, 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(pread_full(tq[k]->fd, (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 */
- posix_fadvise(tw[ord[0]]->fd, tw[ord[0]]->off, wtot, POSIX_FADV_DONTNEED);
- for(int k=0;k<3;k++) posix_fadvise(tq[k]->fd, tq[k]->off, tq[k]->nbytes, POSIX_FADV_DONTNEED);
+ * cache in pressione strangoli il throughput.
+ * The drop targets the fd of the replica READ. */
+ posix_fadvise(rep_bfd(&m->S,tw[ord[0]]->fd,rep), tw[ord[0]]->off, wtot, POSIX_FADV_DONTNEED);
+ for(int k=0;k<3;k++) posix_fadvise(rep_bfd(&m->S,tq[k]->fd,rep), tq[k]->off, tq[k]->nbytes, POSIX_FADV_DONTNEED);
}
QT *qt[3]={&s->g,&s->u,&s->d}; int OO[3]={I,I,D}, II[3]={D,D,I};
for(int k=0;k<3;k++){
int64_t nb=tw[k]->nbytes;
- int fmt = (nb==(int64_t)OO[k]*II[k])?1 : (nb==(int64_t)OO[k]*((II[k]+1)/2))?2 : 3;
int gs=0;
- if(fmt==2) gs=detect_group_size(OO[k],II[k],tq[k]->nbytes);
- if(gs>0) fmt=4;
+ int fmt=qt_resolve_fmt(tw[k]->name,OO[k],II[k],nb,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+pos[k]); qt[k]->q4=s->slab+pos[k]; qt[k]->s=fp[k];
}
@@ -1942,9 +1640,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;
}
@@ -2000,7 +1704,7 @@ static int uring_load_add(UringBatch *b,Model *m,int layer,int eid,ESlot *s,int
int li=b->nload++;
UringLoad *l=&b->load[li]; memset(l,0,sizeof(*l));
l->m=m; l->s=s; l->layer=layer; l->eid=eid; l->fatal=fatal;
- char nm[3][288],qn[300]; const char *suf[3]={"gate_proj","up_proj","down_proj"};
+ char nm[3][288],qn[320]; const char suf[3][16]={"gate_proj","up_proj","down_proj"}; /* bounded suf: see #484 */
for(int k=0;k<3;k++) snprintf(nm[k],sizeof(nm[k]),"model.layers.%d.mlp.experts.%d.%s.weight",layer,eid,suf[k]);
snprintf(qn,sizeof(qn),"%s.qs",nm[0]);
if(g_mmap || !st_has(&m->S,qn))
@@ -2119,8 +1823,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;
@@ -2165,6 +1872,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 */
@@ -2172,6 +1895,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;
@@ -2194,8 +1918,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 */
}
@@ -2213,6 +1942,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. */
+ * 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];
+ 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(&m->S,nm);
- char qs[320]; snprintf(qs,sizeof(qs),"%s.qs",nm); st_prefetch(&m->S,qs);
+ 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);
}
}
@@ -2304,9 +2060,20 @@ 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);
+#if defined(__AVX512F__) && defined(__AVX512BW__)
+ axpy_i4f_avx512(w,c,acc,I); return; /* bit-identical: one fma per element */
+#endif
for(int i=0;i+1>1]; acc[i]+=c*((int)(b&0xF)-8); acc[i+1]+=c*((int)(b>>4)-8); }
if(I&1){ uint8_t b=w[I>>1]; acc[I-1]+=c*((int)(b&0xF)-8); } return; }
const uint8_t *w=t->q4+(int64_t)row*((I+3)/4);
@@ -2317,9 +2084,20 @@ 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;
+#if defined(__AVX512F__) && defined(__AVX512BW__)
+ /* same accumulation-order tradeoff (and gate) as matmul_i4's I4_ACC512 path */
+ if(g_i4_acc512 && !(I&31)){ y[j]=dot_i4f_avx512(w,x,I)*s; continue; }
+#endif
for(int i=0;i+1>1]; acc+=((int)(b&0xF)-8)*x[i]+((int)(b>>4)-8)*x[i+1]; }
if(I&1){ uint8_t b=w[I>>1]; acc+=((int)(b&0xF)-8)*x[I-1]; } a=acc*s; }
else if(t->fmt==4){ /* per-gruppo, come matmul_i4_grouped / per-group, as matmul_i4_grouped */
@@ -2329,6 +2107,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;
@@ -2337,6 +2122,14 @@ 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 */
+static int g_cuda_resid=0; /* COLI_CUDA_RESID=1 (#431 PR-C0): expert-group results stay on device */
+/* pipe -> moe handoff for the resident group path (set per layer by
+ * pipe_layer_sparse, consumed by moe()'s group dispatch, cleared after take) */
+static int g_pres_home=-1; /* home device, -1 = path off */
+static const float *g_pres_xsrc; /* nrm_d on the home device */
+static float *g_pres_slots; /* [ndev][D] partial slots on home */
+static int g_pres_used[COLI_CUDA_MAX_DEVICES], g_pres_nused;
#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){
@@ -2792,7 +2585,26 @@ static void attention_rows(Model *m, Layer *l, int layer, float *x, int S, int p
for(int jj=0;jjLc[layer],t,kvl);
const float *kr=coli_kv_row(ks->Rc[layer],t,c->qk_rope);
- float a=0; for(int i=0;iqk_rope;d++) a+=qr[d]*kr[d];
sc[jj]=a*c->attn_scale;
}
@@ -2800,7 +2612,25 @@ static void attention_rows(Model *m, Layer *l, int layer, float *x, int S, int p
float clat[512]; memset(clat,0,kvl*sizeof(float));
for(int jj=0;jjLc[layer],t,kvl);
- float a=sc[jj]; for(int i=0;ikv_b, rbase+r0v, vh, clat, ctx+((int64_t)s*H+h)*vh);
}
}
@@ -2880,6 +2710,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). */
@@ -2899,7 +2738,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));
@@ -2909,13 +2749,26 @@ 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
+ * si congelano a fine prefill e il tie-breaker LFRU di REPIN gira su punteggi
+ * stantii durante il decode su Metal. */
+ m->elast[layer][idxs[(int64_t)s*K+kk]]=++m->eaccess_clock;
}
for(int d=0;drouter, 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;
}
@@ -3136,6 +2994,8 @@ static void moe(Model *m, Layer *l, int layer, float *x, int S, float *out, int
}
/* ---- FASE C/D: risolvi (pin/cache/disco) e calcola, a blocchi di 64 unici ---- */
float *xg=falloc((int64_t)S*D), *gg=falloc((int64_t)S*I), *uu=falloc((int64_t)S*I), *hh=falloc((int64_t)S*D);
+ float *xe=NULL; /* fmt=6: x under the rotation Q^T, built once per call — all routed
+ * experts of the layer share it (the placement rule in quant.h) */
int *rows=malloc(S*sizeof(int)); float *rw=malloc(S*sizeof(float));
#ifdef COLI_CUDA
/* PIPE Inc.1b: il batch-union del prefill passa dai gruppi GPU — prima di
@@ -3230,7 +3090,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
@@ -3247,6 +3107,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){
@@ -3276,8 +3141,149 @@ static void moe(Model *m, Layer *l, int layer, float *x, int S, float *out, int
}
#undef MB_BUILD
#endif
- if(!metal_done)
+#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;dig.fmt!=2||e->u.fmt!=2||e->d.fmt!=2||e->g.I!=D||e->g.O!=I||e->d.I!=I||e->d.O!=D){ allq4=0; break; } }
+ if(allq4){
+ double t0=now_s();
+ float wj[64]; int okw=1;
+ for(int j=0;jg.q4, *qu=e->u.q4;
+ float *gj=GG+(int64_t)j*I, *uj=UU+(int64_t)j*I;
+ for(int o=c0;og.s[o]*sx0;
+ for(int o=c0;ou.s[o]*sx0;
+ for(int o=c0;od.q4; const int8_t *gq=GQ+(int64_t)j*I;
+ float *hj=HH+(int64_t)j*D;
+ for(int o=c0;od.s[o]*gsc[j];
+ }
+ }
+ for(int j=0;jt_emm+=dt;
+ if(g_prof){ m->t_ecpu+=dt; m->cpu_expert_rows+=(uint64_t)nb;
+ for(int j=0;jcpu_expert_bytes+=qt_bytes(&use[j]->g)+qt_bytes(&use[j]->u)+qt_bytes(&use[j]->d); }
+ free(xq8); free(GG); free(UU); free(HH);
+ xexp_done=1;
+ }
+ }
+ }
+ if(!metal_done && !xexp_done)
for(int j=0;j=1 routing invariant.
@@ -3302,7 +3308,12 @@ static void moe(Model *m, Layer *l, int layer, float *x, int S, float *out, int
ngroup++; continue;
}
#endif
- for(int r=0;rg.fmt==6){
+ if(!xe){ xe=falloc((int64_t)S*D); memcpy(xe,x,(size_t)S*D*sizeof(float)); e8_rot_rows(xe,S,D); }
+ xsrc=xe;
+ }
+ for(int r=0;rg.cuda_eligible && e->u.cuda_eligible &&
@@ -3316,6 +3327,7 @@ static void moe(Model *m, Layer *l, int layer, float *x, int S, float *out, int
#endif
expert_gate_up(gg,uu,xg,&e->g,&e->u,nr);
for(int64_t z=0;z<(int64_t)nr*I;z++) gg[z]=siluf(gg[z])*uu[z];
+ if(e->d.fmt==6) e8_rot_rows(gg,nr,I); /* down input is per-expert — rotate here */
matmul_qt(hh, gg, &e->d, nr);
for(int r=0;rcpu_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];
@@ -3345,14 +3387,58 @@ 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;di=0 && S==1){
+ for(int di=0;di1) schedule(static)
- for(int di=0;diecap){ slot=nn; isnew=1; }
- else { int lru=0; for(int z=1;z=2 demand accesses) AND clearly hotter than the speculation by tier_pick_lfru's
+ * 25%+4-freq hysteresis. The original #441 formula tested the speculation's score
+ * against victim+margin, which — because a speculation is by definition historically
+ * colder than a just-used demand expert — dropped ~all speculations once the cache
+ * was full, collapsing the LRU hit share (#490). Cache placement only -> output
+ * byte-identical (a dropped speculation is demand-loaded later, same value). */
+ if(g_pilot_evict_guard && m->eheat && m->elast && Sl[lru].eid>=0){
+ int vid=Sl[lru].eid; uint32_t vh=m->eheat[layer][vid];
+ if(vh>=2){
+ uint64_t vs=tier_lfru_score(vh,m->elast[layer][vid],m->eaccess_clock);
+ uint64_t cs=tier_lfru_score(m->eheat[layer][eid],m->elast[layer][eid],m->eaccess_clock);
+ if(vs+(vs>>2)+(4u<<8)>cs){ atomic_fetch_add_explicit(&g_pilot_drops,1,memory_order_relaxed);
+ pthread_mutex_unlock(&g_pilot_mx); return; } } } }
ESlot *dst=&Sl[slot];
dst->eid=-1; /* nascondi dagli scan-hint mentre carica */
g_pilot_inflight[layer]++;
pthread_mutex_unlock(&g_pilot_mx);
- int rc=expert_load(m,layer,eid,dst,0); /* pread VERO — fuori dal lock, sovrapposto al compute; fatal=0: un errore su una speculazione NON deve uccidere il server */
+ int rc=expert_load(m,layer,eid,dst,0,0); /* pread VERO — fuori dal lock, sovrapposto al compute; fatal=0: un errore su una speculazione NON deve uccidere il server; demand=0: speculative, never classified */
pthread_mutex_lock(&g_pilot_mx);
if(rc==0){
@@ -3581,6 +3682,19 @@ static void pilot_uring_batch(Model *m){
if(Sl[z].eid< -1) continue; /* URING reservation in flight */
if(slot<0 || Sl[z].used=2 accesses)
+ * AND clearly hotter (25%+4-freq hysteresis). See pilot_realload for the rationale and
+ * the #490 regression the original formula caused. */
+ if(slot>=0 && Sl[slot].eid>=0 && g_pilot_evict_guard && m->eheat && m->elast){
+ int vid=Sl[slot].eid; uint32_t vh=m->eheat[layer][vid];
+ if(vh>=2){
+ uint64_t vs=tier_lfru_score(vh,m->elast[layer][vid],m->eaccess_clock);
+ uint64_t cs=tier_lfru_score(m->eheat[layer][eid],m->elast[layer][eid],m->eaccess_clock);
+ if(vs+(vs>>2)+(4u<<8)>cs){ atomic_fetch_add_explicit(&g_pilot_drops,1,memory_order_relaxed);
+ pthread_mutex_unlock(&g_pilot_mx); continue; }
+ }
+ }
}
if(slot<0){ atomic_fetch_add_explicit(&g_pilot_drops,1,memory_order_relaxed); pthread_mutex_unlock(&g_pilot_mx); continue; }
ESlot *dst=&Sl[slot];
@@ -3791,17 +3905,28 @@ static int pipe_layer_sparse(Model *m, Layer *l, int li, float *x_dev, int S, in
if(!l->sh_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) */
@@ -3819,6 +3944,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.
@@ -3849,7 +4004,30 @@ 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; }
+ float *res_acc=NULL;
+ if(g_cuda_resid && S==1){
+ g_pres_slots=coli_cuda_pipe_scratch(dev,25,(size_t)g_cuda_ndev*D*sizeof(float));
+ res_acc =coli_cuda_pipe_scratch(dev,26,(size_t)D*sizeof(float));
+ if(g_pres_slots && res_acc){ g_pres_home=dev; g_pres_xsrc=nrm_d; g_pres_nused=0; }
+ else { g_pres_slots=NULL; res_acc=NULL; }
+ }
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; }
+ if(g_pres_home>=0){
+ int nused=g_pres_nused; g_pres_home=-1; g_pres_xsrc=NULL;
+ if(nused>0){
+ /* partials are in flight toward our slots; take() orders the legacy
+ * stream behind every issue event and reduces in issue order. A
+ * failure here would silently drop routed experts — that is a wrong
+ * answer, not a slow one, so it is fatal by design. */
+ if(!coli_cuda_expert_group_resident_take(dev,g_pres_used,nused,g_pres_slots,res_acc,D)||
+ !coli_cuda_pipe_add(dev,x_dev,res_acc,(size_t)D)){
+ fprintf(stderr,"[CUDA] resident expert take failed — refusing to drop routed experts\n");
+ exit(1);
+ }
+ }
+ }
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) */
@@ -4212,23 +4390,45 @@ static void mtp_absorb(Model *m, const int *next_ids, const float *x, int S, int
free(hx); free(cat); free(e); free(hn); free(hf); free(nrm); free(tmp);
}
-static inline int argmax_v(const float *lo, int V){
- /* skip NaN (x==x is false for NaN) so a poisoned logit can't pin the argmax
- * to index 0 — pick the max finite/+Inf entry instead. */
- int b=-1; float bv=-INFINITY;
- for(int i=0;ibv){ bv=x; b=i; } }
- return b<0?0:b;
-}
+
+static void repin_pass_limit(Model *m,int limit);
+static void repin_pass(Model *m){ repin_pass_limit(m,16); }
/* ---- METODO F: draft grammaticale (#48) ----
* gr_feed consuma i byte di ogni token EMESSO e tiene il walker in sync con l'output;
* 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;
@@ -4240,59 +4440,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 ----
@@ -4300,113 +4486,6 @@ static int grammar_draft(int *draft, int cap){
* Rejection sampling di Leviathan: accetta il draft x_d con prob p(x_d); al rifiuto
* ricampiona da p con x_d azzerato e rinormalizzato. La distribuzione risultante e'
* ESATTAMENTE p: la speculazione resta invisibile all'output anche col sampling. */
-static uint64_t g_rng=0x9E3779B97F4A7C15ULL;
-static inline double rndu(void){ g_rng^=g_rng<<13; g_rng^=g_rng>>7; g_rng^=g_rng<<17;
- return (double)(g_rng>>11)*(1.0/9007199254740992.0); }
-static float *g_pbuf=NULL; static int *g_pidx=NULL; /* buffer riusati (decode single-thread) */
-/* sift-down su max-heap in h[0..n), chiave = g_pbuf[h[i]] (#335: partial top-p select).
- * Versione "a buco": porta il valore di radice e lo deposita solo alla fine, cosi'
- * heapify e' O(V) e ogni pop e' O(log n) senza qsort sull'intero vocabolario. */
-static void topp_siftdown(int *h, int n, int i){
- int iv=h[i]; float kv=g_pbuf[iv];
- for(;;){ int l=2*i+1;
- if(l>=n) break; /* foglia */
- int b=l; if(l+1g_pbuf[h[l]]) b=l+1; /* figlio maggiore */
- if(g_pbuf[h[b]]<=kv) break; /* nessun figlio supera la radice -> ferma */
- h[i]=h[b]; i=b; }
- h[i]=iv;
-}
-/* costruisce in g_pbuf la distribuzione target: softmax(lo/temp) troncata a top-p g_nuc.
- * Invariante per dist_sample: g_pbuf resta INDICIZZATO per token-id (mai riordinato);
- * la coda troncata va AZZERATA in g_pbuf (dist_sample la legge direttamente per id). */
-static void dist_build(const float *lo, int V){
- if(!g_pbuf){ g_pbuf=falloc(V); g_pidx=malloc(V*sizeof(int)); }
- /* Un solo logit NaN/+Inf avvelenava tutto (#369): +Inf diventava mx, NaN/Inf-mx
- * -> expf NaN -> s NaN -> ogni prob NaN -> dist_sample cade sul fallback
- * `g_pbuf[i]>0` (NaN>0 e' falso ovunque) e ritorna 0 PER SEMPRE, in silenzio.
- * Difesa: mx solo sui finiti; un logit non finito contribuisce prob 0;
- * se la distribuzione degenera (tutti non finiti / somma non valida) si
- * ripiega sull'argmax dei finiti e si avvisa UNA volta, mai in silenzio. */
- int mxi=-1; float mx=0;
- for(int i=0;imx)){ mx=lo[i]; mxi=i; }
- double s=0; float invt=1.f/(g_temp>1e-4f?g_temp:1e-4f);
- if(mxi>=0){
- for(int i=0;i=0)?mxi:0; /* mxi = argmax dei logit FINITI (robusto
- * anche se lo[0] e' NaN, dove argmax_v fallirebbe) */
- for(int i=0;i0 && g_nuc<1.f){
- for(int i=0;i=0;i--) topp_siftdown(g_pidx,V,i); /* heapify O(V) */
- /* pop verso la coda: i vincitori (testa top-p) cadono in g_pidx[out..V-1] in ordine
- * DECRESCENTE, come il vecchio qsort, quindi s2 accumula nello stesso ordine ->
- * head bit-identical sui casi senza pareggi (i pareggi erano gia' non specificati
- * sotto il qsort instabile e restano tali). Il prefisso g_pidx[0..out-1) e' la coda. */
- double s2=0, cum=0; int out=V;
- do{ int root=g_pidx[0]; /* massimo corrente */
- g_pidx[0]=g_pidx[--out]; g_pidx[out]=root; /* sposta il max in coda */
- s2+=g_pbuf[root]; cum+=g_pbuf[root];
- if(out>0) topp_siftdown(g_pidx,out,0);
- } while(cum0);
- for(int i=0;i=0 -> quel token e' escluso (rinormalizzando al volo) */
-static int dist_sample(int V, int ban){
- double z = 1.0 - (ban>=0 ? g_pbuf[ban] : 0.0); if(z<=1e-12) z=1e-12;
- double u = rndu()*z, cum=0;
- for(int i=0;i