Upload folder using huggingface_hub
Browse files- docs/features/advanced_kv.md +909 -0
- docs/features/fused_nextn_mtp.md +272 -0
- docs/features/gemma4_dca.md +485 -0
- docs/features/introspection.md +245 -0
- docs/features/llamafile_build.md +444 -0
- docs/features/local_llamafile.md +323 -0
- docs/features/memory_embedder.md +668 -0
- docs/features/opencoti_server.md +1010 -0
- docs/features/poly_kv.md +817 -0
- docs/features/rolling_kv.md +1004 -0
- docs/features/rolling_kv_compute_reserve.md +126 -0
- docs/features/rolling_kv_step_prefetch.md +194 -0
- docs/features/rys_layer_duplication.md +326 -0
- docs/features/rys_probe.md +204 -0
- docs/features/sparse_attn.md +453 -0
- docs/features/tiered_inference.md +805 -0
- docs/features/tstar_auto_spill.md +189 -0
docs/features/advanced_kv.md
ADDED
|
@@ -0,0 +1,909 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# F5 β Advanced KV Cache Techniques
|
| 2 |
+
|
| 3 |
+
> Parent: [../MASTER_PLAN.md](../MASTER_PLAN.md)
|
| 4 |
+
> Status: **planning**
|
| 5 |
+
> Owner: TBD
|
| 6 |
+
|
| 7 |
+
## Problem
|
| 8 |
+
|
| 9 |
+
The vendored llamafile (F2) handles a single model + a single
|
| 10 |
+
context window competently, but the KV cache is the dominant
|
| 11 |
+
memory consumer at long contexts and the dominant time consumer
|
| 12 |
+
across agentic turns that share a prefix. The research
|
| 13 |
+
landscape has produced several techniques that attack different
|
| 14 |
+
parts of this problem:
|
| 15 |
+
|
| 16 |
+
- **Context-aware eviction** β drop tokens the model is least
|
| 17 |
+
likely to attend to again, instead of allocating capacity for
|
| 18 |
+
every token in the window.
|
| 19 |
+
- **Per-head GPU/CPU split** β selective attention heads stay
|
| 20 |
+
on GPU, the rest stream from host RAM, dramatically
|
| 21 |
+
shrinking the GPU memory footprint.
|
| 22 |
+
- **Asymmetric GPU-CPU pipelining** β load-aware scheduling
|
| 23 |
+
that keeps both compute resources busy.
|
| 24 |
+
- **Layer-ahead pre-computation** β the CPU starts the next
|
| 25 |
+
layer's attention while the GPU finishes the current one.
|
| 26 |
+
- **Shared compressed pool** β a single asymmetrically-
|
| 27 |
+
compressed KV pool across concurrent agents.
|
| 28 |
+
|
| 29 |
+
opencoti adopts these as a *sequence of patches* against the
|
| 30 |
+
vendored llamafile/llama.cpp tree, each developed and benched in
|
| 31 |
+
isolation, then validated as a stack.
|
| 32 |
+
|
| 33 |
+
## Goals
|
| 34 |
+
|
| 35 |
+
- **G1.** Land a series of numbered patches under
|
| 36 |
+
`vendors/patches/llamafile/` that implement (or port the
|
| 37 |
+
ideas of) **ReST-KV**, **HeadInfer**, **NEO**,
|
| 38 |
+
**ScoutAttention**, and **PolyKV**.
|
| 39 |
+
- **G2.** Each technique gets its own per-patch bench in
|
| 40 |
+
`perf/llamafile/<technique>.bench.ts` measuring the
|
| 41 |
+
documented claim against baseline.
|
| 42 |
+
- **G3.** A glue milestone (`0050-glue-bench.patch`) validates
|
| 43 |
+
the stack composes and provides a standing bench harness for
|
| 44 |
+
all techniques + the full stack.
|
| 45 |
+
- **G4.** Patches stay rebasable on upstream pin bumps. Each
|
| 46 |
+
patch ships with a header naming its bench file, milestone,
|
| 47 |
+
and (when applicable) the upstreaming PR.
|
| 48 |
+
|
| 49 |
+
## Non-goals (for now)
|
| 50 |
+
|
| 51 |
+
- **LMCache** β vLLM/SGLang-bound, llama.cpp portability 1/5.
|
| 52 |
+
Tracked as M7 (deferred). Re-evaluate once M0βM6 are in.
|
| 53 |
+
- Full multi-tenant serving infrastructure. PolyKV ships in
|
| 54 |
+
single-agent mode at M6 (degenerates to a 1-tenant pool);
|
| 55 |
+
multi-tenant value lights up only when opencoti adds
|
| 56 |
+
multi-agent fan-out on Tier 0 (future roadmap).
|
| 57 |
+
- A single "advanced features" toggle in user config. Each
|
| 58 |
+
technique is its own opt-in flag with sensible defaults
|
| 59 |
+
(most default off pending stability).
|
| 60 |
+
|
| 61 |
+
## Sequencing rationale
|
| 62 |
+
|
| 63 |
+
ReST-KV first (memory savings reduce load on every later
|
| 64 |
+
technique). HeadInfer second (per-head GPU residency is a
|
| 65 |
+
clean orthogonal axis on top of eviction). NEO third (load-
|
| 66 |
+
aware GPU-CPU scheduling assumes per-head granularity to
|
| 67 |
+
schedule against). ScoutAttention fourth (layer-ahead pre-
|
| 68 |
+
compute sits naturally on top of NEO's CPU compute path).
|
| 69 |
+
Glue fifth. PolyKV **immediately after glue** (keep momentum
|
| 70 |
+
from the preceding patch series β deferring loses the
|
| 71 |
+
implementation context and forces a costly cold-context
|
| 72 |
+
re-visit later).
|
| 73 |
+
|
| 74 |
+
llama.cpp-portability scores from research:
|
| 75 |
+
|
| 76 |
+
| Technique | Score | License | Reference |
|
| 77 |
+
| --- | --- | --- | --- |
|
| 78 |
+
| ReST-KV | 3/5 | unknown | arXiv 2605.09649 (Make Each Token Count) β no public code yet |
|
| 79 |
+
| HeadInfer | 2/5 | MIT | arXiv 2502.12574 β [github.com/wdlctc/headinfer](https://github.com/wdlctc/headinfer) |
|
| 80 |
+
| NEO | 2/5 | Apache-2.0 | arXiv 2411.01142 β [github.com/NEO-MLSys25/NEO](https://github.com/NEO-MLSys25/NEO) |
|
| 81 |
+
| ScoutAttention | 2/5 | unknown | arXiv 2603.27138 β code pending (DAC 2026) |
|
| 82 |
+
| PolyKV | 1/5 | MIT | arXiv 2604.24971 β [github.com/ishan1410/PolyKV](https://github.com/ishan1410/PolyKV) |
|
| 83 |
+
| LMCache *(deferred)* | 1/5 | Apache-2.0 | arXiv 2510.09665 β [github.com/lmcache/lmcache](https://github.com/lmcache/lmcache) |
|
| 84 |
+
|
| 85 |
+
## Patch number reservations
|
| 86 |
+
|
| 87 |
+
```
|
| 88 |
+
0001..0005-*.patch F4 M3 lazy-slot (these are F4's, not F5's)
|
| 89 |
+
0006-kv-reuse-prefix.patch F5 M0 (was F2 M4; SHIPPED 2026-05-24)
|
| 90 |
+
0007-build-header-deps.patch build infra (SHIPPED 2026-05-27; not a feature β see below)
|
| 91 |
+
0010-rest-kv-eviction.patch F5 M1 (SHIPPED 2026-05-27)
|
| 92 |
+
0011-rest-kv-config.patch F5 M1 (optional split; NOT built β single-patch v1)
|
| 93 |
+
0020-headinfer-per-head.patch F5 M2 (SHIPPED 2026-05-28)
|
| 94 |
+
0030-neo-pipeline.patch F5 M3 (SHIPPED 2026-05-28)
|
| 95 |
+
0031-per-stream-split.patch F4 M3 Phase 4 (SHIPPED 2026-05-29; task #109)
|
| 96 |
+
0032-m2-state-io.patch F5 M2 hardening / bug-222 fix (SHIPPED 2026-05-29)
|
| 97 |
+
0033-cpy-tie-blck-align.patch F5 bug-225 fix (SHIPPED 2026-05-29)
|
| 98 |
+
0034-m2-pinned-host.patch F5 M7-A foundation β pinned host KV (SHIPPED 2026-05-29)
|
| 99 |
+
0035-concat-q-block-aware.patch F5 bug-226 fix (SHIPPED 2026-05-30)
|
| 100 |
+
0036-pcie-probe-consume.patch F5-opt W1 β PCIe probe consume #293 (SHIPPED 2026-05-30)
|
| 101 |
+
0040-iqk-flash-attn.patch F5-opt W2 β wholesale iqk CPU-FA import #290 (SHIPPED 2026-05-30)
|
| 102 |
+
0041-q8kv.patch RETIRED 2026-05-30 β Q8_KV dropped (#291); redundant 8-bit (q8_0 covers it), TurboQuant M6 is the KV-quant driver
|
| 103 |
+
0042-fused-moe.patch F5-opt W3 β op-level fused MoE up+gate+GLU #292 (SHIPPED 2026-05-30; separate-up/gate MoE only β N/A to Gemma-4 fused gate_up)
|
| 104 |
+
(M5 glue+bench shipped as TS β perf/llamafile/, NOT a patch; old 0050 slot retired)
|
| 105 |
+
0070-rolling-kv.patch F5 M7 Rolling KV #296 (was LMCache; redesigned β see rolling_kv.md) (SHIPPED)
|
| 106 |
+
0071-state-io-cosmocc.patch F5 M7 cosmocc state-IO open fix #349 / bug-269 (SHIPPED; applies after 0070)
|
| 107 |
+
0072-poly-kv-pool.patch F5 M6 PolyKV S1 SharedKVPool #370 (SHIPPED 2026-06-06; applies after 0071)
|
| 108 |
+
0073-turboquant-kv.patch F5 M6 PolyKV S2 TurboQuant tiers+InnerQ+Level-A+Level-B #371 (SHIPPED 2026-06-06; GGML_OP_TURBO_WHT; applies after 0071)
|
| 109 |
+
0090-scout-layer-ahead.patch F5 M4 ScoutAttention (DEFERRED Aug 2026, #264 β moved off the 0040 slot)
|
| 110 |
+
```
|
| 111 |
+
|
| 112 |
+
Gaps (0002β0009, 0012β0019, ...) are intentional β they leave
|
| 113 |
+
room for per-technique refinements without renumbering
|
| 114 |
+
downstream patches.
|
| 115 |
+
|
| 116 |
+
## Milestones
|
| 117 |
+
|
| 118 |
+
### M0 β KV-cache reuse across agentic turns *(absorbed from F2 M4; shipped 2026-05-24)*
|
| 119 |
+
|
| 120 |
+
Reuse the resident KV across agentic turns instead of re-prefilling the
|
| 121 |
+
shared prefix (system prompt + tool defs + history) every turn.
|
| 122 |
+
|
| 123 |
+
**Key finding (shaped the design).** The vendored llama.cpp server
|
| 124 |
+
**already** reuses KV for a matching token-prefix: `cache_prompt` defaults
|
| 125 |
+
to `true`, and `get_available_slot()` (server-context.cpp) picks the idle
|
| 126 |
+
slot whose cached prefix best matches the incoming prompt (above
|
| 127 |
+
`--slot-prompt-similarity`, then LRU). So single-session turn-to-turn reuse
|
| 128 |
+
at the default `--parallel 1` works out of the box β the original "add a
|
| 129 |
+
server-side prefix-hash cache" plan predates that capability. What upstream
|
| 130 |
+
lacks is **sessionβslot affinity**: under `--parallel > 1`, a concurrent
|
| 131 |
+
opencode session can be routed to another session's slot and evict a prefix
|
| 132 |
+
that would otherwise be reused.
|
| 133 |
+
|
| 134 |
+
**What shipped.**
|
| 135 |
+
- `vendors/patches/llamafile/0006-kv-reuse-prefix.patch` (touches only
|
| 136 |
+
`tools/server/{server-task.h,server-task.cpp,server-context.cpp}`, disjoint
|
| 137 |
+
from the F4 0001β0005 lazy-slot patches): an optional `session_id` request
|
| 138 |
+
field + a `session_id β slot.id` affinity map. `get_available_slot()`
|
| 139 |
+
prefers the session's own idle slot (its resident KV *is* that session's
|
| 140 |
+
prefix) before the existing LCPβLRU path; `launch_slot_with_task()` records
|
| 141 |
+
the binding, dropping any stale session on that slot. **Regression shield:**
|
| 142 |
+
empty `session_id` (every non-opencoti client) skips the affinity phase, so
|
| 143 |
+
upstream slot selection is byte-for-byte unchanged.
|
| 144 |
+
- `@opencoti/llamafile`: a session-aware `fetch` wrapper
|
| 145 |
+
(`withKvReuseFetch`) tags each chat-completions body with the session's id
|
| 146 |
+
+ `cache_prompt: true`; `TierStreamRequest.sessionID` already carries it, so
|
| 147 |
+
no `runtime.ts` / surgical-hook change. Typed, env-backed server flags
|
| 148 |
+
(`--parallel`/`-sps`/`--cache-reuse`/`-c`), omitted when unset.
|
| 149 |
+
- "(session_id, prefix) reuse" falls out: affinity routes to the slot, the
|
| 150 |
+
server's existing token-LCP supplies the prefix match β **no separate
|
| 151 |
+
prefix hash needed**.
|
| 152 |
+
|
| 153 |
+
**Bench** (`perf/llamafile/turn-2-latency.bench.ts`, live on solidPC,
|
| 154 |
+
Qwen2.5-Coder-0.5B, CPU): scenario A turn-2 prefill collapses **2061 β 20
|
| 155 |
+
tokens** (202ms β 12ms; `cache_n` 2076) vs a no-cache control at 2096 tokens;
|
| 156 |
+
scenario B (`--parallel 2`, two sessions sharing a system prefix) each reuse
|
| 157 |
+
their own KV under affinity (`cache_n` 1990 each). 5/5 checks pass.
|
| 158 |
+
|
| 159 |
+
**Deferred (optional `0007`, evidence-driven):** an explicit `prefix_hash`
|
| 160 |
+
+ cross-restart disk persistence (`--slot-save-path` keyed by
|
| 161 |
+
`session_id+hash`). Within a server lifetime the resident-token LCP already
|
| 162 |
+
supplies the prefix match, so the hash adds nothing until restart-durable or
|
| 163 |
+
distributed reuse is wanted. No plugin, no HTTP surface, no TS client, no
|
| 164 |
+
opencode surgical hook β the surgical-hook grep count stays 18 (the vendored
|
| 165 |
+
C++ marker is `// opencoti F5 M0`, a vendored-source tag, not a counted hook).
|
| 166 |
+
|
| 167 |
+
**Build gotcha (buglog bug-167):** `server-task.h` is included by
|
| 168 |
+
`server-context.cpp`, `server-task.cpp` **and** `server-queue.cpp`. Editing
|
| 169 |
+
that header and rebuilding with `bun run build:llamafile:make` leaves
|
| 170 |
+
`server-queue.cpp.o` compiled against the old `task_params` layout β ABI
|
| 171 |
+
skew β `bad_array_new_length` on every generation request. After a vendored
|
| 172 |
+
**header** change, force a consistent recompile (`rm -f
|
| 173 |
+
vendors/sources/llamafile/o//llama.cpp/tools/server/*.o` then `:make`, or the
|
| 174 |
+
full reproducible `build:llamafile`).
|
| 175 |
+
|
| 176 |
+
### M1 β ReST-KV / retention-aware eviction *(shipped 2026-05-27)*
|
| 177 |
+
|
| 178 |
+
When a slot exceeds `n_ctx`, upstream context-shift keeps the first `n_keep`
|
| 179 |
+
tokens and blindly discards the **positional middle** `n_discard` chunk. M1
|
| 180 |
+
makes the choice **retention-aware**: discard the lowest-value contiguous
|
| 181 |
+
window instead, so long-context generation keeps the evidence that matters.
|
| 182 |
+
|
| 183 |
+
**Reference decision (resolved the open question).** The named paper
|
| 184 |
+
(arXiv 2605.09649) is **TRIM-KV** β *learned* retention gates trained by
|
| 185 |
+
distillation, needing a per-model pipeline + published gates; not a drop-in
|
| 186 |
+
runtime patch. **SnapKV / H2O** score tokens from the materialized attention
|
| 187 |
+
matrix, which **Flash Attention never produces** (the vendored pin defaults
|
| 188 |
+
`flash_attn_type = AUTO`) β incompatible with the FA path opencoti wants on.
|
| 189 |
+
So M1 substitutes a **score-free, FA-compatible** criterion: **KeyDiff**
|
| 190 |
+
(arXiv 2504.15364) β evict the keys least distinctive by cosine similarity
|
| 191 |
+
(distinctiveness = mean over heads of `1 - cos(key_head, mean_head)`; higher =
|
| 192 |
+
keep). `domvox/triattention-ggml` is a working ggml reference but scores every
|
| 193 |
+
decode interval (K copied GPUβCPU each `--tri-interval`, "the main
|
| 194 |
+
bottleneck"); M1 avoids that by scoring **only at the infrequent
|
| 195 |
+
context-shift trigger**, amortizing the K read.
|
| 196 |
+
|
| 197 |
+
**Depth decision: server-layer window (contiguous).** Eviction stays
|
| 198 |
+
contiguous so the server's CPU token mirror (`slot.prompt.tokens`) stays 1:1
|
| 199 |
+
with KV positions and all prefix-reuse / sampler logic is reused untouched.
|
| 200 |
+
Only *which* width-`n_discard` window is discarded changes β the
|
| 201 |
+
minimum-score one over `[n_keep, n_tokens β rest_kv_recent)`, not the
|
| 202 |
+
positional middle. Full per-token **scattered** KeyDiff (a core
|
| 203 |
+
`evict_to_budget` + a server rework to tolerate holes) is **deferred** β
|
| 204 |
+
revisit only if M1's bench shows window granularity costs meaningful quality.
|
| 205 |
+
|
| 206 |
+
**What shipped.**
|
| 207 |
+
- `vendors/patches/llamafile/0010-rest-kv-eviction.patch` (build-time; no
|
| 208 |
+
opencode upstream file touched):
|
| 209 |
+
- `src/llama-kv-cache.{h,cpp}`: concrete `seq_key_scores(seq_id,
|
| 210 |
+
layer_hint)` β enumerates the seq's cells in position order, reads each
|
| 211 |
+
post-RoPE K row to host, reshapes into `n_head_kv` heads, returns the
|
| 212 |
+
per-position KeyDiff score over a representative layer (default mid).
|
| 213 |
+
- **ABI-safe interface choice:** `src/llama-memory.h` gains a **non-pure**
|
| 214 |
+
virtual `seq_key_scores` with an empty default body β no backend is forced
|
| 215 |
+
to implement it and no vtable churn/RTTI is needed (the pure-virtual
|
| 216 |
+
`llama_memory_i` methods are left untouched).
|
| 217 |
+
- `src/llama-kv-cache-iswa.{h,cpp}`: delegate to the base cache.
|
| 218 |
+
- `include/llama.h` + `src/llama-context.cpp`: C shim
|
| 219 |
+
`llama_memory_seq_key_scores(...)` that returns 0 for non-KV memory (or a
|
| 220 |
+
key type with no float converter) β the caller falls back to positional.
|
| 221 |
+
- `tools/server/server-context.cpp`: inside the **existing** shift block,
|
| 222 |
+
when `rest_kv_eviction` is on, build prefix sums over the scores and pick
|
| 223 |
+
the min-sum window start `w0`; run the existing `seq_rm`/`seq_add`/mirror
|
| 224 |
+
rewrite parameterized by `w0` instead of `n_keep`. **Off (default) =
|
| 225 |
+
byte-for-byte upstream** (the off-path never calls the scorer).
|
| 226 |
+
- `common/common.h` + `common/arg.cpp`: `--rest-kv-eviction` /
|
| 227 |
+
`--rest-kv-recent N` / `--rest-kv-layer N` (server example).
|
| 228 |
+
- `@opencoti/llamafile` (`config.ts`/`launch.ts`): typed, env-backed
|
| 229 |
+
(`OPENCOTI_LLAMAFILE_REST_KV_*`) fields β `buildServerArgs` emits the flags
|
| 230 |
+
when set, omits when unset. Global flag, not per-request β no fetch wrapper.
|
| 231 |
+
|
| 232 |
+
**Bench** (`perf/llamafile/rest-kv-eviction.bench.ts`, needle-in-the-middle,
|
| 233 |
+
live on solidPC, Qwen2.5-Coder-0.5B, CPU): **hard checks 3/3** β a context
|
| 234 |
+
shift fired, the rest-kv eviction path executed **only** with eviction on, and
|
| 235 |
+
the off-path never logged it (proving the off-path is upstream-identical).
|
| 236 |
+
These deterministic, model-independent checks gate the exit code. The
|
| 237 |
+
quality-delta **headline is soft and model-dependent**: a 0.5B model either
|
| 238 |
+
states the needle code *before* the shift fires (generous headroom β both runs
|
| 239 |
+
recover) or degrades so far *after* the shift that neither restates a random
|
| 240 |
+
code even when the needle survives in KV β demonstrating the delta needs a
|
| 241 |
+
stronger model. `0011` stays reserved (single-patch v1, mirroring M0's `0006`).
|
| 242 |
+
|
| 243 |
+
**Surgical-hook count stays 18** β the C++ marker is `// opencoti F5 M1
|
| 244 |
+
rest-kv-eviction`, a vendored-source tag, not a counted opencode hook.
|
| 245 |
+
|
| 246 |
+
**Build-infra prerequisite (`0007`, buglog bug-170).** Iterating M1's vendored
|
| 247 |
+
C++ surfaced that the build had **no working header-dependency tracking**
|
| 248 |
+
(mkdeps' `o//depend` can't resolve llama.cpp's `-iquote` includes β zero src
|
| 249 |
+
edges β header edits never recompiled β forced from-scratch rebuilds) **and**
|
| 250 |
+
that **ccache cached nothing** (a `.d`-name mismatch made it abort every
|
| 251 |
+
compile with "internal error"). Patch `0007-build-header-deps.patch` fixes both
|
| 252 |
+
(`-MMD -MF $@.d` in the compile rules + `-include` the per-object `.o.d`), and
|
| 253 |
+
`build-pipeline` runs the full reset-build with `CCACHE_RECACHE=1` so a warm
|
| 254 |
+
cache can't orphan a cosmocc fat object's `.aarch64` twin. See
|
| 255 |
+
`docs/features/llamafile_build.md`. This is what made M1 (and every later F5
|
| 256 |
+
milestone) tractable to iterate.
|
| 257 |
+
|
| 258 |
+
**Deferred / not done:** scattered per-token KeyDiff; quantized-K cosine
|
| 259 |
+
scoring (falls back to positional when K is quantized); the optional `0011`
|
| 260 |
+
per-request override; the full **reset-based reproducible build** verification
|
| 261 |
+
(would `git clean -fdx` the in-tree vendored state β run when a reset window is
|
| 262 |
+
authorized; `0010`/`0007` are reverse-verified against pristine).
|
| 263 |
+
|
| 264 |
+
### M2 β HeadInfer (head-wise GPU/CPU split) *(shipped 2026-05-28)*
|
| 265 |
+
|
| 266 |
+
When a model is GPU-offloaded, the *entire* KV cache for offloaded layers lives
|
| 267 |
+
in VRAM and caps how much context fits on the card. M2 keeps a configurable
|
| 268 |
+
fraction of attention heads' KV resident on GPU and offloads the rest to host
|
| 269 |
+
memory, reassembling the full head set per attention step via `ggml_concat` on
|
| 270 |
+
the head axis. Resident VRAM drops proportionally to the offloaded fraction;
|
| 271 |
+
correctness is preserved.
|
| 272 |
+
|
| 273 |
+
**Reference decision (resolved at impl time).** The named paper (arXiv
|
| 274 |
+
2502.12574, [wdlctc/headinfer](https://github.com/wdlctc/headinfer)) is
|
| 275 |
+
HF-Transformers; not portable. A parallel investigation of `/shared/dev/lightseek`
|
| 276 |
+
β which has deep, working llama.cpp head-splitting surgery β confirmed it is
|
| 277 |
+
**distributed tensor-parallelism** (heads split across MPI/RPC ranks, each on
|
| 278 |
+
its own GPU) on a different llama.cpp lineage (vanilla upstream, not the
|
| 279 |
+
Mozilla-Ocho llamafile fork). lightseek informed the design (GQA-ratio guard,
|
| 280 |
+
"logical dims full / physical alloc partial" framing) but its backend-level
|
| 281 |
+
machinery is **reference only, not code to port** β porting it wholesale would
|
| 282 |
+
blow past opencoti's minimal-additive-patch rule.
|
| 283 |
+
|
| 284 |
+
**Depth decision: head-RESIDENCY split, not head-COMPUTE split (foundation-first).**
|
| 285 |
+
M2 delivers offload + scheduler-driven stream-back for correctness; the per-head
|
| 286 |
+
attention COMPUTE split (run CPU-resident heads' attention on the CPU backend to
|
| 287 |
+
avoid the per-step stream-back) is the headline perf win and is **deferred to
|
| 288 |
+
optional `0021`** β revisit only if the bench shows the stream-back cost
|
| 289 |
+
dominates. The "heavy" depth option, consciously deferred (mirrors M1's
|
| 290 |
+
window-vs-scattered).
|
| 291 |
+
|
| 292 |
+
**What shipped.**
|
| 293 |
+
- `vendors/patches/llamafile/0020-headinfer-per-head.patch` (build-time;
|
| 294 |
+
no opencode upstream file touched; 14 files / 28 hunks / 520 lines):
|
| 295 |
+
- `src/llama-kv-cache.{h,cpp}`: per-layer `kv_layer` gains optional
|
| 296 |
+
`k_cpu`/`v_cpu` sub-tensors + a `gpu_heads` count (0/absent = no split).
|
| 297 |
+
Constructor allocates the GPU half (gpu-head rows) in the device buffer-type
|
| 298 |
+
and the CPU half (cpu-head rows) in a CPU buffer-type via the existing
|
| 299 |
+
`ctx_for_buft(ggml_backend_cpu_buffer_type())` β a CPU sub-tensor on an
|
| 300 |
+
otherwise-GPU layer just adds a CPU ctx/buffer pair, **no allocator
|
| 301 |
+
rework**. GQA-safe split: clamp `gpu_heads` to a whole number of KV-head
|
| 302 |
+
groups (lightseek lesson). Phase-1/2/3 hooks (`ensure_cleared`,
|
| 303 |
+
`shrink_if_idle`) loop the extra sub-tensor.
|
| 304 |
+
- `get_k`/`get_v`: when split, build the gpu/cpu views and
|
| 305 |
+
`ggml_concat(ctx, kg_v, kc_v, /*dim=*/1)` β the head axis is dim 1, so the
|
| 306 |
+
concat produces the same `[head_dim, n_head_kv, n_kv, ns]` shape the
|
| 307 |
+
non-split path returns. Backend scheduler auto-inserts the cross-backend
|
| 308 |
+
copy. Off path (no split) is unchanged.
|
| 309 |
+
- `cpy_k`/`cpy_v`: split branch slices `k_cur` by head β two `ggml_set_rows`
|
| 310 |
+
+ a dependency-tying `ggml_add` so the two scatter nodes propagate in one
|
| 311 |
+
graph node. Off path unchanged.
|
| 312 |
+
- `ggml/src/ggml-cuda/ggml-cuda.cu`: **one-line fix** to CUDA `supports_op`
|
| 313 |
+
for `GGML_OP_CONCAT` β was over-broad (any non-I32/I16) while the kernel
|
| 314 |
+
`ggml_cuda_op_concat` is F32-only (three asserts at `concat.cu:158-160`).
|
| 315 |
+
Tightened to `op->src[0]->type == GGML_TYPE_F32` so F16 concat correctly
|
| 316 |
+
routes to the CPU backend (which has a real F16 path at
|
| 317 |
+
`ggml-cpu/ops.cpp:1980 concat_f16`). This is a genuine upstream bug β
|
| 318 |
+
`supports_op` was lying about what the kernel implements.
|
| 319 |
+
- Plumbing: `common/common.h` + `common/arg.cpp` add
|
| 320 |
+
`--headinfer-gpu-heads-frac F` (server example), threaded through
|
| 321 |
+
`cparams`/`llama-context.cpp`/`llama-cparams.h`/`llama-model.cpp` into the
|
| 322 |
+
cache constructor (mirroring the `--no-kv-unified` wiring template). Both
|
| 323 |
+
iswa and hybrid memory ctors forward `1.0f` (off) β they don't thread
|
| 324 |
+
headinfer themselves; the split is unified-mode only by design.
|
| 325 |
+
- `@opencoti/llamafile` (`config.ts`/`launch.ts`): typed, env-backed
|
| 326 |
+
(`OPENCOTI_LLAMAFILE_HEADINFER_GPU_HEADS_FRAC`) `headinferGpuHeadsFrac` field
|
| 327 |
+
β `buildServerArgs` emits `--headinfer-gpu-heads-frac` only when set
|
| 328 |
+
(omitted = server default = off). Three new launch-args tests
|
| 329 |
+
(omit/emit/precede-extraArgs); typecheck clean, **78/0 tests**.
|
| 330 |
+
|
| 331 |
+
**Bench** (`perf/llamafile/headinfer-residency.bench.ts`, live on solidPC,
|
| 332 |
+
RTX 3090, Qwen2.5-Coder-0.5B-IQ4_XS, `-ngl 99 -fa on --parallel 1`, ctx 4096):
|
| 333 |
+
**6/6 PASS** β
|
| 334 |
+
| | baseline `frac=1.0` | split `frac=0.5` |
|
| 335 |
+
| --- | --- | --- |
|
| 336 |
+
| `CUDA0` KV | **48 MiB** | **24 MiB** (-50%) |
|
| 337 |
+
| `CPU` KV | 0 (off-path identity) | **24 MiB** (+ host half) |
|
| 338 |
+
| greedy decode (temp=0) | reference | **byte-identical to baseline** |
|
| 339 |
+
|
| 340 |
+
Identical-text is the strongest correctness check possible (implies identical
|
| 341 |
+
token ids; stronger than a cosine β₯ 0.999 bar). Reproducible against
|
| 342 |
+
Qwen2.5-1.5B-Instruct at the same frac (linearly scaled, 112 β 56 / 56). The
|
| 343 |
+
deferred-perf cost is the GPUβCPUβGPU concat bounce per step; option (a)
|
| 344 |
+
two-`ggml_cpy`-into-preallocated-dest can replace concat for steady-state perf
|
| 345 |
+
in a follow-up (post-M2 or `0021`).
|
| 346 |
+
|
| 347 |
+
**Off-path identity:** `frac=1.0` (default, every non-opencoti client) β `k_cpu`
|
| 348 |
+
is never allocated, `get_k`/`get_v`/`cpy_k`/`cpy_v` take the non-split branch;
|
| 349 |
+
the binary's KV path is byte-for-byte upstream-equivalent.
|
| 350 |
+
|
| 351 |
+
**Surgical-hook count stays 18** β the in-tree marker is `// opencoti F5 M2
|
| 352 |
+
headinfer`, a vendored-source tag, not a counted opencode hook.
|
| 353 |
+
|
| 354 |
+
**Build-infra prerequisite that surfaced (buglog `bug-178`).** Iterating M2's
|
| 355 |
+
vendored C++ surfaced that an earlier `CCACHE_RECACHE=1` from-scratch rebuild
|
| 356 |
+
(M1-E / `#244` lineage) had silently produced a binary with **broken C++
|
| 357 |
+
exception unwinding** β any `throw` aborts with `ud2` / SIGILL. This is a
|
| 358 |
+
distinct variant of `bug-176`: that defect's known symptom was a *link failure*
|
| 359 |
+
(`concomitant .aarch64 file missing`); this variant **links successfully** but
|
| 360 |
+
the consolidated `.eh_frame` in the APE is corrupt, so the unwinder traps β
|
| 361 |
+
masked from M1-E because `--version` (the only thing M1-E verified) never
|
| 362 |
+
exercises unwinding. Disambiguated from non-causes: NOT app/M2 source
|
| 363 |
+
(gated-off `headinfer_gpu_heads_frac=1.0` AND pure-upstream `std::stoi` both
|
| 364 |
+
trap); NOT toolchain or flags (a 5-line `throw`/`catch` compiled with the exact
|
| 365 |
+
build flags via `cosmoc++` catches correctly). Fix: rebuild non-destructively
|
| 366 |
+
with **`OPENCOTI_NO_CCACHE=1 bun run build:llamafile:make`** (the `make`
|
| 367 |
+
subcommand calls `runMake()` which never git-resets the submodule;
|
| 368 |
+
`OPENCOTI_NO_CCACHE=1` drops the ccache `CC=/CXX=` wrapper so `cosmocc` itself
|
| 369 |
+
writes both fat-object arches in one invocation, no stale x86-only twin).
|
| 370 |
+
**Lesson:** every llamafile build verification MUST run a real CPU inference
|
| 371 |
+
or a throw-probe (`-n notanum`), never just `--version`.
|
| 372 |
+
|
| 373 |
+
**Deferred / not done:**
|
| 374 |
+
- **Per-stream device buffers + host-pageable** (`#109` Phase 4): the M2 plan's
|
| 375 |
+
gated extension. Gate fired β landing per-stream now would expand the
|
| 376 |
+
`cpy_k`/`cpy_v` multi-stream scatter surface (the plan's flagged hot-path
|
| 377 |
+
regression risk) with no validated need; M2-A/B/C delivered the head-residency
|
| 378 |
+
headline win on unified mode cleanly without it. **Carries to optional `0021`**.
|
| 379 |
+
- **Per-head attention COMPUTE split** (run CPU-resident heads' attention on the
|
| 380 |
+
CPU backend to eliminate the per-step stream-back). The headline perf win;
|
| 381 |
+
consciously deferred per the foundation-first depth decision above. Revisit
|
| 382 |
+
only if a future bench shows the stream-back cost dominates.
|
| 383 |
+
- **Quantized-K head-split** edge cases; multi-GPU head residency β later tuning.
|
| 384 |
+
- **Steady-state concat optimization** (two `ggml_cpy` into a pre-allocated F16
|
| 385 |
+
destination on GPU): replaces the GPUβCPUβGPU concat bounce with a single
|
| 386 |
+
CPUβGPU copy + a GPU-side memcpy. Larger surgery, post-M2 follow-up.
|
| 387 |
+
|
| 388 |
+
### M3 β NEO (asymmetric GPU/CPU attention pipelining) *(shipped 2026-05-29)*
|
| 389 |
+
|
| 390 |
+
**Reference:** arXiv 2411.01142, Apache-2.0 at
|
| 391 |
+
[github.com/NEO-MLSys25/NEO](https://github.com/NEO-MLSys25/NEO).
|
| 392 |
+
Reference is swiftLLM (PyTorch). Like M2's lightseek precedent,
|
| 393 |
+
**not directly portable** β NEO ports the structural split + concurrent
|
| 394 |
+
dispatch concept into ggml's decode loop rather than wrapping the
|
| 395 |
+
upstream code. The published design covers three pieces (per-head
|
| 396 |
+
COMPUTE split + async overlap + load-aware scheduling); M3 bundled
|
| 397 |
+
all three into one `0030-neo-pipeline.patch` per locked decision
|
| 398 |
+
(absorbing the `0021` "per-head compute split" reservation from M2).
|
| 399 |
+
|
| 400 |
+
**Depth decision:** full structural-split + orchestrator + load-aware
|
| 401 |
+
controller was the planned scope; the orchestrator and structural
|
| 402 |
+
split shipped; the EWMA load-aware controller (M3-C) was **deferred**
|
| 403 |
+
after the M3-D gate fired (see "What shipped" below).
|
| 404 |
+
|
| 405 |
+
**Patch:** [`vendors/patches/llamafile/0030-neo-pipeline.patch`](../../vendors/patches/llamafile/0030-neo-pipeline.patch).
|
| 406 |
+
Sits on top of `0020` (M2 head-residency split). The historical
|
| 407 |
+
"per-head compute split" reservation at `0021` was absorbed by `0030`
|
| 408 |
+
(M3 bundled it in). **F4 M3 Phase 4** (per-stream KV tensor split,
|
| 409 |
+
SHIPPED 2026-05-29 β see `docs/decisions/0001-lazy-slot-context.md
|
| 410 |
+
Β§Phase 4 β shipped notes` and the patches README row for
|
| 411 |
+
`0031-per-stream-split`) lands at `0031`, *after* `0030`, because
|
| 412 |
+
Phase 4-D lifts M2's dedicated `get_k_gpu`/`get_v_gpu` accessors that
|
| 413 |
+
`0030` introduces for NEO's two-FA dispatch. The originally-reserved
|
| 414 |
+
`0021` slot stays vacant.
|
| 415 |
+
|
| 416 |
+
**What shipped:**
|
| 417 |
+
|
| 418 |
+
- **M3-A structural two-FA split.** New `llm_graph_context::build_attn_mha_neo`
|
| 419 |
+
in `llama.cpp/src/llama-graph.cpp` is invoked from `build_attn`'s
|
| 420 |
+
flash-attn branch when `cparams.neo_pipeline_mode != 0 &&
|
| 421 |
+
mctx_cur->headinfer_split_active(il)`. It slices Q on dim 1 at the
|
| 422 |
+
GQA-clamped `gpu_heads * gqa_ratio` boundary (a clean `ggml_view_4d`
|
| 423 |
+
with no extra permute), slices mask and sinks correspondingly, builds
|
| 424 |
+
TWO `ggml_flash_attn_ext` ops on disjoint head ranges, and concats
|
| 425 |
+
the outputs on the head axis. The K/V halves come from new
|
| 426 |
+
`llama_memory_context` accessors `get_k_gpu`/`_cpu`/`get_v_gpu`/`_cpu`
|
| 427 |
+
+ `headinfer_split_active(il)` (added in `llama-memory.h`,
|
| 428 |
+
implemented in `llama-kv-cache.{h,cpp}`, delegated through
|
| 429 |
+
`llama_kv_cache_context` wrappers). The scheduler auto-routes each
|
| 430 |
+
FA op to the backend hosting its K/V views (CUDA for `cur_g`, CPU
|
| 431 |
+
for `cur_c`) β no scheduler surgery needed. This **eliminates M2's
|
| 432 |
+
per-step `O(n_kv Γ head_dim_cpu Γ 2 B)` CPUβGPU stream-back** and
|
| 433 |
+
replaces it with an `O(n_tokens Γ head_dim_cpu)` bounce on the FA
|
| 434 |
+
output. The original `get_k`/`get_v` paths are unchanged so
|
| 435 |
+
`--neo-pipeline off` runs the M2 concat path byte-identically.
|
| 436 |
+
|
| 437 |
+
- **M3-B NEO orchestrator (wired-but-no-observable-win).** New vendored
|
| 438 |
+
source files `vendors/sources/llamafile/llama.cpp/ggml/include/ggml-neo-pipeline.h`
|
| 439 |
+
+ `ggml/src/ggml-neo-pipeline.cpp` provide a registry of `(cur_g, cur_c)`
|
| 440 |
+
pairs + a `cudaEvent` slot per `cur_g`. The graph builder calls
|
| 441 |
+
`ggml_neo_pipeline_register_pair(cur_g, cur_c)` after constructing each
|
| 442 |
+
pair. The CUDA backend's `ggml_backend_cuda_graph_compute` consults the
|
| 443 |
+
registry: if the split's first node is a registered `cur_g`, it sets
|
| 444 |
+
`cuda_ctx->curr_stream_no = 1` so all kernels dispatch on the alternate
|
| 445 |
+
CUDA stream, records a `cudaEvent` on stream 1 at end-of-graph, then
|
| 446 |
+
restores `curr_stream_no = 0`. When a downstream split reads a
|
| 447 |
+
registered `cur_g` (typically the concat join), the hook inserts a
|
| 448 |
+
`cudaStreamWaitEvent(cuda_ctx->stream(), event, 0)` so the consuming
|
| 449 |
+
stream waits for the stream-1 work. CUDA graph capture is disabled for
|
| 450 |
+
NEO-engaged splits; off-path graphs keep the fast `cuda_graph` path.
|
| 451 |
+
|
| 452 |
+
- **M3-D adapter.** `--neo-pipeline off|on|auto` in `common/arg.cpp`
|
| 453 |
+
(env: `LLAMA_ARG_NEO_PIPELINE`). `cparams.neo_pipeline_mode` field
|
| 454 |
+
plumbed through `include/llama.h`, `common/common.{h,cpp}`,
|
| 455 |
+
`llama-cparams.h`, `llama-context.cpp`. `@opencoti/llamafile` typed
|
| 456 |
+
field `neoPipelineMode: "off"|"on"|"auto"|undefined` + env
|
| 457 |
+
`OPENCOTI_LLAMAFILE_NEO_PIPELINE` mirror the M2 frac template; 3 new
|
| 458 |
+
launch-args tests pass (13 total).
|
| 459 |
+
|
| 460 |
+
- **Build system.** New `ggml-neo-pipeline.cpp` listed in
|
| 461 |
+
`llama.cpp/BUILD.mk` `GGML_SRCS_CPP` (for the main binary) and in
|
| 462 |
+
`llamafile/build-functions.sh` `compile_ggml_core` source list (for the
|
| 463 |
+
CUDA DSO). Both rebuild cleanly; 14 `ggml_neo_pipeline_*` symbols
|
| 464 |
+
exported from `ggml-cuda.so`.
|
| 465 |
+
|
| 466 |
+
**Bench numbers β `perf/llamafile/neo-pipeline.bench.ts`** (solidPC RTX 3090
|
| 467 |
+
+ Qwen2.5-Coder-0.5B IQ4_XS, ctx=4096, predict=128, frac=0.5):
|
| 468 |
+
|
| 469 |
+
| Run | tps | Ξ vs M2 |
|
| 470 |
+
| --- | --- | --- |
|
| 471 |
+
| B baseline (no split) | 326.69 | +75% |
|
| 472 |
+
| M m2-only (split, M2 path) | 186.96 | β |
|
| 473 |
+
| **N neo-on (split, M3 path)** | **187.04** | **+0.04%** |
|
| 474 |
+
| O off-identity (`--neo-pipeline off`) | 151.65 | β |
|
| 475 |
+
|
| 476 |
+
Hard checks:
|
| 477 |
+
- **C1 PASS** β N completion === B completion (correctness intact).
|
| 478 |
+
- **C2 FAIL** β N tps - M tps < 5% (observed 0.04%).
|
| 479 |
+
- **C3 PASS** β N GPU KV (24 MiB) === M GPU KV (24 MiB) β residency preserved.
|
| 480 |
+
- **C4 PASS** β O completion === M completion (`--neo-pipeline off` byte-identical).
|
| 481 |
+
|
| 482 |
+
Soft check S1 (NEO β₯ 80% of baseline tps): **WARN** at 57.3% β the
|
| 483 |
+
CPU FA on half-heads is the wall-time bottleneck; no overlap can
|
| 484 |
+
recover the baseline because the baseline runs zero CPU work.
|
| 485 |
+
|
| 486 |
+
**Why C2 failed:** the M3-B stream-swap mechanism is correct (M3-A
|
| 487 |
+
`cur_g` is in fact dispatched on CUDA stream 1, downstream consumers
|
| 488 |
+
correctly wait on the stream-1 event, `cur_c` runs concurrently on
|
| 489 |
+
the CPU backend, the GPU FA finishes long before the CPU FA), but
|
| 490 |
+
for Qwen-Coder-0.5B at ctx=4096 the per-step GPU FA on half-heads
|
| 491 |
+
finishes in single-digit ΞΌs while the CPU FA on half-heads takes
|
| 492 |
+
~10Γ longer. Total wall time is bounded by `max(GPU FA, CPU FA) + concat`
|
| 493 |
+
which is dominated by the CPU FA. Even at long context (ctx=16384,
|
| 494 |
+
predict=256 with a 4480-char prompt) the speedup stayed marginal
|
| 495 |
+
(+1.4%, still below C2's 5% threshold). The overlap WOULD pay off
|
| 496 |
+
on workloads where GPU FA cost approaches CPU FA cost β multi-GPU
|
| 497 |
+
splits, much larger models with smaller `frac`, or quantized-K
|
| 498 |
+
paths where the GPU side does more work per head. M3-B is correct
|
| 499 |
+
infrastructure for those regimes and for M4 (ScoutAttention) to
|
| 500 |
+
reuse.
|
| 501 |
+
|
| 502 |
+
**Per the M3-D gate decision:** C2 failed but the orchestrator is
|
| 503 |
+
**correct** (no crashes, no deadlocks, C1+C3+C4 pass at the
|
| 504 |
+
short-ctx bench) and the off-path is byte-identical to the
|
| 505 |
+
M2-shipped binary. The orchestrator ships **wired-but-no-observable-win**
|
| 506 |
+
(not dormant-stub as the original plan text described) β the CUDA
|
| 507 |
+
hook is live and registered pairs route to stream 1 β but the
|
| 508 |
+
mechanism produces no measurable throughput improvement on this
|
| 509 |
+
hardware/model. The M3-A structural win (eliminating the per-step
|
| 510 |
+
CPUβGPU K/V stream-back) is the actual M3 deliverable.
|
| 511 |
+
|
| 512 |
+
**M3-C (EWMA load-aware controller) deferred.** The plan called for
|
| 513 |
+
a per-layer `neo_layer_state` measuring GPU/CPU FA times and
|
| 514 |
+
adjusting `gpu_heads_active` toward load balance. Without an
|
| 515 |
+
observable M3-B overlap win this is cosmetic β there's nothing for
|
| 516 |
+
the controller to optimize toward. Revisit when M3-B's mechanism
|
| 517 |
+
finds a regime where it actually wins (multi-GPU, much larger
|
| 518 |
+
models, quantized-K), or when M4 ScoutAttention reuses the same
|
| 519 |
+
orchestrator and the load-balance trade-off becomes meaningful.
|
| 520 |
+
|
| 521 |
+
**Off-path identity proven.** M2's `perf/llamafile/headinfer-residency.bench.ts`
|
| 522 |
+
re-runs **6/6 PASS** against the M3-built binary β the M2 concat
|
| 523 |
+
path is byte-equivalent to the 0020-shipped binary. `0030` does not
|
| 524 |
+
regress M2. Surgical-hook count stays **18** (all M3 changes are
|
| 525 |
+
in vendored llama.cpp source captured into `0030`, not in upstream
|
| 526 |
+
opencode `packages/`).
|
| 527 |
+
|
| 528 |
+
**Build / capture rules carried forward:** `OPENCOTI_NO_CCACHE=1
|
| 529 |
+
bun run build:llamafile:make` for the main binary; `bun run
|
| 530 |
+
build:llamafile:cuda` for the CUDA DSO (~24 min build on solidPC).
|
| 531 |
+
Patch captured via **snapshot-diff** under `.opencoti/snap-m3-pre/`
|
| 532 |
+
(NOT `git diff HEAD`; bug-121). Apply-tested clean against the
|
| 533 |
+
snapshot. Vendored submodule's dirty state intentionally not staged
|
| 534 |
+
on the substance commit.
|
| 535 |
+
|
| 536 |
+
**Deferred / non-goals (carry forward):**
|
| 537 |
+
- **M3-B win on this hardware** β see Why C2 failed; revisit when
|
| 538 |
+
workload reshapes.
|
| 539 |
+
- **M3-C EWMA load-aware controller** β see above.
|
| 540 |
+
- **Multi-GPU NEO** (orchestrating multiple CUDA devices alongside
|
| 541 |
+
CPU) β single-CUDA-device + CPU is the current scope.
|
| 542 |
+
- **Vulkan / ROCm backends** β CUDA-only; the orchestrator's CUDA
|
| 543 |
+
hook is unreachable on other GPU backends, so M3-A's structural
|
| 544 |
+
split runs without M3-B's stream-swap there.
|
| 545 |
+
- **Per-stream device buffers + host-pageable** (#109) β still
|
| 546 |
+
carries from M2 / F4 Phase 4.
|
| 547 |
+
- **Persistence of EWMA state across server restarts** β moot since
|
| 548 |
+
M3-C deferred.
|
| 549 |
+
- **Reallocating `gpu_heads` mid-decode** β the runtime controller
|
| 550 |
+
is deferred; physical `gpu_heads` is still ctor-set.
|
| 551 |
+
|
| 552 |
+
### M4 β ScoutAttention (layer-ahead CPU pre-compute) *(deferred 2026-05-29)*
|
| 553 |
+
|
| 554 |
+
- **Reference:** arXiv 2603.27138 β paper published 2026-03-28, accepted
|
| 555 |
+
DAC '26 (July 26-29, Long Beach). Authors: Qiuyang Zhang, Kai Zhou,
|
| 556 |
+
Ding Tang, Kai Lu, Cheng Li, Zhenyu Yang, Peng Xu, Jiguang Wan (HUST +
|
| 557 |
+
Huawei + Zhejiang Lab).
|
| 558 |
+
- **Reference-code status (verified 2026-05-29):** **ABSENT.** Authoritative
|
| 559 |
+
search via `gh search repos`/`gh search code` for `ScoutAttention`,
|
| 560 |
+
`scout-attention layer-ahead`, `layer-ahead pre-computation`,
|
| 561 |
+
`asynchronous periodic recall sparse attention` returns zero
|
| 562 |
+
implementation hits. HF Hub model + space search empty. The full paper
|
| 563 |
+
HTML has no GitHub/GitLab/artifact link and no code-availability
|
| 564 |
+
statement. Authors are HUST + Huawei + Zhejiang Lab β academic
|
| 565 |
+
publication, code not advertised.
|
| 566 |
+
- **Algorithmic substance (from paper Algorithm 1, recorded so a future
|
| 567 |
+
picker-up has the spec cold):**
|
| 568 |
+
1. Predict next-layer query `Q_pred^(i+1) β W_Q^(i+1) Β· X^i` (residual-
|
| 569 |
+
similarity trick, cosine > 0.93 across Gemma 3, Llama 3.1, Mistral,
|
| 570 |
+
GLM 4 tested in the paper)
|
| 571 |
+
2. Block-wise top-k: `B_pred^(i+1) β TopK(Q_pred^(i+1) Β· K_digest^(i+1)^T)`
|
| 572 |
+
3. Identify CPU-resident blocks: `B_cpu^(i+1) β B_pred^(i+1) \ B_gpu^(i+1)`
|
| 573 |
+
4. Async spawn `CPUAttn(B_cpu^(i+1))`
|
| 574 |
+
5. GPU runs full layer i (attn + FFN + QKV projections) overlapped
|
| 575 |
+
6. `A^i β Merge(A_gpu^i, A_cpu^i)` via FlashAttention's online-softmax
|
| 576 |
+
merge (the previously-spawned CPU work joins here)
|
| 577 |
+
|
| 578 |
+
Plus asynchronous periodic recall (Ξ² = 12% threshold β avg ~8.7 layers
|
| 579 |
+
between recalls, CPU compute ratio ~8.2%). Hyperparameters tested: block
|
| 580 |
+
size 32 default, sparse budget 1024-2048, ctx up to 64k, batches 16-64,
|
| 581 |
+
Qwen3-8B/14B/32B.
|
| 582 |
+
- **Why portability is harder than M2/M3.** Paper ships on SGLang +
|
| 583 |
+
FlashInfer + IPEX. M2 (lightseek) and M3 (NEO/swiftLLM) were paper-ports
|
| 584 |
+
onto **existing** ggml primitives. M4 requires **new primitives**:
|
| 585 |
+
a block-wise top-k CUDA kernel (FlashInfer-equivalent), a block-sparse
|
| 586 |
+
variant of `ggml_flash_attn_ext`, a `K_digest` side tensor + its
|
| 587 |
+
computation kernel, and an IPEX-equivalent CPU worker accepting a
|
| 588 |
+
selected-blocks input (llama.cpp's `flash_attn_ext` CPU kernel does
|
| 589 |
+
full-K/V only). Multi-week kernel work with C1 byte-identity risk on
|
| 590 |
+
every downstream check.
|
| 591 |
+
- **Decision (user 2026-05-29):** defer until reference code surfaces.
|
| 592 |
+
Re-evaluation August 2026 (post-DAC). Progression updates to M5 Glue
|
| 593 |
+
("compose-validate M0+M1+M2+M3", M4 removed from the compose set until
|
| 594 |
+
it lands) β M6 PolyKV (has reference code) β revisit M4.
|
| 595 |
+
- **If/when M4 lights up later, options remain on the table:** (a)
|
| 596 |
+
reimplement from spec, (b) substitute a simpler layer-ahead
|
| 597 |
+
K_pred-prefetch-only approximation that skips the block-sparse FA
|
| 598 |
+
refactor, or (c) compose with M3's NEO orchestrator (run NEO's GPU
|
| 599 |
+
half dense, run the CPU half block-sparse) β the registry surface
|
| 600 |
+
added in `0030-neo-pipeline.patch` is the natural integration point.
|
| 601 |
+
- **Config flag (when M4 ships):** `--scout-attention on|off` (default off).
|
| 602 |
+
|
| 603 |
+
### M5 β Glue + bench harness *(shipped 2026-05-29)*
|
| 604 |
+
|
| 605 |
+
**Goal.** Compose-validate the F5 stack β M0 (session-keyed KV reuse) +
|
| 606 |
+
M1 (retention-aware eviction) + M2 (head-residency split) + M3 (NEO
|
| 607 |
+
concurrent FA). Each prior milestone landed its own bench in isolation;
|
| 608 |
+
M5 is the first that lights all four up simultaneously. M4 is removed
|
| 609 |
+
from the compose set per its 2026-05-29 deferral.
|
| 610 |
+
|
| 611 |
+
**What shipped.**
|
| 612 |
+
|
| 613 |
+
- `perf/llamafile/_bench-lib.ts` β first shared helper module under
|
| 614 |
+
`perf/llamafile/` (293 lines). Exports `resolveBin`, `resolveModel`
|
| 615 |
+
(`"qwen" | "gemma" | "auto" | <abs-path>`), `pickPort`, `probeGpu`
|
| 616 |
+
(enforces the 2026-05-28 Cerebrum free-VRAM rule), `spawnServer`
|
| 617 |
+
(returns `{baseURL, stop, log}`; `stop()` is the bug-191
|
| 618 |
+
SIGTERMβ2 s raceβSIGKILL reap with `await proc.exited` in both
|
| 619 |
+
branches), `waitForHealth`, `metric`, `parseKvBuffers`,
|
| 620 |
+
`callCompletion`, `tps`. Underscore prefix keeps the file out of
|
| 621 |
+
`*.bench.ts` glob runners. **Existing four benches stay on their
|
| 622 |
+
inlined helpers** β no regression risk to landed work; future
|
| 623 |
+
benches (M6 PolyKV onwards) inherit the lib.
|
| 624 |
+
|
| 625 |
+
- `perf/llamafile/advanced-kv-stack.bench.ts` β the compose bench
|
| 626 |
+
(510 lines). Six configurations Γ one multi-turn needle workload Γ
|
| 627 |
+
9 hard checks + 2 soft checks. CLI: `--model qwen|gemma|<abs-path>`
|
| 628 |
+
defaulting to autoβqwen for fast CI; `--ctx`, `--predict`,
|
| 629 |
+
`--filler`, `--frac` for one-off tuning. Self-skips on no bin /
|
| 630 |
+
no model / no GPU+DSO / insufficient free VRAM. Results JSON to
|
| 631 |
+
`.opencoti/m5-stack-bench.json`.
|
| 632 |
+
|
| 633 |
+
**Configurations.**
|
| 634 |
+
|
| 635 |
+
| ID | Flags | Tests |
|
| 636 |
+
|----|------------------------------------------------------------------------------------------------------|-----------------------------|
|
| 637 |
+
| B | `-ngl 99 -fa on --parallel 1 -c 1024` | Baseline |
|
| 638 |
+
| A0 | B + `--kv-unified --parallel 2 -c 2048 --cache-reuse 256` | M0 alone |
|
| 639 |
+
| A1 | B + `--rest-kv-eviction --rest-kv-recent 256 --rest-kv-layer -1` | M1 alone |
|
| 640 |
+
| A2 | B + `--headinfer-gpu-heads-frac 0.5` | M2 alone |
|
| 641 |
+
| A3 | B + `--headinfer-gpu-heads-frac 0.5 --neo-pipeline on` | M3 alone |
|
| 642 |
+
| S | B + ALL of A0+A1+A2+A3 simultaneously (parallel=2, ctx=2048, kv-unified, all four flag groups) | **The compose validation** |
|
| 643 |
+
|
| 644 |
+
**Live bench (solidPC RTX 3090, Qwen2.5-Coder-0.5B IQ4_XS, ctx=1024 per
|
| 645 |
+
slot, n_predict=256, filler=35, frac=0.5):**
|
| 646 |
+
|
| 647 |
+
```
|
| 648 |
+
B baseline : t1_tps=328 cuda_mib=12 (per cell 0.0117 KiB)
|
| 649 |
+
A0 M0 alone : t1_tps=350 cuda_mib=24 (per cell 0.0117 KiB; -kv-unified at parallel=2)
|
| 650 |
+
A1 M1 alone : t1_tps=337 cuda_mib=12 (per cell 0.0117 KiB)
|
| 651 |
+
A2 M2 alone : t1_tps=159 cuda_mib=6 + cpu_mib=6 (per cell 0.0059 KiB β 50% saving)
|
| 652 |
+
A3 M3 alone : t1_tps=160 cuda_mib=6 + cpu_mib=6 (NEO on; identical residency to A2)
|
| 653 |
+
S STACK : t1_tps=159 cuda_mib=12 + cpu_mib=12 (per cell 0.0059 KiB β M2 saving preserved)
|
| 654 |
+
|
| 655 |
+
Hard checks: 9/9 PASS (C3, C7 cleanly SKIPPED β workload didn't force
|
| 656 |
+
context shift on this model + ctx + filler combo;
|
| 657 |
+
the bench mirrors M1's skip-not-fail pattern.)
|
| 658 |
+
Soft checks: 2/2 PASS
|
| 659 |
+
```
|
| 660 |
+
|
| 661 |
+
**Key findings.**
|
| 662 |
+
|
| 663 |
+
1. **M1 + M2 compounding cost is negligible.** Phase-1 explore flagged
|
| 664 |
+
a soft compose risk β M1's `seq_key_scores` dequantizes K rows at
|
| 665 |
+
context-shift; M2's `get_k` concatenates `k_cpu` on every step. The
|
| 666 |
+
bench measured **S t1_tps 158.55 vs A2 alone 158.90 β within noise**
|
| 667 |
+
(soft check W1 PASS). No follow-up needed.
|
| 668 |
+
|
| 669 |
+
2. **M2's per-cell GPU saving is exactly preserved in the stack.** Soft
|
| 670 |
+
check W2: S per-cell CUDA = A2 per-cell CUDA = 0.0059 KiB/cell. The
|
| 671 |
+
M2 split is identically engaged in S as in A2 alone.
|
| 672 |
+
|
| 673 |
+
3. **`--kv-unified` must be explicit when `--parallel > 1`.** First
|
| 674 |
+
compose-conflict surfaced: llamafile's `llama_context_params`
|
| 675 |
+
defaults `kv_unified = false`, so `--parallel N > 1` β `n_stream = N`,
|
| 676 |
+
and M2's split gate (patch 0020 line 176, `n_stream == 1`) silently
|
| 677 |
+
fails. M0 + M2 wouldn't have composed otherwise. The bench encodes
|
| 678 |
+
the fix: A0 and S pass `--kv-unified` explicitly. **This is exactly
|
| 679 |
+
the kind of finding M5's compose-validation exists to surface.**
|
| 680 |
+
A future M5 follow-up could make `--kv-unified` the default when
|
| 681 |
+
`--headinfer-gpu-heads-frac < 1.0`, or document a hard error if both
|
| 682 |
+
`--parallel > 1` and frac are set without `--kv-unified`. Tracked as
|
| 683 |
+
a soft TODO in the bench's config block.
|
| 684 |
+
|
| 685 |
+
4. **M3 stack equality.** Hard check C5: A3 turn-1 completion ===
|
| 686 |
+
A2 turn-1 completion (byte-identical). The 0030 patch's off-or-on
|
| 687 |
+
structural equality on M2 is preserved when other flags are on.
|
| 688 |
+
|
| 689 |
+
5. **No pathological combined slowdown.** C9 floor at 40% of baseline
|
| 690 |
+
(A2 alone runs at 48% of B β the cost of M2's CPU stream-back).
|
| 691 |
+
Stack tps lands at A2 alone's level (158 vs 159 β no additional
|
| 692 |
+
M0/M1/M3 cost stacked on M2's floor).
|
| 693 |
+
|
| 694 |
+
**Deferred / non-goals.**
|
| 695 |
+
|
| 696 |
+
- **Helper-module extraction in M0/M1/M2/M3 benches.** Per the M5
|
| 697 |
+
scope-locking decision, leave the four existing benches on their
|
| 698 |
+
inlined copies to protect landed work. Follow-up task can refactor
|
| 699 |
+
them when convenient; the lib API was designed compatible.
|
| 700 |
+
- **Force shift on Qwen workload.** Tuning `(filler, predict)` to
|
| 701 |
+
cross `n_ctx` mid-decode under Qwen2.5-Coder IQ4_XS hit a BPE
|
| 702 |
+
merge-breakpoint at filler β 36-37 that jumps prompt tokenization
|
| 703 |
+
from ~750 β ~1050. Filler=35 leaves prompt at ~740 + decode 256 =
|
| 704 |
+
996 < 1024 β under the shift threshold. C3 / C7 skip cleanly
|
| 705 |
+
rather than false-fail; override with `--filler N --ctx N
|
| 706 |
+
--predict N` from the CLI for one-off shift-forced runs. The
|
| 707 |
+
M1 bench already exercises shift in isolation, so M5's skip is
|
| 708 |
+
fine for compose-validation.
|
| 709 |
+
- **CI runner / fixed GPU image** (the F2 M6 dependency from the
|
| 710 |
+
original M5 sketch) β out of scope; the bench self-skips when no
|
| 711 |
+
GPU is present, so it's CI-safe today.
|
| 712 |
+
- **Make `--kv-unified` the auto-default when M2 is engaged.** Could
|
| 713 |
+
land in a tiny `0030`-tier follow-up patch; deferred until either
|
| 714 |
+
M6 PolyKV or a user request makes it worth touching the cparam
|
| 715 |
+
wiring.
|
| 716 |
+
- **Gemma profile run.** Bench supports `--model gemma` (probes for
|
| 717 |
+
14 GB free VRAM); Qwen-default run is the CI path. Realistic-perf
|
| 718 |
+
Gemma numbers can land in a future one-off study.
|
| 719 |
+
|
| 720 |
+
**Hook count unchanged** (`git grep "opencoti-hook:" packages/ docs/`
|
| 721 |
+
returns 19 β same as before M5, where the 19th is a docs-text mention
|
| 722 |
+
from the M3-H ship, not a real new surgical hook). No new marker in
|
| 723 |
+
opencode source, no llama.cpp source change, no patch.
|
| 724 |
+
`0050-glue-bench.patch` reservation slot in
|
| 725 |
+
`vendors/patches/llamafile/README.md` stays empty β M5 is pure
|
| 726 |
+
application-side perf harness.
|
| 727 |
+
|
| 728 |
+
### M6 β PolyKV (shared compressed KV pool)
|
| 729 |
+
|
| 730 |
+
> **Child plan: [poly_kv.md](poly_kv.md)** β **M6 SHIPPED 2026-06-06.** Staged fast-first: **S0**
|
| 731 |
+
> asymmetric KV-compression via stock `-ctk q8_0 -ctv q4_0` through M7's dequant-on-lift (zero new
|
| 732 |
+
> kernel, banked correct at 4k) β **S1** shared read-only prefix pool (multi-tenant core, O(1) in
|
| 733 |
+
> agents, 6.9Γ fan-out memory win) β **S2** TurboQuant `TURBO{2,3,4,8}_0` family + InnerQ + Level-A
|
| 734 |
+
> dequant-on-lift + Level-B fused FA-VEC (`GGML_OP_TURBO_WHT`; 4/4 tiers logit-equiv) β **S3** glue
|
| 735 |
+
> bench + compose gate + patches `0072`/`0073` (captured + byte-identical-proven). Key insight: M7's
|
| 736 |
+
> dequant-on-lift (S3d) lets opencoti decompress the pool **in-attention**, a win the PyTorch
|
| 737 |
+
> reference (decompress-then-attend) can't get. Remaining: **S4** MTP draft head (#373).
|
| 738 |
+
|
| 739 |
+
- Reference: arXiv 2604.24971, MIT at
|
| 740 |
+
[github.com/ishan1410/PolyKV](https://github.com/ishan1410/PolyKV).
|
| 741 |
+
Single asymmetrically-compressed KV pool across multiple
|
| 742 |
+
concurrent agents; ~97.7% memory reduction at 3β4+
|
| 743 |
+
concurrent agents.
|
| 744 |
+
- **Sequenced immediately after M5**, not deferred. Reasoning:
|
| 745 |
+
deferring loses the implementation context from the
|
| 746 |
+
preceding patch series β the same code paths (slot KV
|
| 747 |
+
management, allocator hooks, attention kernel
|
| 748 |
+
modifications) get re-visited with cold context if PolyKV
|
| 749 |
+
lands in a separate work unit later. Keeping it in this
|
| 750 |
+
work unit reuses the mental model. The fact that opencoti's
|
| 751 |
+
multi-agent fan-out on Tier 0 isn't shipped yet is *not* a
|
| 752 |
+
blocker β PolyKV is a memory-pool architecture that can
|
| 753 |
+
land in single-agent mode (degenerates to a 1-tenant pool)
|
| 754 |
+
and light up its full value once fan-out arrives.
|
| 755 |
+
- Smoke at M6: single-tenant correctness. Multi-tenant bench
|
| 756 |
+
follows whenever multi-agent fan-out lands.
|
| 757 |
+
- Compose-with-glue: re-run M5's `advanced-kv-stack.bench.ts`
|
| 758 |
+
with PolyKV on the stack; document any conflict resolution.
|
| 759 |
+
|
| 760 |
+
### M7 β LMCache *(DEFERRED)*
|
| 761 |
+
|
| 762 |
+
- Reference: arXiv 2510.09665, Apache-2.0 at
|
| 763 |
+
[github.com/lmcache/lmcache](https://github.com/lmcache/lmcache).
|
| 764 |
+
Cache offloading + prefix-decode disaggregation; modular
|
| 765 |
+
connector for vLLM/SGLang.
|
| 766 |
+
- **Deferred because**: vLLM/SGLang-bound architecture,
|
| 767 |
+
llama.cpp portability 1/5. Re-evaluate when M0βM6 are
|
| 768 |
+
landed and there's a clear marginal-win case.
|
| 769 |
+
- **Superseded 2026-05-29**: the M7 slot is now **Rolling KV**
|
| 770 |
+
(streaming double-buffered KV pipeline, M2-as-runtime-tactic),
|
| 771 |
+
designed in [`rolling_kv.md`](rolling_kv.md), patch `0070`. LMCache
|
| 772 |
+
remains an external reference only.
|
| 773 |
+
|
| 774 |
+
## F5-opt β concurrency=1 optimization round *(task #287)*
|
| 775 |
+
|
| 776 |
+
> **RESOLVED 2026-06-04 β superseded by M7.** A fresh concurrency=1 re-baseline of the
|
| 777 |
+
> **shipped M7 binary** (default `--kv-residency-mode auto`) vs vanilla-0.10.1 on the
|
| 778 |
+
> prefill-bound regime that defined this task lands at **parity**: prefill 4k/32k ratio
|
| 779 |
+
> 1.00/1.01, decode 1.03/0.94 β all in [0.94, 1.03]. The old worst case (niah@32k prefill,
|
| 780 |
+
> **3.6Γ = 0.28**) is now **1.01**. The cause is structural: M7's GPU_RESIDENT-by-default
|
| 781 |
+
> retired the always-on M2 CPU-half this round was created to optimize (server log:
|
| 782 |
+
> `GPU_RESIDENT=5/25, POSITION_WINDOW=0, CPU_FA_TAIL=0` at every tested ctx). The lone
|
| 783 |
+
> residual β decode@32k 0.94 (~6%, per-layer tactic-table bookkeeping on the resident path)
|
| 784 |
+
> β is a future micro-opt, not a blocker. Full matrix + artifact note:
|
| 785 |
+
> [`.opencoti/m7-c1-rebaseline/RESULT.md`](../../.opencoti/m7-c1-rebaseline/RESULT.md).
|
| 786 |
+
> The four workstreams below shipped en route and remain the building blocks (W1 probe,
|
| 787 |
+
> W2 CPU-FA fallback, W3 fused-MoE, W4 the Rolling KV residency redesign itself).
|
| 788 |
+
|
| 789 |
+
The 2026-05-29 RULER F5 comparison held quality at 100% (4k/32k) but ran
|
| 790 |
+
2.6β4.7Γ slower than vanilla at concurrency=1, dominated by the M2
|
| 791 |
+
CPU-half. Four workstreams attack compute / transfer / residency before
|
| 792 |
+
the deferred RULER 256k re-run (see the program plan). Shipped so far:
|
| 793 |
+
|
| 794 |
+
- **W1 (`0036`, #293) β PCIe/ReBAR probe consumption.** Boot-time
|
| 795 |
+
`pcie_profile` reader + `--pcie-autodetect`/`--pcie-bw-gbps`; feeds W4's
|
| 796 |
+
tile sizing. SHIPPED 2026-05-30.
|
| 797 |
+
**Measured proof (#367, 2026-06-04, RTX 3090, `perf/llamafile/rebar-probe.cu`).**
|
| 798 |
+
Pinned vs pageable hostβdevice bandwidth (GB/s), pinned/pageable ratio:
|
| 799 |
+
|
| 800 |
+
| transfer | pinned H2D/D2H | pageable H2D/D2H | pinned/pageable H2D Β· D2H | ReBAR verdict |
|
| 801 |
+
|----------|----------------|------------------|---------------------------|---------------|
|
| 802 |
+
| 1 MiB | 6.397 / 6.361 | 5.545 / 4.437 | **1.154Γ Β· 1.434Γ** | partial/degraded |
|
| 803 |
+
| 16 MiB | 6.656 / 6.590 | 6.400 / 6.334 | 1.040Γ Β· 1.040Γ | active |
|
| 804 |
+
| 64 MiB | 6.669 / 6.593 | 6.537 / 6.468 | 1.020Γ Β· 1.019Γ | active |
|
| 805 |
+
|
| 806 |
+
Link = **x8 @ 8.0 GT/s (PCIe 3.0 x8**; iGPU absorbs 8 of 16 lanes), ReBAR active
|
| 807 |
+
(max x16 @ 16.0 GT/s). The pinned win is **size-dependent**: large at small,
|
| 808 |
+
latency-bound transfers (+15% H2D / +43% D2H at 1 MiB), negligible once the x8 link
|
| 809 |
+
saturates at 64 MiB (~6.67 GB/s, the cached `effective_bw_gbps`). This validates the
|
| 810 |
+
**#289/M7-A pinned-host KV residency**: M7 streams the spill tail in *small* tiles β
|
| 811 |
+
exactly the regime where pinned staging earns +15β43%, not the 2% a bandwidth-bound
|
| 812 |
+
read would suggest. `effective_bw_gbps` (pinned plateau, 6.67) is the M7 tile-sizing
|
| 813 |
+
input (`tile_bytes_max = compute_ms Γ eff_bw Γ 0.8`). Raw JSONs:
|
| 814 |
+
`.opencoti/rebar-probe-solidpc-dev0-{1,16,64}MiB-20260604-*.json`.
|
| 815 |
+
- **W2 (`0040`, #290) β wholesale ik_llama.cpp CPU-FA engine.** Vendored
|
| 816 |
+
behind `GGML_IQK_FLASH_ATTENTION` + `--iqk-flash-attn on|off` (default
|
| 817 |
+
off) + one dispatch hook (`opencoti-hook: f5-opt-cpufa`). SHIPPED
|
| 818 |
+
2026-05-30. **Verification:** flag-OFF byte-identical (neo-pipeline C4
|
| 819 |
+
PASS); flag-ON RULER vt+niah @ 4k/32k (Gemma-4 A4B + M2 + full stack)
|
| 820 |
+
100% all 8 cells, **ON-vs-OFF wall-time flat**. The flat result is the
|
| 821 |
+
key finding: on prefill-bound RULER cells the CPU-FA *kernel speed* is
|
| 822 |
+
not the lever β **the CPU split existing at all while VRAM is free** is.
|
| 823 |
+
That motivated the M7 **GPU_RESIDENT-by-default / maximize-VRAM**
|
| 824 |
+
redesign (`rolling_kv.md` Decision 4): start GPU-resident, engage
|
| 825 |
+
CPU/stream relief only under VRAM pressure, release it as load drains.
|
| 826 |
+
W2 stays valuable as the CPU-spill *fallback* kernel. **#291 Q8_KV
|
| 827 |
+
DROPPED 2026-05-30**: recon found it needs a full mainline-ggml-type
|
| 828 |
+
registration (it is the pseudo-type `(ggml_type)151` inside the engine,
|
| 829 |
+
not a real type), not the "nearly-free" flip assumed β and it is a
|
| 830 |
+
redundant 8-bit option (`q8_0` covers it; **TurboQuant** TBQ3_0/4_0 in M6
|
| 831 |
+
is the aggressive-compression KV-quant driver). `0041` retired; the
|
| 832 |
+
engine's dormant Q8_KV FA path stays gated off at zero cost.
|
| 833 |
+
- **W3 (`0042`, #292) β op-level fused MoE up+gate+GLU. SHIPPED 2026-05-30.**
|
| 834 |
+
New ggml op `GGML_OP_MOE_FUSED_UP_GATE` collapses a MoE FFN's two separate
|
| 835 |
+
per-expert projections (`up_exps @ cur`, `gate_exps @ cur`) + the GLU into
|
| 836 |
+
ONE decode-time op, dispatched to our **existing** fused mmvq kernel
|
| 837 |
+
(`ggml_cuda_mul_mat_vec_q` + `fusion.gate`/`glu_op`) β eliminating the
|
| 838 |
+
gate_up HBM round-trip + the standalone GLU launch per MoE layer per token.
|
| 839 |
+
**Zero ik_llama code vendored** β only the op-level-fusion *idea*
|
| 840 |
+
(ik_llama PR #229/#520, MIT). Off by default (`--fused-moe-up-gate on`).
|
| 841 |
+
**Scope discovery:** the hook only fires for **separate-up/gate** layouts
|
| 842 |
+
(`gate_exps && !gate_up_exps`, same type, SILU\|GELU, decode `n_tokens==1`)
|
| 843 |
+
= **Qwen2-MoE / Qwen3-MoE / OLMoE / Mixtral-style**. The original target
|
| 844 |
+
**Gemma-4 A4B already fuses gate+up into one `ffn_gate_up_exps` matmul**
|
| 845 |
+
(`gemma4-iswa.cpp:158` passes `gate/up = nullptr`), so W3 is **N/A to
|
| 846 |
+
Gemma-4** β it gains nothing (only a cheap elementwise GLU remains, <1%
|
| 847 |
+
ceiling, confirmed by flat OFF/ON tok/s on Gemma-4). CPU backend aborts the
|
| 848 |
+
op (CUDA-only). **Validated on OLMoE-1B-7B Q4_K_M** (nsys can't trace
|
| 849 |
+
llamafile's `dlopen`'d CUDA β engagement via `GGML_SCHED_DEBUG=2 --verbose`
|
| 850 |
+
graph dump: 34 op-nodes on CUDA0 ON / 0 OFF): greedy decode **byte-identical**
|
| 851 |
+
OFF vs ON, **+2.4% decode tok/s** (265.9β272.2). ABI `GGML_OP_COUNT` 96β97
|
| 852 |
+
(binary + `ggml-cuda.so` rebuilt & paired). Adapter `--fused-moe-up-gate`
|
| 853 |
+
after `--iqk-flash-attn` (87 tests).
|
| 854 |
+
- **W4 (`0070`, #296) β M7 Rolling KV β IN FLIGHT (2026-05-31).** The
|
| 855 |
+
residency/transfer redesign the W2 data points to. Shipped on `dev` in
|
| 856 |
+
cosine-gated rungs: **Rung 0** (M7-A auto-residency β GPU_RESIDENT when KV fits
|
| 857 |
+
the VRAM budget, spill only under pressure) + **Rung 1** (M7-B runtime
|
| 858 |
+
`layer_tactic[]` table + `rolling_kv_plan`, populated/logged, inert on the
|
| 859 |
+
compute path) + **Rung 2 R2-a** (the `GGML_OP_STREAMING_FLASH_ATTN` op + the
|
| 860 |
+
`get_layer_tactic(il)==GPU_STREAM` dispatch shell β CUDA forward reuses
|
| 861 |
+
`ggml_cuda_flash_attn_ext`, byte-identical to GPU_RESIDENT/CPU_SPILL). Canonical
|
| 862 |
+
`0070` captured (47 hunks / 19 files, 5 `f5-rolling-kv` markers; bug-250
|
| 863 |
+
patch-chain blocker fixed first, #309). **The streaming kernel proper is still
|
| 864 |
+
unwritten** β R2-a is only the op-dispatch shell (`n_slots == 0`, KV still
|
| 865 |
+
device-resident, no tile loop). Next: pinned-host residency + slot pool + tile
|
| 866 |
+
loop + *inter-tile* online-softmax via `flash_attn_combine_results` (M7-C/M7-D),
|
| 867 |
+
then double-buffer overlap (R2-b) + scheduler (R2-c), then M7-F/G/H. See
|
| 868 |
+
[`rolling_kv.md`](rolling_kv.md) "Rung 2 implementation blueprint." bug-226 not
|
| 869 |
+
yet superseded.
|
| 870 |
+
|
| 871 |
+
## Surgical-hook footprint
|
| 872 |
+
|
| 873 |
+
- Zero hooks in the opencode source tree from F5.
|
| 874 |
+
- Each milestone adds 1 (or 2) new patch files under
|
| 875 |
+
`vendors/patches/llamafile/`. The patch protocol header is
|
| 876 |
+
enforced by `vendors/patches/llamafile/README.md`.
|
| 877 |
+
|
| 878 |
+
## Open questions
|
| 879 |
+
|
| 880 |
+
- **ReST-KV reference choice.** Resolved at M1 start: pick the
|
| 881 |
+
cleanest retention-scoring paper-or-code combination
|
| 882 |
+
available at that time. SnapKV and GraphKV are both live
|
| 883 |
+
candidates if Make-Each-Token-Count remains code-less.
|
| 884 |
+
- ~~**ScoutAttention publication date.**~~ **Resolved 2026-05-29.** Paper
|
| 885 |
+
published 2026-03-28; reference code confirmed absent at M4-start.
|
| 886 |
+
Decision: defer M4 to August 2026 post-DAC. See M4 section above.
|
| 887 |
+
- **PolyKV β HeadInfer conflict.** Research suggests these
|
| 888 |
+
conflict on shared-pool vs per-head residency assumptions.
|
| 889 |
+
Resolution path is M6's compose-with-glue step; if the
|
| 890 |
+
conflict is irreconcilable, document it and let users pick
|
| 891 |
+
one or the other via flags.
|
| 892 |
+
|
| 893 |
+
## Risks
|
| 894 |
+
|
| 895 |
+
- **Each patch starts against the pinned upstream SHA; later
|
| 896 |
+
patches modify the same source files.** Lexical apply order
|
| 897 |
+
is enforced by the `NNNN-` prefix protocol. Gaps in the
|
| 898 |
+
numbering leave room for per-technique tweaks without
|
| 899 |
+
renumbering downstream patches.
|
| 900 |
+
- **M4 (ScoutAttention) has no public reference code.** Riskiest
|
| 901 |
+
milestone. Same escalation path as above.
|
| 902 |
+
- **Llama.cpp portability scores β€ 3/5 for all six techniques.**
|
| 903 |
+
Each technique is a port-from-PyTorch-or-vLLM, not a direct
|
| 904 |
+
drop-in. Estimate scope generously when picking the next
|
| 905 |
+
milestone after M0.
|
| 906 |
+
- **Multi-tenant value of PolyKV doesn't land until Tier 0
|
| 907 |
+
multi-agent fan-out ships.** M6's M5 compose-step assertion
|
| 908 |
+
is the contract; the bench is only meaningful once concurrency
|
| 909 |
+
> 1.
|
docs/features/fused_nextn_mtp.md
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Fused NextN draft graph (task #590 / bug-858, bug-867)
|
| 2 |
+
|
| 3 |
+
**Status:** scoped β design only, not built. This document is the implementation
|
| 4 |
+
blueprint; the go/no-go on building it is a separate decision.
|
| 5 |
+
|
| 6 |
+
**Problem.** Qwen NextN self-speculative MTP (`--spec-type draft-mtp`) on 35B-A3B
|
| 7 |
+
decodes ~16% slower than upstream llama.cpp `b9859` on the *same* GGUF/HW, with
|
| 8 |
+
**acceptance and cycle count at parity** (ours 0.79β0.80 accept, 307 draft_n, 244
|
| 9 |
+
accepted @ 244 t/s; upstream 0.815, 303, 247 @ 283 t/s). Base single-decode is at
|
| 10 |
+
parity. The gap is therefore **per-cycle host-sync/launch overhead**, not drafter
|
| 11 |
+
quality and not verify criterion β see `docs/evaluations/three-way-tps.md` Β§"#590
|
| 12 |
+
diagnostic" and buglog bug-864/866/867. The overhead lives in the **un-fused
|
| 13 |
+
per-step draft loop**: `common_speculative_impl_draft_mtp::draft`
|
| 14 |
+
(`common/speculative.cpp:702-803`) does, *per drafted token*, a
|
| 15 |
+
`llama_decode(ctx_dft)` + a `llama_get_embeddings_pre_norm_ith` **host readback**
|
| 16 |
+
(`:751-752, :796`) β one launch + one D2H sync per step.
|
| 17 |
+
|
| 18 |
+
**Fix.** Give NextN a **fused draft graph**: the NextN analogue of the
|
| 19 |
+
Gemma-assistant `decode_mtp_fused` (`src/llama-context.cpp:2900`). Build the N draft
|
| 20 |
+
steps as *one* graph with an on-device argmaxβembedβhidden chain between steps, so
|
| 21 |
+
the whole block costs **one launch + one readback** (`get_argmax()` I32[N] +
|
| 22 |
+
`get_embd()`), eliminating the per-step host round-trip. Payoff scales with n_max
|
| 23 |
+
(modest at the n=2 cap, expected to fix the n=3 collapse).
|
| 24 |
+
|
| 25 |
+
`decode_mtp_fused` is **not reusable as-is** β it is hard-wired to Gemma
|
| 26 |
+
(`model.mtp_assistant` at `:2908`; `dynamic_cast<llama_kv_cache_iswa>` at `:2916`).
|
| 27 |
+
The NextN drafter is a *separate* draft context (`ctx_dft`) with its own model, its
|
| 28 |
+
own **unified** (non-iSWA) KV cache, and its own NextN block. So this is a
|
| 29 |
+
from-scratch build that *reuses the scheduler/readback machinery* but adds a new
|
| 30 |
+
Qwen fused builder + a new driver.
|
| 31 |
+
|
| 32 |
+
---
|
| 33 |
+
|
| 34 |
+
## The two MTP lanes (why reuse is partial)
|
| 35 |
+
|
| 36 |
+
`src/llama-graph.h:37-39` graph types:
|
| 37 |
+
|
| 38 |
+
| type | drafter | context | KV cache | fused today? |
|
| 39 |
+
|---|---|---|---|---|
|
| 40 |
+
| `LLM_GRAPH_TYPE_MTP` | Gemma-4 **assistant** (sub-model in target ctx) | target | iSWA (shared) | **yes** (`decode_mtp_fused`) |
|
| 41 |
+
| `LLM_GRAPH_TYPE_DECODER_MTP` | Qwen **NextN** (own block in draft model) | separate `ctx_dft` | unified | **no** (per-step loop) |
|
| 42 |
+
|
| 43 |
+
The Gemma assistant shares the *target's* iSWA KV via cross-attention
|
| 44 |
+
(`build_attn_mtp`, `src/llama-graph.cpp:2977`); Qwen NextN runs a full draft context
|
| 45 |
+
with its own unified KV (`src/llama-model.cpp:1979-1995`) and reuses the trunk
|
| 46 |
+
layer's weights for its single NextN block (`src/models/qwen35.cpp:510-656`,
|
| 47 |
+
`qwen35moe.cpp:575`).
|
| 48 |
+
|
| 49 |
+
---
|
| 50 |
+
|
| 51 |
+
## Reuse as-is (model-agnostic machinery)
|
| 52 |
+
|
| 53 |
+
From `src/llama-context.cpp`, all shared once the iSWA/assistant specifics are
|
| 54 |
+
generalized out:
|
| 55 |
+
|
| 56 |
+
- `process_ubatch_mtp` (`:1538-1588`) β dedicated `sched_mtp` + reusable
|
| 57 |
+
`gf_res_prev_mtp`, reuse gate + `set_inputs`.
|
| 58 |
+
- `graph_compute_mtp` (`:1591`).
|
| 59 |
+
- `ensure_sched_mtp` NΓ-nodes reserve pattern (`:1419-1445`:
|
| 60 |
+
`max_nodes = graph_max_nodes * n_steps`).
|
| 61 |
+
- The readback contract: `res->get_argmax()` β I32[n_steps] +
|
| 62 |
+
`res->get_embd()` (`:3000-3013`); accessors `src/llama-graph.h:679-719`
|
| 63 |
+
(`t_argmax` is set by **direct field assignment**, no `set_argmax` method β
|
| 64 |
+
reset at `src/llama-graph.cpp:898`).
|
| 65 |
+
|
| 66 |
+
The **only** things inside these that must be generalized: the
|
| 67 |
+
`dynamic_cast<llama_kv_cache_iswa>` guards (`:1451`, `:2916`) and the
|
| 68 |
+
`mtp_assistant`-derived arch/width (`:2681-2685`, `:2930`).
|
| 69 |
+
|
| 70 |
+
---
|
| 71 |
+
|
| 72 |
+
## The 7 deltas (what must be built)
|
| 73 |
+
|
| 74 |
+
1. **N-step chaining loop in the NextN builder.** `qwen35.cpp:graph_mtp`
|
| 75 |
+
(`:510-656`) is strictly single-block/single-pass. Add a `for k in n_steps`
|
| 76 |
+
unroll modeled on `gemma4-assistant.cpp:341-357`, re-invoking
|
| 77 |
+
eh_projβattnβffnβhead N times in one graph. Factor the current body into a
|
| 78 |
+
`graph_mtp_build_one_step` primitive (mirror
|
| 79 |
+
`gemma4_mtp_build_one_step`, `gemma4-assistant.cpp:45-258`).
|
| 80 |
+
|
| 81 |
+
2. **`n_mtp_steps` plumbing to the Qwen path.** `n_mtp_steps`
|
| 82 |
+
(`llama-graph.h:595-599`) is today only read by the Gemma builder and only set
|
| 83 |
+
via `mtp_fused_steps`β`graph_params_mtp` (`llama-context.cpp:2696`). The Qwen
|
| 84 |
+
draft ctx builds through the *normal* `process_ubatch`/`build_graph`, so there
|
| 85 |
+
is **no wire** carrying a step count into `graph_mtp`. Add a Qwen equivalent of
|
| 86 |
+
`graph_params_mtp` (arch = draft model's own arch, `gtype =
|
| 87 |
+
LLM_GRAPH_TYPE_DECODER_MTP`, `n_mtp_steps = N`) used by the new driver.
|
| 88 |
+
|
| 89 |
+
3. **In-graph argmax on the NextN head.** `graph_mtp` sets only `t_logits`
|
| 90 |
+
(`:654`) + `t_h_pre_norm` (`:637`), never `t_argmax`. Add per-step
|
| 91 |
+
`ggml_argmax` on the LM-head output + concat into I32[N] (mirror
|
| 92 |
+
`gemma4-assistant.cpp:252, 362-368`), publish `res->t_argmax` (direct assign).
|
| 93 |
+
**Consequence:** the fused path is **greedy-only** and drops the host `p_min`
|
| 94 |
+
early-stop confidence gate (`speculative.cpp:767`) β see Risks.
|
| 95 |
+
|
| 96 |
+
4. **On-device tokenβembed + hidden chaining.** `graph_mtp` already does
|
| 97 |
+
`ggml_get_rows(embed_tokens, tokens)` (`:540-543`) but on a *host-supplied*
|
| 98 |
+
`tokens`. Fused: step k+1's token = step k's on-device argmax feeding
|
| 99 |
+
`ggml_get_rows` (mirror `:355` + `:72`); step k+1's `h_input` = step k's
|
| 100 |
+
`t_h_pre_norm` (mirror `:356`) β entirely on-device.
|
| 101 |
+
|
| 102 |
+
5. **NextN per-step position input β RESOLVED: reuse `llm_graph_input_mtp`.**
|
| 103 |
+
Its setter (`llama-graph.cpp:116-131`) is **width-agnostic** β `inp_h_prev` is
|
| 104 |
+
written `inp_h_prev->ne[0]` floats β so Qwen reuses it verbatim by creating
|
| 105 |
+
`inp_h_prev` at `n_embd` width (not Gemma's `n_embd_out_impl`). The only change
|
| 106 |
+
needed: extend `llm_graph_input_mtp::can_reuse` (`:134-145`) to also accept
|
| 107 |
+
`gtype==LLM_GRAPH_TYPE_DECODER_MTP` (today it hard-returns false unless
|
| 108 |
+
`LLM_GRAPH_TYPE_MTP` at `:135`), else the fused Qwen graph rebuilds every draft
|
| 109 |
+
and the perf win is lost (correct but slow).
|
| 110 |
+
|
| 111 |
+
6. **KV for the N fused steps β RESOLVED (Option A, 2026-07-02): read-only
|
| 112 |
+
cross-attend the frozen prefix; NO new cache surgery.** Reading `build_attn_mtp`
|
| 113 |
+
(`llama-graph.cpp:2977`, esp. `:3011` `get_k`/`:3012` `get_v`) showed the
|
| 114 |
+
proven Gemma fused path is a **read-only cross-attention into the frozen prefix
|
| 115 |
+
KV** β it never writes draft KV; the recurrence is carried entirely by the
|
| 116 |
+
hidden-state chain (`h_post`βnext `h`). That is why one slot suffices:
|
| 117 |
+
`llama_kv_cache::mtp_slot_info` (`llama-kv-cache.cpp:1985`) returns a **single**
|
| 118 |
+
idx (the `pmax` cell), and it already exists on the **unified** cache (iSWA's
|
| 119 |
+
`init_mtp` just calls it on `kv_base`/`kv_swa`). So the unified path needs **no**
|
| 120 |
+
new `init_mtp`/N-cell/causal-mask work β build one `llama_kv_cache_context` from
|
| 121 |
+
`mtp_slot_info(seq_id)` (mirror `llama-kv-cache-iswa.cpp:238`).
|
| 122 |
+
|
| 123 |
+
**Fork decided: Option A "Gemma-mirror" (prototype-and-measure).** Qwen's
|
| 124 |
+
per-step loop uses *self-attention that writes each draft token's KV* (so step
|
| 125 |
+
k sees drafts 0..k-1); Option A cross-attends only the frozen prefix (step k
|
| 126 |
+
sees the prefix, not 0..k-1) β drafts differ (`real_fracβ 0`) and acceptance may
|
| 127 |
+
dip, but correctness is guaranteed by verify. Chosen because it's the proven
|
| 128 |
+
pattern with no KV-cache surgery. Measure acceptance in S5; only build the
|
| 129 |
+
self-attn N-cell exact-match variant (Option B) if A regresses materially vs
|
| 130 |
+
the 0.80 baseline. **Impl:** the fused steps need a Qwen read-only cross-attn
|
| 131 |
+
into the unified cache (an analog of `build_attn_mtp` β get_k/get_v from the
|
| 132 |
+
unified `llama_kv_cache_context` + the `build_attn_inp_kv` mask exposing the
|
| 133 |
+
prefix + `build_attn_mha`, no KV write), branched inside `build_one_step` on a
|
| 134 |
+
`fused` flag so the single-step path stays byte-identical.
|
| 135 |
+
|
| 136 |
+
7. **New driver = `decode_mtp_fused` twin without the Gemma guards.** Drop:
|
| 137 |
+
`model.mtp_assistant` checks (`:2908, 1428`) β replace with "draft ctx is
|
| 138 |
+
`LLAMA_CONTEXT_TYPE_MTP`"; the iSWA cast (`:2916-2920, 1451-1457`) β unified
|
| 139 |
+
cache + its `init_mtp`; seed width `n_bb = n_embd_out_impl` (`:2930`) β plain
|
| 140 |
+
`n_embd` (Qwen's `t_h_pre_norm` is `[n_embd, n_outputs]`, read via
|
| 141 |
+
`get_embeddings_pre_norm_ith`, `:1061`); `graph_params_mtp`'s
|
| 142 |
+
`arch/gtype` (`:2681-2685`) β draft model's own arch + `DECODER_MTP`.
|
| 143 |
+
|
| 144 |
+
Then **swap the driver into the loop**: `common_speculative_impl_draft_mtp::draft`
|
| 145 |
+
(`speculative.cpp:702-803`) calls the new fused driver once instead of the per-step
|
| 146 |
+
`llama_decode`+readback loop, and reads back I32[N] drafts + last hidden.
|
| 147 |
+
|
| 148 |
+
---
|
| 149 |
+
|
| 150 |
+
## Progress (2026-07-02)
|
| 151 |
+
|
| 152 |
+
- **S0 β DONE.** vendor-backup `20260702-143936-pre-fused-nextn-590` (9338
|
| 153 |
+
entries, 37 patches, verified).
|
| 154 |
+
- **S1 β DONE + gated byte-identical.** `build_one_step` lambda extracted in
|
| 155 |
+
`qwen35.cpp` + `qwen35moe.cpp` (`this`-capturing generic lambda, minimal-diff,
|
| 156 |
+
no re-indent). Incremental host build OK (2 TUs, host-only β no CUDA DSO
|
| 157 |
+
touched). bs2 35B NextN n2 chash `f80af37c9a` == pre-refactor baseline; decode
|
| 158 |
+
243β245, accept 0.79β0.80. **Build cadence confirmed cheap:** edit β incremental
|
| 159 |
+
`bun run build:llamafile:make` (2 TUs + relink) β rsync binary to bs2 (reuses
|
| 160 |
+
cached sm120 DSO, correct since host-only) β gate ~5 min.
|
| 161 |
+
- **S2 β DONE + gated byte-identical.** `build_one_step` now takes the token index
|
| 162 |
+
(embeds internally via `ggml_get_rows`) and returns `{h_pre_norm, logits, arg}`
|
| 163 |
+
where `arg = ggml_argmax(logits)`. Added the **dormant** fused N-step branch
|
| 164 |
+
(gated `n_mtp_steps>1`, inert at default 1) in both files: reuses
|
| 165 |
+
`llm_graph_input_mtp` (N per-step I32[1] positions), chains `tok_k=arg_k` /
|
| 166 |
+
`h_k=h_pre_norm_k` on-device, concats argmaxes β `res->t_argmax`. Build OK; bs2
|
| 167 |
+
35B NextN n2 chash `f80af37c9a` (single-step path byte-identical, argmax node
|
| 168 |
+
pruned when unreferenced). **All graph-builder work is now in place, dormant.**
|
| 169 |
+
|
| 170 |
+
## Staged plan (de-risk ordering)
|
| 171 |
+
|
| 172 |
+
- **S0 β vendor-backup WHOLE tree** (`scripts/vendor-backup.sh backup
|
| 173 |
+
pre-fused-nextn`). Mandatory before source edits + build cycles. β
|
| 174 |
+
- **S1 β refactor, no behaviour change.** Extract `build_one_step` from
|
| 175 |
+
`qwen35.cpp:graph_mtp` (and the MoE twin `qwen35moe.cpp:575`); the single-step
|
| 176 |
+
path calls it once. Build + prove **byte-identical** decode vs current
|
| 177 |
+
(chash) β pure refactor gate. β
(chash `f80af37c9a`).
|
| 178 |
+
- **S2 β fused builder (greedy N-step) + input + argmax.** Deltas 1,3,4,5. Publish
|
| 179 |
+
`t_argmax`/`t_embd`. Still driven single-step (N=1) first to prove the argmax
|
| 180 |
+
path matches host `common_sampler_sample` (real_frac=0 vs the per-step loop).
|
| 181 |
+
- **S3 β KV slots + driver.** Deltas 2,6,7. New `decode_mtp_fused_nextn` +
|
| 182 |
+
unified-cache `init_mtp`. Wire into `speculative.cpp` behind an env gate
|
| 183 |
+
(`OPENCOTI_MTP_FUSED_NEXTN`, default **off** until proven, then flip on like the
|
| 184 |
+
Gemma default per the standing "if it wins, default it" rule).
|
| 185 |
+
- **S4 β build + restamp.** Host `rm -rf o` + make on solidPC (consistent tree),
|
| 186 |
+
CUDA DSO if any device code touched (argmax/get_rows are existing ops β likely
|
| 187 |
+
**no** new kernel, host-only), restamp both DSO paths, `nm -D` verify.
|
| 188 |
+
- **S5 β gates.** (a) **Correctness:** logit-equiv / `real_frac=0` between fused
|
| 189 |
+
and per-step drafts on the same prompt (drafts must be identical token IDs since
|
| 190 |
+
both are greedy-argmax over the same NextN head); niah retrieval unchanged;
|
| 191 |
+
byte-identical final decode with fused **off**. (b) **Perf:** ours-fused vs
|
| 192 |
+
upstream b9859 on 35B NextN n=2 **and** n=3 β target: close the +16% at n2 and
|
| 193 |
+
kill the n3 collapse. Deploy binary to bs2 (GPU0), reuse `m592-accept.sh`
|
| 194 |
+
harness shape.
|
| 195 |
+
- **S6 β ship.** Additive patch(es) into `vendors/patches/llamafile/` (backup +
|
| 196 |
+
snapshot-diff, byte-identical re-apply), `opencoti-hook:` markers +
|
| 197 |
+
UPSTREAM_SYNC registry, `docs/evaluations/mtp.md` + this doc's status, README
|
| 198 |
+
row, `.wolf` (anatomy/cerebrum/memory), pgvector. Commit on dev only when asked.
|
| 199 |
+
|
| 200 |
+
---
|
| 201 |
+
|
| 202 |
+
## Risks / open questions
|
| 203 |
+
|
| 204 |
+
- **Loses `p_min` adaptive early-stop β negligible at the n=2 cap, and
|
| 205 |
+
recoverable.** The fused path is greedy-argmax with a fixed N (like Gemma), so
|
| 206 |
+
the host confidence gate (`speculative.cpp:767`, `cur_p->data[0].p < p_min` β
|
| 207 |
+
stop drafting; llama.cpp default ~0.75) is gone and it always drafts exactly N.
|
| 208 |
+
- **Correctness impact: none.** The **target verify pass rejects bad drafts**
|
| 209 |
+
(`llama-context.cpp:2896-2899`); output is exactly the target's regardless of
|
| 210 |
+
draft depth or greedy-vs-sampled. Fused greedy draft was measured
|
| 211 |
+
**byte-identical** to the sequential path in #454.
|
| 212 |
+
- **Throughput impact: near-neutral at n=2.** `p_min` exists to avoid spending a
|
| 213 |
+
*sequential* per-step `llama_decode`+readback on a token that will likely be
|
| 214 |
+
rejected. The fused path computes all N steps in **one** launch, so that
|
| 215 |
+
per-step cost the gate protected against no longer exists. Always-drafting-2
|
| 216 |
+
costs only the fused graph's marginal step-2 compute + one extra position in
|
| 217 |
+
the verify batch (3 vs 2 β negligible), and can *gain* acceptance when a
|
| 218 |
+
sub-threshold second token is actually correct. At large n_max the waste
|
| 219 |
+
grows, but that's where fused's cheap launch wins most, and we cap n anyway.
|
| 220 |
+
- **Recovery if measurement ever shows it matters (no per-step sync):** emit a
|
| 221 |
+
second small in-graph tensor β the per-step softmax-max (confidence) alongside
|
| 222 |
+
the argmax β read it back in the *same* single D2H, and trim the proposed
|
| 223 |
+
draft block host-side by `p_min` before submitting to verify. That restores
|
| 224 |
+
adaptive depth for the price of one extra tiny readback, not N syncs. Keep as a
|
| 225 |
+
follow-up lever; do not build unless S5's acceptance-delta check regresses.
|
| 226 |
+
- **Unified-cache `init_mtp` is new surface.** iSWA has `init_mtp`; the unified
|
| 227 |
+
cache does not. This is the highest-uncertainty delta (S3) β the N draft
|
| 228 |
+
positions must be placeable and each step's KV visible to the next step's
|
| 229 |
+
attention. De-risk by reading `llama_kv_cache_iswa::init_mtp` and mirroring on
|
| 230 |
+
the unified type.
|
| 231 |
+
- **Likely host-only (no CUDA rebuild).** The chain uses existing ops
|
| 232 |
+
(`ggml_argmax`, `ggml_get_rows`, attention). If true, S4 skips the CUDA DSO
|
| 233 |
+
rebuild β confirm no new device kernel is introduced before assuming so.
|
| 234 |
+
- **Effort:** multi-session. S1 (refactor) + S2 (builder) are the bulk; S3
|
| 235 |
+
(KV/driver) is the risk. Payoff is ~16% on the 35B-class NextN spec path only β
|
| 236 |
+
base and the KV/DCA/turbo moat are unaffected either way.
|
| 237 |
+
|
| 238 |
+
---
|
| 239 |
+
|
| 240 |
+
## Measured β Qwen3.6-35B-A3B NextN, ours vs b9859 (bs2, 2026-07-14, #654 short4)
|
| 241 |
+
|
| 242 |
+
Coherent prompt, `n_ctx=4096`, greedy, `cache_prompt=false`, `n_predict=300`, `--spec-type
|
| 243 |
+
draft-mtp --spec-draft-n-max {1,2,3}`. ours = `llamafile.dualctx 7abd684039a3`; b9859 =
|
| 244 |
+
upstream `llama-server`.
|
| 245 |
+
|
| 246 |
+
| config | ours decode t/s | ours accept | b9859 decode t/s | b9859 accept |
|
| 247 |
+
|--------|-----------------|-------------|------------------|--------------|
|
| 248 |
+
| base (no spec) | 208.3 | β | 212.0 | β |
|
| 249 |
+
| n1 | 226.4 | 0.892 | 258.3 | 0.869 |
|
| 250 |
+
| n2 | 230.4 | 0.739 | 269.9 | 0.770 |
|
| 251 |
+
| n3 | 240.1 | 0.638 | 277.3 | 0.735 |
|
| 252 |
+
|
| 253 |
+
**Read:** base is at **parity** (208 vs 212). ours' NextN decode still trails b9859 by **~12β15 %**
|
| 254 |
+
(n1 β12 %, n2 β15 %, n3 β13 %) β the unchanged **bug-858 Qwen verify/CUDA-graph gap** (accept is at
|
| 255 |
+
or above upstream at n1; the tps deficit is spec-orchestration/verify host overhead, not draft
|
| 256 |
+
quality). This supersedes the pre-#614 "~16 % slower (244 vs 283)" note with current-binary numbers.
|
| 257 |
+
The earlier #654 pass BOOT-FAILED on ours (harness omitted `--server`; llamafile came up in CLI chat
|
| 258 |
+
mode β buglog bug-2179); re-run with the conditional `--server` fix produced the table above.
|
| 259 |
+
|
| 260 |
+
---
|
| 261 |
+
|
| 262 |
+
## Anchors (quick index)
|
| 263 |
+
|
| 264 |
+
- Un-fused loop: `common/speculative.cpp:702-803` (readback `:752`).
|
| 265 |
+
- Qwen NextN builder: `src/models/qwen35.cpp:510-656` (dispatch `:133-138`);
|
| 266 |
+
MoE `src/models/qwen35moe.cpp:156-158, 575`.
|
| 267 |
+
- Gemma fused model: `src/models/gemma4-assistant.cpp:45-258` (one-step),
|
| 268 |
+
`:260-374` (N-step chain), argmax concat `:362-368`.
|
| 269 |
+
- Gemma fused driver: `src/llama-context.cpp:2900-3017`;
|
| 270 |
+
runner `:1538-1588`; params `:2673-2698`; reserve `:1419-1470`.
|
| 271 |
+
- Graph types: `src/llama-graph.h:37-39`; `n_mtp_steps` `:595-599`;
|
| 272 |
+
`llm_graph_input_mtp` `:128-145`; result accessors `:679-719`.
|
docs/features/gemma4_dca.md
ADDED
|
@@ -0,0 +1,485 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Feature β Gemma-4 A4B Dual Chunk Attention (DCA), training-free 1M context
|
| 2 |
+
|
| 3 |
+
> **Status (2026-06-10): DESIGN LANDED, patch `0078` not yet captured.** Feature home for the
|
| 4 |
+
> forthcoming `vendors/patches/llamafile/0078-gemma4-dca.patch`. The patch capture + build + RULER
|
| 5 |
+
> gate run in an **out-of-tree clean-room** (a separate llamafile checkout) β see "Capture without
|
| 6 |
+
> reset" β because the live `vendors/sources/llamafile` working tree holds the only applied source of
|
| 7 |
+
> patches `0001β0077` and MUST NOT be `reset`/`clean`ed (buglog 2026-06-04: `build:llamafile:reset`
|
| 8 |
+
> = `git reset --hard` + `git clean -fdx` wipes uncommitted patch source). T87.pD.
|
| 9 |
+
|
| 10 |
+
## Goal
|
| 11 |
+
Extend Gemma-4 26B-A4B (and 31B dense) to ~1M context **training-free**, matching Qwen2.5-14B-1M's
|
| 12 |
+
RULER class (92.5 @ 1M), by porting **Dual Chunk Attention** (DCA, the Qwen 1M mechanism) into the
|
| 13 |
+
llamafile graph. Runs on consumer GPUs (RTX 3090) β the path our users actually have β instead of
|
| 14 |
+
the parked vLLM FA4-sm120 route (which needs an unfinished CuTe Blackwell kernel; see backup_models
|
| 15 |
+
task #592). DCA composes with YaRN mscale for the final 2Γβ4Γ past the chunk grid.
|
| 16 |
+
|
| 17 |
+
## Why llamafile is sufficient (feasibility verified 2026-06-10, read-only)
|
| 18 |
+
Gemma-4 26B-A4B attention is **mixed-head** (config + tensor-shape confirmed):
|
| 19 |
+
- **25 sliding layers @ head_dim=256** (window 1024, ΞΈ=1e4) β bounded by their window, need NO
|
| 20 |
+
long-range handling.
|
| 21 |
+
- **5 global/full layers @ head_dim=512** (idx **5, 11, 17, 23, 29**; `global_head_dim=512`,
|
| 22 |
+
`num_global_key_value_heads=2`, `attention_k_eq_v=true` β no `v_proj`, K=V; `partial_rotary_factor
|
| 23 |
+
0.25` β n_rot=128 of 512; ΞΈ=1e6, `rope_type=proportional`). **These carry all long-range
|
| 24 |
+
information β DCA touches ONLY these 5 layers.**
|
| 25 |
+
- No `attn_logit_softcapping` (Gemma 3/4 dropped it; only `final_logit_softcapping=30` on the LM
|
| 26 |
+
head) β the chunk merge is the plain no-softcap online-softmax.
|
| 27 |
+
|
| 28 |
+
Every primitive DCA needs already exists in `vendors/sources/llamafile/llama.cpp` (RULER-validated):
|
| 29 |
+
- **Proportional rope** β `src/models/gemma4-iswa.cpp:56-59` feeds `model.layers[il].rope_freqs` as
|
| 30 |
+
`freq_factors` to `ggml_rope_ext` for `!is_swa` layers; `rope_freqs` loads at
|
| 31 |
+
`src/llama-model.cpp:2955`. Per-layer `freq_base_l/freq_scale_l/n_rot_l` (`gemma4-iswa.cpp:47-49`).
|
| 32 |
+
- **YaRN** β `ggml_rope_ext(..., ext_factor, attn_factor, beta_fast, beta_slow)` (`gemma4-iswa.cpp:73,97`)
|
| 33 |
+
+ hparams `n_ctx_orig_yarn`/`yarn_*` + `rope_scaling_type β {none,linear,yarn,longrope}`.
|
| 34 |
+
- **hd=512 flash-attention** β patch `0075-d512-turbo-vec` (D-generic FA-VEC for the global layers).
|
| 35 |
+
- **LSE-emitting FA + exact online-softmax merge** β patch `0070-rolling-kv`: `launch_fattn` per-row
|
| 36 |
+
`dst_lse = max + logf(rowsum)` via the `opencoti_fattn_dst_lse` consume-once channel +
|
| 37 |
+
`GGML_OP_STREAMING_FLASH_ATTN` + `streaming_combine_kernel`. **This is DCA's chunk-partial merge**;
|
| 38 |
+
generalizes from 2 regions (windowβtail) to N chunk-groups.
|
| 39 |
+
- **Training-free position-remap precedent** β `grp_attn_n/grp_attn_w` (LongLM Self-Extend),
|
| 40 |
+
`tools/completion/completion.cpp:511-517`. Same CLASS as DCA. **Caveat: completion-tool-only**, so
|
| 41 |
+
DCA hooks at the **graph** `build_attn` seam (serves the server opencoti drives).
|
| 42 |
+
- Clean per-layer seam: `build_attn(inp_attn, wo, β¦, Qcur, Kcur, Vcur, β¦, f_attention_scale, il)`
|
| 43 |
+
(`gemma4-iswa.cpp:102`).
|
| 44 |
+
|
| 45 |
+
**No blocking limitation.** Nuances handled by design: extension config (YaRN factor / `n_ctx_orig`)
|
| 46 |
+
via GGUF metadata or CLI; Self-Extend is completion-only so we hook the graph; our A4B GGUFs already
|
| 47 |
+
carry `rope_freqs` (they serve at 256k today).
|
| 48 |
+
|
| 49 |
+
## DCA algorithm (global layers only)
|
| 50 |
+
Split positions into chunks of size `c` (`c` < trained length; e.g. 65536). For each global layer,
|
| 51 |
+
attention is computed in position regimes merged by LSE:
|
| 52 |
+
- **intra-chunk** β queries attend keys in the SAME chunk at LOCAL positions `[0,c)` (preserves the
|
| 53 |
+
trained short-range rope behavior).
|
| 54 |
+
- **inter-chunk** β queries attend EARLIER chunks at a clamped inter-chunk (chunk-index) position so
|
| 55 |
+
relative positions stay inside the trained range.
|
| 56 |
+
- **successive-chunk** β the immediately-preceding chunk uses a continuous bridging position so the
|
| 57 |
+
intra/inter seam doesn't jump.
|
| 58 |
+
On Gemma-4 only the **rotary 128 dims** (partial_rotary 0.25) are remapped; the other 384 pass
|
| 59 |
+
through. Each regime = a separate rope'd-Q Γ KV attention emitting LSE; the partials merge via the
|
| 60 |
+
existing online-softmax combine. YaRN mscale (attn_factor) optionally rides on top for the final
|
| 61 |
+
2Γβ4Γ.
|
| 62 |
+
|
| 63 |
+
## Patch `0078-gemma4-dca` structure
|
| 64 |
+
Base for capture: live tree = `0001-0042 + 0070..0077` applied. Number `0078` (applies AFTER `0077`).
|
| 65 |
+
|
| 66 |
+
### A. Flags (additive, default-OFF β byte-identical when off)
|
| 67 |
+
`common/arg.cpp` + `common/common.h` + `src/llama-cparams.h`: `--dca {off,on}` (default off),
|
| 68 |
+
`--dca-chunk-size N` (default = n_ctx_train β inert: `c β₯ seqlen` collapses to a single intra
|
| 69 |
+
regime = plain attention), optional `--dca-yarn-factor F`. `LLAMA_ARG_DCA*`, `LLAMA_EXAMPLE_SERVER`.
|
| 70 |
+
New `cparams`: `bool dca_enabled; uint32_t dca_chunk; float dca_yarn_factor;`. OFF path grep-clean
|
| 71 |
+
byte-identical to pre-0078.
|
| 72 |
+
|
| 73 |
+
### B. Graph hook (one surgical marker) β `src/models/gemma4-iswa.cpp`
|
| 74 |
+
For `!hparams.is_swa(il) && cparams.dca_enabled` ONLY, route attention through the new additive
|
| 75 |
+
builder instead of the single `build_attn(...)`. Marker `// opencoti F5 gemma4-dca`. Sliding layers
|
| 76 |
+
and `dca_enabled==false` keep the unchanged `build_attn` β OFF byte-identical.
|
| 77 |
+
|
| 78 |
+
### C. Additive builder β NEW `src/models/gemma4-dca.cpp` (+ decl in `src/models/models.h`)
|
| 79 |
+
`build_attn_dca_gemma4(...)`: builds intra/successive/inter rope'd-Q variants (3Γ `ggml_rope_ext`
|
| 80 |
+
on `inp_pos`-derived remapped position tensors), runs 3 `build_attn`-style FA calls over the global
|
| 81 |
+
KV emitting `dst_lse` (reuse `0070`'s channel) with chunk masks, merges the 3 partials with a
|
| 82 |
+
`streaming_combine`-family reduce (extend the 2-region form to N-region). Keeps the in-tree hook to a
|
| 83 |
+
single marker (NEW file per UPSTREAM_SYNC Β§Additive).
|
| 84 |
+
|
| 85 |
+
### D. Position inputs β additive `llm_graph_input_dca`
|
| 86 |
+
Mirrors `llm_graph_input_mtp` (0074) / position-window inputs (0070): holds intra/inter/succ position
|
| 87 |
+
vectors derived from `inp_pos` + `dca_chunk`. Registered in graph-input assembly; no upstream-source
|
| 88 |
+
edit beyond registration.
|
| 89 |
+
|
| 90 |
+
### E. Acceptance gate β NEW `.opencoti/dca-ruler-gate.sh`
|
| 91 |
+
- **OFF byte-identity**: `--dca off` greedy decode byte-identical to the pre-0078 binary.
|
| 92 |
+
- **Below-one-chunk no-op**: at ctx β€ 256k with `--dca-chunk-size β₯ ctx`, `--dca on` RULER vt/niah
|
| 93 |
+
== `--dca off` (DCA must be a no-op when `c β₯ seqlen`). Uses the RULER native runner.
|
| 94 |
+
- **Extension win**: RULER vt/niah at 512k & 1M with `--dca on` vs YaRN-only degradation β target
|
| 95 |
+
the T87 1M class (β₯ Qwen2.5-14B-1M's 92.5@1M behavior on the global layers). Uses the
|
| 96 |
+
mergeβserveβRULER-ladder driver.
|
| 97 |
+
- Acceptance = logit-distribution / RULER-score equivalence, NOT greedy byte-equality past one chunk
|
| 98 |
+
(online-softmax is fp-non-associative β same standard as `0070`).
|
| 99 |
+
|
| 100 |
+
### F. README row + UPSTREAM_SYNC
|
| 101 |
+
Add the `0078-gemma4-dca` row to `vendors/patches/llamafile/README.md` (## Current series) and
|
| 102 |
+
register the single `// opencoti F5 gemma4-dca` marker in `docs/protocols/UPSTREAM_SYNC.md` (same
|
| 103 |
+
mechanism as `0076`/`0077`). Provenance: opencoti-original; algorithm from the DCA paper
|
| 104 |
+
(Qwen2.5-1M) + the deleted v0 vLLM `dual_chunk_flash_attn.py` reference (read, not transcribed).
|
| 105 |
+
|
| 106 |
+
## Capture without reset (the live tree holds the only patch source)
|
| 107 |
+
The standard `bun run build:llamafile:{check,apply,build}` flow assumes a pristine submodule
|
| 108 |
+
(`git apply --check`/end-to-end apply), which the live tree is NOT (0001-0077 already applied) and we
|
| 109 |
+
must NOT `reset` it. Therefore:
|
| 110 |
+
1. **Clean-room build/gate**: in a SEPARATE out-of-tree llamafile checkout (not the held submodule),
|
| 111 |
+
`reset β bootstrap β apply 0001-0077 β apply 0078 edits β build (cosmocc + ggml-cuda DSO) β run
|
| 112 |
+
gate E`. The held `vendors/sources/llamafile` is never touched.
|
| 113 |
+
2. **Capture the diff** against an out-of-tree base **copy** of the touched files (NOT `git apply`
|
| 114 |
+
from inside an in-tree baseline β that is a silent no-op, buglog bug-431; use `diff -u base live`
|
| 115 |
+
to produce the patch and `patch -p1` to re-apply into a copy when verifying).
|
| 116 |
+
3. **Verify round-trip**: `0078-gemma4-dca.patch` reverse-applies clean vs the post-0078 tree and
|
| 117 |
+
forward-reproduces it byte-for-byte (0 diffs), both directions.
|
| 118 |
+
4. **Land** only the artifacts in the opencoti repo: `0078-gemma4-dca.patch`, the README row, the
|
| 119 |
+
UPSTREAM_SYNC entry, `.opencoti/dca-ruler-gate.sh`, this doc. The held submodule working tree
|
| 120 |
+
stays exactly as the other session left it.
|
| 121 |
+
|
| 122 |
+
## Cheaper baseline to A/B first
|
| 123 |
+
`grp_attn` (Self-Extend) already exists; A/B its RULER@512k for A4B as the bar DCA must beat. Blocker:
|
| 124 |
+
completion-tool-only β needs a tiny server-path `grp_attn` hook OR running the completion tool for
|
| 125 |
+
the probe. Document the gap vs full DCA before committing to the 0078 build.
|
| 126 |
+
|
| 127 |
+
## Section-C merge: implementation decision (grounded 2026-06-10, read-only)
|
| 128 |
+
|
| 129 |
+
The graph seam (`gemma4-iswa.cpp` global block, `!is_swa(il)`): `rope_ext(Qcur/Kcur, inp_pos, freq_factors=rope_freqs, n_rot_l, freq_base_l=1e6, ...)` β `build_attn(inp_attn, wo, β¦, Qcur, Kcur, Vcur, β¦, f_attention_scale, il)`. Hook here for `dca_enabled && !is_swa`.
|
| 130 |
+
|
| 131 |
+
0070 primitives available host-side: `ggml_streaming_flash_attn(q,k,v,mask,scale,max_bias,softcap)` (emits O+LSE), `ggml_streaming_flash_attn_window(q,k_win,v_win,k_tail,v_tail,mask,β¦)` (2-region FUSED FA+combine), `ggml_flash_attn_ext_tail_partial(q,k,v,β¦)` β un-normalized partial `[2+DV, n_head, n_q, n_b]` = (m, s, VKQ). **There is NO standalone host-exposed N-way (O,lse) combine** β the online-softmax merge is fused inside `GGML_OP_STREAMING_FLASH_ATTN` (`fattn.cu streaming_combine_kernel`).
|
| 132 |
+
|
| 133 |
+
DCA needs to merge **3** regime partials (intra/successive/inter). Two paths:
|
| 134 |
+
- **C1 β graph-math merge (host-only, PREFERRED, no DSO rebuild):** per regime, emit the un-normalized partial (m_i, s_i, O_iΒ·s_i); combine N partials with a small subgraph of EXISTING ggml ops: `m=max_i(m_i)`, `w_i=exp(m_iβm)`, `S=Ξ£ w_i s_i`, `O=Ξ£ w_i O_i s_i / S`. All elementwise/reduce ops already in ggml β compiles into the host binary, executes on the existing fat `ggml-cuda.so 0.10.1` (sm_86+sm_120). Build = cosmocc `make` only. **Risk:** must get each regime's FA to emit the GPU partial to a graph tensor (verify `ggml_streaming_flash_attn` partial-output mode is graph-addressable, not just the thread-local channel; the `_tail_partial` op is CPU-only β too slow at 512k, so the GPU streaming op's partial path is required).
|
| 135 |
+
- **C2 β new N-way combine kernel (DSO rebuild):** add `ggml_streaming_combine_n(partials[])` + a `streaming_combine_n_kernel` generalizing the 2-region kernel β rebuild `ggml-cuda.so` via `cuda.sh`. Cleaner/faster but heavier build + a CUDA-source edit (per UPSTREAM_SYNC, a NEW kernel file to keep the in-tree hook minimal).
|
| 136 |
+
|
| 137 |
+
**Decision: implement C1 first** (host-only build, fastest to a gated result); fall back to C2 only if the GPU partial isn't graph-addressable or numerics demand a fused kernel. Either way the 3 regime Qs come from 3 `ggml_rope_ext` on remapped `inp_pos` views (intra=local [0,c); inter=chunk-index clamp; succ=continuous bridge), remapping only the rotary 128 dims (partial_rotary 0.25); the other 384 pass through.
|
| 138 |
+
|
| 139 |
+
**Build path is cosmocc, NOT cmake** (confirmed: patch sources `ggml-neo-pipeline/iqk/turbo/d512-vec` are NOT in any `CMakeLists.txt`; native `cmake --build` link-fails on those symbols). Clean-room = out-of-tree copy of the held `vendors/sources/llamafile` tree (already has 0001-0077 applied to source); edit C1 there; `bun run build:llamafile:make` (host binary, reuses fat DSO); gate E with `--dca on` + `--override-kv gemma4.context_length` serve; capture 0078 by `diff` vs a frozen copy of the held tree. Held submodule never reset.
|
| 140 |
+
|
| 141 |
+
### Section-C VERDICT (2026-06-10, op contract verified read-only) β C1 FALSIFIED β C2 required
|
| 142 |
+
|
| 143 |
+
Read the actual 0070 op definitions (`vendors/patches/llamafile/0070-rolling-kv.patch`). The "implement
|
| 144 |
+
C1 first" decision above is **falsified by the op contract**:
|
| 145 |
+
|
| 146 |
+
1. The GPU op `ggml_streaming_flash_attn` returns **normalized O only** β `ne[4]={DV, n_head, n_q,
|
| 147 |
+
n_batch}`, F32 (patch L539). Its per-tile LSE lives in **internal pool allocs** `O_tiles`/`lse_tiles`
|
| 148 |
+
(L1327-8) consumed by a **`static __global__ streaming_combine_kernel`** *inside* fattn.cu (L1113,
|
| 149 |
+
invoked L1859). **LSE is never a graph tensor.** β cannot call it 3Γ and merge (O,LSE) host-side.
|
| 150 |
+
2. The only op that emits the addressable un-normalized partial `[2+DV,n_head,n_q,n_b]`=(M,S,VKQ),
|
| 151 |
+
`ggml_flash_attn_ext_tail_partial`, is **CPU-only** (dispatched solely in ggml-cpu/ops.cpp L694/811;
|
| 152 |
+
**no CUDA case** β CUDA dispatch L1895/1912 is `STREAMING_FLASH_ATTN` only) **and decode-only `n_q==1`**
|
| 153 |
+
(its own header, L618-620). Useless for the 512k-token **prefill** where DCA matters; CPU scalar at
|
| 154 |
+
512k is intractable regardless.
|
| 155 |
+
3. Eager/materialized DCA (3 masked QK^T blocks combined by pure graph math, single softmax β which
|
| 156 |
+
*would* be genuine C1, no DSO rebuild) needs the full O(nΒ²) score matrix: β1 TB/head @512k. Infeasible.
|
| 157 |
+
|
| 158 |
+
**Therefore: the performant DCA path requires a CUDA-source edit β DSO rebuild (C2).** There is no
|
| 159 |
+
host-only route to per-regime (O,LSE) at prefill scale. Build flips from cheap `build:llamafile:make`
|
| 160 |
+
(reuse fat DSO) to **`cuda.sh` ggml-cuda.so rebuild** (the multi-hour cosmocc CUDA path), done in the
|
| 161 |
+
out-of-tree clean-room (held submodule still never touched).
|
| 162 |
+
|
| 163 |
+
**C2, minimal-footprint shape (reuses machinery that already exists internally):** expose the existing
|
| 164 |
+
per-tile partial + combine as graph-addressable β
|
| 165 |
+
- (a) a GPU op variant that runs FA over one regime and writes the **un-normalized partial** (M,S,VKQ)
|
| 166 |
+
to a graph tensor (the streaming op already computes exactly this per tile before its internal
|
| 167 |
+
combine β emit it instead of/in addition to normalized O), and
|
| 168 |
+
- (b) a host-exposed N-way `streaming_combine_n(partials[3])` generalizing `streaming_combine_kernel`
|
| 169 |
+
(which already combines N tile-partials) so the 3 regime partials merge on-device.
|
| 170 |
+
Per UPSTREAM_SYNC, new CUDA code lands in a NEW file; the in-tree hook stays a single marker. The 3
|
| 171 |
+
regime Qs still come from 3 `ggml_rope_ext` on remapped `inp_pos` views (intra=local [0,c);
|
| 172 |
+
inter=chunk-index clamp; succ=continuous bridge), rotary-128 dims only (partial_rotary 0.25); other
|
| 173 |
+
384 pass through. Numerics validated against the existing op's internal combine (same LSE math).
|
| 174 |
+
|
| 175 |
+
## MULTI-ARCH generalization (2026-06-10, user directive: "don't limit DCA to Gemma4 β same 1M extension on Qwen")
|
| 176 |
+
|
| 177 |
+
DCA is now an **architecture-general** training-free 1M extension, not a Gemma4 one-off. Patch renamed
|
| 178 |
+
`0078-dca` (was `0078-gemma4-dca`). Verified the Qwen seam is identical to Gemma4's:
|
| 179 |
+
`qwen2.cpp`: `ggml_rope_ext(Q/K, inp_pos, freq_factors=nullptr, n_rot=n_embd_head /*full rotary*/,
|
| 180 |
+
rope_type, n_ctx_orig, freq_base, freq_scale, β¦)` β `build_attn(inp_attn, wo, β¦, Q,K,V, β¦, scale, il)`
|
| 181 |
+
β same shape, differing only in `freq_factors` (Gemma4 global = `rope_freqs`; Qwen = null) and
|
| 182 |
+
`n_rot` (Gemma4 partial-128 of 512; Qwen full head_dim). So **one generic helper serves all**:
|
| 183 |
+
|
| 184 |
+
- **Section C (core, arch-agnostic)** β new `dca.{cpp,h}`: `llm_graph_input_dca` (the 3-regime remapped
|
| 185 |
+
position vectors, built once like `inp_pos`) + `llm_graph_context::build_attn_dca(inp_attn, inp_dca,
|
| 186 |
+
wo, wo_b, Qcur_PREROPE, Kcur_PREROPE, Vcur, β¦, dca_params, rope_params, kq_scale, il)`. It applies the
|
| 187 |
+
3 regime ropes (intra/successive/inter) via `ggml_rope_ext` on remapped positions, runs the
|
| 188 |
+
partial-emit FA per regime, merges with `streaming_combine_n`, then the `wo` projection (mirrors
|
| 189 |
+
`build_attn`'s tail). `rope_params` carries exactly the args each builder already passes to
|
| 190 |
+
`ggml_rope_ext` (n_rot, rope_type, n_ctx_orig, freq_base/scale, ext/attn_factor, beta_fast/slow,
|
| 191 |
+
freq_factors) β handles Gemma4 partial-rotary AND Qwen full-rotary with no special-casing.
|
| 192 |
+
- **Section B (per-arch hooks, small)** β in each builder, gate the full-attention layers through
|
| 193 |
+
`build_attn_dca` instead of `rope_ext+build_attn`: Gemma4 `is_dca_layer = !is_swa(il)` (5 global only);
|
| 194 |
+
Qwen `is_dca_layer = true` (all layers, no SWA). First-landing hooks: `gemma4-iswa.cpp`, `qwen2.cpp`
|
| 195 |
+
(Qwen2.5-1M, the canonical DCA target), `qwen3.cpp`, `qwen3moe.cpp`; `qwen35/qwen35moe/qwen2moe` are
|
| 196 |
+
one-marker each, added as needed. Marker `// opencoti F5 dca`.
|
| 197 |
+
- **Section D (CUDA, arch-agnostic)** β partial-emitter op + `streaming_combine_n`; operates on tensors,
|
| 198 |
+
no arch knowledge. This is the C2 DSO-rebuild payload.
|
| 199 |
+
- **Gate E** β RULER-VT @512k on BOTH A4B Q4_K_M AND a Qwen 1M model, vs the naive-ext bar (VT 0.36).
|
| 200 |
+
|
| 201 |
+
### Clean-room standup (confirmed mechanics)
|
| 202 |
+
Build pipeline: `bun --cwd packages/opencoti-llamafile script/build-pipeline.ts {reset,apply,make,cuda}`.
|
| 203 |
+
`apply` = `reset`(`git clean -fdx`+checkout pin `6490e16`) β `bootstrap`(download cosmocc + Mozilla-Ocho
|
| 204 |
+
`make setup`) β `git apply` 0001-0077. The held submodule has 8 uncommitted WIP files (`cuda.sh`,
|
| 205 |
+
`llamafile.c/h`, build mk, nested `llama.cpp`) β `reset` would WIPE them (buglog bug-431). So the
|
| 206 |
+
clean-room is a **separate `git clone /shared/dev/opencoti <dca-clone>`** (committed state only; held WIP
|
| 207 |
+
excluded by design), where `apply`βedit 0078β`cuda` runs freely. Capture 0078 by diffing the clone's
|
| 208 |
+
post-0078 source vs its post-0077 (pre-edit) source. Held tree never reset, never touched.
|
| 209 |
+
|
| 210 |
+
## Section-C ENGINEERING DESIGN (2026-06-10, grounded in clone applied source) β the cache-rope problem
|
| 211 |
+
|
| 212 |
+
Reading `llm_graph_context::build_attn` (kv variant) exposed the crux: **K is stored to the KV cache
|
| 213 |
+
already rope'd** (model builder ropes Kcur β `build_attn` β `mctx_cur->cpy_k(Kcur)`), and attention
|
| 214 |
+
reads it back rope'd. DCA needs the SAME keys re-rope'd with DIFFERENT positions per regime
|
| 215 |
+
(intra=native pos, inter=chunk-clamped, succ=bridge) β a single cached rope can't provide that.
|
| 216 |
+
|
| 217 |
+
**Decision: DCA layers cache K UN-ROPE'd and apply all rope at attention time, per regime, inside
|
| 218 |
+
`build_attn_dca`.** Consequences:
|
| 219 |
+
- The per-arch hook (Section B) passes **pre-rope Q AND pre-rope K** to `build_attn_dca` (not just
|
| 220 |
+
re-routes the call). On Gemma4 global, pre-rope K = `attn_k_norm(K_proj)` (the tensor right before
|
| 221 |
+
`ggml_rope_ext`); V = `rms_norm(K_proj)` is already un-rope'd (k_eq_v) and unchanged. On Qwen,
|
| 222 |
+
pre-rope K = the reshaped projection before its `ggml_rope_ext`.
|
| 223 |
+
- `build_attn_dca` stores pre-rope K + V to cache (`cpy_k`/`cpy_v`), then per regime rβ{intra,succ,
|
| 224 |
+
inter}: `Q_r=rope(Qcur_pre, pos_q_r)`, `K_r=rope(get_k_raw, pos_k_r)`, `partial_r =
|
| 225 |
+
streaming_flash_attn_PARTIAL(Q_r, K_r, V, mask_r, kq_scale)`; then `O =
|
| 226 |
+
streaming_combine_n([partial_intra, partial_succ, partial_inter])`; then `wo` projection (mirrors
|
| 227 |
+
build_attn tail: `build_lora_mm(wo,Β·)` + optional `wo_b`).
|
| 228 |
+
- Rope-on-cached-raw-K is an ESTABLISHED pattern here: `llama_kv_cache::build_rope_shift`
|
| 229 |
+
(llama-kv-cache.cpp:3893, used by context-shift) re-ropes cached K via a position/`k_rot` tensor β
|
| 230 |
+
the structural template (DCA ropes a READ COPY with absolute regime positions, does NOT mutate the
|
| 231 |
+
cache).
|
| 232 |
+
- **`llm_graph_input_dca`** (built once like `inp_pos`, host-filled from batch positions + KV-cache
|
| 233 |
+
cell positions) supplies, per regime: `pos_q_r` (len n_tokens), `pos_k_r` (len n_kv), and `mask_r`
|
| 234 |
+
([n_kv Γ n_tokens] additive). The pos remaps + masks encode the DCA chunk math
|
| 235 |
+
(chunk_size c = `--dca-chunk-size`, default β n_ctx_orig). intra = same-chunk causal, native pos;
|
| 236 |
+
inter = q-chunk > k-chunk, k pos within-chunk + q pos clamped β€ pretrain window; succ = adjacent
|
| 237 |
+
chunk bridge. rope uses `rope_params` (n_rot, rope_type, freq_base/scale, ext/attn_factor,
|
| 238 |
+
beta_fast/slow, freq_factors) β Gemma4 partial-128 / Qwen full-rotary handled by the n_rot arg.
|
| 239 |
+
|
| 240 |
+
**Toggle is launch-time only** (cache contents differ: raw-K for DCA vs rope'd-K for non-DCA) β fine,
|
| 241 |
+
it's a serve flag. Wiring caveat: Gemma4 0076 shared-KV means some global layers `reuse KV cache of
|
| 242 |
+
earlier layers` (`has_kv(il)` false) β DCA must rope-on-read consistently for the layer-group that
|
| 243 |
+
owns the cache; verify against the 0076 shared-kv mechanics when wiring Section B.
|
| 244 |
+
|
| 245 |
+
## Implementation progress (2026-06-10) + Section-D simplification
|
| 246 |
+
|
| 247 |
+
Patch number **0078 confirmed** (user, keep 0078). Build host = solidpc RTX 3090 (sm_86, CUDA 13.2).
|
| 248 |
+
|
| 249 |
+
**Section-D shrinks to ONE new op + graph-math combine** (no new combine kernel). Insight: if the new
|
| 250 |
+
FA op emits NORMALIZED O packed with per-row LSE as `[DV+1]` (reusing 0070's existing `dst_lse`
|
| 251 |
+
channel β the `lse = m + logf(sum exp)` math already lives in `launch_fattn`), the 3-regime merge is
|
| 252 |
+
pure existing-ggml graph math: `M = max_r(lse_r)` (binary max via `a + relu(bβa)`), `w_r =
|
| 253 |
+
exp(lse_rβM)`, `O = Ξ£_r w_rΒ·O_r / Ξ£_r w_r` (`ggml_exp/mul/add/div`, broadcasting w over DV). So the
|
| 254 |
+
DSO rebuild only adds a CUDA *forward* for the new op (FA + write lse), not a bespoke combine kernel.
|
| 255 |
+
|
| 256 |
+
**DONE this session (clean-room `/shared/dev/opencoti-dca`, baseline nested `43ccc8a56`):**
|
| 257 |
+
- **Section A (flags)** β `--dca`/`--dca-chunk-size`/`--dca-yarn-factor` through all 6 opencoti sites
|
| 258 |
+
(arg.cpp, common.h, common.cpp, llama-context.cpp Γ2, llama-cparams.h, llama.h). Verified.
|
| 259 |
+
Script: `backup_models/scripts/dca/apply_section_a_flags.py`.
|
| 260 |
+
- **Section D-host** β `GGML_OP_FLASH_ATTN_EXT_LSE` + `ggml_flash_attn_ext_lse(q,k,v,mask,scale,
|
| 261 |
+
max_bias,logit_softcap)` β dst `[DV+1, n_head, n_q, n_batch]` ([0,DV)=O, [DV]=lse). Registered in
|
| 262 |
+
ggml.h enum+decl, ggml.c name/symbol arrays, both static_asserts (100β101), constructor.
|
| 263 |
+
Script: `backup_models/scripts/dca/apply_section_d_host.py`.
|
| 264 |
+
|
| 265 |
+
**REMAINING:** D-cuda (CUDA forward for the new op: `ggml_cuda_flash_attn_ext` path + dst_lseβdst[DV]
|
| 266 |
+
slot; supports_op + scheduler-pin like the bug-259 STREAMING_FLASH_ATTN hook) β Section C
|
| 267 |
+
(dca.cpp/h: `llm_graph_input_dca` + `build_attn_dca`) β Section B (per-arch hooks) β cuda.sh DSO
|
| 268 |
+
rebuild β RULER-VT gate (A4B + Qwen) β capture `0078-dca.patch`.
|
| 269 |
+
|
| 270 |
+
## Section D-cuda DONE (2026-06-10)
|
| 271 |
+
|
| 272 |
+
`ggml_cuda_flash_attn_ext_lse` authored + wired (8 sites, `apply_section_d_cuda.py`):
|
| 273 |
+
- **fattn.cu** β the forward: O via an O-typed view of the packed dst β existing
|
| 274 |
+
`ggml_cuda_flash_attn_ext` (proven streaming-op `ggml_tensor o=*dst; o.op=FLASH_ATTN_EXT` trick);
|
| 275 |
+
per-row lse via the existing static `streaming_lse_kernel` into the trailing `n_rows` (softcap-aware,
|
| 276 |
+
works for any n_q incl. prefill β avoids the `decode_lse`/bug-263 finalize fragility). Asserts Q f32 / K f16.
|
| 277 |
+
- **fattn.cuh** decl; **ggml-cuda.cu** compute_forward dispatch + supports_op (validates the O FA at
|
| 278 |
+
ne[0]=DV, not the packed DV+1); **ggml-backend.cpp** bug-259 scheduler CUDA-pin; **ggml-cpu** Γ3
|
| 279 |
+
(compute_forward abort, n_tasks single-task, supports_op=false) β CUDA-only mirror of STREAMING_FLASH_ATTN.
|
| 280 |
+
|
| 281 |
+
Op fully complete (host + CUDA). Compile-verified later in the single cuda.sh DSO build (after C+B).
|
| 282 |
+
**REMAINING:** Section C (dca.cpp/h: `llm_graph_input_dca` + `build_attn_dca` with DCA position-remap +
|
| 283 |
+
graph-math combine β O view ne=[DV,H,nq,nb] @offset0, lse view ne=[1,H,nq,nb] @offset DV*n_rows) β
|
| 284 |
+
Section B (per-arch hooks) β cuda.sh DSO rebuild β RULER-VT gate β capture 0078-dca.patch.
|
| 285 |
+
|
| 286 |
+
## Section C design (2026-06-10) β grounded against the clean-room seams
|
| 287 |
+
|
| 288 |
+
Read of the clean-room (`43ccc8a56`) settles the algorithm and the integration points.
|
| 289 |
+
|
| 290 |
+
**Seams (verified):**
|
| 291 |
+
- kv-variant `build_attn` (llama-graph.cpp:2571) and **iswa**-variant `build_attn`
|
| 292 |
+
(llama-graph.cpp:2841). Both: `cpy_k/cpy_v(k_cur,β¦)` store **whatever K they're handed** β
|
| 293 |
+
`get_k/get_v(ctx0,il)` read-back β `build_attn_mha` β `build_lora_mm(wo)` + `wo_b`. Gemma-4
|
| 294 |
+
globals ride the **iswa** path (`build_attn_inp_kv_iswa`, `is_swa(il)==false` β base mctx);
|
| 295 |
+
Qwen rides the **plain-kv** path. β `build_attn_dca` needs **both** input-type overloads.
|
| 296 |
+
- Host position source: `set_input_kq_mask` (llama-kv-cache.cpp:3773) / `set_input_pos_bucket`
|
| 297 |
+
(:3812) read `v_cells[strm].pos_get(j)` / `cells.is_empty(j)` per cached cell. A
|
| 298 |
+
`llm_graph_input_i::set_input` only gets the ubatch β the per-cell fill **must** be a new
|
| 299 |
+
`llama_kv_cache[_context]::set_input_dca` (mirrors `set_input_kq_mask`), delegated to from
|
| 300 |
+
`llm_graph_input_dca::set_input`.
|
| 301 |
+
- Rope contract (gemma4-iswa.cpp:73/97): `ggml_rope_ext(x, inp_pos, freq_factors, n_rot_l,
|
| 302 |
+
rope_type, n_ctx_orig, freq_base_l, freq_scale_l, ext, attn, beta_fast, beta_slow)` β Gemma-4
|
| 303 |
+
globals use `n_rot_l=hparams.n_rot(il)` (partial-128 on the hd512 global), `freq_base_l=1e6`.
|
| 304 |
+
Qwen full-rotary uses `n_rot = n_embd_head`, `freq_base=1e6`/yarn. `build_attn_dca` reproduces
|
| 305 |
+
this call three (Q) + one (K) times with regime position vectors instead of `inp_pos`.
|
| 306 |
+
|
| 307 |
+
**Algorithm β rope-K-ONCE, rope-Q-THRICE (ChunkLlama scheme).** All three regimes share one key
|
| 308 |
+
assignment `pos_k[j] = cellpos(j) mod c`; only the query position differs:
|
| 309 |
+
|
| 310 |
+
| regime | `pos_q[i]` | keep-mask (causal `jβ€i`, chunk `=βpos/cβ`) |
|
| 311 |
+
|---|---|---|
|
| 312 |
+
| **intra** | `i mod c` | `chunk(j) == chunk(i)` |
|
| 313 |
+
| **succ** | `(i mod c) + c` | `chunk(j) == chunk(i) β 1` |
|
| 314 |
+
| **inter** | `c` (const) | `chunk(j) < chunk(i) β 1` |
|
| 315 |
+
|
| 316 |
+
The three keep-masks **partition the causal lower-triangle exactly** β the LSE-merge of the three
|
| 317 |
+
softmaxes equals full attention under DCA-remapped positions (no double-count, no gap). No q-k
|
| 318 |
+
distance exceeds the pretrain window: intra `<c`, succ `β(0,2c)`, inter `=cβokβ[1,c]`. `c` =
|
| 319 |
+
`dca_chunk_size` or (auto) `n_ctx_orig_yarn`. YaRN mscale composes via the existing rope `ext/attn`
|
| 320 |
+
factors (`dca_yarn_factor` feeds `cparams.yarn_*`; the rope kernel itself is unchanged).
|
| 321 |
+
|
| 322 |
+
**`build_attn_dca(inp, wo, wo_b, Qpre, Kpre, Vcur, β¦, kq_scale, il)` graph body:**
|
| 323 |
+
1. `cpy_k(Kpre)` / `cpy_v(Vcur)` to cache (Section B hands **pre-rope** K + un-roped V).
|
| 324 |
+
2. `Kraw = get_k(ctx0,il)`; `V = get_v(ctx0,il)`; `Kdca = ggml_rope_ext(Kraw, inp_dca->pos_k, β¦)`
|
| 325 |
+
β **one** rope of the large cached K (raw cells, regime-invariant key positions).
|
| 326 |
+
3. For `r β {intra,succ,inter}`: `Qr = ggml_rope_ext(Qpre, inp_dca->pos_q[r], β¦)`;
|
| 327 |
+
`Pr = ggml_flash_attn_ext_lse(Qr, Kdca, V, inp_dca->mask[r], kq_scale, max_bias, softcap)`
|
| 328 |
+
β packed `[DV+1, H, nq, nb]`. O view `ggml_view_4d(Pr, DV,H,nq,nb,β¦,0)`; lse view
|
| 329 |
+
`ggml_view_4d(Pr, 1,H,nq,nb,β¦, DV*nb0)`.
|
| 330 |
+
4. Combine (pure ggml): `M = a+relu(bβa)` over the 3 lse; `wr = ggml_exp(lse_r β M)` (broadcast over
|
| 331 |
+
DV); `O = (Ξ£ wrΒ·Or) / (Ξ£ wr)`; reshape `[n_embd, n_tokens]`; `build_lora_mm(wo)` + `wo_b`.
|
| 332 |
+
|
| 333 |
+
**`llm_graph_input_dca`** (new, dca.h): `pos_k` (I32 [n_kv]), `pos_q[3]` (I32 [n_tokens]), `mask[3]`
|
| 334 |
+
(F32βF16 cnv, `[n_kv, GGML_PAD(n_tps,GGML_KQ_MASK_PAD), 1, n_stream]` like `build_attn_inp_kq_mask`),
|
| 335 |
+
`const llama_kv_cache_context * mctx`, `uint32_t c`. `set_input(ubatch)` β
|
| 336 |
+
`mctx->set_input_dca(pos_k, pos_q, mask, ubatch, c)`.
|
| 337 |
+
|
| 338 |
+
**Files touched by Section C:** new `src/dca.cpp` + `src/dca.h` (the two builders) **and**
|
| 339 |
+
`llama-kv-cache.{cpp,h}` (the `set_input_dca` host-fill β folded into C) **and** `llama-graph.h`
|
| 340 |
+
(the `build_attn_dca` decls + `llm_graph_input_dca`). dca.cpp compiles against the already-present
|
| 341 |
+
host `ggml_flash_attn_ext_lse` (D-host); the CUDA forward (D-cuda) need not exist until the DSO
|
| 342 |
+
build. **Sub-blocks:** C1 = dca.h + `llm_graph_input_dca` + `set_input_dca` host-fill (this block);
|
| 343 |
+
C2 = `build_attn_dca` graph bodies (both overloads).
|
| 344 |
+
|
| 345 |
+
## Section C2a (host-fill) DONE (2026-06-10)
|
| 346 |
+
|
| 347 |
+
`dca.h` (C1) + the host-fill half of C2 landed in the clean-room. Builds independently of the
|
| 348 |
+
`build_attn_dca` graph bodies (next block) and of D-cuda (host `ggml_flash_attn_ext_lse` already
|
| 349 |
+
present from D-host).
|
| 350 |
+
- **`src/dca.cpp`** (new) β `dca_resolve_chunk_size` (cparams.dca_chunk_size or auto =
|
| 351 |
+
n_ctx_orig_yarn / n_ctx_train), `llm_graph_input_dca::set_input` (delegates to
|
| 352 |
+
`mctx->set_input_dca`), `::can_reuse` (false β pos/masks are position-specific), and
|
| 353 |
+
`llm_graph_context::build_attn_inp_dca(mctx_kv)` (allocates pos_k I32[n_kv], 3Γ pos_q I32[n_tokens],
|
| 354 |
+
3Γ mask F32[n_kv,n_tokens,1,1] + F16 cnv; single-stream, mirrors `build_attn_inp_kq_mask`).
|
| 355 |
+
- **`src/llama-kv-cache.cpp`** β `llama_kv_cache::set_input_dca` (reads `v_cells[0]` per-cell
|
| 356 |
+
positions β `pos_k=cellpos%c`, 3Γ pos_q, and the 3 partition masks; `n_stream==1` assert like
|
| 357 |
+
`set_input_pos_bucket`; causal + same-seq + nonempty band test) + the `llama_kv_cache_context`
|
| 358 |
+
per-batch wrapper.
|
| 359 |
+
- **`src/llama-kv-cache.h`** β `set_input_dca` decl in both classes (Γ2).
|
| 360 |
+
- **`src/llama-graph.h`** β `build_attn_inp_dca` method decl + `class llm_graph_input_dca;` fwd-decl.
|
| 361 |
+
|
| 362 |
+
Apply script `backup_models/scripts/dca/apply_section_c_hostfill.py` (5 anchored edits, all OK).
|
| 363 |
+
Verified: kv-cache.cpp Γ2, kv-cache.h Γ2, graph.h Γ2, dca.cpp def Γ1, braces balanced.
|
| 364 |
+
|
| 365 |
+
**β Build-wiring TODO for the cuda.sh/make block (#598):** confirm the cosmocc Makefile globs
|
| 366 |
+
`src/*.cpp` (so the new `src/dca.cpp` compiles) β else add it to the file list explicitly, or
|
| 367 |
+
`build_attn_inp_dca` link-errors.
|
| 368 |
+
|
| 369 |
+
**REMAINING:** Section C2b (`build_attn_dca` two overloads β rope cached raw-K once with pos_k +
|
| 370 |
+
rope Q thrice + 3Γ `ggml_flash_attn_ext_lse` under the partition masks + LSE graph-math combine +
|
| 371 |
+
wo/wo_b; declared in llama-graph.h alongside this block's decls). **Before authoring C2b, read
|
| 372 |
+
`get_k`'s returned cache-K layout (llama-kv-cache.cpp:2204) + `build_rope_shift`'s caller
|
| 373 |
+
(~:4000-4030)** to nail the rope-on-cached-K axis (positions vary on the cell axis). β Section B
|
| 374 |
+
(per-arch hooks) β cuda.sh DSO rebuild β RULER-VT gate β capture 0078-dca.patch.
|
| 375 |
+
|
| 376 |
+
## Section C2b (build_attn_dca bodies) DONE (2026-06-10) β Section C COMPLETE
|
| 377 |
+
|
| 378 |
+
The rope-on-cached-K axis is settled by `get_k` (llama-kv-cache.cpp:2305): it returns
|
| 379 |
+
`[n_embd_head, n_head_kv, n_kv, 1]` β **cells on ne[2]** β so `ggml_rope_ext(get_k, pos_k, β¦)` with
|
| 380 |
+
`pos_k` length `n_kv` indexes the cell axis exactly as `inp_pos` indexes live tokens. K is roped
|
| 381 |
+
ONCE; Q thrice; both pre-permute (matching the model rope), then permuted into the flash-attn layout.
|
| 382 |
+
|
| 383 |
+
- **`src/dca.cpp`** β `build_attn_dca_core` (rope cached raw-K once; read+permute V once; per regime
|
| 384 |
+
rope Q + `ggml_flash_attn_ext_lse` under `mask_cnv[r]`; extract O-view `[DV,H,NQ,1]@0` + lse-view
|
| 385 |
+
`[1,H,NQ,1]@DVΒ·nb0`, both `ggml_cont`; combine `M=a+relu(bβa)`, `w=ggml_exp(lseβM)`, `O=Ξ£wΒ·O/Ξ£w`;
|
| 386 |
+
`reshape_2d`β`build_lora_mm(wo)`+`wo_b`) + the two public overloads (iswa: store to
|
| 387 |
+
`inp->mctx->get_base()`, asserts `!is_swa`; plain-kv: `inp->mctx`; both guard `k_cur`/`v_cur` for
|
| 388 |
+
Gemma-4 shared-KV layers).
|
| 389 |
+
- **`src/dca.h`** β `dca_rope` struct (per-layer freq_base/freq_scale/n_rot/freq_factors).
|
| 390 |
+
- **`src/llama-graph.h`** β 2 `build_attn_dca` overloads + `build_attn_dca_core` decls + `dca_rope`
|
| 391 |
+
fwd-decl. Apply script `apply_section_c2b_decls.py`.
|
| 392 |
+
|
| 393 |
+
**Empty-regime safety (verified, no code needed):** a chunk-0 query's succ/inter regimes have an
|
| 394 |
+
all-`-inf` mask row β FA O=0 and `streaming_lse_kernel` (fattn.cu:693 `lse = (l>0)?β¦:-INFINITY`)
|
| 395 |
+
emits `-inf` (the `l<=0`/NaN guard already there) β `w=exp(-infβM)=0`, dropping the regime cleanly.
|
| 396 |
+
Intra is never empty (diagonal), so `M` is always finite.
|
| 397 |
+
|
| 398 |
+
**Two runtime caveats for the gate (#598):**
|
| 399 |
+
1. ~~**f16 KV required**~~ β **LIFTED by #444 (DCA all-KV C1, 2026-06-18).** `build_attn_dca_core` no
|
| 400 |
+
longer asserts f16; `dca_lift_to_f16` dequant-on-lifts a quantized/bf16/turbo cache to a transient
|
| 401 |
+
f16 for the f16-only fused kernel (the stored cache stays quantized β the long-ctx memory win). Scalar
|
| 402 |
+
quants (q8_0/q4_0/q4_1/q5_0/q5_1) hop `q8βf32βf16` because CUDA CPY has no direct scalar-quantβf16
|
| 403 |
+
kernel (that pair CPU-spills); f16 no-op, bf16/turbo cast direct. Host-only (no `.cu` change). Gated
|
| 404 |
+
`real_frac=0` vs f16-DCA on Qwen3-8B-Q8 at multi-chunk ctx (`.opencoti/m444-dca-allkv-gate.sh`). **#445
|
| 405 |
+
(C2/C3) extends the gate to the full matrix β q8_0, q5_1, q5_0, q4_0, bf16 + asymmetric q8_0-K/q4_0-V
|
| 406 |
+
ALL PASS** (`.opencoti/m445-dca-scalar-matrix-gate.sh`): per type `real_frac(<t>-DCA vs f16-DCA) ==
|
| 407 |
+
real_frac(<t>-noDCA) == 0.0`, so DCA adds zero argmax error beyond the type's own quant loss. (Prefill
|
| 408 |
+
note: noDCA-quant FA prefill is type-specific β q4_0/bf16 have a fast batched tile-FA kernel ~3.5k
|
| 409 |
+
tok/s, q8_0/q5_0/q5_1 fall to the decode-VEC path ~12β42 tok/s; DCA's f16-lift prefills uniformly
|
| 410 |
+
~1.5k tok/s β a win for the slow types.) q8-DCA
|
| 411 |
+
decode β0.47Γ f16-DCA, and that 2Γ is characterized (not a regression to chase away): it is
|
| 412 |
+
GPU-resident (CPU-spill would be <5 tps) and NOT cuda-graph loss (`GGML_CUDA_DISABLE_GRAPHS=1`
|
| 413 |
+
changes neither path β the fused DCA op uses no graph). The cost is the dequant-on-lift itself,
|
| 414 |
+
DOMINATED by the `quantβf32` CPY kernel's `<<<ne, 1>>>` one-thread-per-CUDA-block launch geometry
|
| 415 |
+
(cpy.cu, ~1/32 warp utilization vs the 256-thread scalar cpy), with the f32 two-pass hop secondary;
|
| 416 |
+
raw cast bandwidth alone (~3 ms/tok) does not explain it. The single-pass direct `quantβf16` CPY
|
| 417 |
+
kernel (proper thread geometry, no f32 intermediate) recovers it β C-series perf follow-on at #448.
|
| 418 |
+
So `-ctk q8_0 -ctv q8_0` is now valid with `--dca on`. The 0078 patch re-capture is batched at #448
|
| 419 |
+
(umbrella C6); the live source is banked `.opencoti/dca/m444-dca.cpp.banked`.
|
| 420 |
+
2. **FA precision defaults** β unlike `build_attn_mha` (which forces `GGML_PREC_F32` via
|
| 421 |
+
`ggml_flash_attn_ext_set_prec`, an op-specific call that would assert on `_LSE`), the lse op runs
|
| 422 |
+
at default prec. If RULER shows precision drift at 1M, force F32 on the O-view in D-cuda's
|
| 423 |
+
`ggml_cuda_flash_attn_ext_lse` (set the prec slot before dispatching the inner FA).
|
| 424 |
+
|
| 425 |
+
Verified: dca.cpp 3 defs + braces 26/26; graph.h 3 decls + `dca_rope` fwd; ruff clean.
|
| 426 |
+
|
| 427 |
+
**Section C COMPLETE** (C1 header + C2a host-fill + C2b bodies). Compiles against host
|
| 428 |
+
`ggml_flash_attn_ext_lse` (D-host); CUDA forward (D-cuda) needed only at the DSO build.
|
| 429 |
+
**REMAINING:** Section B (per-arch hooks: gemma4-iswa global layers + qwen2/3/3moe all layers β gate
|
| 430 |
+
on `cparams.dca_enabled`, pass PRE-rope Q+K, build `build_attn_inp_dca(base)` once, fill `dca_rope`)
|
| 431 |
+
β cuda.sh DSO rebuild (confirm `src/dca.cpp` globbed) β RULER-VT gate β capture 0078-dca.patch.
|
| 432 |
+
|
| 433 |
+
## Section B (gemma4) DONE (2026-06-10)
|
| 434 |
+
|
| 435 |
+
`src/models/gemma4-iswa.cpp` now routes the 5 GLOBAL (`!is_swa`) layers through `build_attn_dca`
|
| 436 |
+
when `cparams.dca_enabled`; SWA layers + the dca-disabled path are byte-unchanged. 7 anchored edits
|
| 437 |
+
(`apply_section_b_gemma.py`):
|
| 438 |
+
- `#include "dca.h"`.
|
| 439 |
+
- before the layer loop: `llm_graph_input_dca * inp_dca = cparams.dca_enabled ?
|
| 440 |
+
build_attn_inp_dca(inp_attn->mctx->get_base()) : nullptr;` (built once on the base/global context).
|
| 441 |
+
- per layer: `const bool use_dca = cparams.dca_enabled && !hparams.is_swa(il);`.
|
| 442 |
+
- Q-rope + K-rope wrapped in `if (!use_dca) {β¦}` β DCA layers cache **PRE-rope** Q+K (rope happens
|
| 443 |
+
per regime inside build_attn_dca). Vcur stays the rms-normed un-roped projection.
|
| 444 |
+
- both `build_attn(β¦)` call sites (has_kv + shared-KV reuse-KV branch) gain an `if (use_dca)`
|
| 445 |
+
arm calling `build_attn_dca(inp_attn, inp_dca, wo, nullptr, Qcur, [Kcur|null], [Vcur|null],
|
| 446 |
+
{freq_base_l, freq_scale_l, n_rot_l, freq_factors}, f_attention_scale, il)`.
|
| 447 |
+
|
| 448 |
+
DCA reduces to identity for n_ctx β€ c (all positions in chunk 0 β intra-only, succ/inter empty,
|
| 449 |
+
distances < c β native rope), so enabling `--dca` never regresses short-context serving. Verified:
|
| 450 |
+
2 build_attn_dca calls, 1 inp builder, use_dca 5Γ, braces 27/27, ruff clean.
|
| 451 |
+
|
| 452 |
+
**REMAINING:** Section B (qwen2/qwen3/qwen3moe β all layers full-attention, `use_dca =
|
| 453 |
+
cparams.dca_enabled`, same PRE-rope Q+K + `build_attn_inp_dca(static_cast<β¦kv_cache_context*>(mctx))`
|
| 454 |
+
on the plain-kv overload) β cuda.sh DSO rebuild (confirm `src/dca.cpp` globbed) β RULER-VT gate
|
| 455 |
+
(Gemma-4 A4B first, f16 KV) β capture 0078-dca.patch.
|
| 456 |
+
|
| 457 |
+
## Section B (qwen) DONE (2026-06-10) β Section B COMPLETE β ALL SOURCE AUTHORED
|
| 458 |
+
|
| 459 |
+
`qwen2.cpp` / `qwen3.cpp` / `qwen3moe.cpp` route **all** layers through `build_attn_dca` when
|
| 460 |
+
`cparams.dca_enabled` (Qwen = the canonical training-free 1M arch; every layer is full attention).
|
| 461 |
+
The Q/K-rope + `build_attn` blocks are byte-identical across the three files, so one shared anchor
|
| 462 |
+
set (`apply_section_b_qwen.py`, 5 edits Γ3) applies to each: `#include "dca.h"`; `use_dca =
|
| 463 |
+
cparams.dca_enabled` + `inp_dca = build_attn_inp_dca(inp_attn->mctx)` (plain-kv context, no
|
| 464 |
+
get_base); Q/K rope wrapped in `if (!use_dca)`; the `build_attn` site gains an `if (use_dca)` arm
|
| 465 |
+
calling `build_attn_dca(..., model.layers[il].wo, model.layers[il].bo, Qcur, Kcur, Vcur,
|
| 466 |
+
{freq_base, freq_scale, n_rot, nullptr}, 1/βhead, il)` (wo_b = bo carried; qwen3/3moe `wo_s`
|
| 467 |
+
post-scale untouched). Verified: each file 1 dca-call, 1 inp builder, use_dca 5Γ, braces balanced,
|
| 468 |
+
ruff clean.
|
| 469 |
+
|
| 470 |
+
**ALL 0078-dca SOURCE AUTHORED** β A (flags) + D (LSE op host+CUDA) + C (input + build_attn_dca) +
|
| 471 |
+
B (gemma4 globals + qwen2/3/3moe all layers). Nothing compiled yet β the single `cuda.sh` DSO build
|
| 472 |
+
(#598) compiles A+C+D+B together. New files: `src/dca.{h,cpp}`. Apply scripts (8) in
|
| 473 |
+
`backup_models/scripts/dca/`.
|
| 474 |
+
|
| 475 |
+
**REMAINING (#598, the build+gate+land block):**
|
| 476 |
+
1. **Build wiring** β confirm the cosmocc Makefile globs `src/*.cpp` (so `src/dca.cpp` compiles);
|
| 477 |
+
add it explicitly if not. Then `bun --cwd packages/opencoti-llamafile script/build-pipeline.ts
|
| 478 |
+
make` + `β¦ cuda` (the multi-hour `ggml-cuda.so` DSO rebuild, sm_86). Fix any compile errors
|
| 479 |
+
surfaced (first real compile of A/C/D/B).
|
| 480 |
+
2. **RULER-VT gate** β serve A4B Q4_K_M (or bf16) with `--dca on --override-kv
|
| 481 |
+
gemma4.context_length=int:1048576`, **f16 KV** (NOT `-ctk q8_0` β DCA core asserts f16), default
|
| 482 |
+
prec. RULER-VT @ β₯256k vs the naive-extension bar (VT=0.36 from T87.ext). Then a Qwen 1M model.
|
| 483 |
+
3. **Capture** β `git diff 43ccc8a56` in the clean-room β `0078-dca.patch`; write patch + README row
|
| 484 |
+
+ UPSTREAM_SYNC entry + `.opencoti/dca-ruler-gate.sh` into the HELD opencoti repo (top-level
|
| 485 |
+
tracked only; submodule never touched); round-trip `apply` verify.
|
docs/features/introspection.md
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# opencoti runtime introspection & control surface (#676/#677)
|
| 2 |
+
|
| 3 |
+
Complete reference for **observing and controlling the opencoti agentic-serving
|
| 4 |
+
features** (PolyKV, SharedKVPool, rolling-KV residency, DCA, MTP/spec-decode,
|
| 5 |
+
session KV-reuse, retention eviction, sparse-attn, RYS) β what is configurable
|
| 6 |
+
at boot, what a client can set per request, and what the server reports back at
|
| 7 |
+
runtime.
|
| 8 |
+
|
| 9 |
+
Status: implemented 2026-07-15 (tasks #676 `/props` echo + core KV-info API,
|
| 10 |
+
#677 `/slots` enrichment). Vendored-source additive edits; host-only (no CUDA).
|
| 11 |
+
|
| 12 |
+
---
|
| 13 |
+
|
| 14 |
+
## 1. The three planes
|
| 15 |
+
|
| 16 |
+
| Plane | Mechanism | When |
|
| 17 |
+
|---|---|---|
|
| 18 |
+
| **Boot knobs** | CLI flags (`common/arg.cpp`) β typed TS adapter fields β `OPENCOTI_LLAMAFILE_*` env | server launch |
|
| 19 |
+
| **Per-request control** | 3 JSON body fields (`session_id`, `shared_pool_slot`, `shared_prefix_n_tokens`) | every completion request |
|
| 20 |
+
| **Runtime introspection** | `/props β "opencoti"` (boot-state echo + effective KV), `/slots β per-slot "opencoti"` (live state), completion `timings` fields | any time |
|
| 21 |
+
|
| 22 |
+
---
|
| 23 |
+
|
| 24 |
+
## 2. Boot knobs β by feature
|
| 25 |
+
|
| 26 |
+
Typed fields live in `packages/opencoti-llamafile/src/config.ts` (each maps to
|
| 27 |
+
env `OPENCOTI_LLAMAFILE_<SNAKE_CASE>`); CLI flags are registered in
|
| 28 |
+
`vendors/sources/llamafile/llama.cpp/common/arg.cpp`. Flags marked *(extraArgs
|
| 29 |
+
only)* have no typed adapter field yet.
|
| 30 |
+
|
| 31 |
+
### PolyKV β KV-quant tiers (M6)
|
| 32 |
+
- `ctypeK` / `ctypeV` β `-ctk` / `-ctv` β window KV types; scalar
|
| 33 |
+
(`f16,bf16,q8_0,q6_0,q5_1,q5_0,q4_0`) + turbo (`turbo2/3/4/8`) + TCQ
|
| 34 |
+
(`turbo2_tcq,turbo3_tcq`).
|
| 35 |
+
- `ctypeKTail` / `ctypeVTail` β `-ctkt` / `-ctvt` β **spilled-tail** types
|
| 36 |
+
(#622 P2 mixed-KV); unset = same as window.
|
| 37 |
+
- `kvAutoTier` β env **`OPENCOTI_KV_AUTO_TIER=1`** β boot policy picks the
|
| 38 |
+
least-compressing scalar pair that stays fully resident (#621). Explicit
|
| 39 |
+
`-ctv` disables auto entirely; explicit `-ctk` holds K, walks V only. Dense
|
| 40 |
+
full-attention models only; iSWA keeps f16.
|
| 41 |
+
- env **`OPENCOTI_KV_AUTO_TIER_TAIL=1`** β when auto-tier still spills, also
|
| 42 |
+
auto-pick a q4_0 tail (separate opt-in).
|
| 43 |
+
|
| 44 |
+
### SharedKVPool (M6-S1) β per-request only, see Β§3. No boot flag.
|
| 45 |
+
|
| 46 |
+
### Rolling-KV residency / spill (M7, #582)
|
| 47 |
+
- `vramTargetMiB` β `--vram-target` β VRAM budget cap (MiB); 0 = free-VRAM auto.
|
| 48 |
+
- `kvResidencyMode` β `--kv-residency-mode` β residency tactic knob (0 = auto).
|
| 49 |
+
- `headinferGpuHeadsFrac` β `--headinfer-gpu-heads-frac` (default `auto`).
|
| 50 |
+
|
| 51 |
+
### Session KV-reuse (M0) + host-state guard
|
| 52 |
+
- `nParallel` β `--parallel`, `slotPromptSimilarity` β `-sps`,
|
| 53 |
+
`cacheReuse` β `--cache-reuse`, `ctxSize` β `-c`.
|
| 54 |
+
- `cacheRamMiB` β `--cache-ram`, `ctxCheckpoints` β `--ctx-checkpoints`
|
| 55 |
+
(bounded host-RAM defaults, bug-1325).
|
| 56 |
+
- `--kv-unified` β required for assistant-MTP with `--parallel > 1` (bug-2110 guard).
|
| 57 |
+
|
| 58 |
+
### Retention eviction (M1)
|
| 59 |
+
- `restKvEviction` β `--rest-kv-eviction`, `restKvRecent` β `--rest-kv-recent`,
|
| 60 |
+
`restKvLayer` β `--rest-kv-layer`. Boot-global (no per-request override).
|
| 61 |
+
|
| 62 |
+
### DCA β Dual Chunk Attention (0078)
|
| 63 |
+
- `dcaEnabled` β `--dca on|off`, `dcaChunkSize` β `--dca-chunk-size`,
|
| 64 |
+
`dcaYarnFactor` β `--dca-yarn-factor`.
|
| 65 |
+
|
| 66 |
+
### Spec-decode / MTP (M6-S4, bug-858)
|
| 67 |
+
- `mtpHead` β `--mtp-head` (alias of `-md`; gemma4-assistant GGUF),
|
| 68 |
+
`specType` β `--spec-type` (`draft-assistant` | `draft-mtp` | β¦),
|
| 69 |
+
`draftNGpuLayers` β `-ngld`, `--spec-draft-n-max` / `--spec-draft-n-min`,
|
| 70 |
+
`--draft-block-size`, draft KV types `-ctkd` / `-ctvd`.
|
| 71 |
+
|
| 72 |
+
### Sparse attention (#551) *(extraArgs only)*
|
| 73 |
+
- `--sparse-attn`, `--sparse-attn-block-size/-topk/-recent/-sink/-refresh/-mode`.
|
| 74 |
+
|
| 75 |
+
### RYS layer duplication (#656)
|
| 76 |
+
- `repeatLayers` β `--repeat-layers`; probe: `--rys-probe*` *(extraArgs only)*.
|
| 77 |
+
|
| 78 |
+
### Lazy slot context (F4 M3) *(server: extraArgs only; typed on embedder)*
|
| 79 |
+
- `--slot-initial-ctx`, `--slot-shrink-idle-ms`.
|
| 80 |
+
|
| 81 |
+
---
|
| 82 |
+
|
| 83 |
+
## 3. Per-request control (JSON body fields)
|
| 84 |
+
|
| 85 |
+
Parsed in `tools/server/server-task.cpp` (~line 275):
|
| 86 |
+
|
| 87 |
+
| Field | Default | Feature | Effect |
|
| 88 |
+
|---|---|---|---|
|
| 89 |
+
| `session_id` | `""` | M0 session KV-reuse | Slot affinity: the same session routes back to the slot that holds its KV (prevents cross-session eviction at `--parallel > 1`). opencode gets this injected automatically by `withKvReuseFetch` (which also forces `cache_prompt: true`). |
|
| 90 |
+
| `shared_pool_slot` | `-1` | SharedKVPool (M6-S1) | Bit-share this request's prefix KV with pool slot N (read-only). |
|
| 91 |
+
| `shared_prefix_n_tokens` | `0` | SharedKVPool | Length of the shared prefix in tokens. |
|
| 92 |
+
|
| 93 |
+
Everything else (tier, residency, DCA, retention) is **boot-global by design** β
|
| 94 |
+
changing it requires a relaunch.
|
| 95 |
+
|
| 96 |
+
---
|
| 97 |
+
|
| 98 |
+
## 4. Runtime introspection
|
| 99 |
+
|
| 100 |
+
### 4.1 `GET /props` β `"opencoti"` object (#676)
|
| 101 |
+
|
| 102 |
+
Boot-state echo, computed once at model load (sleep-safe β never touches the
|
| 103 |
+
live context at request time). Shape:
|
| 104 |
+
|
| 105 |
+
```jsonc
|
| 106 |
+
"opencoti": {
|
| 107 |
+
"kv": {
|
| 108 |
+
"cache_type_k": "q8_0", // configured (-ctk)
|
| 109 |
+
"cache_type_v": "q4_0", // configured (-ctv)
|
| 110 |
+
"cache_type_k_tail": null, // configured (-ctkt); null = same as window
|
| 111 |
+
"cache_type_v_tail": null,
|
| 112 |
+
"auto_tier": false, // OPENCOTI_KV_AUTO_TIER set
|
| 113 |
+
"auto_tier_tail": false, // OPENCOTI_KV_AUTO_TIER_TAIL set
|
| 114 |
+
"effective": { // READ BACK from the live KV cache β
|
| 115 |
+
"type_k": "q8_0", // reflects auto-tier + residency sizing,
|
| 116 |
+
"type_v": "q4_0", // not just what was asked for
|
| 117 |
+
"type_k_tail": "q8_0", // == window type when no distinct tail
|
| 118 |
+
"type_v_tail": "q4_0",
|
| 119 |
+
"n_cells": 524288, // allocated KV cells per stream
|
| 120 |
+
"n_cells_resident": 524288, // device-resident window (min over spilling layers)
|
| 121 |
+
"n_layers_spilling": 0, // layers with an engaged rolling-KV window
|
| 122 |
+
"fully_resident": true,
|
| 123 |
+
"is_iswa": true // reported from the iSWA base (full-attn) cache
|
| 124 |
+
}
|
| 125 |
+
},
|
| 126 |
+
"residency": { "kv_residency_mode": 0, "vram_target_mib": 0, "headinfer_gpu_heads_frac": 1.0 },
|
| 127 |
+
"dca": { "enabled": true, "chunk_size": 0, "yarn_factor": 1.0 },
|
| 128 |
+
"sparse_attn":{ "enabled": false, "block_size": 128, "topk": 0, "recent": 0, "sink": 1, "refresh": 8, "mode": 0 },
|
| 129 |
+
"speculative":{ "types": ["none", "draft-assistant"], // "none" is always slot 0 of the type list
|
| 130 |
+
"draft_model": "β¦assistant-Q8_0.gguf",
|
| 131 |
+
"n_max": 3, "n_min": 0, "draft_block_size": 0, "n_gpu_layers": 99,
|
| 132 |
+
"cache_type_k": "f16", "cache_type_v": "f16" },
|
| 133 |
+
"kv_reuse": { "n_parallel": 4, "kv_unified": true, // kv_unified auto-forced for assistant-MTP (bug-2110)
|
| 134 |
+
"cache_reuse": 0, "slot_prompt_similarity": 0.1,
|
| 135 |
+
"ctx_checkpoints": 32, "cache_ram_mib": 8192 },
|
| 136 |
+
"rest_kv": { "eviction": false, "recent": 256, "layer": -1 },
|
| 137 |
+
"repeat_layers": null
|
| 138 |
+
}
|
| 139 |
+
```
|
| 140 |
+
|
| 141 |
+
(Verified live 2026-07-15 against the v7-coder server on :8231 β values above are the
|
| 142 |
+
actual output for `-ctk q8_0 -ctv q4_0 -c 524288 --dca on --spec-type draft-assistant`.)
|
| 143 |
+
|
| 144 |
+
The `kv.effective` block is the **only authoritative source for the auto-tier
|
| 145 |
+
decision** β the decision happens inside the KV-cache constructor and is read
|
| 146 |
+
back through the new core API (Β§5). `configured != effective` is expected
|
| 147 |
+
whenever auto-tier engaged. Drafter note: on dual-ctx MTP the target context is
|
| 148 |
+
reported; the drafter's own KV layer is f16 by design.
|
| 149 |
+
|
| 150 |
+
### 4.2 `GET /slots` β per-slot `"opencoti"` object (#677)
|
| 151 |
+
|
| 152 |
+
Live state per slot (requires `--slots`, i.e. `params.endpoint_slots`):
|
| 153 |
+
|
| 154 |
+
```jsonc
|
| 155 |
+
{
|
| 156 |
+
"id": 3, "n_ctx": 131072, "speculative": true, "is_processing": false,
|
| 157 |
+
"opencoti": {
|
| 158 |
+
"draft_n_total": 114, // LIFETIME draft tokens generated (this slot) β
|
| 159 |
+
"draft_n_accepted": 62, // survives task end; resets only on server restart
|
| 160 |
+
"draft_acceptance": 0.544, // lifetime ratio; null before any draft ran
|
| 161 |
+
"session_id": "ses_09f4β¦", // current/last task's session binding (M0)
|
| 162 |
+
"shared_pool_slot": -1, // current/last task's SharedKVPool binding
|
| 163 |
+
"shared_prefix_n_tokens": 0
|
| 164 |
+
}
|
| 165 |
+
}
|
| 166 |
+
```
|
| 167 |
+
|
| 168 |
+
The draft counters are **lifetime accumulators** (deliberately not cleared by the
|
| 169 |
+
slot's per-task reset β the per-task counters feed `timings.draft_n` instead). To
|
| 170 |
+
watch a single long turn, poll twice and diff. The session/pool fields appear only
|
| 171 |
+
once the slot has served at least one task (they come from the current-or-last
|
| 172 |
+
task's params); a freshly booted idle slot shows just the three draft fields.
|
| 173 |
+
|
| 174 |
+
Practical use: **draft_acceptance β³ 0.95 sustained at turn end is the
|
| 175 |
+
rumination/loop tell** (healthy agentic decode sits ~0.4β0.9); this is now
|
| 176 |
+
pollable instead of log-scraping.
|
| 177 |
+
|
| 178 |
+
### 4.3 Per-completion `timings` (pre-existing, unchanged)
|
| 179 |
+
|
| 180 |
+
When timings are requested, each response carries: `cache_n` (KV prefix-reuse
|
| 181 |
+
hits β the M0/PolyKV effect), and `draft_n` / `draft_n_accepted` for that
|
| 182 |
+
response's spec rounds.
|
| 183 |
+
|
| 184 |
+
### 4.4 Still log-only (known gaps)
|
| 185 |
+
|
| 186 |
+
PolyKV pool share/reject lines (`poly-kv-pool: shared β¦`), retention-eviction
|
| 187 |
+
discards, rolling-KV tactic selection detail, auto-tier WARN line. Engine
|
| 188 |
+
events do **not** feed opencoti-server `/v1/tier-events` (that store records
|
| 189 |
+
tier-*routing* only).
|
| 190 |
+
|
| 191 |
+
---
|
| 192 |
+
|
| 193 |
+
## 5. Core C API (for embedders / tools)
|
| 194 |
+
|
| 195 |
+
Added in `include/llama.h`:
|
| 196 |
+
|
| 197 |
+
```c
|
| 198 |
+
struct llama_opencoti_kv_info {
|
| 199 |
+
enum ggml_type type_k, type_v; // effective window types (post auto-tier)
|
| 200 |
+
enum ggml_type type_k_tail, type_v_tail; // spilled-tail types (== window when none)
|
| 201 |
+
uint32_t n_cells; // allocated KV cells per stream
|
| 202 |
+
uint32_t n_cells_resident; // min resident window across spilling layers
|
| 203 |
+
uint32_t n_layers_spilling; // 0 = fully resident
|
| 204 |
+
bool is_iswa; // reported from iSWA base cache
|
| 205 |
+
};
|
| 206 |
+
LLAMA_API bool llama_memory_opencoti_kv_info(llama_memory_t mem,
|
| 207 |
+
struct llama_opencoti_kv_info * info);
|
| 208 |
+
```
|
| 209 |
+
|
| 210 |
+
Dispatch: virtual on `llama_memory_i` (default false β pure-recurrent memories
|
| 211 |
+
report nothing); `llama_kv_cache` reads its live tensors; `llama_kv_cache_iswa`
|
| 212 |
+
forwards to its non-SWA base; `llama_memory_hybrid` forwards to its attention
|
| 213 |
+
child. Boot-static β safe to read once after context creation and cache (the
|
| 214 |
+
server does exactly that in `get_meta()`).
|
| 215 |
+
|
| 216 |
+
Touched files: `include/llama.h`, `src/llama-memory.h`,
|
| 217 |
+
`src/llama-kv-cache.{h,cpp}`, `src/llama-kv-cache-iswa.{h,cpp}`,
|
| 218 |
+
`src/llama-memory-hybrid.{h,cpp}`, `src/llama-context.cpp`,
|
| 219 |
+
`tools/server/server-context.{h,cpp}`, plus `llamafile/BUILD.mk` (bug-2186: the
|
| 220 |
+
llamafile-mode `server.cpp.o` rule had no header deps β a stale object linked an
|
| 221 |
+
old inline `update_meta` with a mismatched `server_context_meta` layout, silently
|
| 222 |
+
yielding `"opencoti": null`; the rule now depends on `tools/server/*.h`). All
|
| 223 |
+
additive; off-path behavior byte-identical (pure read-side additions to two JSON
|
| 224 |
+
endpoints).
|
| 225 |
+
|
| 226 |
+
## 6. Quick recipes
|
| 227 |
+
|
| 228 |
+
```sh
|
| 229 |
+
# What is this server actually running?
|
| 230 |
+
curl -s :8231/props | jq .opencoti
|
| 231 |
+
|
| 232 |
+
# Did auto-tier change my KV types?
|
| 233 |
+
curl -s :8231/props | jq '.opencoti.kv | {configured: {k:.cache_type_k, v:.cache_type_v}, effective}'
|
| 234 |
+
|
| 235 |
+
# Is anything spilling?
|
| 236 |
+
curl -s :8231/props | jq .opencoti.kv.effective.fully_resident
|
| 237 |
+
|
| 238 |
+
# Live loop-tell: per-slot draft acceptance
|
| 239 |
+
curl -s :8231/slots | jq '.[] | {id, processing:.is_processing, acc:.opencoti.draft_acceptance}'
|
| 240 |
+
```
|
| 241 |
+
|
| 242 |
+
Related docs: [advanced_kv.md](advanced_kv.md) (M0-M3), [poly_kv.md](poly_kv.md)
|
| 243 |
+
(M6/SharedKVPool), [rolling_kv.md](rolling_kv.md) (M7 residency),
|
| 244 |
+
[gemma4_dca.md](gemma4_dca.md), [mtp.md](../evaluations/mtp.md),
|
| 245 |
+
[tstar_auto_spill.md](tstar_auto_spill.md) (auto-tier/auto-spill policy).
|
docs/features/llamafile_build.md
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# F4 M6 β Production llamafile build pipeline
|
| 2 |
+
|
| 3 |
+
Owns: producing the `.llamafile` artifacts the embedder package
|
| 4 |
+
consumes at runtime, from source under `vendors/sources/llamafile/`
|
| 5 |
+
plus the opencoti patch series at `vendors/patches/llamafile/`.
|
| 6 |
+
|
| 7 |
+
The build orchestrator is
|
| 8 |
+
`packages/opencoti-llamafile/script/build-pipeline.ts`. Helpers live
|
| 9 |
+
at `packages/opencoti-llamafile/src/build-pipeline.ts`.
|
| 10 |
+
|
| 11 |
+
## Why this exists
|
| 12 |
+
|
| 13 |
+
Through F4 M0-M4 the embedder ran a prebuilt
|
| 14 |
+
`mann1x/claude-hooks v1.4.0 qwen3-embedding-0.6b-16k.llamafile`
|
| 15 |
+
asset pinned at `vendors/pin/embedder.txt`. That binary embeds
|
| 16 |
+
`ggml-cuda.so` and a Qwen3 GGUF, and predates every opencoti
|
| 17 |
+
patch (lazy slot context, shrink-on-idle, et al.). For F4
|
| 18 |
+
production we need:
|
| 19 |
+
|
| 20 |
+
- **All backend DSOs built from source** (CUDA / ROCm / Vulkan),
|
| 21 |
+
not a runtime side-load nobody controls.
|
| 22 |
+
- **The opencoti patch series actually compiled in** β the prebuilt
|
| 23 |
+
rejects `--slot-initial-ctx` and `--slot-shrink-idle-ms` because
|
| 24 |
+
those flags don't exist in upstream 0.10.1.
|
| 25 |
+
- **No cross-repo dependency** on `mann1x/claude-hooks` for an
|
| 26 |
+
opencoti artifact.
|
| 27 |
+
|
| 28 |
+
The result is one or two artifacts under `dist/llamafile/`:
|
| 29 |
+
|
| 30 |
+
- `opencoti-llamafile-<ver>-<arch>.llamafile` β thin binary +
|
| 31 |
+
every staged DSO. The "GPU runtime", no model bundled.
|
| 32 |
+
- `opencoti-embedder-<ver>-<arch>.llamafile` β the above + an
|
| 33 |
+
embedded GGUF + `.args`. The embedder package's launcher target.
|
| 34 |
+
|
| 35 |
+
Each artifact has a sibling `MANIFEST.json` recording SHA256 of
|
| 36 |
+
the artifact, the embedded patches, and per-DSO digests.
|
| 37 |
+
|
| 38 |
+
## Toolchains per backend
|
| 39 |
+
|
| 40 |
+
| Backend | Build script | Requirement | Probe env | Default path |
|
| 41 |
+
|---------|-------------------------------------------------|--------------------------|------------------------------|------------------------|
|
| 42 |
+
| CUDA | `vendors/sources/llamafile/llamafile/cuda.sh` | NVIDIA CUDA 12.x or 13.x | `OPENCOTI_CUDA_PATH` | `/usr/local/cuda-12.6` |
|
| 43 |
+
| ROCm | `vendors/sources/llamafile/llamafile/rocm.sh` | AMD ROCm 5+ (hipcc) | `OPENCOTI_ROCM_PATH` | `/opt/rocm` |
|
| 44 |
+
| Vulkan | `vendors/sources/llamafile/llamafile/vulkan.sh` | Vulkan SDK 1.3+ (glslc) | `OPENCOTI_VULKAN_SDK` | system `glslc` on PATH |
|
| 45 |
+
|
| 46 |
+
Each backend probe runs read-only β missing toolchain produces a
|
| 47 |
+
WARN and the backend is skipped, not a hard build failure. Only
|
| 48 |
+
the `package` subcommand hard-fails if zero backends produced a
|
| 49 |
+
DSO (and `--cpu-only` wasn't passed explicitly).
|
| 50 |
+
|
| 51 |
+
### CUDA version per GPU generation
|
| 52 |
+
|
| 53 |
+
Mozilla-Ocho's `cuda.sh` enables different `-gencode` arch lists
|
| 54 |
+
depending on the detected CUDA toolkit version:
|
| 55 |
+
|
| 56 |
+
| Target GPU | Compute cap | Minimum CUDA | Notes |
|
| 57 |
+
|------------------------------------|---------------|--------------|--------------------------------------------------------------------------|
|
| 58 |
+
| Turing (RTX 20xx, T4) | sm_75 | 12.x | Baseline arch β included in every build |
|
| 59 |
+
| Ampere (A100, RTX 30xx) | sm_80 / sm_86 | 12.x | Baseline arch |
|
| 60 |
+
| Ada Lovelace (RTX 40xx) | sm_89 | 12.x | Baseline arch |
|
| 61 |
+
| Hopper (H100) | sm_90 | 12.x | Baseline arch |
|
| 62 |
+
| **Blackwell consumer (RTX 50xx)** | **sm_120** | **13.x** | Requires CUDA 13.x branch β adds `sm_120f` + `--compress-mode=size` (4x DSO size reduction) |
|
| 63 |
+
| Blackwell Jetson Thor (aarch64) | sm_110 | 13.x | aarch64 only |
|
| 64 |
+
| Blackwell DGX Spark GB10 (aarch64) | sm_121 | 13.x | aarch64 only |
|
| 65 |
+
|
| 66 |
+
**For RTX 50xx targets, set `OPENCOTI_CUDA_PATH=/usr/local/cuda-13.x`
|
| 67 |
+
when building** β the 12.x branch will emit a DSO with no sm_120
|
| 68 |
+
kernels and the driver cannot JIT-fallback (the `code=sm_X` form
|
| 69 |
+
embeds SASS only, not PTX). Symptom on RTX 50xx with a 12.x-built
|
| 70 |
+
DSO: `ggml-cuda.cu:97: CUDA error` during first kernel launch.
|
| 71 |
+
|
| 72 |
+
## End-to-end pipeline
|
| 73 |
+
|
| 74 |
+
```bash
|
| 75 |
+
# 1. Reset vendored sources, init nested submodules, apply
|
| 76 |
+
# Mozilla-Ocho's own patches, then apply opencoti's series.
|
| 77 |
+
bun run build:llamafile:apply
|
| 78 |
+
|
| 79 |
+
# 2. Build the thin patched llamafile binary (no DSOs embedded yet).
|
| 80 |
+
bun run build:llamafile # = apply + make
|
| 81 |
+
# Output: vendors/sources/llamafile/o/llamafile/llamafile
|
| 82 |
+
|
| 83 |
+
# 3. Build every available backend DSO. Soft-fails per backend on
|
| 84 |
+
# missing toolchain; hard-fails only if zero built.
|
| 85 |
+
bun run build:llamafile:all-backends
|
| 86 |
+
# Output (per backend that built):
|
| 87 |
+
# vendors/dist/llamafile/<backend>/<ver>/ggml-<backend>.so
|
| 88 |
+
# ~/.llamafile/v/<ver>/ggml-<backend>.so (runtime side-load mirror)
|
| 89 |
+
|
| 90 |
+
# 4. Assemble production artifacts.
|
| 91 |
+
bun run build:llamafile:package
|
| 92 |
+
# Always emits:
|
| 93 |
+
# dist/llamafile/opencoti-llamafile-<ver>-<tag>-<arch>.llamafile
|
| 94 |
+
# dist/llamafile/opencoti-llamafile-<ver>-<tag>-<arch>.llamafile.MANIFEST.json
|
| 95 |
+
|
| 96 |
+
# 5. Or, for the composite-embedder asset (binary + DSOs + Qwen3 GGUF + .args):
|
| 97 |
+
bun run build:llamafile:package -- --with-model ~/.opencoti/qwen3-embedding-0.6b.gguf
|
| 98 |
+
# Additionally emits:
|
| 99 |
+
# dist/llamafile/opencoti-embedder-<ver>-<arch>.llamafile
|
| 100 |
+
# dist/llamafile/opencoti-embedder-<ver>-<arch>.llamafile.MANIFEST.json
|
| 101 |
+
```
|
| 102 |
+
|
| 103 |
+
Iteration on a single backend uses its dedicated subcommand
|
| 104 |
+
(`build:llamafile:cuda`, `:rocm`, `:vulkan`). The `:make` subcommand
|
| 105 |
+
re-runs cosmocc-make alone β keeps in-tree edits in place while
|
| 106 |
+
you're polishing a patch you haven't yet captured under
|
| 107 |
+
`vendors/patches/llamafile/`.
|
| 108 |
+
|
| 109 |
+
## `package` subcommand details
|
| 110 |
+
|
| 111 |
+
```
|
| 112 |
+
build:llamafile:package [--with-model PATH] [--with-args PATH] [--arch ID] [--cpu-only]
|
| 113 |
+
```
|
| 114 |
+
|
| 115 |
+
- `--with-model PATH` β GGUF to embed. Filename is preserved; the
|
| 116 |
+
embedded `.args` points at `/zip/<basename>`.
|
| 117 |
+
- `--with-args PATH` β explicit `.args` to embed instead of the
|
| 118 |
+
generated one. The generated default matches `buildEmbedderArgs()`
|
| 119 |
+
in `packages/opencoti-embedder/src/manager.ts` (--server,
|
| 120 |
+
--embedding, --pooling last, --ctx-size 16384, --parallel 2,
|
| 121 |
+
--slot-initial-ctx 4096, --slot-shrink-idle-ms 30000).
|
| 122 |
+
- `--arch ID` β architecture string in the output filename.
|
| 123 |
+
Detected from `os.arch()` by default (`x86_64`/`aarch64`).
|
| 124 |
+
- `--cpu-only` β skip embedding any DSO. Produces a binary that
|
| 125 |
+
works on any host but always runs on CPU. Useful for CI smoke
|
| 126 |
+
on machines with no GPU.
|
| 127 |
+
|
| 128 |
+
Embedding uses the vendored zipalign at
|
| 129 |
+
`vendors/sources/llamafile/o/third_party/zipalign/zipalign`. For DSOs
|
| 130 |
+
the `-j0` flag strips directory prefixes so cosmo_dlopen resolves
|
| 131 |
+
the entry as the bare filename it expects.
|
| 132 |
+
|
| 133 |
+
## Embedder package: local-asset modes (dev escape hatch)
|
| 134 |
+
|
| 135 |
+
The embedder downloader at
|
| 136 |
+
`packages/opencoti-embedder/src/download.ts` understands two
|
| 137 |
+
local-asset modes added in M6 to avoid a GitHub Release round trip
|
| 138 |
+
during dev cycles:
|
| 139 |
+
|
| 140 |
+
### `file:` pin
|
| 141 |
+
|
| 142 |
+
Change the first field of `vendors/pin/embedder.txt` from
|
| 143 |
+
`<owner>/<repo>` to `file:<path>`. Path is anything `path.resolve()`
|
| 144 |
+
can interpret (absolute or relative to caller cwd). The SHA256 is
|
| 145 |
+
still verified.
|
| 146 |
+
|
| 147 |
+
```
|
| 148 |
+
file:./dist/llamafile/opencoti-embedder-0.10.1-x86_64.llamafile v0 opencoti-embedder.llamafile <sha256-from-manifest>
|
| 149 |
+
```
|
| 150 |
+
|
| 151 |
+
### `OPENCOTI_EMBEDDER_LOCAL` env
|
| 152 |
+
|
| 153 |
+
Bypasses the pin's path entirely while still verifying SHA. Set
|
| 154 |
+
`OPENCOTI_EMBEDDER_LOCAL=$(pwd)/dist/llamafile/opencoti-embedder-β¦llamafile`
|
| 155 |
+
and run the embedder smoke as usual β the downloader copies bytes
|
| 156 |
+
from disk instead of fetching from GitHub.
|
| 157 |
+
|
| 158 |
+
Both modes still write to `destDir/<asset>` so the rest of the
|
| 159 |
+
launcher pipeline works unchanged.
|
| 160 |
+
|
| 161 |
+
## Verifying a built / deployed binary carries the full patch chain
|
| 162 |
+
|
| 163 |
+
A dev `o/` build (`:make` + `:cuda`) emits **no** `MANIFEST.json` β only
|
| 164 |
+
`package`/`release-cut` do. So when you have a binary of uncertain vintage
|
| 165 |
+
(an `o/` artifact after a source reset, or a copy deployed to another host)
|
| 166 |
+
and need to answer *"is this the full all-patches build, or vanilla/partial?"*
|
| 167 |
+
**without** a 45β86 min rebuild, verify by symbols + arg names. The vendored
|
| 168 |
+
`llama.cpp` tree is normally **clean** at rest (patches apply at build time and
|
| 169 |
+
the tree is reset for upstream-syncability), so a clean tree + a feature-rich
|
| 170 |
+
binary is the *expected* steady state β never infer "missing patches" from a
|
| 171 |
+
clean source tree; inspect the artifact instead.
|
| 172 |
+
|
| 173 |
+
**CUDA DSO** (`~/.llamafile/v/<ver>/ggml-cuda.so` and
|
| 174 |
+
`vendors/dist/llamafile/cuda/<ver>/ggml-cuda.so` β must be byte-identical, G4):
|
| 175 |
+
|
| 176 |
+
```sh
|
| 177 |
+
nm -D --defined-only ~/.llamafile/v/<ver>/ggml-cuda.so | c++filt | \
|
| 178 |
+
grep -ciE 'dca_fused_case|ggml_flash_attn_ext_dca_fused|ggml_cuda_flash_attn_ext_lse' # DCA (0078+0081)
|
| 179 |
+
# expect: DCA family ~150 syms incl ~144 dca_fused_case<β¦> (the 0082/#531 shard),
|
| 180 |
+
# turbo ~24 (ggml_turbo_wht, turbo_cpu_fwht_inverse, ggml_cpy_turbo_f16_cuda),
|
| 181 |
+
# tcq ~12, q6_0 present. Zero DCA/turbo syms β vanilla or pre-0078 DSO.
|
| 182 |
+
```
|
| 183 |
+
|
| 184 |
+
**Host binary** (cosmo APE β `nm` won't read it; use `strings`):
|
| 185 |
+
|
| 186 |
+
```sh
|
| 187 |
+
strings -n4 vendors/sources/llamafile/o/llamafile/llamafile | \
|
| 188 |
+
grep -E '^(q6_0|turbo[2348]|turbo[23]_tcq|dca-chunk-size|QK_TURBO3_TCQ)$'
|
| 189 |
+
# all present β arg.cpp kv_cache_types + DCA flag patches (0073/0078/0081) are in.
|
| 190 |
+
```
|
| 191 |
+
|
| 192 |
+
**Host vs DSO mtime split is benign.** `:make` (host) and `:cuda` (DSO) build
|
| 193 |
+
independently; when the most recent patches are CUDA/build-only (e.g. 0082
|
| 194 |
+
build-parallelism, 0083 tcq-warp, 0084 cuda-graphs), only the DSO is rebuilt, so
|
| 195 |
+
the host binary legitimately predates the DSO. Verify by *symbols*, not mtime.
|
| 196 |
+
To make a build self-describing, run `package` (emits the sibling
|
| 197 |
+
`MANIFEST.json` with per-artifact SHA256); the in-tree `o/` build has none.
|
| 198 |
+
|
| 199 |
+
## What's NOT in M6
|
| 200 |
+
|
| 201 |
+
These are tracked separately and intentionally out of M6 scope:
|
| 202 |
+
|
| 203 |
+
- **GitHub Releases publish pipeline.** The artifacts produced by
|
| 204 |
+
`package` are local; uploading them as a `mann1x/opencoti`
|
| 205 |
+
release asset (and flipping the pin away from claude-hooks) is
|
| 206 |
+
the M7 ticket.
|
| 207 |
+
- **macOS / Metal backend.** `metal.c` is macOS-only; the M6
|
| 208 |
+
pipeline is Linux-first. Adding macOS needs a Darwin CI runner.
|
| 209 |
+
- **Cross-arch builds.** cosmocc APE is dual-arch by design, but
|
| 210 |
+
ggml-*.so are arch-specific. The `<arch>` token in the output
|
| 211 |
+
filename is anticipatory; M6 ships x86_64 only.
|
| 212 |
+
- **`build:llamafile:cuda --cublas`.** Mozilla-Ocho's cuda.sh
|
| 213 |
+
optionally links against vendor cuBLAS instead of TinyBLAS. F4
|
| 214 |
+
defaults to TinyBLAS (works on every NVIDIA GPU without runtime
|
| 215 |
+
CUDA install); cuBLAS is a follow-up if profile-guided choices
|
| 216 |
+
ever need it.
|
| 217 |
+
|
| 218 |
+
## Verification checklist
|
| 219 |
+
|
| 220 |
+
After a successful `package` run, the manifest tells you exactly
|
| 221 |
+
what was embedded. To cross-check from the shell:
|
| 222 |
+
|
| 223 |
+
```bash
|
| 224 |
+
# Confirm DSOs are inside the bare artifact.
|
| 225 |
+
unzip -l dist/llamafile/opencoti-llamafile-0.10.1-x86_64.llamafile | grep "ggml-"
|
| 226 |
+
|
| 227 |
+
# Confirm the manifest matches the artifact's actual SHA.
|
| 228 |
+
sha256sum dist/llamafile/opencoti-llamafile-0.10.1-x86_64.llamafile
|
| 229 |
+
jq -r .sha256 dist/llamafile/opencoti-llamafile-0.10.1-x86_64.llamafile.MANIFEST.json
|
| 230 |
+
```
|
| 231 |
+
|
| 232 |
+
The embedder smoke at
|
| 233 |
+
`packages/opencoti-embedder/script/smoke.ts` runs the full launcher
|
| 234 |
+
against whatever asset the downloader resolves. Set
|
| 235 |
+
`OPENCOTI_EMBEDDER_LOCAL` to point at a freshly-built composite to
|
| 236 |
+
exercise the new patches without a release upload.
|
| 237 |
+
|
| 238 |
+
## Incremental rebuilds, header dependencies, and ccache
|
| 239 |
+
|
| 240 |
+
Iterating the vendored patch series (F4/F5) means editing C++ inside
|
| 241 |
+
`vendors/sources/llamafile` and rebuilding with
|
| 242 |
+
`bun run build:llamafile:make` (the incremental path β it does **not**
|
| 243 |
+
reset the tree, so in-flight edits survive). Two build-infra facts make
|
| 244 |
+
that loop work; both bit hard before being fixed (see buglog `bug-170`).
|
| 245 |
+
|
| 246 |
+
### Header dependency tracking (`0007-build-header-deps.patch`)
|
| 247 |
+
|
| 248 |
+
The upstream build resolves header deps via cosmopolitan's `mkdeps`
|
| 249 |
+
(`build/deps.mk` β `o//depend`). `mkdeps` only follows cosmopolitan's
|
| 250 |
+
root-relative include style; llama.cpp sources use `#include "foo.h"`
|
| 251 |
+
resolved through `-iquote` dirs, which `mkdeps` cannot follow β so it
|
| 252 |
+
emitted **zero** dependency edges for `llama.cpp/src` headers. Editing a
|
| 253 |
+
header therefore never marked its dependent objects stale, and `make`
|
| 254 |
+
reported them "up to date" β the only way to push a header change through
|
| 255 |
+
was to delete objects by hand and rebuild from scratch.
|
| 256 |
+
|
| 257 |
+
Patch `0007` adds `-MMD -MF $@.d` to the `COMPILE.c`/`COMPILE.cc`
|
| 258 |
+
commands so cosmocc emits an accurate per-translation-unit `<obj>.o.d`
|
| 259 |
+
(project headers only), and `-include`s those files in `build/deps.mk`.
|
| 260 |
+
clang resolves the real `-iquote` includes, so the header set is exact. A
|
| 261 |
+
header edit now recompiles **exactly** its dependents (both fat-object
|
| 262 |
+
arches, via the existing rule) and nothing else.
|
| 263 |
+
|
| 264 |
+
The explicit `-MF $@.d` is **load-bearing, not cosmetic** β and it's
|
| 265 |
+
what made ccache start working at all (see below). cosmocc defaults the
|
| 266 |
+
dep file to `<obj>.d` by *appending* (`foo.cpp.o` β `foo.cpp.o.d`), but
|
| 267 |
+
ccache derives the expected name from `-o foo.cpp.o` by *stripping* `.o`
|
| 268 |
+
(β `foo.cpp.d`). With bare `-MMD` the two disagree: ccache `stat`s a file
|
| 269 |
+
cosmocc never wrote, logs *"failed to stat β¦foo.cpp.d"*, and aborts every
|
| 270 |
+
compile with *"ccache internal error"* β **nothing is ever cached** (the
|
| 271 |
+
cache sat at 0 files). Pinning `-MF` to cosmocc's actual name makes them
|
| 272 |
+
agree, and ccache caches/restores normally.
|
| 273 |
+
|
| 274 |
+
> The **first** build after this patch lands is a one-time full compile
|
| 275 |
+
> that populates the `.o.d` set. Every build after that is correctly
|
| 276 |
+
> incremental. Verify with: rebuild a TU, `touch` a header it includes β
|
| 277 |
+
> only its dependents recompile; `touch` a header it does *not* include β
|
| 278 |
+
> nothing recompiles.
|
| 279 |
+
|
| 280 |
+
### ccache + cosmocc fat objects
|
| 281 |
+
|
| 282 |
+
ccache (enabled by default; cache at `.opencoti/ccache/`, disable with
|
| 283 |
+
`OPENCOTI_NO_CCACHE=1`) wraps cosmocc's **fat** driver: one
|
| 284 |
+
`cosmoc++ -c -o foo.o` writes **both** `foo.o` (x86-64) and
|
| 285 |
+
`.aarch64/foo.o`, but ccache only knows the x86 `-o` output. So a cache
|
| 286 |
+
**hit restores the x86 object only β never its `.aarch64` twin**. A hit
|
| 287 |
+
is therefore safe only when the twin already exists on disk.
|
| 288 |
+
|
| 289 |
+
Consequences, and how they're handled:
|
| 290 |
+
|
| 291 |
+
- **Never delete a single-arch object** to force a rebuild. If you must
|
| 292 |
+
force one, `touch` its source (or delete *both* `foo.o` **and**
|
| 293 |
+
`.aarch64/foo.o`). Deleting only the x86 object and rebuilding makes
|
| 294 |
+
ccache restore it while leaving no aarch64 twin β the APE link fails
|
| 295 |
+
with *"concomitant .aarch64 file missing"*. With header tracking fixed,
|
| 296 |
+
manual deletion is no longer needed at all.
|
| 297 |
+
- **The full `build` resets the tree** (`git clean -fdx` wipes `o/`). A
|
| 298 |
+
warm cache restoring x86-only objects into an empty tree would orphan
|
| 299 |
+
every aarch64 twin. So `runBuild` sets `CCACHE_RECACHE=1`: cosmocc
|
| 300 |
+
re-runs and writes both arches while still refreshing the cache for the
|
| 301 |
+
next incremental `:make`. The incremental path runs against a populated
|
| 302 |
+
`o/`, where hits are safe, so it uses the cache normally.
|
| 303 |
+
|
| 304 |
+
## Troubleshooting β DSO load & symbol resolution
|
| 305 |
+
|
| 306 |
+
These two cost ~1h during M6-S2 (turbo3 KV). Both masquerade as logic
|
| 307 |
+
bugs. **Do the diagnostic-first steps before touching any source.**
|
| 308 |
+
|
| 309 |
+
### 1. A CUDA change has "no effect" β confirm which `.so` is actually loaded
|
| 310 |
+
|
| 311 |
+
The dev binary probes for `ggml-cuda.so` at **two** paths and loads the
|
| 312 |
+
first that resolves:
|
| 313 |
+
|
| 314 |
+
1. `~/.llamafile/v/<version>/ggml-cuda.so` β the cache. `build:llamafile:cuda`
|
| 315 |
+
mirrors the freshly-built DSO **here only**.
|
| 316 |
+
2. `$HOME/ggml-cuda.so` β `cuda.sh`'s default `OUTPUT=${HOME}/ggml-cuda.so`.
|
| 317 |
+
|
| 318 |
+
A stale `$HOME/ggml-cuda.so` (left from an old manual `cuda.sh` run)
|
| 319 |
+
silently shadows every rebuild that only updated the cache; the boot log
|
| 320 |
+
gives no hint. Before theorizing about source/object/enum, confirm the
|
| 321 |
+
loaded inode:
|
| 322 |
+
|
| 323 |
+
```sh
|
| 324 |
+
pid=$(pgrep -f 'llamafile.*--server' | head -1)
|
| 325 |
+
grep ggml-cuda.so /proc/$pid/maps # which path is mapped
|
| 326 |
+
stat -c '%i %n' /root/ggml-cuda.so vendors/dist/llamafile/cuda/*/*/ggml-cuda.so
|
| 327 |
+
```
|
| 328 |
+
|
| 329 |
+
If the loaded inode isn't the file you just built, sync it (`cp` the fresh
|
| 330 |
+
DSO over `$HOME/ggml-cuda.so`, or `rm` it to force the cache path).
|
| 331 |
+
|
| 332 |
+
### 2. "no usable GPU found" with a healthy GPU == DSO `dlopen` failure
|
| 333 |
+
|
| 334 |
+
The dlopen error is swallowed at default verbosity. **Re-run with
|
| 335 |
+
`--verbose`** to get the real line, e.g.
|
| 336 |
+
`cuda: INFO: failed to load library .../ggml-cuda.so: undefined symbol: dequantize_row_turbo2_0`.
|
| 337 |
+
|
| 338 |
+
### Adding a new `ggml/src/*.c` core source β it must land in THREE places
|
| 339 |
+
|
| 340 |
+
The DSO is **self-contained**: `llamafile/build-functions.sh`
|
| 341 |
+
`ggml_core_sources` compiles `ggml.c` + `ggml-quants.c` into the `.so` as
|
| 342 |
+
`ggml-core-*.o`. `ggml.c`'s `type_traits` table holds **data-relocation
|
| 343 |
+
function pointers** to every type's CPU `to_float`/`from_float` impl β
|
| 344 |
+
resolved **eagerly at dlopen**, so a missing impl is an undefined-symbol
|
| 345 |
+
*load* failure even with `RTLD_LAZY`. A new core `.c` (e.g.
|
| 346 |
+
`ggml-turbo-quant.c`) must therefore be added to:
|
| 347 |
+
|
| 348 |
+
1. `llamafile/build-functions.sh` β `ggml_core_sources` (DSO self-compiles it).
|
| 349 |
+
2. `llama.cpp.patches/llamafile-files/BUILD.mk` β `GGML_SRCS_C` (host links it).
|
| 350 |
+
3. **Force the host relink** β `make` does *not* propagate a `GGML_SRCS_C`
|
| 351 |
+
addition into the final binary. `rm o/llamafile/llamafile o/llama.cpp/llama.cpp.a`,
|
| 352 |
+
rebuild, and verify the binary mtime advanced.
|
| 353 |
+
|
| 354 |
+
If a previously-working file goes missing from these lists, suspect a
|
| 355 |
+
`build:llamafile:reset` having wiped outer-vendor edits
|
| 356 |
+
([DESTRUCTIVE_OPS.md](../protocols/DESTRUCTIVE_OPS.md)).
|
| 357 |
+
|
| 358 |
+
## #613 β the packaged engine as a zero-dependency single-file product
|
| 359 |
+
|
| 360 |
+
Shipped 2026-07-05. The `package` subcommand's bare artifact is now the
|
| 361 |
+
PRIMARY deliverable of the llamafile lane: a self-contained APE that runs the
|
| 362 |
+
full opencoti inference engine (llama-server OpenAI-compatible HTTP API + CLI)
|
| 363 |
+
with **zero external dependencies** β no opencoti framework, no side files.
|
| 364 |
+
|
| 365 |
+
### Versioning
|
| 366 |
+
|
| 367 |
+
`opencoti-<llamafileVersion>-<opencotiTag>`, e.g. `opencoti-0.10.3-c2`. The
|
| 368 |
+
tag lives in the committed `vendors/llamafile/OPENCOTI_TAG` (comment-tolerant,
|
| 369 |
+
same format as the pin file; bump on every packaged cut whose binary or DSO
|
| 370 |
+
bytes change). C-series lineage: `c1` = the un-packaged split deploy on bs2
|
| 371 |
+
(thin binary + external side-loaded DSO at `/srv/ml/opencoti-c1/`), `c2` =
|
| 372 |
+
first packaged single-file cut. The MANIFEST.json records `versionString`,
|
| 373 |
+
`opencotiTag`, `gitCommit`, the full patch list, and the sha256 of the
|
| 374 |
+
artifact AND of every embedded DSO β the binaryβDSO pairing is self-verifying
|
| 375 |
+
(motivated by bug-937: a deleted external DSO silently fell back to CPU).
|
| 376 |
+
|
| 377 |
+
### Zero-dependency guarantee (what's compiled in)
|
| 378 |
+
|
| 379 |
+
- **DSO**: zipalign-embedded; on first boot the APE extracts it to
|
| 380 |
+
`~/.llamafile/v/<ver>/` and side-loads from there. A fresh `$HOME` is
|
| 381 |
+
sufficient β proven by the c2 smoke on bs2 (scratch `HOME`, extracted DSO
|
| 382 |
+
sha == manifest sha `eec7436fβ¦`, Blackwell GPU decode at 272.8 tps, dual-ctx
|
| 383 |
+
MTP engaged at accept 0.728).
|
| 384 |
+
- **TCQ codebooks**: decode AND encode use compiled-in `__constant__`
|
| 385 |
+
codebooks in the DSO (`fattn-tcq.cuh` / `turbo-tcq-cuda.cuh`). The
|
| 386 |
+
`TURBO_TCQ_CB` env is an opt-in override for codebook experiments only; if
|
| 387 |
+
set but unreadable it warns on stderr and keeps the compiled-in default on
|
| 388 |
+
BOTH encode and decode (consistent, no KV split). Exotic `turbo*_tcq` KV
|
| 389 |
+
tiers therefore need no side file. Same story for `TURBO_INNERQ_*` β all
|
| 390 |
+
opt-in calibration hooks, inert unless set.
|
| 391 |
+
- **All opencoti features** are flag/env-activated on the standalone binary
|
| 392 |
+
(`--spec-type`, `-ctk/-ctv` turbo tiers, `--dca-*`, `--vram-target`,
|
| 393 |
+
`--kv-residency-mode`, β¦); nothing requires the TS/Bun or Go framework.
|
| 394 |
+
|
| 395 |
+
### Launch caveat (cross-platform)
|
| 396 |
+
|
| 397 |
+
The APE is cosmocc cross-platform (Linux/Mac/Windows) but GPU support is
|
| 398 |
+
CUDA-only today (Metal is a known gap; `runRocm`/`runVulkan` exist in the
|
| 399 |
+
pipeline but aren't part of the c2 cut). On Linux without binfmt_misc,
|
| 400 |
+
`exec` of the APE fails ENOEXEC (bug-156) β launch as `sh ./opencoti-llamafile-β¦.llamafile`.
|
| 401 |
+
Mac/Windows have not been smoke-tested for c2 (Linux-only exercised so far).
|
| 402 |
+
|
| 403 |
+
### Deploys
|
| 404 |
+
|
| 405 |
+
The c2 artifact is deployed next to the split c1 binaries at
|
| 406 |
+
`bs2:/srv/ml/opencoti-c1/opencoti-llamafile-0.10.3-c2-x86_64.llamafile`.
|
| 407 |
+
Eval scripts still drive the split `llamafile.dualctx` + external DSO (fine
|
| 408 |
+
for dev inner loop β a raw source edit only needs make, not packaging); the
|
| 409 |
+
packaged artifact is the distribution/embedding vehicle. For packaged
|
| 410 |
+
deploys the bug-224 DSO-cache-clear ritual is unnecessary: each cut extracts
|
| 411 |
+
its own DSO keyed by version dir, and clearing `~/.llamafile/v` is harmless.
|
| 412 |
+
|
| 413 |
+
### Secondary native-ELF lane
|
| 414 |
+
|
| 415 |
+
A plain-gcc/CMake build of the vendored `llama.cpp` tree (standard rpath
|
| 416 |
+
`libggml-cuda.so`, clean gdb/nsys) for Linux/container embedders. Only one
|
| 417 |
+
patch in the chain is cosmo-specific (`0071-state-io-cosmocc`), and the tree
|
| 418 |
+
carries upstream CMakeLists (models/*.cpp is globbed), so the source is
|
| 419 |
+
~toolchain-agnostic. Note the CMake build uses cuBLAS where the cosmocc DSO
|
| 420 |
+
uses TinyBLAS β a different perf profile (bug-2103/2104 class), so gate perf
|
| 421 |
+
claims per-toolchain. **SHIPPED 2026-07-05 as patch `0097-native-elf-cmake`**
|
| 422 |
+
(#619): five CMakeLists surgical hooks (opencoti TUs into the upstream source
|
| 423 |
+
lists + `-Dmain=llama_server` for the overlay server.cpp) plus a
|
| 424 |
+
CMake-lane-only stub TU (`ggml-cpu/opencoti-elf-stubs.cpp`) for the
|
| 425 |
+
cosmocc-bound `llamafile/sgemm.cpp` CPU-fastpath dispatchers β return-false =
|
| 426 |
+
fall back to upstream generic paths (Mozilla's own `fa_helpers_unsupported`
|
| 427 |
+
contract), so the ELF build loses only CPU fastpaths, never correctness.
|
| 428 |
+
|
| 429 |
+
Build (bs2-proven):
|
| 430 |
+
|
| 431 |
+
```sh
|
| 432 |
+
cmake -S llama.cpp -B build-elf -DGGML_CUDA=ON -DGGML_CUDA_FA_ALL_QUANTS=ON \
|
| 433 |
+
-DCMAKE_CUDA_ARCHITECTURES=120 -DCMAKE_BUILD_TYPE=Release \
|
| 434 |
+
-DLLAMA_CURL=OFF -DGGML_NATIVE=OFF
|
| 435 |
+
cmake --build build-elf -j24 --target llama-server
|
| 436 |
+
```
|
| 437 |
+
|
| 438 |
+
`-DGGML_CUDA_FA_ALL_QUANTS=ON` is REQUIRED: upstream's CMake only globs
|
| 439 |
+
`fattn-vec*.cu` (all turbo/TCQ FA-VEC instances + codebook setters) under
|
| 440 |
+
that flag; the cosmo build always sets it. Gate (bs2, RTX 6000 Blackwell):
|
| 441 |
+
A4B `-ngl 99` boots, dual-ctx assistant-MTP accept 0.764, **310.2 tps**
|
| 442 |
+
decode (vs 274 on the APE β native-arch cuBLAS build), coherent chat output,
|
| 443 |
+
`ldd` shows rpath `libggml-cuda.so.0`. The cosmocc lane is untouched
|
| 444 |
+
(BUILD.mk lists sources explicitly and never reads CMakeLists).
|
docs/features/local_llamafile.md
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# F2 β Local llamafile Model Serving
|
| 2 |
+
|
| 3 |
+
> Parent: [../MASTER_PLAN.md](../MASTER_PLAN.md)
|
| 4 |
+
> Status: **planning**
|
| 5 |
+
> Owner: TBD
|
| 6 |
+
|
| 7 |
+
## Problem
|
| 8 |
+
|
| 9 |
+
The GPU realization of Tier 0 in the tiered inference engine
|
| 10 |
+
([F1](tiered_inference.md)) wants a local backend that works **out of
|
| 11 |
+
the box** β no separate Ollama install required, no separate model
|
| 12 |
+
server. We want a single binary opencoti can launch on demand.
|
| 13 |
+
|
| 14 |
+
Beyond out-of-box, the GPU path is where opencoti gets to expose
|
| 15 |
+
"advanced features" the no-GPU path (Ollama Cloud) can't: KV-cache
|
| 16 |
+
reuse across agentic turns, big-context throughput, and direct mount
|
| 17 |
+
of an existing Ollama blob without duplicating the download. The
|
| 18 |
+
mainline llamafile / llama.cpp do parts of this β opencoti wants a
|
| 19 |
+
patched build that does all of it.
|
| 20 |
+
|
| 21 |
+
## Goals
|
| 22 |
+
|
| 23 |
+
- **G1.** Ship a patched llamafile binary inside opencoti release
|
| 24 |
+
artifacts.
|
| 25 |
+
- **G2.** Provide a small TypeScript launcher that starts / stops the
|
| 26 |
+
llamafile, exposes an OpenAI-compatible endpoint to the tier
|
| 27 |
+
engine, and pins it to loopback by default.
|
| 28 |
+
- **G3.** Maintain patches against upstream llamafile / llama.cpp in
|
| 29 |
+
`vendors/patches/llamafile/`, applied at build time.
|
| 30 |
+
- **G4.** Patches focus on three areas:
|
| 31 |
+
- **Big-context throughput** β keep tokens/sec usable at the
|
| 32 |
+
context sizes agents actually use (64K+, up to the model's 256K cap).
|
| 33 |
+
- **KV-cache reuse across agentic turns** β when the new turn
|
| 34 |
+
shares a prefix with the previous turn (system prompt + tool
|
| 35 |
+
log), skip reprocessing.
|
| 36 |
+
- **Ollama-blob mount** β load a GGUF directly from
|
| 37 |
+
`~/.ollama/models/blobs/sha256-β¦` without copying. Single
|
| 38 |
+
download, reusable by both Ollama and the patched llamafile.
|
| 39 |
+
|
| 40 |
+
## Non-goals
|
| 41 |
+
|
| 42 |
+
- Replacing Ollama for users who prefer it as Tier 0. The no-GPU
|
| 43 |
+
path of F1's Tier 0 explicitly targets Ollama Cloud as an
|
| 44 |
+
OpenAI-compatible provider β that doesn't go through this package.
|
| 45 |
+
- Bundling models. Models are downloaded on first use into the
|
| 46 |
+
opencoti data dir (or imported from an existing Ollama install);
|
| 47 |
+
we don't ship weights inside the binary.
|
| 48 |
+
|
| 49 |
+
## Default model
|
| 50 |
+
|
| 51 |
+
**Primary GPU target:**
|
| 52 |
+
[`ManniX-ITA/gemma-4-A4B-98e-v5-coder-it`](https://huggingface.co/ManniX-ITA/gemma-4-A4B-98e-v5-coder-it-GGUF).
|
| 53 |
+
|
| 54 |
+
- Architecture: Gemma 4 (MoE), 98-expert prune of Gemma 4 26B-A4B
|
| 55 |
+
(30 experts dropped per layer, `protect_top=16`, no shared FFN
|
| 56 |
+
scaling).
|
| 57 |
+
- Total ~20.8 B params, ~4 B active per token.
|
| 58 |
+
- Context: **256 K max** (recipe defaults to a smaller window for
|
| 59 |
+
speed but the model supports the full 256 K).
|
| 60 |
+
- Pitch: HumanEval 98.17 %, HumanEval+ 92.68 %, LCB-medium 85.45 %.
|
| 61 |
+
Top of the 14β22 B coder field at this writing.
|
| 62 |
+
|
| 63 |
+
**Quant table** (the GGUFs we surface as choices during first-run
|
| 64 |
+
setup, see F1 M5.7):
|
| 65 |
+
|
| 66 |
+
| Quant | Size | HE+ | When to pick |
|
| 67 |
+
|-------------|----------|--------|----------------------------------------|
|
| 68 |
+
| IQ2_S | 7.83 GB | 85.37% | tight VRAM, 8 GB cards |
|
| 69 |
+
| IQ3_M | 9.82 GB | 91.46% | **default for β₯10 GB VRAM** |
|
| 70 |
+
| CD-IQ4_K_M | 10.29 GB | 92.07% | per-layer "Canary W" variant |
|
| 71 |
+
| IQ4_XS | 11.01 GB | 93.29% | 12 GB cards |
|
| 72 |
+
| Q4_K_S | 12.21 GB | 93.29% | 16 GB cards, good headroom |
|
| 73 |
+
| CD-Q4_K_M_L | 13.00 GB | 93.29% | per-layer mid-quant variant |
|
| 74 |
+
| Q5_K_L | 15.25 GB | 93.29% | 24 GB cards, room for big context |
|
| 75 |
+
| Q8_0 | 21.16 GB | 93.90% | 24 GB cards, maximum quality |
|
| 76 |
+
| F16 | 39.8 GB | β | reference only |
|
| 77 |
+
|
| 78 |
+
The CD-* variants are "ContribDynamic" per-layer quants from the
|
| 79 |
+
model card. Both plain and CD- families are presented; the setup
|
| 80 |
+
flow recommends one based on detected VRAM, the user can override.
|
| 81 |
+
|
| 82 |
+
**Secondary target (deferred):** smaller Gemma 4 variants (E2B, E4B,
|
| 83 |
+
or a non-pruned 26B-A4B). Out of scope until the primary target is
|
| 84 |
+
working end-to-end. Stock Gemma 3 / Gemma 3 1B are not on the
|
| 85 |
+
roadmap.
|
| 86 |
+
|
| 87 |
+
## Design sketch
|
| 88 |
+
|
| 89 |
+
### Layout
|
| 90 |
+
|
| 91 |
+
```
|
| 92 |
+
vendors/
|
| 93 |
+
βββ sources/
|
| 94 |
+
β βββ llamafile/ # upstream llamafile tree (vendored, unmodified)
|
| 95 |
+
βββ patches/
|
| 96 |
+
βββ llamafile/
|
| 97 |
+
βββ 0001-kv-reuse.patch
|
| 98 |
+
βββ 0002-big-context-throughput.patch
|
| 99 |
+
βββ README.md # what each patch does, against which upstream
|
| 100 |
+
packages/
|
| 101 |
+
βββ opencoti-llamafile/ # launcher + adapter for the tier engine
|
| 102 |
+
βββ src/
|
| 103 |
+
β βββ launch.ts
|
| 104 |
+
β βββ adapter.ts # OpenAI-compatible HTTP client wired to tier engine
|
| 105 |
+
β βββ kv-cache.ts # session-aware cache key derivation
|
| 106 |
+
βββ package.json
|
| 107 |
+
```
|
| 108 |
+
|
| 109 |
+
Pattern: same as Ollama's vendoring of `llama.cpp`.
|
| 110 |
+
|
| 111 |
+
### Build-time patch application
|
| 112 |
+
|
| 113 |
+
A small build script under `vendors/llamafile/build.{sh,ts}` does:
|
| 114 |
+
|
| 115 |
+
1. `git -C vendors/sources/llamafile clean -fdx && git checkout <pinned-sha>`
|
| 116 |
+
2. Apply every patch under `vendors/patches/llamafile/*.patch` in
|
| 117 |
+
lexical order. Bail on conflict.
|
| 118 |
+
3. Build (mode depending on target β release CI builds binaries,
|
| 119 |
+
local dev can use system llamafile if present).
|
| 120 |
+
4. Emit the resulting binary to a known location the launcher checks.
|
| 121 |
+
|
| 122 |
+
The pinned upstream SHA is recorded in
|
| 123 |
+
`vendors/sources/llamafile/UPSTREAM.txt`. Updating the SHA is a
|
| 124 |
+
normal PR β patches that no longer apply must be rebased in the same
|
| 125 |
+
PR.
|
| 126 |
+
|
| 127 |
+
### Launcher
|
| 128 |
+
|
| 129 |
+
`packages/opencoti-llamafile/src/launch.ts` is a thin process
|
| 130 |
+
manager: starts the binary, waits for the OpenAI-compatible endpoint
|
| 131 |
+
to respond healthy, exposes `port` + `baseURL`. Tears down on
|
| 132 |
+
opencoti shutdown.
|
| 133 |
+
|
| 134 |
+
### Adapter
|
| 135 |
+
|
| 136 |
+
`packages/opencoti-llamafile/src/adapter.ts` implements whatever the
|
| 137 |
+
tier engine consumes (provider interface β to be defined in F1 M1).
|
| 138 |
+
It is purely an HTTP client to the local llamafile + cache-key
|
| 139 |
+
plumbing.
|
| 140 |
+
|
| 141 |
+
### KV-cache reuse β sketch
|
| 142 |
+
|
| 143 |
+
The agentic loop has a strong invariant: turn N+1 reuses turn N's
|
| 144 |
+
context prefix verbatim, then appends the model output + new tool
|
| 145 |
+
result. A KV-reuse patch:
|
| 146 |
+
|
| 147 |
+
- Adds a session id to the request schema.
|
| 148 |
+
- On the server side, persists the KV cache state keyed by
|
| 149 |
+
`(session_id, hash(prefix))`.
|
| 150 |
+
- On the next request with matching `(session_id, hash(prefix))`,
|
| 151 |
+
skips re-prefilling the prefix tokens.
|
| 152 |
+
|
| 153 |
+
This is conceptually like prompt caching in cloud providers β applied
|
| 154 |
+
to a local llamafile.
|
| 155 |
+
|
| 156 |
+
### Ollama-blob mount β sketch
|
| 157 |
+
|
| 158 |
+
If the user already has Ollama installed and has pulled the target
|
| 159 |
+
model, opencoti should not re-download the GGUF. Ollama stores blobs
|
| 160 |
+
content-addressed at `~/.ollama/models/blobs/sha256-<hash>`, with a
|
| 161 |
+
manifest at
|
| 162 |
+
`~/.ollama/models/manifests/registry.ollama.ai/library/<model>/<tag>`
|
| 163 |
+
mapping the model+tag to a digest list (the GGUF blob is one of the
|
| 164 |
+
layers).
|
| 165 |
+
|
| 166 |
+
The detection helper (F1 M5.7) parses the manifest and returns the
|
| 167 |
+
blob path. The patched llamafile accepts that path directly via
|
| 168 |
+
`-m`. Single download, no duplication β the same physical file is
|
| 169 |
+
used by both Ollama and llamafile.
|
| 170 |
+
|
| 171 |
+
The patch in llamafile itself is small: it already accepts `-m
|
| 172 |
+
<path>`, but blob filenames are hash-named (no `.gguf` extension), so
|
| 173 |
+
any code path that gates on file-extension needs adjusting. That's
|
| 174 |
+
the patch.
|
| 175 |
+
|
| 176 |
+
## Milestones
|
| 177 |
+
|
| 178 |
+
### M1 β Llamafile vendor present, launcher boots it *(done β 2026-05-21)*
|
| 179 |
+
|
| 180 |
+
- [x] Vendor llamafile under `vendors/sources/llamafile/` (git
|
| 181 |
+
submodule pinned to `0.10.1` / `6490e16`).
|
| 182 |
+
- [x] `vendors/patches/llamafile/` exists with `README.md`
|
| 183 |
+
documenting the patch protocol (numeric prefixes, lexical apply
|
| 184 |
+
order, upstreaming policy). No patches yet.
|
| 185 |
+
- [x] `packages/opencoti-llamafile/` (`@opencoti/llamafile`) ships
|
| 186 |
+
`config.ts` (binary resolution: env β vendored build path β PATH),
|
| 187 |
+
`launch.ts` (`Bun.spawn` + `waitForReady` polling `/v1/models`),
|
| 188 |
+
`adapter.ts` (M2 stub), and `script/smoke.ts`. Typecheck is clean.
|
| 189 |
+
- Smoke script exits 2 with a clear diagnostic when no binary is
|
| 190 |
+
resolvable, and 0 when one is. Live `--server` launch + `/v1/models`
|
| 191 |
+
ping is gated on `OPENCOTI_LLAMAFILE_MODEL` being set.
|
| 192 |
+
|
| 193 |
+
### M2 β Adapter wired to tier engine *(done β 2026-05-21)*
|
| 194 |
+
|
| 195 |
+
- [x] `packages/opencoti-llamafile/src/tier-provider.ts` exports
|
| 196 |
+
`tierProvider()` returning a `TierProvider` (the interface
|
| 197 |
+
defined by F1 M3 in `@opencoti/tiers/provider`).
|
| 198 |
+
- [x] Implementation uses `@ai-sdk/openai-compatible` to build a
|
| 199 |
+
`LanguageModel` against the running llamafile's `/v1` endpoint;
|
| 200 |
+
the previous M1 stub `adapter.ts` is preserved alongside (it's
|
| 201 |
+
used by the smoke script).
|
| 202 |
+
- [x] Singleton llamafile process per host (`runningPromise` module
|
| 203 |
+
cache); cleanup is process-exit (good enough for M3, refined
|
| 204 |
+
later).
|
| 205 |
+
- [x] `@opencoti/llamafile/package.json` declares deps on
|
| 206 |
+
`@ai-sdk/openai-compatible` and `@opencoti/tiers` (for the
|
| 207 |
+
interface).
|
| 208 |
+
- [x] F1 M3 (Tier 0 = llamafile end-to-end) is achieved jointly
|
| 209 |
+
with this milestone.
|
| 210 |
+
|
| 211 |
+
### M3 β Build pipeline applies patches *(done β 2026-05-22)*
|
| 212 |
+
|
| 213 |
+
Pure-helpers + CLI orchestrator split (same testability pattern as
|
| 214 |
+
M5.7's setup-flow):
|
| 215 |
+
|
| 216 |
+
- `packages/opencoti-llamafile/src/build-pipeline.ts` (new) β
|
| 217 |
+
pure helpers: `discoverPatches`, `validatePatchName`,
|
| 218 |
+
`summarizePatchSet`, `readPinnedSha`. Re-exported from the
|
| 219 |
+
package and from a new `./build-pipeline` subpath.
|
| 220 |
+
- `packages/opencoti-llamafile/script/build-pipeline.ts` (new) β
|
| 221 |
+
CLI orchestrator with subcommands `check`, `reset`, `apply`,
|
| 222 |
+
`build`, plus `help`. Shells to git + make via `Bun.spawn`.
|
| 223 |
+
Detects uninitialized submodule and emits a clear "run
|
| 224 |
+
`git submodule update --init`" message.
|
| 225 |
+
- `vendors/pin/llamafile.txt` (new) β pinned SHA + tag,
|
| 226 |
+
machine-readable. Lives OUTSIDE the submodule so
|
| 227 |
+
`git clean -fdx` doesn't wipe it. The user-facing pin table in
|
| 228 |
+
`vendors/README.md` is kept in sync manually.
|
| 229 |
+
- Root `package.json` scripts: `build:llamafile`,
|
| 230 |
+
`build:llamafile:check`, `build:llamafile:apply`,
|
| 231 |
+
`build:llamafile:reset`.
|
| 232 |
+
|
| 233 |
+
Patch protocol enforced:
|
| 234 |
+
|
| 235 |
+
- `NNNN-<kebab>.patch` with 4-digit prefix and lowercase kebab title.
|
| 236 |
+
- Duplicate prefixes are flagged (both names appear in the
|
| 237 |
+
`invalid` list, neither in `patches`).
|
| 238 |
+
- Empty patch set produces "no patches" β the build still runs to
|
| 239 |
+
completion.
|
| 240 |
+
- `check` warns when the submodule HEAD has drifted from the pin
|
| 241 |
+
but doesn't fail (a developer may be intentionally bumping).
|
| 242 |
+
|
| 243 |
+
Test coverage: 21 new unit tests in
|
| 244 |
+
`packages/opencoti-llamafile/test/build-pipeline.test.ts` covering
|
| 245 |
+
all helpers + the fixture-based discover path. Fixtures live under
|
| 246 |
+
`test/fixtures/patches/` (`0001-foo.patch`, `0002-bar-baz.patch`,
|
| 247 |
+
README.md to exercise the non-`.patch`-ignore path). The CLI
|
| 248 |
+
orchestrator itself is NOT unit-tested (it shells out to git/make;
|
| 249 |
+
integration testing belongs to F2 M6).
|
| 250 |
+
|
| 251 |
+
Verification: `bun run build:llamafile:check` against the current
|
| 252 |
+
empty patch set:
|
| 253 |
+
```
|
| 254 |
+
[build-pipeline] Pinned upstream: 6490e16
|
| 255 |
+
[build-pipeline] Vendor HEAD: 6490e16f7a8fb7d8a0a0734b41490c3f8db78763
|
| 256 |
+
[build-pipeline] no patches
|
| 257 |
+
[build-pipeline] check OK
|
| 258 |
+
```
|
| 259 |
+
|
| 260 |
+
Zero new surgical hooks. Hook footprint stays at 4 markers.
|
| 261 |
+
|
| 262 |
+
### M4 β First patch: KV-cache reuse v1 *(absorbed into F5 M0 β shipped 2026-05-24)*
|
| 263 |
+
|
| 264 |
+
This milestone was absorbed into the F5 advanced-KV series as **F5 M0**
|
| 265 |
+
(homologous to the rest of that patch series) and has **shipped**. It landed
|
| 266 |
+
as `vendors/patches/llamafile/0006-kv-reuse-prefix.patch` (not `0001-`, since
|
| 267 |
+
F4 M3's lazy-slot work consumed 0001β0005): an optional `session_id` request
|
| 268 |
+
field + sessionβslot affinity, with the adapter threading the session id and
|
| 269 |
+
`cache_prompt` through a `fetch` wrapper. Bench
|
| 270 |
+
`perf/llamafile/turn-2-latency.bench.ts` shows turn-2 prefill collapsing from
|
| 271 |
+
2061β20 tokens. See
|
| 272 |
+
[docs/features/advanced_kv.md](advanced_kv.md) "M0" for the design, the
|
| 273 |
+
upstream-already-reuses finding, and the deferred prefix-hash/disk extension.
|
| 274 |
+
|
| 275 |
+
### M5 β Big-context throughput patches
|
| 276 |
+
|
| 277 |
+
- Targeted micro-patches that improve tokens/sec at 64K+ contexts
|
| 278 |
+
(up to the 256K model cap).
|
| 279 |
+
- Each patch documented in `vendors/patches/llamafile/README.md`.
|
| 280 |
+
- Each patch with a benchmark in `perf/` (we can reuse the existing
|
| 281 |
+
`perf/` dir from opencode).
|
| 282 |
+
|
| 283 |
+
### M6 β Release artifact bundling
|
| 284 |
+
|
| 285 |
+
- CI builds llamafile-with-patches for the platforms opencoti ships
|
| 286 |
+
on. Binary is included in the opencoti release tarball under a
|
| 287 |
+
known path the launcher will find.
|
| 288 |
+
|
| 289 |
+
### M7 β Ollama-blob mount patch
|
| 290 |
+
|
| 291 |
+
- Patch lands in `vendors/patches/llamafile/0002-ollama-blob-mount.patch`.
|
| 292 |
+
- The patched llamafile accepts hash-named (extension-less) GGUF
|
| 293 |
+
paths from `~/.ollama/models/blobs/sha256-β¦` directly.
|
| 294 |
+
- Wired into F1 M5.7's first-run setup flow as the "Import from
|
| 295 |
+
local Ollama" option.
|
| 296 |
+
- Smoke test: install Ollama, `ollama pull gemma3:1b` (or any test
|
| 297 |
+
model), point the patched llamafile at the blob, assert health.
|
| 298 |
+
|
| 299 |
+
## Open questions
|
| 300 |
+
|
| 301 |
+
- **Llamafile vs. raw llama.cpp.** Llamafile gives us the
|
| 302 |
+
single-binary-cross-platform property. If KV-reuse patching is
|
| 303 |
+
easier against llama.cpp upstream, we may switch vendors. Still
|
| 304 |
+
open after M1 β re-evaluate when the first patch (M4) is in flight.
|
| 305 |
+
- ~~**Default model.** Qwen2.5-Coder-7B vs Qwen2.5-Coder-14B.~~
|
| 306 |
+
Resolved: `ManniX-ITA/gemma-4-A4B-98e-v5-coder-it` (see *Default
|
| 307 |
+
model* section above). Smaller Gemma 4 variants are explicitly
|
| 308 |
+
deferred secondary targets.
|
| 309 |
+
- **Sandboxing.** Launching a long-running binary needs robust
|
| 310 |
+
shutdown. Use a process group + signal forwarding so an opencoti
|
| 311 |
+
crash doesn't leave a llamafile orphan.
|
| 312 |
+
- **`head_dim=512` constraint.** Gemma 4 has `head_dim=512`, which
|
| 313 |
+
means FlashAttention 2 is NOT supported (per the model card).
|
| 314 |
+
Patches need to leave the `attn_implementation="eager"` path
|
| 315 |
+
intact and not enable FA2 indiscriminately for big-context work.
|
| 316 |
+
|
| 317 |
+
## Risks
|
| 318 |
+
|
| 319 |
+
- Patches drift when upstream moves. Mitigation: pinned SHA + the
|
| 320 |
+
rebase-on-update rule, and per-patch tests.
|
| 321 |
+
- Binary size. Llamafile binaries are large. Mitigation: ship the
|
| 322 |
+
binary as a separate artifact downloaded on first run, not inside
|
| 323 |
+
the main opencoti package, if it gets prohibitive.
|
docs/features/memory_embedder.md
ADDED
|
@@ -0,0 +1,668 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# F4 β Memory / Embedder
|
| 2 |
+
|
| 3 |
+
> Parent: [../MASTER_PLAN.md](../MASTER_PLAN.md)
|
| 4 |
+
> Status: **planning** (M0+M1+M2 done β 2026-05-22)
|
| 5 |
+
> Owner: TBD
|
| 6 |
+
> Reference: `/shared/dev/claude-hooks` (the embedder lifecycle is
|
| 7 |
+
> mirrored from claude-hooks' Python implementation)
|
| 8 |
+
|
| 9 |
+
## Problem
|
| 10 |
+
|
| 11 |
+
opencoti needs persistent memory on day-one β well before F3
|
| 12 |
+
(`opencoti-server`, the Go companion daemon) is ready. Memory
|
| 13 |
+
needs an embedder, and the embedder needs to work without
|
| 14 |
+
requiring the user to install a separate runtime. The same
|
| 15 |
+
constraint that drove F2 (bundle llamafile so opencoti runs out
|
| 16 |
+
of the box) drives F4: ship a prebuilt embedder llamafile, manage
|
| 17 |
+
it with a small TypeScript launcher, default to CPU, allow
|
| 18 |
+
opt-in GPU.
|
| 19 |
+
|
| 20 |
+
There's a second, sharper problem the user surfaced from
|
| 21 |
+
real-world claude-hooks operation: the canonical embedder
|
| 22 |
+
(`qwen3-embedding-0.6b-16k.llamafile`, ~1.5 GB on disk for a
|
| 23 |
+
700 MB model) allocates **3.32 GB RSS** at idle. The bottleneck
|
| 24 |
+
is *per-slot context buffer* allocation, not the autoregressive
|
| 25 |
+
KV cache. The embedder server runs `--parallel 2 --ctx-size
|
| 26 |
+
16384` and pre-allocates the full 16K context for both slots up
|
| 27 |
+
front. Live workload shows most requests at 7β56 tokens with
|
| 28 |
+
only 1 slot active most of the time β so the eager allocation
|
| 29 |
+
pays full price for capacity that is almost never used.
|
| 30 |
+
|
| 31 |
+
F4 ships the embedder, *and* ships a lazy-slot-context patch
|
| 32 |
+
against the vendored llamafile/llama.cpp tree so the embedder's
|
| 33 |
+
RSS scales with actual demand instead of `slot_count Γ n_ctx_slot`.
|
| 34 |
+
|
| 35 |
+
## Goals
|
| 36 |
+
|
| 37 |
+
- **G1.** TypeScript package `@opencoti/embedder` that owns the
|
| 38 |
+
embedder lifecycle: spawn, health-probe, idle-reap, GPU/CPU
|
| 39 |
+
mode, `embed(text)` / `embedBatch(texts)`.
|
| 40 |
+
- **G2.** First-run setup downloads the prebuilt asset
|
| 41 |
+
(`qwen3-embedding-0.6b-16k.llamafile`, dim 1024, default port
|
| 42 |
+
**47092** in opencoti's reserved 47000-48000 range) into the
|
| 43 |
+
opencoti data dir. Resume + SHA256 verify.
|
| 44 |
+
- **G2a.** **Daemon-launch policy** (project-wide convention).
|
| 45 |
+
Any opencoti or opencoti-server daemon that opens a listening
|
| 46 |
+
port MUST: (1) pick its default port from the **47000-48000
|
| 47 |
+
range** β opencoti must never collide with claude-hooks'
|
| 48 |
+
38000-39000 / 18790-18811 ranges so the two systems coexist
|
| 49 |
+
on one host; (2) ask the user at first-run setup where to
|
| 50 |
+
bind β `127.0.0.1` (default), all interfaces, or a specific
|
| 51 |
+
IP β and (3) propose the default port but allow the user to
|
| 52 |
+
override it before persisting the choice. The setup flow
|
| 53 |
+
validates that the chosen port is free and within range
|
| 54 |
+
before writing the config.
|
| 55 |
+
- **G3.** GPU mode = auto-detect with CPU fallback; default = CPU.
|
| 56 |
+
Reuses F1 M5.7's `gpu-detect` chain (nvidia β amd β vulkan).
|
| 57 |
+
- **G4.** Composite fallback chain: an `EmbedderClient`
|
| 58 |
+
interface + a `CompositeEmbedder` that tries primary then
|
| 59 |
+
fallback on `EmbedderError`, with a dim-match assertion to
|
| 60 |
+
prevent vector-space corruption.
|
| 61 |
+
- **G5.** Lazy-slot-context patch (`0001-lazy-slot-context.patch`)
|
| 62 |
+
against the vendored llamafile so RSS scales with real demand:
|
| 63 |
+
defer per-slot allocation, grow on demand, **shrink on idle**.
|
| 64 |
+
|
| 65 |
+
## Non-goals
|
| 66 |
+
|
| 67 |
+
- Replacing claude-hooks for users who already run it on the
|
| 68 |
+
same host. opencoti's embedder is independent and uses its own
|
| 69 |
+
port + data dir.
|
| 70 |
+
- Bundling weights. The model GGUF is inside the .llamafile
|
| 71 |
+
asset; we don't ship a separate GGUF download.
|
| 72 |
+
- A full vector store + recall pipeline. F4 M5 wires recall +
|
| 73 |
+
store as a thin seam; the real vector-store ownership lives
|
| 74 |
+
in F3 (opencoti-server) when it ships.
|
| 75 |
+
|
| 76 |
+
## Default asset
|
| 77 |
+
|
| 78 |
+
- **Repo:** [`mann1x/claude-hooks`](https://github.com/mann1x/claude-hooks)
|
| 79 |
+
releases β **transitional pin, two-stage migration**:
|
| 80 |
+
- **Stage 1 (immediate)**: mirror the byte-identical asset to
|
| 81 |
+
a `mann1x/opencoti` release (proposed tag:
|
| 82 |
+
`embedder-v1.4.0`) and flip just the `repo` field in
|
| 83 |
+
`vendors/pin/embedder.txt`. Same SHA, no rebuild β
|
| 84 |
+
decouples opencoti's release stream from claude-hooks'.
|
| 85 |
+
- **Stage 2 (after F4 M3 lazy-context patch ships)**: rebuild
|
| 86 |
+
the embedder composite-llamafile to include the lazy-
|
| 87 |
+
slot-context patch. The new binary has a different SHA;
|
| 88 |
+
`tag` + `sha256` in the pin file both bump (proposed tag:
|
| 89 |
+
`embedder-v1.5.0`). F4 M6 (release bundling) owns the
|
| 90 |
+
apply-patches β make β zipalign-with-GGUF-and-args
|
| 91 |
+
pipeline. Without Stage 2, the embedder still benefits
|
| 92 |
+
from the lazy-context patch only when run against a
|
| 93 |
+
user-supplied locally-built embedder binary β not the
|
| 94 |
+
prebuilt asset.
|
| 95 |
+
- **Tag:** `v1.4.0`
|
| 96 |
+
- **Asset:** `qwen3-embedding-0.6b-16k.llamafile`
|
| 97 |
+
- **SHA256:** `414f616689aaba44f6982b474918e8549ad764c03b2f82f6b56a5d6e582ef59b`
|
| 98 |
+
- **Pinned in:** [`vendors/pin/embedder.txt`](../../vendors/pin/embedder.txt)
|
| 99 |
+
- **Default port:** **47092** (opencoti's reserved 47000-48000
|
| 100 |
+
range β see G2a).
|
| 101 |
+
- **Endpoint:** `POST /embedding` (llama.cpp server style, not
|
| 102 |
+
OpenAI-compat). Request: `{"content": "<text>"}` or batch
|
| 103 |
+
`{"content": ["t1", "t2"]}`. Response: `{"embedding": [...]}`
|
| 104 |
+
(single) / `[{"embedding": [...]}, ...]` (batch).
|
| 105 |
+
|
| 106 |
+
## Design sketch
|
| 107 |
+
|
| 108 |
+
### Layout
|
| 109 |
+
|
| 110 |
+
```
|
| 111 |
+
packages/opencoti-embedder/ # NEW
|
| 112 |
+
βββ package.json
|
| 113 |
+
βββ tsconfig.json
|
| 114 |
+
βββ src/
|
| 115 |
+
β βββ index.ts
|
| 116 |
+
β βββ client.ts # EmbedderClient interface + types
|
| 117 |
+
β βββ llamafile-embedder.ts # POST /embedding HTTP client
|
| 118 |
+
β βββ composite.ts # primary β fallback chain
|
| 119 |
+
β βββ manager.ts # process spawn / health / idle reap
|
| 120 |
+
β βββ gpu-mode.ts # CPU | auto resolution
|
| 121 |
+
β βββ download.ts # asset fetch + SHA256 verify
|
| 122 |
+
β βββ config.ts # Zod schema for embedder config
|
| 123 |
+
βββ script/
|
| 124 |
+
β βββ smoke.ts
|
| 125 |
+
βββ test/
|
| 126 |
+
|
| 127 |
+
vendors/
|
| 128 |
+
βββ pin/
|
| 129 |
+
β βββ embedder.txt # NEW (M0)
|
| 130 |
+
βββ patches/
|
| 131 |
+
βββ llamafile/
|
| 132 |
+
βββ 0001-lazy-slot-context.patch # NEW (M3)
|
| 133 |
+
```
|
| 134 |
+
|
| 135 |
+
### Reused, not rebuilt
|
| 136 |
+
|
| 137 |
+
- `@opencoti/tiers/registry/gpu-detect` β nvidia/amd/vulkan probe
|
| 138 |
+
chain (F1 M5.7).
|
| 139 |
+
- `@opencoti/tiers/registry/hf-download` β resume + SHA256 verify
|
| 140 |
+
download. To be generalized into a shared
|
| 141 |
+
`@opencoti/tiers/util/download` helper consumed by both HF
|
| 142 |
+
downloads and GitHub Release downloads.
|
| 143 |
+
|
| 144 |
+
## Milestones
|
| 145 |
+
|
| 146 |
+
### M0 β Vendoring decision + asset URL pin *(done β 2026-05-22)*
|
| 147 |
+
|
| 148 |
+
- `vendors/pin/embedder.txt` records repo / tag / asset / SHA256.
|
| 149 |
+
- Decision recorded: pin claude-hooks v1.4.0 directly until
|
| 150 |
+
opencoti has its own release stream. Switch the pin URL to
|
| 151 |
+
`mann1x/opencoti` when that exists.
|
| 152 |
+
- This feature plan + F5 plan + MASTER_PLAN.md updated together
|
| 153 |
+
in the same commit.
|
| 154 |
+
|
| 155 |
+
### M1 β Embedder package, CPU mode, smoke green *(done β 2026-05-22)*
|
| 156 |
+
|
| 157 |
+
- `@opencoti/embedder` shipped with client interface,
|
| 158 |
+
llamafile-embedder HTTP impl (POST /embedding, response-shape
|
| 159 |
+
tolerant for A/B/C/D variants seen in the wild),
|
| 160 |
+
download.ts (resume + SHA verify, builds the canonical
|
| 161 |
+
github-release URL from the pin), manager.ts (spawn +
|
| 162 |
+
`/health` poll + LRU idle reap @ 5 min default + idempotent
|
| 163 |
+
stop), config.ts (Zod strict schema, defaults claude-hooks-
|
| 164 |
+
parity), gpu-mode.ts (CPU default + auto-resolution),
|
| 165 |
+
composite.ts (primaryβfallback chain with dim-match guard).
|
| 166 |
+
- Shared `downloadWithResume` helper extracted to
|
| 167 |
+
`@opencoti/tiers/util/download`; `hf-download.ts` refactored
|
| 168 |
+
to consume it. New subpath exports:
|
| 169 |
+
`@opencoti/tiers/util/download`,
|
| 170 |
+
`@opencoti/tiers/registry/gpu-detect`,
|
| 171 |
+
`@opencoti/tiers/registry/hf-download`.
|
| 172 |
+
- `script/smoke.ts` downloads the asset to `<repo>/.opencoti/embedder/`
|
| 173 |
+
(skips on SHA match), spawns CPU-mode, embeds "hello world",
|
| 174 |
+
asserts dim=1024 + L2-norm. Live smoke run is opt-in (asset
|
| 175 |
+
is 1.5 GB).
|
| 176 |
+
- 52 unit tests across 7 files. Hook footprint unchanged
|
| 177 |
+
(still 4 `opencoti-hook:` markers).
|
| 178 |
+
- **Out of scope (deferred to M5)**: opencode session
|
| 179 |
+
injection, recall-into-prompt, vector store.
|
| 180 |
+
|
| 181 |
+
### M2 β Baseline measurement *(done β 2026-05-22)*
|
| 182 |
+
|
| 183 |
+
Ships two scripts and three fixture files:
|
| 184 |
+
|
| 185 |
+
- `script/generate-corpus.ts` β deterministic 48-prompt corpus
|
| 186 |
+
generator spanning 8-3000 approx_tokens. Buckets: 10 short
|
| 187 |
+
(8-32 tok), 11 medium (33-128), 19 medium-long (129-512), 6
|
| 188 |
+
long (513-2048), 2 very-long (2049-8192). Original draft had
|
| 189 |
+
5 in the very-long bucket; 2 mega prompts (60+ LOREM repeats)
|
| 190 |
+
aborted past the 60s embedder request-timeout during the live
|
| 191 |
+
baseline run, so they were dropped from the generator.
|
| 192 |
+
- `script/measure-baseline.ts` β boots the embedder via the M1
|
| 193 |
+
manager (or attaches to a running instance with `--use-running
|
| 194 |
+
<url> --pid <int>`), captures RSS from `/proc/<pid>/status`,
|
| 195 |
+
extracts allocation lines from the embedder startup log,
|
| 196 |
+
embeds the corpus, writes the fixture.
|
| 197 |
+
|
| 198 |
+
Fixtures (in `test/fixtures/`):
|
| 199 |
+
- `quality-prompts.json` β 48 prompts, deterministic.
|
| 200 |
+
- `quality-baseline.json` β 48 vectors (dim 1024) M3 must
|
| 201 |
+
reproduce to cosine β₯ 0.999.
|
| 202 |
+
- `baseline-meta.json` β RSS + allocation breakdown +
|
| 203 |
+
startup-log excerpt.
|
| 204 |
+
|
| 205 |
+
**Live measurement (2026-05-22, attached to claude-hooks
|
| 206 |
+
embedder, parallel=4 default, ctx=16384, CPU mode)**:
|
| 207 |
+
|
| 208 |
+
| Component | Size |
|
| 209 |
+
| --- | --- |
|
| 210 |
+
| Model weights (CPU_Mapped) | 603.87 MiB |
|
| 211 |
+
| KV cache (all slots) | **1792.00 MiB** |
|
| 212 |
+
| Compute buffer | 330.24 MiB |
|
| 213 |
+
| Output buffer | 2.33 MiB |
|
| 214 |
+
| RSS idle (process VmRSS) | **2.52 GB** |
|
| 215 |
+
| RSS peak (after 48-prompt run) | **3.81 GB** |
|
| 216 |
+
| Avg latency per prompt | 5352 ms |
|
| 217 |
+
| Total corpus embed time | 256.9 s |
|
| 218 |
+
|
| 219 |
+
Startup-log key lines (captured in baseline-meta.json):
|
| 220 |
+
`llama_context: n_ctx = 16384`,
|
| 221 |
+
`llama_kv_cache: CPU KV buffer size = 1792.00 MiB`,
|
| 222 |
+
`sched_reserve: CPU compute buffer size = 330.24 MiB`,
|
| 223 |
+
`server_main: embeddings enabled with n_batch (2048) >
|
| 224 |
+
n_ubatch (512) β setting n_batch = n_ubatch = 512 to avoid
|
| 225 |
+
assertion failure`, `slot load_model: id 0/1/2/3 | new slot,
|
| 226 |
+
n_ctx = 16384`. The binary defaults to parallel=4 with no
|
| 227 |
+
`--parallel` override.
|
| 228 |
+
|
| 229 |
+
The earlier user observation of **3.32 GB RSS** came from a
|
| 230 |
+
2-slot config explicitly setting `--parallel 2 --ctx-size
|
| 231 |
+
16384`. The M3 patch targets that production-shape config:
|
| 232 |
+
|
| 233 |
+
- Pre-patch per-slot KV allocation = `n_ctx_slot Γ 2 (K+V) Γ
|
| 234 |
+
layers Γ per-token-dim` β 448 MiB / slot in CPU mode,
|
| 235 |
+
allocated eagerly for every slot at startup.
|
| 236 |
+
- M3 target: per-slot allocation deferred to first prompt,
|
| 237 |
+
sized to `--slot-initial-ctx 4096` (~112 MiB), grown on
|
| 238 |
+
demand up to 16384, shrunk back to 4096 after
|
| 239 |
+
`--slot-shrink-idle-ms`. Combined with 2 slots + model +
|
| 240 |
+
compute, idle RSS target lands near 1.0 GB.
|
| 241 |
+
|
| 242 |
+
**Two corpus prompts intentionally dropped**: `lorem-mega-00`
|
| 243 |
+
and `lorem-mega-01` (60+ LOREM repeats) aborted past the
|
| 244 |
+
embedder's 60s default request-timeout on the CPU-mode
|
| 245 |
+
production instance β n_ubatch is forced to 512 in embedding
|
| 246 |
+
mode, so 9000+ token requests need ~18 forward passes that
|
| 247 |
+
exceed 60s. Not an M3 concern (the patch addresses
|
| 248 |
+
*allocation*, not *throughput*).
|
| 249 |
+
|
| 250 |
+
### M3 β Lazy-slot-context patch
|
| 251 |
+
|
| 252 |
+
> **Architecture reference:** [ADR 0001 β Lazy slot-context
|
| 253 |
+
> allocation](../decisions/0001-lazy-slot-context.md). The ADR
|
| 254 |
+
> documents the llama.cpp KV-cache layout, the three+1 orthogonal
|
| 255 |
+
> axes of "lazy," the four-phase rollout (defer / grow / shrink /
|
| 256 |
+
> per-stream split), the CLI surface, and the F5 milestone
|
| 257 |
+
> hand-offs that depend on the allocator hooks landed here.
|
| 258 |
+
> Implementation is multi-session per user directive B2.
|
| 259 |
+
>
|
| 260 |
+
> **Phase status (2026-05-23):**
|
| 261 |
+
> - **Phase 1 β deferred zero-fill: shipped** as
|
| 262 |
+
> `vendors/patches/llamafile/0001-lazy-slot-context-defer.patch`.
|
| 263 |
+
> Patch applies cleanly; build succeeds on x86_64 + aarch64;
|
| 264 |
+
> smoke validation on Qwen2-Math-1.5B confirms the
|
| 265 |
+
> `KV cache zero-fill deferred until first batch` startup log
|
| 266 |
+
> line and the matching `ensure_cleared: ... (deferred)`
|
| 267 |
+
> first-batch log line. Bench against M2 corpus + RSS table
|
| 268 |
+
> for the embedder workload pending (Phase 1 alone is a
|
| 269 |
+
> cold-start win; the headline warm-state RSS savings come
|
| 270 |
+
> in Phases 2-3).
|
| 271 |
+
> - **Phase 2 β grow on demand + partial zero-fill: shipped**
|
| 272 |
+
> as `vendors/patches/llamafile/0002-lazy-slot-context-grow.patch`.
|
| 273 |
+
> Adds `--slot-initial-ctx N` (default 0 = Phase 1 behavior) that
|
| 274 |
+
> caps initial KV-cell allocation per sequence; find_slot honors
|
| 275 |
+
> the soft cap; prepare() grows on overflow up to kv_size_max via
|
| 276 |
+
> next-pow2 geometric ramp; ensure_cleared partial-memsets only
|
| 277 |
+
> the [n_cells_cleared, target) range via `ggml_backend_tensor_memset`.
|
| 278 |
+
> With `--slot-initial-ctx 4096` for the production embedder,
|
| 279 |
+
> warm-state RSS scales with actual workload instead of the
|
| 280 |
+
> `slot_count Γ n_ctx_slot` product. Bench numbers vs M2 corpus
|
| 281 |
+
> pending.
|
| 282 |
+
> - **Phase 3 β shrink on idle: shipped** as
|
| 283 |
+
> `vendors/patches/llamafile/0005-lazy-slot-context-shrink.patch`.
|
| 284 |
+
> Adds `--slot-shrink-idle-ms N` (default 30000); per-stream
|
| 285 |
+
> idle timer sweeps at `init_batch` and reclaims buffers back
|
| 286 |
+
> to `--slot-initial-ctx` after the timeout. Bench delta:
|
| 287 |
+
> **391.7 MB returned to kernel** on the CPU embedder after 32 s
|
| 288 |
+
> idle (1.68 GB peak β 1.30 GB post-shrink, cosine 0.999797 vs
|
| 289 |
+
> M2 baseline).
|
| 290 |
+
> **CPU-only β the shrink is gated on
|
| 291 |
+
> `ggml_backend_buffer_is_host()` inside
|
| 292 |
+
> `opencoti_decommit_layer_range()` because
|
| 293 |
+
> `madvise(MADV_DONTNEED)` is a no-op against device-backed
|
| 294 |
+
> pages.** On GPU (`-ngl > 0`) the `shrink_if_idle` log marker
|
| 295 |
+
> still fires and the soft-cap bookkeeping rewinds, but the
|
| 296 |
+
> CUDA/ROCm/Vulkan backend buffer is not freed and VRAM stays
|
| 297 |
+
> at the peak the workload reached. The GPU-shrink path is
|
| 298 |
+
> tracked at **F5 M2 (HeadInfer)**, not Phase 4.
|
| 299 |
+
> - **Phase 4 β per-stream tensor split: DEFERRED** (task #109,
|
| 300 |
+
> target F5 M2). 2026-05-23 user decision after planning-session
|
| 301 |
+
> exploration showed Phase 4 is a deep refactor (per-stream
|
| 302 |
+
> tensors + per-stream backend buffers + reworked
|
| 303 |
+
> `cpy_k`/`cpy_v` scatter ops that currently rely on
|
| 304 |
+
> `ggml_reshape_2d(k, n_embd, kv_size*n_stream)` to flatten all
|
| 305 |
+
> streams into a single `ggml_set_rows`) and only adds value in
|
| 306 |
+
> non-unified mode. The embedder ships unified by default and
|
| 307 |
+
> already gets all Phase 1-3 wins with the contiguous tensor
|
| 308 |
+
> intact. Phase 4's real beneficiary is F5 (HeadInfer's per-head
|
| 309 |
+
> GPU/CPU split + PolyKV's shared pool). Full implementation
|
| 310 |
+
> map persisted in
|
| 311 |
+
> [docs/decisions/0001-lazy-slot-context.md Β§Phase 4 β implementation map](../decisions/0001-lazy-slot-context.md)
|
| 312 |
+
> for the F5 owner to pick up cold. **Not the GPU-shrink fix β
|
| 313 |
+
> that's F5 M2 (HeadInfer), independently of Phase 4.**
|
| 314 |
+
|
| 315 |
+
- `vendors/patches/llamafile/0001-lazy-slot-context.patch`.
|
| 316 |
+
- Behavior:
|
| 317 |
+
- Defer per-slot KV/compute-buffer allocation until slot's
|
| 318 |
+
first real prompt.
|
| 319 |
+
- Initial allocation rounded up to `--slot-initial-ctx`
|
| 320 |
+
(default 4096).
|
| 321 |
+
- Grow on demand (next power-of-two), cap at `n_ctx_slot`.
|
| 322 |
+
- **Shrink on idle** (`--slot-shrink-idle-ms`, default
|
| 323 |
+
30000): free buffers and return to initial-ctx after the
|
| 324 |
+
timeout, otherwise the first 16K request permanently sticks
|
| 325 |
+
the slot at 16K and the lazy-allocation win evaporates.
|
| 326 |
+
- Bench (`perf/llamafile/embedder-rss.bench.ts`): RSS at idle /
|
| 327 |
+
after 8-token request / after 16K request / after idle
|
| 328 |
+
timeout + 8-token request. Quality: cosine β₯ 0.999 vs M2's
|
| 329 |
+
baseline.
|
| 330 |
+
- Header: `Milestone: F4 M3`, `Upstreaming: TBD β propose to
|
| 331 |
+
llamafile + llama.cpp upstream once bench numbers are public`.
|
| 332 |
+
|
| 333 |
+
### M4 β Auto-GPU mode + composite fallback
|
| 334 |
+
|
| 335 |
+
- `gpu-mode.ts` adds `"auto"`: probe nvidia β amd β vulkan; if
|
| 336 |
+
VRAM β₯ 1.6 GB, launch with `--gpu auto` + n-gpu-layers fully
|
| 337 |
+
offloaded; otherwise silent CPU fallback.
|
| 338 |
+
- `CompositeEmbedder` ships: primary (local llamafile) +
|
| 339 |
+
fallback (cloud-route delegate, when Tier 1 is configured).
|
| 340 |
+
Dim-mismatch assertion lives here.
|
| 341 |
+
|
| 342 |
+
### M5 β Opencode integration: recall + storage seam *(2026-05-23 β shipped, rescoped mid-milestone)*
|
| 343 |
+
|
| 344 |
+
**Original scope:** one synthetic tools pair + one surgical session-end hook +
|
| 345 |
+
setup-flow listen-address question. **Actual scope after the user rescope
|
| 346 |
+
(verbatim): "memory store and recall is going to be another crucial point,
|
| 347 |
+
we support sqlite + sqlite_vec as base memory backend, managed by
|
| 348 |
+
opencoti-server. opencoti-server will manage optional sharing on the
|
| 349 |
+
network. the pgvector db is an add on memory connector. both works in
|
| 350 |
+
parallel. memory in opencoti can be managed per-session. user can manage
|
| 351 |
+
which memories to use and list, delete, connect to the memories via the
|
| 352 |
+
harness menu. shared memory for all sessions, specific memories can be
|
| 353 |
+
added and or connected. for each session the user can decide which one
|
| 354 |
+
to read/write/rw"**
|
| 355 |
+
|
| 356 |
+
Path taken (after user decision): **in-process M5 now, refactor to
|
| 357 |
+
opencoti-server later**. The opencoti-server piece is queued for F3
|
| 358 |
+
once F4 closes. The data model and ACL contract land here so they
|
| 359 |
+
survive that refactor unchanged.
|
| 360 |
+
|
| 361 |
+
What M5 actually shipped:
|
| 362 |
+
|
| 363 |
+
- **M5-A β `@opencoti/memory` package.** `SqliteVecStore` over
|
| 364 |
+
bun:sqlite + sqlite-vec 0.1.9 vec0 virtual table. Embedding dim
|
| 365 |
+
locked at DB creation (default 1024). Schema: `collections`,
|
| 366 |
+
`memories` (with `content_hash` UNIQUE for idempotent insert),
|
| 367 |
+
`memory_acl` (per-session override), `vec_memories` (vec0
|
| 368 |
+
virtual table with FLOAT[dim]). Default DB path
|
| 369 |
+
`~/.opencoti/memory/state.db`. ACL model: explicit overrides win;
|
| 370 |
+
defaults are "rw" for global collections, "rw" for the owning
|
| 371 |
+
session of a session-private collection, "none" otherwise. The
|
| 372 |
+
resolver lives in pure code (`resolveAccessMode`, `canRead`,
|
| 373 |
+
`canWrite`) and is exhaustively tested.
|
| 374 |
+
- **M5-B β synthetic tools.** `__memory_recall(query, k?, collections?)`,
|
| 375 |
+
`__memory_store(content, collection?)`, `__memory_list()`. All
|
| 376 |
+
three are ACL-aware (sessionID is the actor) and best-effort β
|
| 377 |
+
embedder failure returns `{ok: false, error: "embedder_unavailable"}`
|
| 378 |
+
rather than throwing. `ensureSessionCollection` is idempotent.
|
| 379 |
+
- **M5-C β runtime wire-up via dynamic-import bridge.**
|
| 380 |
+
`@opencoti/tiers/src/memory-bridge.ts` resolves
|
| 381 |
+
`@opencoti/memory` + `@opencoti/embedder` via dynamic import to
|
| 382 |
+
break the tiersβmemoryβembedderβtiers cycle. Wired into
|
| 383 |
+
`runtime.ts:maybeRoute` via `mergeTools` (no new surgical
|
| 384 |
+
hook β extends an existing additive seam). The default
|
| 385 |
+
embedder is `createDefaultEmbedFn` (loopback LlamafileEmbedder
|
| 386 |
+
on 47092). Workspace dep `@opencoti/memory` added on
|
| 387 |
+
`packages/opencode` to make the dynamic import resolvable;
|
| 388 |
+
registered in UPSTREAM_SYNC.md as `memory-tools-resolvability`
|
| 389 |
+
(dep).
|
| 390 |
+
- **M5-D2 β `@opencoti/memory-plugin` (opencode SERVER plugin).**
|
| 391 |
+
Two hooks: `experimental.chat.system.transform` appends a
|
| 392 |
+
system-prompt suffix advertising the memory tool surface +
|
| 393 |
+
listing the session's accessible collections;
|
| 394 |
+
`event` listens for `session.idle` and, when
|
| 395 |
+
`auto_ingest: true` (default FALSE), ensures the session-private
|
| 396 |
+
collection. Defaults reflect the rescoped vision: advertise=true
|
| 397 |
+
(the point of loading the plugin), auto_ingest=false (the agent
|
| 398 |
+
already has `__memory_store`).
|
| 399 |
+
- **M5-D1 β `@opencoti/tui-memory` (opencode TUI plugin).** One
|
| 400 |
+
command `opencoti.memory` opens a DialogSelect listing every
|
| 401 |
+
collection (global + session-private), with per-session
|
| 402 |
+
resolved-mode annotations when in a session route. Drill-down
|
| 403 |
+
per collection offers: ACL toggle r/w/rw/none (session-only),
|
| 404 |
+
DialogConfirm-guarded delete, Create flow
|
| 405 |
+
(DialogPrompt name β scope DialogSelect). Lazy store open,
|
| 406 |
+
closed cleanly on `api.lifecycle.onDispose`.
|
| 407 |
+
|
| 408 |
+
**Default-plugin auto-wiring (2026-05-23 follow-up to user
|
| 409 |
+
directive: "opencoti will need to wire them by default"):**
|
| 410 |
+
The opencoti plugins remain opt-in for upstream-opencode users
|
| 411 |
+
(they stay out of upstream's plugin list unless explicitly
|
| 412 |
+
declared), but opencoti's distribution auto-wires them whenever a
|
| 413 |
+
project has any `opencoti.*` config section. Implementation in
|
| 414 |
+
`@opencoti/tiers/default-plugins`: a pure helper
|
| 415 |
+
`applyDefaultPlugins(cfg)` gap-fills `@opencoti/memory-plugin`,
|
| 416 |
+
`@opencoti/tui-memory`, `@opencoti/tui-tiers` into the final
|
| 417 |
+
`plugin_origins` list β user-declared entries (including their
|
| 418 |
+
options) are preserved untouched. Wired via two surgical hooks
|
| 419 |
+
in `packages/opencode/src/config/config.ts` (import + call-site,
|
| 420 |
+
registered as `opencoti-default-plugins`). Net change vs M5
|
| 421 |
+
substance: TWO new opencoti-hook markers (still tagged + in
|
| 422 |
+
registry). With opencoti disabled (no `cfg.opencoti` block) the
|
| 423 |
+
helper is a no-op and the byte-output is identical to upstream.
|
| 424 |
+
|
| 425 |
+
What M5 deliberately did NOT ship:
|
| 426 |
+
|
| 427 |
+
- The session-end **surgical** hook (the original plan). Replaced by
|
| 428 |
+
the optional `event: "session.idle"` listener in `@opencoti/memory-plugin`
|
| 429 |
+
with `auto_ingest: false` default. Net new `opencoti-hook:`
|
| 430 |
+
markers from M5 substance + follow-up: TWO (the
|
| 431 |
+
`opencoti-default-plugins` import + call-site). The
|
| 432 |
+
`memory-tools-resolvability` dep is a registry-only entry (no
|
| 433 |
+
source marker β JSON has no comments).
|
| 434 |
+
- The pgvector add-on connector. Deferred β the in-process backend
|
| 435 |
+
is sqlite-vec only for now. Pgvector becomes a parallel
|
| 436 |
+
`MemoryStore` implementation once opencoti-server (F3) lands.
|
| 437 |
+
- The network-sharing layer. That's opencoti-server's job (F3).
|
| 438 |
+
- The "Clear ACL override" UI option in the TUI panel. The
|
| 439 |
+
`MemoryStore` interface does NOT currently expose a clear-ACL
|
| 440 |
+
method β adding it would require a new schema/interface revision
|
| 441 |
+
(deferred to a future minor bump).
|
| 442 |
+
- **M5-E β setup-flow embedder daemon listen address question
|
| 443 |
+
(shipped 2026-05-23).** `@opencoti/tiers/cli/setup-flow`
|
| 444 |
+
extended with an embedder listen-address prompt in
|
| 445 |
+
`finalizePatch`. Steps: (1) DialogSelect over
|
| 446 |
+
loopback / all-interfaces / specific-IP / skip; (2) if
|
| 447 |
+
specific-IP, prompt for the IP with an IPv4 validator;
|
| 448 |
+
(3) text prompt for the port, validated against the opencoti
|
| 449 |
+
47000-48000 range; (4) port-free check via injected
|
| 450 |
+
`isPortAvailable(host, port)` (live: TCP bind probe in
|
| 451 |
+
setup.ts) β busy port logs a warn and asks the user whether to
|
| 452 |
+
proceed anyway; "no" recursively re-asks the bind block. The
|
| 453 |
+
chosen `{host, port}` lands in
|
| 454 |
+
`~/.config/opencode/opencode.jsonc` under
|
| 455 |
+
`opencoti.embedder.host` + `.port` via the jsonc-parser
|
| 456 |
+
modify-then-applyEdits path (comments preserved). A new
|
| 457 |
+
`embedderConfigured` sentinel (true when BOTH host AND port
|
| 458 |
+
already present in the user's config) gates the prompt on
|
| 459 |
+
re-runs. 16 new tests across the flow + the `validatePort` and
|
| 460 |
+
`isValidIPv4` pure helpers.
|
| 461 |
+
|
| 462 |
+
### M6 β Production llamafile build pipeline *(2026-05-23 β promoted ahead of M5)*
|
| 463 |
+
|
| 464 |
+
Driver: user directive 2026-05-23 β the post-F4 M4 state (prebuilt
|
| 465 |
+
claude-hooks asset + manual zipalign of ggml-cuda.so + cross-repo
|
| 466 |
+
pin dependency) is "patchwork [β¦] not acceptable for production".
|
| 467 |
+
M6 was originally scoped to the embedder composite alone; the
|
| 468 |
+
expanded M6 below covers all three backends (CUDA / ROCm / Vulkan)
|
| 469 |
+
and the local-asset modes the embedder downloader needs to
|
| 470 |
+
consume an opencoti-built artifact.
|
| 471 |
+
|
| 472 |
+
M6 lands before M5 (opencode session-end hook) because there's no
|
| 473 |
+
point storing memories on the back of a launcher binary that
|
| 474 |
+
doesn't engage the M3 lazy-context patch.
|
| 475 |
+
|
| 476 |
+
**What M6 ships:**
|
| 477 |
+
|
| 478 |
+
1. **Multi-backend DSO builders in
|
| 479 |
+
`packages/opencoti-llamafile/script/build-pipeline.ts`:**
|
| 480 |
+
`runCuda` / `runRocm` / `runVulkan` / `runAllBackends`,
|
| 481 |
+
driven by a shared `buildBackend(spec)` helper.
|
| 482 |
+
- Each backend has a toolchain probe: `OPENCOTI_CUDA_PATH/bin/nvcc`
|
| 483 |
+
(default `/usr/local/cuda-12.6` β CUDA 13.x is known-broken
|
| 484 |
+
with `cuda.sh`), `OPENCOTI_ROCM_PATH/bin/hipcc` (default
|
| 485 |
+
`/opt/rocm`), `OPENCOTI_VULKAN_SDK` env / system `glslc`.
|
| 486 |
+
- Soft-fail per backend: a host without ROCm produces a clean
|
| 487 |
+
CUDA-only build instead of failing the pipeline.
|
| 488 |
+
- Each built DSO is staged at
|
| 489 |
+
`vendors/dist/llamafile/<backend>/<ver>/ggml-<backend>.so`
|
| 490 |
+
AND mirrored to `~/.llamafile/v/<ver>/` so devs running the
|
| 491 |
+
thin binary directly still get GPU support locally.
|
| 492 |
+
|
| 493 |
+
2. **`package` subcommand in the same script:**
|
| 494 |
+
Zipaligns the thin patched binary with every staged DSO via
|
| 495 |
+
the vendored `vendors/sources/llamafile/o/third_party/zipalign/
|
| 496 |
+
zipalign -j0` invocation. Always emits
|
| 497 |
+
`dist/llamafile/opencoti-llamafile-<ver>-<arch>.llamafile`
|
| 498 |
+
plus a sibling `MANIFEST.json` (artifact SHA + per-DSO SHA +
|
| 499 |
+
patches list).
|
| 500 |
+
- With `--with-model PATH` ALSO emits
|
| 501 |
+
`dist/llamafile/opencoti-embedder-<ver>-<arch>.llamafile`
|
| 502 |
+
by copying the bare and zipalign-embedding the GGUF + a
|
| 503 |
+
`.args` file generated from the F2 M3 canonical embedder
|
| 504 |
+
args contract (`-m /zip/<basename> --server --embedding
|
| 505 |
+
--pooling last --ctx-size 16384 --parallel 2
|
| 506 |
+
--slot-initial-ctx 4096 --slot-shrink-idle-ms 30000`).
|
| 507 |
+
- `--with-args PATH` overrides the generated `.args`.
|
| 508 |
+
- `--cpu-only` produces a binary with NO DSO embedded (for CI
|
| 509 |
+
smoke on hosts without GPU).
|
| 510 |
+
|
| 511 |
+
3. **Pure helpers in
|
| 512 |
+
`packages/opencoti-llamafile/src/build-pipeline.ts`** (kept
|
| 513 |
+
side-effect-free per F2 M3 convention): `Backend` type +
|
| 514 |
+
`ALL_BACKENDS`, `dsoFilename`, `stagingDir`, `artifactDir`,
|
| 515 |
+
`readBackendArtifacts`, `sha256OfFile`, `composeEmbedderArgs`,
|
| 516 |
+
`parseBuildArgv`, `ArtifactManifest` type. 19 new unit tests
|
| 517 |
+
under `test/build-pipeline.test.ts`.
|
| 518 |
+
|
| 519 |
+
4. **`@opencoti/embedder` downloader local-asset modes
|
| 520 |
+
(`src/download.ts`):**
|
| 521 |
+
- `file:` prefix on the pin's repo field β bytes come from
|
| 522 |
+
disk; SHA still verified.
|
| 523 |
+
- `OPENCOTI_EMBEDDER_LOCAL=<abs-path>` env override β
|
| 524 |
+
bypasses pin path entirely; SHA still verified.
|
| 525 |
+
- Both routed through a shared `localCopyPath` helper; the
|
| 526 |
+
HTTPS+resume path is unchanged. 12 new tests in
|
| 527 |
+
`test/download.test.ts`.
|
| 528 |
+
|
| 529 |
+
5. **Hook footprint unchanged.** M6 is pure build orchestration +
|
| 530 |
+
downloader extension; no surgical hooks in upstream opencode.
|
| 531 |
+
|
| 532 |
+
**Deferred to F4 M7 (release bundling):**
|
| 533 |
+
|
| 534 |
+
- GitHub Releases publish pipeline. Producing artifacts under
|
| 535 |
+
`dist/llamafile/` is M6; uploading them as `mann1x/opencoti`
|
| 536 |
+
release assets and flipping `vendors/pin/embedder.txt` to
|
| 537 |
+
`mann1x/opencoti` (Stage 2 of the migration the existing pin
|
| 538 |
+
file header describes) is M7. The pin migration is a one-line
|
| 539 |
+
change once the release exists.
|
| 540 |
+
- Metal / macOS backend. `vendors/sources/llamafile/llamafile/
|
| 541 |
+
metal.c` is macOS-only; M6 ships Linux-only since the build
|
| 542 |
+
host is solidPC. Needs a Darwin CI runner.
|
| 543 |
+
- aarch64 / multi-arch DSO orchestration. cosmocc APE is dual-
|
| 544 |
+
arch by design, but ggml-*.so are arch-specific. The `<arch>`
|
| 545 |
+
token in the output filename is anticipatory; M6 ships x86_64
|
| 546 |
+
only.
|
| 547 |
+
|
| 548 |
+
### M7 β Release bundling + dev-cut workflow alignment with claude-hooks
|
| 549 |
+
|
| 550 |
+
The build pipeline (M6) produces `dist/llamafile/*.llamafile` artifacts.
|
| 551 |
+
M7 wraps that with the **commit-the-contract, gitignore-the-bytes**
|
| 552 |
+
release scheme claude-hooks uses, plus a `release-cut` subcommand
|
| 553 |
+
that prepares (but does NOT auto-publish) a `mann1x/opencoti`
|
| 554 |
+
GitHub Release.
|
| 555 |
+
|
| 556 |
+
**What's tracked (committed):**
|
| 557 |
+
|
| 558 |
+
- `vendors/llamafile/README.md` β audit-trail doc describing the
|
| 559 |
+
pinned upstream + the contract layout. Mirrors claude-hooks's
|
| 560 |
+
`vendor/llamafile/README.md`.
|
| 561 |
+
- `vendors/llamafile/LICENSE.upstream` β single-file copy of the
|
| 562 |
+
four upstream LICENSE files (Mozilla-Ocho/llamafile Apache-2.0 +
|
| 563 |
+
llama.cpp/whisper.cpp/stable-diffusion.cpp MIT Γ 3) at the
|
| 564 |
+
pinned commit. Survives independently of the submodule.
|
| 565 |
+
- `vendors/llamafile/SHA256SUMS.composite` β the contract.
|
| 566 |
+
`sha256sum`-compatible lines listing every released composite
|
| 567 |
+
artifact. Refreshed by `build:llamafile:release-cut`. Verified
|
| 568 |
+
by the embedder downloader at install time.
|
| 569 |
+
|
| 570 |
+
**What's gitignored (reproducible from source):**
|
| 571 |
+
|
| 572 |
+
- `vendors/dist/llamafile/<backend>/<ver>/ggml-*.so` β per-backend
|
| 573 |
+
staging DSOs.
|
| 574 |
+
- `dist/llamafile/*.llamafile` β final composite artifacts.
|
| 575 |
+
- `dist/llamafile/*.MANIFEST.json` β per-artifact manifests with
|
| 576 |
+
embedded-backend metadata.
|
| 577 |
+
|
| 578 |
+
**Release-cut workflow** (matches claude-hooks's
|
| 579 |
+
`make -C vendor/llamafile/dist`):
|
| 580 |
+
|
| 581 |
+
1. `bun run build:llamafile` β patched binary.
|
| 582 |
+
2. `bun run build:llamafile:all-backends` β every available DSO.
|
| 583 |
+
3. `bun run build:llamafile:package -- --with-model PATH` β bare
|
| 584 |
+
+ composite artifacts.
|
| 585 |
+
4. `bun run build:llamafile:release-cut` β refreshes the SHA
|
| 586 |
+
contract; prints the suggested `gh release create` invocation
|
| 587 |
+
(the actual publish is a user-confirmation step, not
|
| 588 |
+
automated).
|
| 589 |
+
5. User executes `gh release create` + `gh release upload`, then
|
| 590 |
+
commits the refreshed `SHA256SUMS.composite` + the bumped
|
| 591 |
+
`vendors/pin/embedder.txt`.
|
| 592 |
+
|
| 593 |
+
**Downloader behavior:** when `shaContractText` is provided to
|
| 594 |
+
`downloadEmbedder`, the pin's SHA is cross-checked against the
|
| 595 |
+
contract before any download or local-copy. Disagreement is a
|
| 596 |
+
hard error with a "redownload or rebuild" breadcrumb (matches
|
| 597 |
+
claude-hooks's `install.py`). An asset not present in the
|
| 598 |
+
contract is a soft warning, not an error β supports the
|
| 599 |
+
transitional state where the pin still references claude-hooks
|
| 600 |
+
pre-flip.
|
| 601 |
+
|
| 602 |
+
**What's NOT in M7:**
|
| 603 |
+
|
| 604 |
+
- Actual first `mann1x/opencoti` GitHub Release publish β that's
|
| 605 |
+
the user-confirmed external step the scaffolding enables.
|
| 606 |
+
- Pin flip from `mann1x/claude-hooks` β `mann1x/opencoti` β only
|
| 607 |
+
meaningful after the first release exists.
|
| 608 |
+
- macOS / aarch64 β same constraints as M6.
|
| 609 |
+
|
| 610 |
+
**Critical files (M6):**
|
| 611 |
+
|
| 612 |
+
- `packages/opencoti-llamafile/script/build-pipeline.ts` β new
|
| 613 |
+
subcommands.
|
| 614 |
+
- `packages/opencoti-llamafile/src/build-pipeline.ts` β pure
|
| 615 |
+
helpers + types.
|
| 616 |
+
- `packages/opencoti-llamafile/test/build-pipeline.test.ts` β
|
| 617 |
+
helper tests (19 new, 46 total).
|
| 618 |
+
- `packages/opencoti-embedder/src/download.ts` β local-asset
|
| 619 |
+
modes.
|
| 620 |
+
- `packages/opencoti-embedder/test/download.test.ts` β local-
|
| 621 |
+
asset tests (12 new, 13 total for download).
|
| 622 |
+
- `docs/features/llamafile_build.md` β toolchain + pipeline
|
| 623 |
+
documentation.
|
| 624 |
+
- Root `package.json` β new `build:llamafile:{rocm,vulkan,
|
| 625 |
+
all-backends,package}` scripts.
|
| 626 |
+
- `.gitignore` β `vendors/dist/`, `/dist/llamafile/`.
|
| 627 |
+
|
| 628 |
+
## Surgical-hook footprint
|
| 629 |
+
|
| 630 |
+
- M1βM4, M6: zero new hooks.
|
| 631 |
+
- M5: exactly one new hook (session-end memory ingest).
|
| 632 |
+
|
| 633 |
+
## Open questions
|
| 634 |
+
|
| 635 |
+
- **Release stream ownership.** Decision (M0): pin claude-hooks
|
| 636 |
+
v1.4.0. When opencoti's own release cadence starts, switch
|
| 637 |
+
the pin URL to `mann1x/opencoti`. Same SHA + same asset on
|
| 638 |
+
disk; just a different `repo` field.
|
| 639 |
+
- **Composite-llamafile build.** F4 consumes the prebuilt
|
| 640 |
+
asset. If F4 M3's lazy-context patch lands and we want it in
|
| 641 |
+
the embedder binary too, we need to build our own
|
| 642 |
+
composite-llamafile (vendored llamafile + Qwen3 GGUF +
|
| 643 |
+
`.args`). That's potentially a small new milestone (F4 M6)
|
| 644 |
+
that overlaps with F2 M6 (release artifact bundling).
|
| 645 |
+
- **Idle-reap vs always-on for the manager.** 5-min idle reap
|
| 646 |
+
matches claude-hooks. For opencoti, sessions are typically
|
| 647 |
+
longer; revisit if cold-start latency turns out to be an
|
| 648 |
+
issue in production use.
|
| 649 |
+
- **Sqlite-vec dep choice.** The Bun-native binding vs
|
| 650 |
+
`better-sqlite3` + the `sqlite-vec` extension. Decide at M5
|
| 651 |
+
based on which builds cleanly across Linux/macOS/Windows.
|
| 652 |
+
|
| 653 |
+
## Risks
|
| 654 |
+
|
| 655 |
+
- **F4 M3 patch surface area.** Slot-init touches code that F5's
|
| 656 |
+
ReST-KV (M1) and HeadInfer (M2) also modify. Ordering: F4 M3
|
| 657 |
+
lands first as patch `0001-`; F5's start at `0010-`. On a
|
| 658 |
+
later upstream pin bump, M3 needs to rebase first.
|
| 659 |
+
- **Asset hosting.** While we pin claude-hooks' release, the
|
| 660 |
+
embedder's lifecycle is tied to claude-hooks' tagging
|
| 661 |
+
cadence. Mitigation: mirror to opencoti's own release once
|
| 662 |
+
it's set up.
|
| 663 |
+
- **`@opencoti/tiers` β `@opencoti/embedder` boundary.** The
|
| 664 |
+
embedder depends on tiers (for GPU detect + download helper),
|
| 665 |
+
and tiers dynamic-imports `@opencoti/llamafile`. No cycle
|
| 666 |
+
introduced (embedder is the new edge of the DAG), but confirm
|
| 667 |
+
with `bun --cwd packages/opencoti-embedder typecheck` after
|
| 668 |
+
the shared-download refactor.
|
docs/features/opencoti_server.md
ADDED
|
@@ -0,0 +1,1010 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# F3 β opencoti-server (Go companion daemon)
|
| 2 |
+
|
| 3 |
+
> Parent: [../MASTER_PLAN.md](../MASTER_PLAN.md)
|
| 4 |
+
> Status: **planning**
|
| 5 |
+
> Owner: TBD
|
| 6 |
+
> Reference: `/shared/dev/claude-hooks` (the inspiration; mirror its
|
| 7 |
+
> goals, then go further)
|
| 8 |
+
|
| 9 |
+
## Problem
|
| 10 |
+
|
| 11 |
+
`claude-hooks` is a Python daemon that wraps Claude Code with a hook
|
| 12 |
+
API and adds persistent memory, multi-session coordination, tool
|
| 13 |
+
glue, and a small zoo of integrations. It works very well *for Claude
|
| 14 |
+
Code*. opencoti has the same shape of needs β and a few extra
|
| 15 |
+
because opencoti has its own tier engine, its own local llamafile,
|
| 16 |
+
and its own session model.
|
| 17 |
+
|
| 18 |
+
We want a Go daemon that is to opencoti what claude-hooks is to
|
| 19 |
+
Claude Code, **but deeper** because we control opencoti's source.
|
| 20 |
+
Instead of bolting onto a hook API from outside, opencoti-server can
|
| 21 |
+
be a first-class peer of opencoti's runtime.
|
| 22 |
+
|
| 23 |
+
## Goals
|
| 24 |
+
|
| 25 |
+
- **G1.** Single Go binary, `opencoti-server`, that exposes a small,
|
| 26 |
+
well-versioned API to opencoti.
|
| 27 |
+
- **G2.** Persistent memory (vector + KG) shared across opencoti
|
| 28 |
+
sessions on a host.
|
| 29 |
+
- **G3.** Multi-session coordination: opencoti CLI instances on the
|
| 30 |
+
same host see each other, share context where the user wants it.
|
| 31 |
+
- **G4.** Tool glue: a place to register external tools (search,
|
| 32 |
+
filesystem extensions, MCP-like) once and have them appear in all
|
| 33 |
+
opencoti sessions.
|
| 34 |
+
- **G5.** Event sink for tier-engine routing decisions (so we can
|
| 35 |
+
log, audit, and later learn from them).
|
| 36 |
+
- **G6.** Hook fan-out compatible with the existing claude-hooks
|
| 37 |
+
ecosystem where it makes sense β but not bound by it.
|
| 38 |
+
|
| 39 |
+
## Non-goals (for now)
|
| 40 |
+
|
| 41 |
+
- Replacing claude-hooks. opencoti-server is opencoti-specific.
|
| 42 |
+
- Becoming a generic "MCP host". MCP integration is a feature, not
|
| 43 |
+
the architecture.
|
| 44 |
+
- Multi-host federation. Single-host first.
|
| 45 |
+
|
| 46 |
+
## Design sketch
|
| 47 |
+
|
| 48 |
+
### Where it lives
|
| 49 |
+
|
| 50 |
+
- `opencoti/server/` β top-level non-bun source root. Go module.
|
| 51 |
+
Putting it outside `packages/` keeps the bun workspace clean.
|
| 52 |
+
- `packages/opencoti-server-client/` β TypeScript client used by
|
| 53 |
+
opencoti to talk to the server.
|
| 54 |
+
- **Surgical hooks** in opencode session lifecycle (session start,
|
| 55 |
+
session end, turn complete, tool invocation, model call result):
|
| 56 |
+
each hook is a one-line "if a client is available, notify it".
|
| 57 |
+
Listed in `docs/protocols/UPSTREAM_SYNC.md`.
|
| 58 |
+
|
| 59 |
+
### Transport
|
| 60 |
+
|
| 61 |
+
Unix domain socket on Linux/macOS, named pipe on Windows. Loopback
|
| 62 |
+
TCP as a fallback. HTTP/JSON wire format for simplicity (no gRPC
|
| 63 |
+
build dependency for the client).
|
| 64 |
+
|
| 65 |
+
**Daemon-launch policy (applies to opencoti-server and every
|
| 66 |
+
opencoti daemon)**:
|
| 67 |
+
|
| 68 |
+
- Listen port MUST come from opencoti's reserved
|
| 69 |
+
**47000-48000 range** β never 38000-39000 or 18790-18811
|
| 70 |
+
(claude-hooks territory). Tentative reservation table to
|
| 71 |
+
keep adjacent ports clear of accidental collision:
|
| 72 |
+
- **47092** β `@opencoti/embedder` (F4)
|
| 73 |
+
- **47190** β opencoti-server daemon (proposed default; revisit
|
| 74 |
+
when F3 M1 ships)
|
| 75 |
+
- **47191** β opencoti-server dashboard (proposed default)
|
| 76 |
+
- First-run setup MUST ask the user explicitly where the
|
| 77 |
+
daemon should bind: `127.0.0.1` (default), all interfaces,
|
| 78 |
+
or a specific IP. The default port from the reservation
|
| 79 |
+
table is proposed; the user may override either field within
|
| 80 |
+
the 47000-48000 range. Setup writes the validated choice to
|
| 81 |
+
the user's config.
|
| 82 |
+
|
| 83 |
+
### Persistence
|
| 84 |
+
|
| 85 |
+
Two backends, picked per-deployment:
|
| 86 |
+
|
| 87 |
+
- **pgvector** β same as solidPC's `claude-hooks` setup. Best when a
|
| 88 |
+
Postgres is already on the host.
|
| 89 |
+
- **sqlite-vec** β single-file, no external dependency. Default for
|
| 90 |
+
fresh installs.
|
| 91 |
+
|
| 92 |
+
The backend choice is config; the memory API is identical.
|
| 93 |
+
|
| 94 |
+
### Surface (illustrative)
|
| 95 |
+
|
| 96 |
+
```
|
| 97 |
+
POST /v1/sessions # register a session
|
| 98 |
+
POST /v1/sessions/:id/turns # record a turn
|
| 99 |
+
POST /v1/memory # store a memory (M2)
|
| 100 |
+
POST /v1/memory/search # vector recall (M2 β POST because the embedding is multi-KB)
|
| 101 |
+
POST /v1/memory/collections # collections CRUD (M2)
|
| 102 |
+
PUT /v1/memory/collections/:name/acl # per-session ACL (M2)
|
| 103 |
+
GET /v1/memory/count # count (M2)
|
| 104 |
+
POST /v1/memory/kg/entities # KG ops
|
| 105 |
+
POST /v1/memory/kg/relations
|
| 106 |
+
POST /v1/tier-events # log a tier-engine decision
|
| 107 |
+
GET /v1/tools # list registered tools
|
| 108 |
+
POST /v1/tools/:name/invoke # invoke (server-side glue)
|
| 109 |
+
GET /v1/healthz # M1; M2 extended with store_ok/store_path/embedding_dim/store_error
|
| 110 |
+
```
|
| 111 |
+
|
| 112 |
+
The server is **embedder-agnostic** at M2: clients pass vectors,
|
| 113 |
+
not text. The textβvector convenience (e.g.,
|
| 114 |
+
`GET /v1/memory?q=<text>` proxying to the embedder daemon) is a
|
| 115 |
+
deferred follow-up β adding it requires a runtime dep on
|
| 116 |
+
`@opencoti/embedder` (F4 M5) and is most naturally landed once
|
| 117 |
+
the TS client (M3) demonstrates the convenience is wanted at
|
| 118 |
+
the HTTP layer vs done client-side.
|
| 119 |
+
|
| 120 |
+
API is **versioned** (`/v1`) and the wire schema is owned by
|
| 121 |
+
`packages/opencoti-server-client/` (so opencoti can rev independently
|
| 122 |
+
of the server binary, within compatible versions).
|
| 123 |
+
|
| 124 |
+
### Mirroring claude-hooks goals
|
| 125 |
+
|
| 126 |
+
Same goals, mapped:
|
| 127 |
+
|
| 128 |
+
| claude-hooks | opencoti-server |
|
| 129 |
+
| --- | --- |
|
| 130 |
+
| `claude-hooks` recall hooks | implicit recall on `/v1/memory` query, surfaced by opencoti's prompt builder |
|
| 131 |
+
| `mcp__pgvector__*` MCP tools | `/v1/memory` (and a thin MCP shim if external MCP clients want it) |
|
| 132 |
+
| Stop hook auto-ingest | server records turn-complete events, applies same heuristics |
|
| 133 |
+
| `claude-hooks` companion tools (Episodic etc.) | first-class tools registered via `/v1/tools` |
|
| 134 |
+
|
| 135 |
+
### Deeper integration than claude-hooks
|
| 136 |
+
|
| 137 |
+
Because we own opencoti, we can:
|
| 138 |
+
|
| 139 |
+
- Receive **tier-engine** decisions directly (provider chosen,
|
| 140 |
+
escalation reasons, cost) without scraping logs.
|
| 141 |
+
- Receive **diff and tool-output** events as structured payloads, not
|
| 142 |
+
parsed from a transcript.
|
| 143 |
+
- Push back: server can suggest a memory recall payload that
|
| 144 |
+
opencoti **injects directly** into the prompt builder, instead of
|
| 145 |
+
appending to a transcript.
|
| 146 |
+
|
| 147 |
+
## Milestones
|
| 148 |
+
|
| 149 |
+
### M1 β Server skeleton + healthz *(2026-05-23 β shipped)*
|
| 150 |
+
|
| 151 |
+
- Go module under `opencoti/server/` (module
|
| 152 |
+
`github.com/mann1x/opencoti/server`, Go 1.22). Single dep:
|
| 153 |
+
`golang.org/x/sys` for the Windows `svc` packages.
|
| 154 |
+
- `opencoti-server serve` binds a UDS by default on Unix
|
| 155 |
+
(`$XDG_RUNTIME_DIR/opencoti/server.sock` or
|
| 156 |
+
`~/.opencoti/server.sock` fallback) or `tcp://127.0.0.1:47190`
|
| 157 |
+
on Windows. `--addr unix:///path` and `--addr tcp://host:port`
|
| 158 |
+
override.
|
| 159 |
+
- One HTTP endpoint: `GET /v1/healthz` returns 200 + JSON
|
| 160 |
+
`{status, version, started_at, uptime_seconds}`. All other
|
| 161 |
+
paths return a 404 envelope with code + message so the wire
|
| 162 |
+
format is consistent for M2+. `POST /v1/healthz` returns 405
|
| 163 |
+
with the same envelope.
|
| 164 |
+
- Graceful shutdown on SIGTERM/SIGINT (Unix) or SCM
|
| 165 |
+
Stop/Shutdown (Windows). UDS file unlinked on Unix shutdown.
|
| 166 |
+
- **Windows service support shipped in M1 (per user directive
|
| 167 |
+
2026-05-23):**
|
| 168 |
+
- `install` subcommand wraps SCM (`golang.org/x/sys/windows/svc/mgr`):
|
| 169 |
+
`--name`, `--display-name`, `--description`,
|
| 170 |
+
`--start-type=auto|manual|disabled`, `--addr`. Registers
|
| 171 |
+
the Event Log source under the same name.
|
| 172 |
+
- `uninstall` removes the SCM entry AND the Event Log source.
|
| 173 |
+
- `start` / `stop` wrap `mgr.Service.Start` /
|
| 174 |
+
`Service.Control(svc.Stop)`. `stop` waits up to 10s for the
|
| 175 |
+
state to become Stopped.
|
| 176 |
+
- When `svc.IsWindowsService()` returns true, `serve` enters
|
| 177 |
+
`svc.Run` with a handler that translates SCM Stop /
|
| 178 |
+
Shutdown into a graceful `http.Server.Shutdown`. Log
|
| 179 |
+
records flow to the Windows Application Event Log via
|
| 180 |
+
`eventlog.Open(name)`.
|
| 181 |
+
- Single source tree, build-tagged platform splits
|
| 182 |
+
(`*_windows.go` / `*_nonwindows.go`). Same binary surface
|
| 183 |
+
on every platform β `install` / `uninstall` / `start` /
|
| 184 |
+
`stop` on Unix print "Windows-only command" and exit 2.
|
| 185 |
+
- 7 cross-platform tests (parser + listener + healthz/404/405
|
| 186 |
+
+ UDS roundtrip + UDS unlink-on-cleanup + double-shutdown
|
| 187 |
+
safety) + 3 Windows-tagged tests for the `BuildMgrConfig`
|
| 188 |
+
install-config builder. All green on Linux; Windows
|
| 189 |
+
cross-compile clean.
|
| 190 |
+
- **Hook footprint: ZERO.** M1 is purely additive β no
|
| 191 |
+
`opencoti-hook:` markers, no UPSTREAM_SYNC.md registry rows.
|
| 192 |
+
The autostart hook lands in M3.
|
| 193 |
+
- **Deferred from M1:** Windows named-pipe TRANSPORT (use TCP
|
| 194 |
+
on Windows in M1; named-pipe support is a follow-up);
|
| 195 |
+
systemd/launchd unit files on Unix; CI workflows (the repo
|
| 196 |
+
has no CI yet β separate concern); the first-run setup
|
| 197 |
+
wizard step ("install as Windows service?" β comes when the
|
| 198 |
+
setup-flow picks up F3 concerns).
|
| 199 |
+
|
| 200 |
+
### M2 β Memory backend (sqlite-vec) + recall API *(2026-05-23 β shipped)*
|
| 201 |
+
|
| 202 |
+
- Go SQLite stack: `github.com/mattn/go-sqlite3` (cgo) +
|
| 203 |
+
`github.com/asg017/sqlite-vec-go-bindings/cgo`, sqlite-vec
|
| 204 |
+
statically linked via `sqlite_vec.Auto()` so the daemon ships
|
| 205 |
+
as a single binary (no `vec0.so` to bundle). CGO becomes a
|
| 206 |
+
hard build requirement at M2; cross-compile to Windows from
|
| 207 |
+
Linux needs MinGW-w64 (`apt install gcc-mingw-w64-x86-64`).
|
| 208 |
+
- `internal/store/store.go` defines a `Store` interface
|
| 209 |
+
mirroring the TS `MemoryStore` in
|
| 210 |
+
`packages/opencoti-memory/src/types.ts` 1:1. M7's pgvector
|
| 211 |
+
backend will implement the same interface, so the HTTP layer
|
| 212 |
+
doesn't change.
|
| 213 |
+
- `internal/store/sqlitevec/` is the M2 backend. DDL mirrors
|
| 214 |
+
`packages/opencoti-memory/src/schema.ts` byte-functionally:
|
| 215 |
+
four tables (`meta`, `collections`, `memories`, `session_acl`)
|
| 216 |
+
plus the `memory_vecs` vec0 virtual table; `SCHEMA_VERSION=1`;
|
| 217 |
+
`embedding_dim` parameter-substituted into the vec0 DDL at
|
| 218 |
+
first creation and stored in `meta` for validation on later
|
| 219 |
+
opens. Content idempotency: SHA256-hex with
|
| 220 |
+
`UNIQUE(collection_name, content_hash)`. Pure `ResolveAccessMode`
|
| 221 |
+
mirrors the TS resolver (global=r, session-owner=rw,
|
| 222 |
+
others=none; explicit `session_acl` rows override). Recall
|
| 223 |
+
is vec0 MATCH + overscan
|
| 224 |
+
(`k * min(8, max(2, len(candidates)))`) + join-back +
|
| 225 |
+
ACL filter, truncated to k.
|
| 226 |
+
- **DB schema is wire-compatible with the in-process
|
| 227 |
+
`@opencoti/memory` package** β a DB created by either side
|
| 228 |
+
opens cleanly under the other. This is the load-bearing
|
| 229 |
+
invariant for M3's path-B refactor.
|
| 230 |
+
- HTTP surface (eight new routes, all under `/v1/memory/*`):
|
| 231 |
+
- `POST /v1/memory/collections` β create (201 / 409
|
| 232 |
+
collection_exists / 400 invalid_collection_name).
|
| 233 |
+
- `GET /v1/memory/collections?session_id=...` β list,
|
| 234 |
+
optionally filtered through the ACL resolver.
|
| 235 |
+
- `DELETE /v1/memory/collections/{name}` β cascade-deletes
|
| 236 |
+
memories + vec0 rows + session_acl entries.
|
| 237 |
+
- `PUT /v1/memory/collections/{name}/acl` β set per-session
|
| 238 |
+
ACL (`r`/`w`/`rw`/`none`).
|
| 239 |
+
- `GET /v1/memory/collections/{name}/acl?session_id=...` β
|
| 240 |
+
read the resolved access mode.
|
| 241 |
+
- `POST /v1/memory` β store. 422 dim_mismatch on length
|
| 242 |
+
mismatch; 403 write_denied on ACL deny.
|
| 243 |
+
- `POST /v1/memory/search` β vector recall (POST because
|
| 244 |
+
a 1024-element Float32 array doesn't fit a query string).
|
| 245 |
+
- `GET /v1/memory/count?collection=...` β count, optionally
|
| 246 |
+
scoped to one collection.
|
| 247 |
+
- All error responses use M1's `{error: {code, message}}`
|
| 248 |
+
envelope. Embeddings on the wire are JSON `[float32, ...]`
|
| 249 |
+
arrays (verbose but trivial for the M3 TS client; base64 raw
|
| 250 |
+
bytes is a follow-up if wire size measurably matters).
|
| 251 |
+
- `/v1/healthz` extended with `store_ok`, `store_path`,
|
| 252 |
+
`embedding_dim`, and `store_error` (omitempty). When the
|
| 253 |
+
store fails to open, the daemon still serves `/v1/healthz`
|
| 254 |
+
with `store_ok=false` and `/v1/memory/*` returns 503
|
| 255 |
+
`store_unavailable` β partial degradation beats refuse-to-start
|
| 256 |
+
so the daemon stays observable when something's wrong with the DB.
|
| 257 |
+
- `serve` gets `--db-path` (default
|
| 258 |
+
`$HOME/.opencoti/memory/state.db` β matches the TS
|
| 259 |
+
`defaultDbPath()` exactly so both implementations point at the
|
| 260 |
+
same file by default) and `--embedding-dim` (default 1024,
|
| 261 |
+
matches `DEFAULT_EMBEDDING_DIM`). `install` bakes both into
|
| 262 |
+
the SCM service-start arguments.
|
| 263 |
+
- 30 tests cover the surface: 18 unit tests on the store
|
| 264 |
+
conformance (collection lifecycle, ACL resolver, idempotent
|
| 265 |
+
store, recall ordering, count, close+reopen, dim-mismatch
|
| 266 |
+
paths) + 12 HTTP integration tests via `httptest.NewServer`
|
| 267 |
+
(every endpoint, happy + 4xx + 5xx envelopes).
|
| 268 |
+
- `testdata/crosslang/` ships a GoβBun schema-compat probe:
|
| 269 |
+
Go writes a DB β optionally invokes a Bun script that opens
|
| 270 |
+
the same DB via `@opencoti/memory` and confirms readback.
|
| 271 |
+
Skips cleanly when bun or the workspace package isn't on
|
| 272 |
+
PATH. `make test-cross-lang` is always safe to run.
|
| 273 |
+
- **Hook footprint: ZERO** (unchanged from M1). M2 is purely
|
| 274 |
+
additive β the API exists but no surgical hooks into opencode
|
| 275 |
+
yet.
|
| 276 |
+
- **Deferred from M2:** textβvector convenience
|
| 277 |
+
(`GET /v1/memory?q=<text>`); streaming recall (SSE);
|
| 278 |
+
pagination on list-collections; authentication for TCP
|
| 279 |
+
transport (UDS owner-only on Unix is the M2 security
|
| 280 |
+
boundary); pgvector backend (M7); migration tools (schemas
|
| 281 |
+
are identical β the DB just opens); server-side embedder
|
| 282 |
+
cache; bulk-store / batch endpoints; base64 raw-byte
|
| 283 |
+
embedding wire format.
|
| 284 |
+
|
| 285 |
+
### M3 β TS client + opencoti hook to start the server *(2026-05-23 β shipped)*
|
| 286 |
+
|
| 287 |
+
- `@opencoti/opencoti-server-client` published in-workspace under
|
| 288 |
+
`packages/opencoti-server-client/`. Single class
|
| 289 |
+
`OpencotiServerClient` implements `MemoryStore` over HTTP β all
|
| 290 |
+
eight `/v1/memory/*` routes from M2 plus a `healthz()` probe.
|
| 291 |
+
Wire format is M2's verbatim: JSON `[float32, ...]` for embeddings;
|
| 292 |
+
M1/M2's `{error: {code, message}}` envelope decoded into a
|
| 293 |
+
sentinel `OpencotiServerError({status, code, message})` so callers
|
| 294 |
+
can match on `code === "store_unavailable"`, `dim_mismatch`, etc.
|
| 295 |
+
Per-request `AbortController` + 5 s default timeout. `close()` is
|
| 296 |
+
a documented no-op (no per-instance handle to close).
|
| 297 |
+
- **Transport** uses Bun's native `fetch` β `tcp://host:port` is
|
| 298 |
+
rewritten to `http://...` in `toFetchBase`; `unix:///path` passes
|
| 299 |
+
through verbatim (Bun supports UDS fetch natively, no custom
|
| 300 |
+
adapter needed). `defaultAddress(platform)` mirrors the Go
|
| 301 |
+
`DefaultAddress()` exactly: `$XDG_RUNTIME_DIR/opencoti/server.sock`
|
| 302 |
+
on Linux (`$HOME/.opencoti/server.sock` fallback), and
|
| 303 |
+
`tcp://127.0.0.1:47190` on Windows.
|
| 304 |
+
- **Autostart** lives in `src/autostart.ts`:
|
| 305 |
+
`startAutostart({address, binaryPath?, dbPath?, embeddingDim?, ...})`
|
| 306 |
+
probes `/v1/healthz` first (200 β `already_running`), locates the
|
| 307 |
+
binary via `Bun.which` if none was passed (β `no_binary` when
|
| 308 |
+
missing), spawns with `stdio: "ignore"` (mirroring the
|
| 309 |
+
`@opencoti/embedder` manager pattern), and polls `/v1/healthz`
|
| 310 |
+
every 200 ms until the deadline (default 5 s; β
|
| 311 |
+
`ready_timeout` if never recovers). Returns a discriminated
|
| 312 |
+
`AutostartOutcome` so consumers can log five distinct states
|
| 313 |
+
(`already_running | spawned | no_binary | spawn_failed | ready_timeout`)
|
| 314 |
+
without exception handling. Test seams: `spawnImpl`, `fetchImpl`,
|
| 315 |
+
`whichImpl`, `sleepImpl`.
|
| 316 |
+
- **Surgical hook footprint: 3 markers + 1 package.json dep.** All
|
| 317 |
+
three TS markers land in `packages/opencode/src/config/config.ts`
|
| 318 |
+
(the same file that already carries `opencoti-default-plugins`):
|
| 319 |
+
the import next to it, the `opencoti.server.*` schema struct
|
| 320 |
+
(`autostart: boolean`, `address: string`, `binary_path: string`,
|
| 321 |
+
`db_path: string`, `embedding_dim: PositiveInt`), and the
|
| 322 |
+
fire-and-forget `void maybeStartOpencotiServer(...)` call site
|
| 323 |
+
right after the existing `applyDefaultPlugins(...)` call. The
|
| 324 |
+
hook's full substance lives in
|
| 325 |
+
`@opencoti/opencoti-server-client/autostart-hook` so the
|
| 326 |
+
opencode-side surface stays minimal β three lines plus the
|
| 327 |
+
package.json dep (no anchor comment in JSON, registered in the
|
| 328 |
+
table).
|
| 329 |
+
- **Plugin swap.** Both `@opencoti/memory-plugin` and
|
| 330 |
+
`@opencoti/tui-memory` accept a new optional
|
| 331 |
+
`server_address?: string`. The default `StoreFactory`:
|
| 332 |
+
if `serverAddress` is set, instantiate `OpencotiServerClient`,
|
| 333 |
+
call `healthz()`, and return the client when
|
| 334 |
+
`storeOk === true`; otherwise silently fall through to the
|
| 335 |
+
existing in-process `SqliteVecStore.open(...)`. The
|
| 336 |
+
`__setStoreFactory` test seam is unchanged β existing tests
|
| 337 |
+
continue to inject stubs that ignore `serverAddress`.
|
| 338 |
+
- **Health envelope mismatch detection.** Client validates that
|
| 339 |
+
`embedding_dim` from the server matches the configured dim;
|
| 340 |
+
on mismatch, `healthz()` returns `storeOk: false` with a
|
| 341 |
+
`dim mismatch` error so the plugin falls back to the
|
| 342 |
+
in-process store rather than corrupting the wire format.
|
| 343 |
+
This is **client-side**; the server's own dim check still
|
| 344 |
+
fires on `POST /v1/memory`.
|
| 345 |
+
- **Tests.** 56 new tests in the client package: 33 mocked-fetch
|
| 346 |
+
client conformance (every endpoint, happy + 4xx + 5xx envelopes,
|
| 347 |
+
dedup, dim mismatch, store_unavailable propagation), 11 pure
|
| 348 |
+
tests for `defaultAddress` / `toFetchBase` / `joinURL` /
|
| 349 |
+
`probeReady`, 6 mocked-spawn `startAutostart` cases (already
|
| 350 |
+
running, no binary, spawned-then-ready, ready timeout,
|
| 351 |
+
spawn failed, --addr passthrough). Plus 2 new cases in the
|
| 352 |
+
memory-plugin tests and 3 new in the tui-memory tests for the
|
| 353 |
+
`server_address` swap path. All 23 workspace packages
|
| 354 |
+
typecheck clean; 82 tests pass across the three affected
|
| 355 |
+
packages.
|
| 356 |
+
- **Live verification.** Manual end-to-end smoke against the
|
| 357 |
+
F3 M2 binary (TCP loopback): `healthz` returns the expected
|
| 358 |
+
shape (`store_ok: true`, `embedding_dim: 64`), `createCollection`
|
| 359 |
+
+ `setSessionAcl(rw)` + `store(...)` (with `deduplicated:
|
| 360 |
+
false` then `true` on idempotency) + `recall(...)` (distance
|
| 361 |
+
0 on exact match) + `count(...)` (returns 1) + `deleteCollection`
|
| 362 |
+
all return the expected shapes. UDS path tested at the
|
| 363 |
+
discover / autostart layer; full UDS smoke deferred to
|
| 364 |
+
M4 when session-event hooks bring it under day-to-day use.
|
| 365 |
+
- **Path-B loop closed.** With M3 in, the F4 M5
|
| 366 |
+
in-process-then-daemon migration path described in
|
| 367 |
+
[memory_embedder.md](memory_embedder.md) is mechanically
|
| 368 |
+
complete: setting `opencoti.server.address` (or `autostart:
|
| 369 |
+
true`) in `opencode.jsonc` is the only change a user makes
|
| 370 |
+
to switch from the in-process store to the Go daemon. No
|
| 371 |
+
code paths change in `@opencoti/memory` itself.
|
| 372 |
+
- **Deferred from M3:** detached / setsid daemon lifecycle
|
| 373 |
+
(M3's spawn dies with opencode β cross-session sharing
|
| 374 |
+
requires running the daemon externally, e.g. the M1 Windows
|
| 375 |
+
service installer or a future systemd unit); the first-run
|
| 376 |
+
"install as a service?" wizard step (a follow-up once the
|
| 377 |
+
setup-flow picks up F3 concerns); a text-`q=` convenience
|
| 378 |
+
endpoint (`GET /v1/memory?q=<text>`) β every M3 caller has
|
| 379 |
+
the embedder in-process and can pre-embed; retry / backoff
|
| 380 |
+
on transient HTTP errors (single attempt, the plugin's
|
| 381 |
+
silent fallback handles the failure case); bulk-store /
|
| 382 |
+
batch endpoints; TLS / shared-secret authentication for TCP
|
| 383 |
+
transport (UDS owner-only remains the M3 security boundary).
|
| 384 |
+
|
| 385 |
+
### M4 β Session + turn events flowing *(2026-05-23 β shipped)*
|
| 386 |
+
|
| 387 |
+
**Design pivot vs the original spec.** M4 was originally specced
|
| 388 |
+
as *surgical hooks at session start/end/turn complete*. Phase-1
|
| 389 |
+
exploration confirmed opencode's `@opencode-ai/plugin` API
|
| 390 |
+
already exposes those events fully typed via `Hooks.event`
|
| 391 |
+
(session.created/updated/deleted/error/idle plus
|
| 392 |
+
session.status). Following the precedent set by M5-D2
|
| 393 |
+
(`@opencoti/memory-plugin`), M4 ships as a plugin with **zero
|
| 394 |
+
opencoti-hook footprint** β strictly richer than a surgical hook
|
| 395 |
+
for this use case (typed payloads, zero upstream-source touch,
|
| 396 |
+
no UPSTREAM_SYNC.md row to maintain on every sync). Hook count
|
| 397 |
+
stays at 9 source markers + 4 JSON deps (unchanged from F3 M3).
|
| 398 |
+
|
| 399 |
+
- **Per-feature schema versioning.** `meta.schema_version` stays
|
| 400 |
+
at 1 (memory tables β kept stable so M2/M3 TypeScript
|
| 401 |
+
`@opencoti/memory` clients still open M4 DBs cleanly). New
|
| 402 |
+
`meta.sessions_schema_version = 1` is written by M4+ daemons
|
| 403 |
+
and reported on `/v1/healthz`. M3 clients without the key
|
| 404 |
+
treat the daemon as pre-M4; M4 plugins disable forwarding
|
| 405 |
+
silently when `sessionsSchemaVersion < 1`.
|
| 406 |
+
- **Three new tables** in the existing `state.db` (no new
|
| 407 |
+
`--flag`, no migration tool):
|
| 408 |
+
- `sessions` β soft-delete via `deleted_at`. Indices on
|
| 409 |
+
`parent_id` (fork-tree queries) and `deleted_at`
|
| 410 |
+
(cheap `WHERE deleted_at IS NULL`). Upsert is idempotent
|
| 411 |
+
on `id`; `INSERT ... ON CONFLICT(id) DO UPDATE SET ...`
|
| 412 |
+
overwrites all columns from the latest snapshot.
|
| 413 |
+
- `session_events` β append-only audit log. `FOREIGN KEY ...
|
| 414 |
+
ON DELETE CASCADE` would normally wipe history on
|
| 415 |
+
`DELETE FROM sessions`; we use soft-delete instead so the
|
| 416 |
+
audit trail outlives the session row.
|
| 417 |
+
- `session_messages` β UPSERT on opencode `MessageID`. The
|
| 418 |
+
plugin re-sends the full latest snapshot on every turn
|
| 419 |
+
boundary; SQLite UPSERT handles dedup.
|
| 420 |
+
- **SessionStore interface** (`internal/store/store.go`): 8
|
| 421 |
+
methods + sentinel errors (`ErrSessionNotFound`,
|
| 422 |
+
`ErrInvalidSessionID`, `ErrSessionPayloadTooLarge`). The
|
| 423 |
+
same `*sqlitevec.Store` implements both `Store` (memory) and
|
| 424 |
+
`SessionStore`; server.go type-asserts at each entry point.
|
| 425 |
+
M7 pgvector will implement the same interface.
|
| 426 |
+
- **HTTP surface** under `/v1/sessions/*`:
|
| 427 |
+
- `POST /v1/sessions` β upsert (201 on insert, 200 on update,
|
| 428 |
+
400 invalid_session_id, 413 payload_too_large).
|
| 429 |
+
- `GET /v1/sessions[?parent_id=&include_deleted=&limit=&offset=]`
|
| 430 |
+
β list, default newest-first by `updated_at`, omitting
|
| 431 |
+
soft-deleted rows.
|
| 432 |
+
- `GET /v1/sessions/{id}` / `DELETE /v1/sessions/{id}` β
|
| 433 |
+
fetch / soft-delete.
|
| 434 |
+
- `POST /v1/sessions/{id}/events` /
|
| 435 |
+
`GET /v1/sessions/{id}/events[?type=&limit=&offset=]` β
|
| 436 |
+
append + list audit entries.
|
| 437 |
+
- `POST /v1/sessions/{id}/messages` /
|
| 438 |
+
`GET /v1/sessions/{id}/messages[?limit=&offset=&since=]` β
|
| 439 |
+
bulk-upsert (idempotent on message id) + list.
|
| 440 |
+
- `GET /v1/sessions/{id}/turns` β coarse server-side
|
| 441 |
+
projection grouping messages by user-message boundaries.
|
| 442 |
+
Authoritative turn-boundary logic stays in opencode's
|
| 443 |
+
`compaction.ts`; this view is for UI / debugging.
|
| 444 |
+
- **healthz extension**: `sessions_schema_version` field
|
| 445 |
+
added; M3 clients without the field treat the daemon as
|
| 446 |
+
pre-M4.
|
| 447 |
+
- **Payload size guards**: 8 MiB hard cap on
|
| 448 |
+
`session_events.payload` and the bulk-upsert messages body
|
| 449 |
+
(413 `payload_too_large`); the plugin chunks at a softer
|
| 450 |
+
1 MiB default before posting.
|
| 451 |
+
- **TS client extension** (`@opencoti/opencoti-server-client`):
|
| 452 |
+
8 new `OpencotiServerClient` methods (`upsertSession`,
|
| 453 |
+
`listSessions`, `getSession`, `deleteSession`,
|
| 454 |
+
`appendSessionEvent`, `listSessionEvents`,
|
| 455 |
+
`upsertSessionMessages`, `listSessionMessages`,
|
| 456 |
+
`listSessionTurns`). Wire format snake_case β camelCase via
|
| 457 |
+
private mappers (same pattern as M2's `mapHit` /
|
| 458 |
+
`mapCollectionInfo`). `healthz()` return type +
|
| 459 |
+
`ProbeResult` gain `sessionsSchemaVersion?: number`.
|
| 460 |
+
- **New plugin `@opencoti/session-events-plugin`**:
|
| 461 |
+
subscribes to `session.created/updated/deleted/error/idle`
|
| 462 |
+
and `session.status` (idle transition) via opencode's
|
| 463 |
+
typed `Hooks.event`; does **not** subscribe to
|
| 464 |
+
`message.updated` (fires thousands of times per turn β
|
| 465 |
+
text deltas, tool calls, reasoning chunks β and forwarding
|
| 466 |
+
every delta would melt the daemon). On idle, the plugin
|
| 467 |
+
fetches the session's latest messages via
|
| 468 |
+
`input.client.session.messages.list` and bulk-upserts in
|
| 469 |
+
byte-bounded chunks. Maintains a per-session merge cache so
|
| 470 |
+
opencode's partial `session.updated` payloads are reconciled
|
| 471 |
+
into a full snapshot before reaching the daemon.
|
| 472 |
+
Best-effort, never-throws: a forwarding plugin must not
|
| 473 |
+
propagate daemon failures into the opencode session.
|
| 474 |
+
- **session.error before sessionID exists** lands on a
|
| 475 |
+
synthetic `__pre_session_errors__` session (lazily upserted
|
| 476 |
+
on first such error) so the audit log captures pre-creation
|
| 477 |
+
failures without losing context.
|
| 478 |
+
- **Auto-wire**: one-line addition to
|
| 479 |
+
`@opencoti/tiers/default-plugins` `OPENCOTI_DEFAULT_PLUGINS`.
|
| 480 |
+
Any user with opencoti config in `opencode.jsonc` gets the
|
| 481 |
+
plugin automatically; without an `opencoti.server.address`
|
| 482 |
+
the plugin's healthz probe fails, the internal client stays
|
| 483 |
+
`undefined`, and every event handler early-returns β same
|
| 484 |
+
no-op-when-unconfigured ergonomic the memory-plugin uses.
|
| 485 |
+
- **Tests**: 26 new Go tests (16 store unit + 10 HTTP
|
| 486 |
+
integration) β total Go test count climbs from 30 β 63.
|
| 487 |
+
14 new TS client tests (33 β 47). 16 new plugin tests in
|
| 488 |
+
`@opencoti/session-events-plugin`. All 24 workspace
|
| 489 |
+
packages typecheck clean.
|
| 490 |
+
- **Live verification**: end-to-end smoke against a real M4
|
| 491 |
+
daemon (TCP loopback) confirmed healthz reports
|
| 492 |
+
`sessions_schema_version: 1`, full upsert β update β get β
|
| 493 |
+
list β events β messages β turns β soft-delete cycle
|
| 494 |
+
works, soft-deleted session's events + messages remain
|
| 495 |
+
queryable through `include_deleted=true`.
|
| 496 |
+
- **Deferred from M4**:
|
| 497 |
+
- **`message.updated` real-time forwarding** β coalescing on
|
| 498 |
+
idle is the right granularity; per-delta forwarding would
|
| 499 |
+
mean O(text-deltas) HTTP roundtrips per turn.
|
| 500 |
+
- **Per-tool-invocation events** as first-class rows β
|
| 501 |
+
captured today inside the message's `parts` JSON; can be
|
| 502 |
+
promoted to a `session_tool_calls` table later if needed.
|
| 503 |
+
- **WebSocket / SSE push from daemon β client** β
|
| 504 |
+
request-response only in M4.
|
| 505 |
+
- **TLS / authentication for `/v1/sessions/*` TCP transport**
|
| 506 |
+
β UDS owner-only on Unix remains the M4 security boundary.
|
| 507 |
+
|
| 508 |
+
### M5 β Tier-engine event sink (shipped 2026-05-23)
|
| 509 |
+
|
| 510 |
+
- **Daemon side: `/v1/tier-events`.**
|
| 511 |
+
- `POST /v1/tier-events` appends a row (201; 400 invalid_tier_event;
|
| 512 |
+
413 payload_too_large at the same 8 MiB cap as `/v1/sessions/*`).
|
| 513 |
+
Body: `{event_type, ts?, session_id?, payload}` where `payload`
|
| 514 |
+
is opaque JSON β the daemon does not introspect it.
|
| 515 |
+
- `GET /v1/tier-events?session_id=&event_type=&since=&limit=&offset=`
|
| 516 |
+
returns the audit log newest-first.
|
| 517 |
+
- One new `tier_events` table: `(id PK AUTO, session_id TEXT,
|
| 518 |
+
event_type TEXT, payload TEXT, ts INTEGER)` + two indices
|
| 519 |
+
`(session_id, ts)` and `(event_type, ts)`. **`session_id` is a
|
| 520 |
+
soft reference β no FK, no cascade.** Tier events fire intra-turn
|
| 521 |
+
and can land before `session.created` reaches the daemon; the
|
| 522 |
+
soft reference guarantees no event is dropped.
|
| 523 |
+
- Per-feature `meta.tier_events_schema_version = 1`. The
|
| 524 |
+
cross-language sqlite_vec contract continues to live on
|
| 525 |
+
`meta.schema_version = 1` (still M2-compatible with the TS
|
| 526 |
+
`@opencoti/memory` reader); the new key is independent.
|
| 527 |
+
|
| 528 |
+
- **TS plumbing.** `@opencoti/opencoti-server-client` gains
|
| 529 |
+
`appendTierEvent` + `listTierEvents` (camelCaseβsnake_case wire
|
| 530 |
+
mapping mirroring `mapSessionMessage`). `healthz()` surfaces
|
| 531 |
+
`tierEventsSchemaVersion?: number`; plugins use it as their
|
| 532 |
+
feature gate.
|
| 533 |
+
|
| 534 |
+
- **Design pivot continues: plugin path > surgical hook.** The
|
| 535 |
+
`Telemetry` interface in `@opencoti/tiers` (designed for this
|
| 536 |
+
milestone β its docstring at `telemetry.ts:1-4` said so) is
|
| 537 |
+
already threaded end-to-end through `runtime.ts:99 β
|
| 538 |
+
executor.ts β escalator.ts β fanout.ts`. M5 plugs a real sink
|
| 539 |
+
into the existing seam via a new module-level
|
| 540 |
+
`registered-telemetry.ts` slot (mirrors `active-config.ts`).
|
| 541 |
+
Precedence chain in `runtime.ts:99` becomes:
|
| 542 |
+
|
| 543 |
+
```ts
|
| 544 |
+
const telemetry =
|
| 545 |
+
hook.telemetry ??
|
| 546 |
+
execOpts.telemetry ??
|
| 547 |
+
getRegisteredTelemetry() ??
|
| 548 |
+
noopTelemetry
|
| 549 |
+
```
|
| 550 |
+
|
| 551 |
+
The new `@opencoti/tier-events-plugin` calls
|
| 552 |
+
`setRegisteredTelemetry(impl)` during plugin boot when the
|
| 553 |
+
daemon's healthz reports `tierEventsSchemaVersion >= 1`. **Zero
|
| 554 |
+
new opencoti-hook source markers** β count unchanged from F3 M4
|
| 555 |
+
(9 source + 4 JSON deps + 3 prose). See the F1 page for where
|
| 556 |
+
the Telemetry interface itself was added.
|
| 557 |
+
|
| 558 |
+
- **Fire-and-forget posture.** The plugin's Telemetry impl maps
|
| 559 |
+
each method to a `client.appendTierEvent(...).catch(() => {})`
|
| 560 |
+
β synchronous to the caller, HTTP swallowed on failure. The
|
| 561 |
+
tier engine never blocks on the daemon. Cf. M4's
|
| 562 |
+
`@opencoti/session-events-plugin`, same design.
|
| 563 |
+
|
| 564 |
+
- **Auto-wire via defaults.** `@opencoti/tier-events-plugin` is in
|
| 565 |
+
`OPENCOTI_DEFAULT_PLUGINS` alongside the M4 session plugin. Any
|
| 566 |
+
user with `opencoti: {...}` in their config gets the audit log
|
| 567 |
+
for free; if the daemon is unreachable, the healthz gate fails
|
| 568 |
+
and the runtime stays on noop.
|
| 569 |
+
|
| 570 |
+
- **Cross-version compat.** An M5 plugin against an M4 daemon
|
| 571 |
+
sees `sessions_schema_version: 1` but no
|
| 572 |
+
`tier_events_schema_version` in healthz, so
|
| 573 |
+
`setRegisteredTelemetry` is never called and the runtime stays
|
| 574 |
+
on noop. An M5 daemon against M4 clients keeps shipping the
|
| 575 |
+
same `/v1/sessions/*` surface untouched.
|
| 576 |
+
|
| 577 |
+
- **Explicitly deferred to later:** aggregation endpoints
|
| 578 |
+
(`/v1/tier-events/stats?...`), real-time push (WS/SSE),
|
| 579 |
+
server-side retention policy, client-side batching/coalescing,
|
| 580 |
+
FK on `session_id`. M5 ships raw events + filters; everything
|
| 581 |
+
on top of that is composable later.
|
| 582 |
+
|
| 583 |
+
### M6 β Tool registry + invocation (shipped 2026-05-23)
|
| 584 |
+
|
| 585 |
+
- **Daemon side: `/v1/tools`.**
|
| 586 |
+
- `GET /v1/tools` lists every registered tool sorted by name:
|
| 587 |
+
`{ tools: [{ name, description, params_schema, handler,
|
| 588 |
+
created_at, updated_at }] }`. `params_schema` is a
|
| 589 |
+
string-serialised JSON Schema; `handler` is a stable opaque
|
| 590 |
+
dispatch ID (debug-only β invocation always goes by name).
|
| 591 |
+
- `GET /v1/tools/{name}` fetches one (404 tool_not_found).
|
| 592 |
+
- `POST /v1/tools/{name}/invoke` runs it. Body
|
| 593 |
+
`{ arguments: {...}, session_id? }` β `{ result: {...} }` (200)
|
| 594 |
+
or `{ error: { code, message } }` (400 invalid_arguments / 404
|
| 595 |
+
tool_not_found / 500 handler_failed / 413 payload_too_large at
|
| 596 |
+
the same 8 MiB cap as `/v1/sessions/*` and `/v1/tier-events`).
|
| 597 |
+
- One `tools` table: `(name PK, description, params_schema,
|
| 598 |
+
handler, created_at, updated_at)`. Process-scoped, no
|
| 599 |
+
session FK. Dispatch is a daemon-internal
|
| 600 |
+
`map[string]ToolHandler` in `toolhandlers.go`; **no external
|
| 601 |
+
registration in M6** (no POST/DELETE on `/v1/tools` itself) β
|
| 602 |
+
the catalog is seeded into the binary at boot.
|
| 603 |
+
- Per-feature `meta.tools_schema_version = 1`, independent of the
|
| 604 |
+
cross-language `meta.schema_version = 1` (still M2-compatible
|
| 605 |
+
with the TS `@opencoti/memory` reader).
|
| 606 |
+
|
| 607 |
+
- **Two seed tools** are `UpsertTool`'d at startup (idempotent β
|
| 608 |
+
`created_at` fixed on first insert, `updated_at` bumps on
|
| 609 |
+
re-seed):
|
| 610 |
+
- **`opencoti_episodic_search`** β lexical `LIKE` search across
|
| 611 |
+
the M4 `session_messages` table joined with `sessions`,
|
| 612 |
+
newest-first. Params `{ query, limit?, session_id_excludes? }`.
|
| 613 |
+
The exclude list lets the plugin pass the live session ID so
|
| 614 |
+
the model recalls *other* sessions, not echoes of the current
|
| 615 |
+
one. The integrated analog of claude-hooks's `episodic_server`
|
| 616 |
+
β same SQLite file the sessions API writes, no second daemon,
|
| 617 |
+
no shell-out. FTS5/vector ranking is an M7+ optimisation.
|
| 618 |
+
- **`opencoti_recall`** β vector recall over the M2 memory store,
|
| 619 |
+
delegating to the same `Store.Recall` path `/v1/memory/search`
|
| 620 |
+
uses. Params `{ embedding, collection?, k?, session_id? }`.
|
| 621 |
+
**Takes a pre-computed embedding, not a query string** β the
|
| 622 |
+
daemon bundles no embedder, so the caller (the plugin, or an
|
| 623 |
+
operator via curl) embeds the query and POSTs the float vector;
|
| 624 |
+
length must equal the daemon's `embedding_dim`. `session_id`
|
| 625 |
+
drives the M2 per-session ACL check. Coexists intentionally
|
| 626 |
+
with `@opencoti/memory`'s in-process `__memory_recall`: that
|
| 627 |
+
one survives a daemon being down; `opencoti_recall` gives the
|
| 628 |
+
same surface to plugins without direct DB access.
|
| 629 |
+
|
| 630 |
+
- **TS plumbing.** `@opencoti/opencoti-server-client` gains
|
| 631 |
+
`listTools` + `getTool` + `invokeTool`; `healthz()` surfaces
|
| 632 |
+
`toolsSchemaVersion?: number` as the plugin's feature gate.
|
| 633 |
+
`ServerTool` is camelCase (`paramsSchema` β `params_schema`),
|
| 634 |
+
`paramsSchema` left as `unknown` (interpreted in the plugin).
|
| 635 |
+
|
| 636 |
+
- **Design pivot continues: plugin path > surgical hook.** A new
|
| 637 |
+
module-level `registered-server-tools.ts` slot in
|
| 638 |
+
`@opencoti/tiers` (mirrors M5's `registered-telemetry.ts`) holds
|
| 639 |
+
the daemon tools as a `Record<string, Tool>`; `runtime.ts` merges
|
| 640 |
+
it **last** into the tier tool list, next to synthetic-tier and
|
| 641 |
+
memory-bridge tools:
|
| 642 |
+
|
| 643 |
+
```ts
|
| 644 |
+
const tools = mergeTools(hook.prepared.tools, {
|
| 645 |
+
...syntheticTools(hook.input.sessionID, telemetry),
|
| 646 |
+
...memoryTools,
|
| 647 |
+
...(getRegisteredServerTools() ?? {}),
|
| 648 |
+
})
|
| 649 |
+
```
|
| 650 |
+
|
| 651 |
+
Server tools merge last so user-explicit + memory tools win on a
|
| 652 |
+
name collision (none expected β server tools are `opencoti_*`,
|
| 653 |
+
memory `__memory_*`, synthetic-tier `__tier_*`). **Zero new
|
| 654 |
+
opencoti-hook source markers** β count unchanged from F3 M4/M5
|
| 655 |
+
(9 source + 4 JSON deps + 3 prose).
|
| 656 |
+
|
| 657 |
+
- **The `@opencoti/server-tools-plugin`** probes `/v1/healthz`
|
| 658 |
+
(gated on `toolsSchemaVersion >= 1`), fetches `/v1/tools` once on
|
| 659 |
+
boot, and builds an AI SDK `dynamicTool` per entry:
|
| 660 |
+
`params_schema` round-trips through `ai`'s `jsonSchema()` (no
|
| 661 |
+
JSON-Schema-to-Zod reimplementation); each `execute` forwards to
|
| 662 |
+
`/v1/tools/{name}/invoke`. Tool names are prefixed (`opencoti_`,
|
| 663 |
+
idempotently). The session ID arrives via `experimental_context`
|
| 664 |
+
on the single `openStream` `streamText` call and is forwarded as
|
| 665 |
+
`session_id`. Invocations are awaited (not fire-and-forget β tool
|
| 666 |
+
calls are model-blocking), but errors crash the *tool call*, not
|
| 667 |
+
the session: 404β`tool_not_found`, 400β`invalid_arguments`,
|
| 668 |
+
5xx/networkβ`tool_handler_failed`, deadlineβ`timeout`
|
| 669 |
+
(plugin-owned `Promise.race`, separate `invoke_timeout_ms`).
|
| 670 |
+
Args over 1 MiB are pre-rejected client-side.
|
| 671 |
+
|
| 672 |
+
- **Auto-wire via defaults.** `@opencoti/server-tools-plugin` is in
|
| 673 |
+
`OPENCOTI_DEFAULT_PLUGINS`. No reachable daemon β healthz gate
|
| 674 |
+
fails β `setRegisteredServerTools` never called β runtime tool
|
| 675 |
+
set unchanged.
|
| 676 |
+
|
| 677 |
+
- **Cross-version compat.** An M6 plugin against an M5 daemon sees
|
| 678 |
+
no `tools_schema_version` in healthz, so the gate fails and the
|
| 679 |
+
tool set is unchanged β a model query expecting the tool gets
|
| 680 |
+
"I don't have that tool", no crash. An M6 daemon serves M5
|
| 681 |
+
clients the `/v1/sessions/*` and `/v1/tier-events` surfaces
|
| 682 |
+
untouched.
|
| 683 |
+
|
| 684 |
+
- **Explicitly deferred:** external tool registration (POST/DELETE
|
| 685 |
+
on `/v1/tools` β needs a per-tool ACL/owner model); streaming
|
| 686 |
+
tool results; tool invocations as `tier_events` audit rows; an
|
| 687 |
+
MCP wrapper for the registry; FTS5/vector ranking in
|
| 688 |
+
`episodic_search`; an in-daemon embedder so `opencoti_recall`
|
| 689 |
+
can take a `query` string directly.
|
| 690 |
+
|
| 691 |
+
### M7 β pgvector backend parity (shipped 2026-05-23)
|
| 692 |
+
|
| 693 |
+
opencoti-server gains its **second storage backend**: PostgreSQL +
|
| 694 |
+
the pgvector extension, via `github.com/jackc/pgx/v5` (pure Go, no new
|
| 695 |
+
cgo). `*pgvector.Store` implements the **full** interface family β
|
| 696 |
+
`Store` + `SessionStore` + `TierEventStore` + `ToolStore` +
|
| 697 |
+
`SchemaInspector` β so the HTTP layer, `/v1/healthz`, and the TS client
|
| 698 |
+
are **untouched**. sqlite-vec stays the zero-dependency default;
|
| 699 |
+
pgvector is for hosts that already run Postgres.
|
| 700 |
+
|
| 701 |
+
**Schema-phrasing correction.** The original M7 stub said *"/v1/memory
|
| 702 |
+
works against pgvector with the same schema as claude-hooks."* That
|
| 703 |
+
predated the M4βM6 buildout. opencoti's binding contract is now its
|
| 704 |
+
**own** `Store` interface (collections + per-session ACL + sessions +
|
| 705 |
+
tier_events + tools), which is a different data model from
|
| 706 |
+
claude-hooks's flat `memories` + `kg_*` schema. So M7 implements
|
| 707 |
+
**opencoti's own model on a dedicated `opencoti` Postgres database**,
|
| 708 |
+
isolated from claude-hooks. "Same as claude-hooks" is reread as *same
|
| 709 |
+
storage technology (Postgres + pgvector)*, not the same tables.
|
| 710 |
+
|
| 711 |
+
- **`internal/store/pgvector/`** mirrors `internal/store/sqlitevec/`
|
| 712 |
+
table-for-table in PG dialect, with three dialect differences:
|
| 713 |
+
- the embedding lives **inline** on `memories` as a `vector(dim)`
|
| 714 |
+
column (no separate vec0 virtual table); an HNSW `vector_l2_ops`
|
| 715 |
+
index is created when `dim β€ 2000` (pgvector's HNSW ceiling) β
|
| 716 |
+
correctness-neutral, perf-positive.
|
| 717 |
+
- `Recall` pushes the ACL filter into the query
|
| 718 |
+
(`WHERE collection_name = ANY($readable) ORDER BY embedding <-> $q
|
| 719 |
+
LIMIT k`) β Postgres can filter + rank in one statement where
|
| 720 |
+
sqlite-vec must overscan + join-back + post-filter. The `<->`
|
| 721 |
+
operator is **L2**, matching sqlite-vec's vec0 default, so
|
| 722 |
+
`MemoryHit.Distance` stays comparable across backends.
|
| 723 |
+
- `Store` dedups via `INSERT β¦ ON CONFLICT (collection_name,
|
| 724 |
+
content_hash) DO NOTHING RETURNING id` in one round trip.
|
| 725 |
+
- **Per-feature schema versions** (`schema_version`,
|
| 726 |
+
`sessions_/tier_events_/tools_schema_version`, all `1`) live in a
|
| 727 |
+
`meta` key/value table, validated on open exactly as the sqlite-vec
|
| 728 |
+
backend does β but they are pgvector's own contract (no cross-language
|
| 729 |
+
reader, since pgvector is daemon-only).
|
| 730 |
+
- **Shared conformance harness** `internal/store/storetest/` is the
|
| 731 |
+
parity guarantee: a single `RunConformance` body (Memory, Sessions,
|
| 732 |
+
TierEvents, Tools sub-suites) that **both** backends opt into via a
|
| 733 |
+
thin `conformance_test.go`. Either backend drifting from the contract
|
| 734 |
+
fails the same assertions. sqlite-vec runs it with `t.TempDir`;
|
| 735 |
+
pgvector with a testcontainers fixture.
|
| 736 |
+
- **Pure-logic lifted to `internal/store/common.go`** (package `store`):
|
| 737 |
+
the ACL resolver (`ResolveAccessMode`/`CanRead`/`CanWrite`), the
|
| 738 |
+
collection/session validators, `Sha256Hex`, and the dim bounds β one
|
| 739 |
+
cgo-free source of truth both backends share. sqlite-vec keeps its
|
| 740 |
+
exported names as thin delegating wrappers, so the HTTP layer's
|
| 741 |
+
`sqlitevec.ValidateCollectionName`/`ValidateSessionID` call sites are
|
| 742 |
+
byte-identical.
|
| 743 |
+
- **Backend selection** is a serve-time flag, not a build flag:
|
| 744 |
+
`--backend sqlite|pgvector` (default `sqlite`) + `--pg-dsn` (falling
|
| 745 |
+
back to `$OPENCOTI_PG_DSN`). `--db-path` stays sqlite-only;
|
| 746 |
+
`--embedding-dim` applies to both (locks the `vector(dim)` column).
|
| 747 |
+
The interface var is assigned only on a successful open (avoids the
|
| 748 |
+
typed-nil trap), so a pgvector open failure degrades to
|
| 749 |
+
`store_ok=false` + a clear `store_error` rather than crashing. The
|
| 750 |
+
Windows `install` command bakes `--backend`/`--pg-dsn` into the
|
| 751 |
+
service args alongside the existing flags.
|
| 752 |
+
- **healthz** reports a **redacted** `store_path` for pgvector
|
| 753 |
+
(`pg://user@host:port/db`, never the password).
|
| 754 |
+
- **Tests**: `pgtest` spins an ephemeral `pgvector/pgvector:pg17`
|
| 755 |
+
container via testcontainers-go and skips cleanly when Docker is
|
| 756 |
+
absent (`testcontainers.SkipIfProviderIsNotHealthy`), so
|
| 757 |
+
`make test` stays green on a Docker-less host. Both backends pass the
|
| 758 |
+
full shared conformance suite; an end-to-end smoke (daemon β
|
| 759 |
+
pgvector container) confirms collection create, store, and `<->` -
|
| 760 |
+
ranked recall over HTTP.
|
| 761 |
+
- **Toolchain**: the modern pgx / pgvector-go / testcontainers-go
|
| 762 |
+
releases require **Go 1.25**, so the module's `go` directive and the
|
| 763 |
+
host toolchain moved to go1.25 (latest stable). No build-posture
|
| 764 |
+
regression β pgx is pure Go; sqlite-vec's existing cgo requirement is
|
| 765 |
+
unchanged.
|
| 766 |
+
- **Zero new surgical hooks.** pgvector is additive Go inside
|
| 767 |
+
`opencoti/server/`; no TS changes. The surgical-hook grep count
|
| 768 |
+
stays at 18 (see `docs/protocols/UPSTREAM_SYNC.md`).
|
| 769 |
+
|
| 770 |
+
**Deferred from M7:** data migration between backends (a `migrate`
|
| 771 |
+
subcommand β "Migration tools later"); a TS pgvector backend for the
|
| 772 |
+
in-process `@opencoti/memory` (pgvector is daemon-only); external tool
|
| 773 |
+
registration over HTTP (still security-gated, from M6); pgxpool tuning
|
| 774 |
+
/ read replicas.
|
| 775 |
+
|
| 776 |
+
### M8 β Multi-session coordination (shipped 2026-05-23)
|
| 777 |
+
|
| 778 |
+
Goal **G3**: opencoti instances/sessions on one host see each other and
|
| 779 |
+
cooperate. M8 ships the *live-coordination* half as an **in-memory,
|
| 780 |
+
ephemeral** hub β deliberately store-independent (neither the `Store`
|
| 781 |
+
interface nor either backend is touched), because presence and locks are
|
| 782 |
+
runtime state that should not survive a daemon restart.
|
| 783 |
+
|
| 784 |
+
- **`internal/coord.Hub`** β backend-agnostic, one `sync.RWMutex`:
|
| 785 |
+
- **Presence**: `RegisterPeer` / `Heartbeat` / `DeregisterPeer` /
|
| 786 |
+
`ListPeers`, with a background TTL sweep that reaps peers whose
|
| 787 |
+
heartbeat lapsed (default 45s) and emits `peer.left`. `ListPeers`
|
| 788 |
+
also filters expired peers lazily.
|
| 789 |
+
- **Broadcasts**: a pub/sub bus β `Publish` assigns a monotonic `seq`,
|
| 790 |
+
appends to a bounded replay ring, and fans out non-blockingly to
|
| 791 |
+
subscribers (a full subscriber channel is dropped + closed so the
|
| 792 |
+
client reconnects with its last seq). `Subscribe(since)` atomically
|
| 793 |
+
snapshots the replay backlog and registers for live events.
|
| 794 |
+
- **Advisory locks**: try-only `AcquireLock` (reentrant-by-holder
|
| 795 |
+
refresh; expired locks reclaimable) / `ReleaseLock` (holder-checked)
|
| 796 |
+
/ `ListLocks`. Lock transitions emit `lock.acquired` / `lock.released`.
|
| 797 |
+
- **HTTP** (`internal/server/coord.go`), hub injected via `Options.Hub`,
|
| 798 |
+
503 `coord_unavailable` when absent:
|
| 799 |
+
|
| 800 |
+
| Method + path | Purpose |
|
| 801 |
+
| --- | --- |
|
| 802 |
+
| `POST /v1/coord/peers` | register/upsert presence β PeerInfo |
|
| 803 |
+
| `POST /v1/coord/peers/{id}/heartbeat` | refresh TTL |
|
| 804 |
+
| `DELETE /v1/coord/peers/{id}` | deregister |
|
| 805 |
+
| `GET /v1/coord/peers` | list live peers |
|
| 806 |
+
| `POST /v1/coord/broadcast` | publish `{peer_id,topic,payload}` β `{seq}` |
|
| 807 |
+
| `GET /v1/coord/events?since=&peer_id=` | **SSE** event stream (the daemon's first) |
|
| 808 |
+
| `POST /v1/coord/locks/{name}` | acquire `{holder,ttl_ms?}`; 200 or 409 `lock_held` |
|
| 809 |
+
| `DELETE /v1/coord/locks/{name}` | release `{holder}`; 200 / 404 `lock_not_held` / 409 `lock_not_holder` |
|
| 810 |
+
| `GET /v1/coord/locks` | list held locks |
|
| 811 |
+
|
| 812 |
+
SSE is viable because the `http.Server` sets no `WriteTimeout`; the
|
| 813 |
+
handler exits on request-context cancellation so graceful shutdown
|
| 814 |
+
releases it within the grace window. `/v1/healthz` gains
|
| 815 |
+
`coord_ok` + `coord_peers`.
|
| 816 |
+
- **TS client**: `registerPeer` / `heartbeatPeer` / `deregisterPeer` /
|
| 817 |
+
`listPeers` / `broadcast` / `acquireLock` / `releaseLock` /
|
| 818 |
+
`listLocks`, plus `subscribeCoordEvents` β the client's first
|
| 819 |
+
streaming method (reads `response.body`, parses `data:` frames).
|
| 820 |
+
- **`@opencoti/coordination-plugin`**: registers each session as a peer,
|
| 821 |
+
heartbeats while active, deregisters on delete, subscribes to the
|
| 822 |
+
event stream (SSE) to keep a live peer view, and advertises
|
| 823 |
+
"N other active opencoti session(s)" in the system prompt. Auto-wired
|
| 824 |
+
via the `@opencoti/tiers` default-plugins list β **no surgical hook**
|
| 825 |
+
(grep count stays 18).
|
| 826 |
+
|
| 827 |
+
**Deferred to F3 M9:** opt-in *shared context* (a thin convention atop
|
| 828 |
+
existing M2 global collections + per-session ACL β a session opts to
|
| 829 |
+
expose a collection to peers); the sqliteβpgvector `migrate` subcommand
|
| 830 |
+
(open from M7); blocking/queued lock acquire (M8 is try-only). Cross-host
|
| 831 |
+
federation remains an explicit F3 non-goal.
|
| 832 |
+
|
| 833 |
+
### M9 β Opt-in shared context (shipped 2026-05-23)
|
| 834 |
+
|
| 835 |
+
Goal **G3**, the *persisted-context* half: a session exposes one of its
|
| 836 |
+
session-scoped memory collections to peer sessions on the same host. The
|
| 837 |
+
storage model from M2 already supports the grant, so M9 adds **no new
|
| 838 |
+
`Store` method, no new schema, no new surgical hook** (grep count stays
|
| 839 |
+
18). It is a thin convention bridging two things that already exist β the
|
| 840 |
+
persisted per-session ACL (`SetSessionACL`) and M8's ephemeral coord bus.
|
| 841 |
+
|
| 842 |
+
- **`internal/share.Manager`** β in-memory registry + one goroutine:
|
| 843 |
+
- `Share(collection, owner, mode)` records the share, grants read-ACL
|
| 844 |
+
to every live peer `!= owner` via `SetSessionACL`, and publishes
|
| 845 |
+
`collection.shared` on the hub.
|
| 846 |
+
- It **subscribes to the hub** and, on `peer.joined`, grants every
|
| 847 |
+
active share to the newcomer β so peers that join *after* a share
|
| 848 |
+
still get access (auto-grant via the bus, dogfooding M8's SSE).
|
| 849 |
+
- `Unshare(collection, owner)` revokes live peers (writes mode `none`,
|
| 850 |
+
the non-owner default) and publishes `collection.unshared`.
|
| 851 |
+
- **Persisted vs ephemeral:** the ACL grants persist (they survive a
|
| 852 |
+
restart); the "keep auto-granting new joiners" intent is in-memory
|
| 853 |
+
and lost on restart by design β existing grants remain, but the owner
|
| 854 |
+
must re-share to resume auto-granting. This keeps M9 storage-free.
|
| 855 |
+
- **Recall "just works":** once a peer holds an `r` grant, its no-filter
|
| 856 |
+
`Recall` includes the shared collection automatically (M2's
|
| 857 |
+
readable-collections resolution), so no recall-path change is needed.
|
| 858 |
+
- **HTTP** (`internal/server/share.go`), Manager constructed in
|
| 859 |
+
`server.New()` when `Store`+`Hub` are present, released on `Shutdown`;
|
| 860 |
+
503 `share_unavailable` when the hub is absent:
|
| 861 |
+
|
| 862 |
+
| Method + path | Purpose |
|
| 863 |
+
| --- | --- |
|
| 864 |
+
| `POST /v1/memory/collections/{name}/share` | owner-only share `{owner_session, mode?}` (mode default `r`) β `{granted}` |
|
| 865 |
+
| `DELETE /v1/memory/collections/{name}/share` | withdraw `{owner_session}` β 200 |
|
| 866 |
+
| `GET /v1/memory/shares` | list active shares |
|
| 867 |
+
|
| 868 |
+
**Owner-only:** the handler looks the collection up via
|
| 869 |
+
`ListCollections` (no filter) and requires `scope == session` and
|
| 870 |
+
`session_id == owner_session`; global collections (already `r`-for-all)
|
| 871 |
+
are rejected `not_shareable`, a different owner `not_owner` (403).
|
| 872 |
+
- **TS client**: `shareCollection` / `unshareCollection` / `listShares`
|
| 873 |
+
+ `SharedCollectionInfo`.
|
| 874 |
+
- **`@opencoti/coordination-plugin`** (extended in place β no new plugin):
|
| 875 |
+
consumes `collection.shared` / `collection.unshared` to advertise
|
| 876 |
+
peer-shared collections in the system prompt, and gains an opt-in
|
| 877 |
+
`share_session_collection` flag (default **false**) that shares the
|
| 878 |
+
session's own collection (`sessionCollectionName(id)`) on
|
| 879 |
+
`session.created` and unshares it on `session.deleted` (best-effort; a
|
| 880 |
+
not-yet-created collection's 404 is swallowed).
|
| 881 |
+
|
| 882 |
+
**Deferred to F3 M10:** the sqliteβpgvector `migrate` subcommand β **not**
|
| 883 |
+
a cutover: both backends are first-class and may run in parallel; migrate
|
| 884 |
+
is an idempotent, re-runnable, either-direction copy (`--from`/`--to`,
|
| 885 |
+
dedupe on `content_hash`, upsert PKs) that tops up a parallel target,
|
| 886 |
+
never abandoning the source. Also deferred: revoke-on-peer-leave cleanup
|
| 887 |
+
(M9 keeps grants on `peer.left`, since peers may return) and wildcard /
|
| 888 |
+
group ACLs.
|
| 889 |
+
|
| 890 |
+
### M10 β sqliteβpgvector `migrate` (shipped 2026-05-24)
|
| 891 |
+
|
| 892 |
+
A daemon-internal `migrate` subcommand that copies data between the two
|
| 893 |
+
first-class backends. It is **not a cutover**: both backends stay
|
| 894 |
+
first-class and may run in parallel (two daemons, or alternating
|
| 895 |
+
`--backend`). `migrate` is an **idempotent, re-runnable, either-direction,
|
| 896 |
+
selectable copy** that *tops up* a target (dedupe on natural keys), never
|
| 897 |
+
"move then abandon source". Additive Go only β **no plugin, no HTTP
|
| 898 |
+
surface, no TS client, no surgical hook** (grep count stays 18).
|
| 899 |
+
|
| 900 |
+
**Use cases (maximum flexibility):**
|
| 901 |
+
|
| 902 |
+
1. **Scale-up (primary).** Start on sqlite; migrate *everything*
|
| 903 |
+
(collections, memories+embeddings, ACL, sessions, session_events,
|
| 904 |
+
session_messages, tier_events, tools) into pgvector; then **switch the
|
| 905 |
+
primary backend** by changing the serve flag to `--backend pgvector`.
|
| 906 |
+
"Switch primary backend" is operational, no extra code: run the full
|
| 907 |
+
migrate, then change `--backend`.
|
| 908 |
+
2. **Way back.** Same command with `--from`/`--to` swapped.
|
| 909 |
+
3. **Single memory container / additional partial backend.** Copy only
|
| 910 |
+
specific collection(s) with `--collection`, so e.g. pgvector holds just
|
| 911 |
+
certain memories while sqlite keeps the rest. The two backends coexist,
|
| 912 |
+
each holding different data.
|
| 913 |
+
4. **Additive top-up.** Re-running, or copying into a populated target,
|
| 914 |
+
merges idempotently (natural-key dedupe).
|
| 915 |
+
|
| 916 |
+
**Selection model β two orthogonal selectors:**
|
| 917 |
+
|
| 918 |
+
- `--include <csv>` of sections: `memory,sessions,tier-events,tools`.
|
| 919 |
+
Default (unset) = **all four** (full dataset). The `memory` section
|
| 920 |
+
carries collections + their memories + their ACL rows.
|
| 921 |
+
- `--collection <name>` (repeatable) restricts the `memory` section to
|
| 922 |
+
those collections only. When `--collection` is given and `--include` is
|
| 923 |
+
unset, the default narrows to **memory-only** (the single-container
|
| 924 |
+
case).
|
| 925 |
+
|
| 926 |
+
So: full switch = no selectors; single container = `--collection notes`;
|
| 927 |
+
logs-only = `--include sessions,tier-events`.
|
| 928 |
+
|
| 929 |
+
**Copy phases (FK-respecting; each gated by the selection):**
|
| 930 |
+
|
| 931 |
+
| Section | Phase | Export | Import |
|
| 932 |
+
| --- | --- | --- | --- |
|
| 933 |
+
| memory | collections | `ListCollections` (filtered) | `PutCollection` |
|
| 934 |
+
| memory | memories | `ListMemories` (per coll, keyset-paged) | `PutMemory` |
|
| 935 |
+
| memory | session_acl | `ListSessionACLs` | `SetSessionACL` |
|
| 936 |
+
| sessions | sessions | `ListSessions(+deleted)` | `UpsertSession` |
|
| 937 |
+
| sessions | session_events | `ListSessionEvents`/sess | `PutSessionEvent` |
|
| 938 |
+
| sessions | session_messages | `ListSessionMessages`/sess | `UpsertSessionMessages` |
|
| 939 |
+
| tier-events | tier_events | `ListTierEvents` (batched) | `PutTierEvent` |
|
| 940 |
+
| tools | tools | `ListTools` | `UpsertTool` |
|
| 941 |
+
|
| 942 |
+
Both backends are the same concrete `*Store` implementing every feature
|
| 943 |
+
interface, so migrate opens each side as `storepkg.Store`, type-asserts
|
| 944 |
+
the optional `MigrationStore`, and copies a section only if **both** sides
|
| 945 |
+
implement it. Per-phase counts (scanned / inserted / skipped) print at the
|
| 946 |
+
end; `--dry-run` reads sources and reports would-copy counts, no writes.
|
| 947 |
+
|
| 948 |
+
**Idempotency / dedup.** Memories dedupe on `(collection_name,
|
| 949 |
+
content_hash)`. The append-only audit logs (`session_events`,
|
| 950 |
+
`tier_events`) have **no natural key** (autoinc id only), so their
|
| 951 |
+
idempotent import dedups on an **insert-if-no-identical-row** check (all
|
| 952 |
+
business columns match) β re-running produces no duplicates. A second full
|
| 953 |
+
run reports 0 inserts everywhere.
|
| 954 |
+
|
| 955 |
+
**Two backend-specific wrinkles (both resolved):**
|
| 956 |
+
|
| 957 |
+
- **sqlite-vec embeddings are an opaque BLOB.** vec0 `memory_vecs` stores
|
| 958 |
+
the embedding as a blob and the Go binding ships `SerializeFloat32` but
|
| 959 |
+
no deserialize. The on-disk format is plain little-endian `float32`
|
| 960 |
+
(`binary.Write(buf, LittleEndian, vector)`), so `deserializeFloat32`
|
| 961 |
+
reverses it with `binary.Read` at the locked dim β **byte-faithful, no
|
| 962 |
+
re-embedding**. pgvector exports via `pgvector.Vector.Scan` + `.Slice()`.
|
| 963 |
+
- **The public `Store()` write path enforces ACL** (a global collection is
|
| 964 |
+
`r`-for-all β `ErrWriteDenied`), so migrate cannot reuse it for imports.
|
| 965 |
+
The new `MigrationStore.PutMemory` is **ACL-free and timestamp-
|
| 966 |
+
preserving**.
|
| 967 |
+
|
| 968 |
+
**Fidelity contract (documented caveat).** Preserved exactly: collection
|
| 969 |
+
`created_at`, memory `ts`+`content`+embedding (byte-identical), session
|
| 970 |
+
`created_at`/`deleted_at`, message `created_at`/`finished_at`, event
|
| 971 |
+
`occurred_at`/`ts`, all ACL modes. Rewritten to migrate-time (reused
|
| 972 |
+
upserts stamp `now()`): `sessions.updated_at`, `tools.created_at`/
|
| 973 |
+
`updated_at` β acceptable "last-written" fields (tools are also re-seeded
|
| 974 |
+
at startup). A byte-identical `PutSession`/`PutTool` is deferred.
|
| 975 |
+
|
| 976 |
+
The new `MigrationStore` optional interface (in `store.go`, mirroring the
|
| 977 |
+
`SchemaInspector`/`SessionStore` convention) is exercised by the
|
| 978 |
+
`storetest` conformance harness on **both** backends β the round-trip
|
| 979 |
+
asserts the exported embedding is byte-identical, validating the sqlite LE
|
| 980 |
+
deserialize and the pgvector scan together. Verified end-to-end across a
|
| 981 |
+
`sqliteβpgvectorβsqlite` hop: recall on the round-tripped file returns
|
| 982 |
+
`distance 0` for the exact source vector.
|
| 983 |
+
|
| 984 |
+
**Deferred to F3 M11+:** continuous-sync / daemon mode (migrate is
|
| 985 |
+
one-shot CLI only), cross-host federation (explicit F3 non-goal),
|
| 986 |
+
per-session selective log copy (selective granularity is per-collection
|
| 987 |
+
for memory + section toggles for the rest), and the byte-identical
|
| 988 |
+
`updated_at`/tool-timestamp imports noted above.
|
| 989 |
+
|
| 990 |
+
## Open questions
|
| 991 |
+
|
| 992 |
+
- **Do we ship the server inside the opencoti binary or alongside it?**
|
| 993 |
+
Alongside (separate binary) is cleaner; explore a single-fat-binary
|
| 994 |
+
option as an opt-in.
|
| 995 |
+
- **Authentication.** Local UDS owner is the user; over loopback TCP
|
| 996 |
+
we need a shared secret. Default to UDS.
|
| 997 |
+
- **claude-hooks coexistence.** If a user runs both Claude Code (with
|
| 998 |
+
claude-hooks) and opencoti (with opencoti-server) on the same host,
|
| 999 |
+
the two memories should *not* collide. They use different stores
|
| 1000 |
+
and different schemas by default; a separate migration tool can
|
| 1001 |
+
bridge them if the user wants.
|
| 1002 |
+
|
| 1003 |
+
## Risks
|
| 1004 |
+
|
| 1005 |
+
- Two persistence backends doubles the test matrix. Mitigation: a
|
| 1006 |
+
storage trait + a shared conformance test.
|
| 1007 |
+
- Drift from claude-hooks features. Mitigation: don't try to replicate
|
| 1008 |
+
feature-for-feature; replicate goals. claude-hooks remains the
|
| 1009 |
+
reference for Claude Code; opencoti-server can diverge where it
|
| 1010 |
+
makes opencoti better.
|
docs/features/poly_kv.md
ADDED
|
@@ -0,0 +1,817 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# F5 M6 β PolyKV (shared compressed KV pool)
|
| 2 |
+
|
| 3 |
+
Child plan for the PolyKV milestone. Parent: [advanced_kv.md](advanced_kv.md) Β§M6.
|
| 4 |
+
Composed **atop M7 Rolling KV** ([rolling_kv.md](rolling_kv.md)). Status: **M6 SHIPPED (2026-06-06)** β
|
| 5 |
+
S0 asymmetric q8/q4 (#369, banked correct at 4k; large-ctx GPU-fast asymmetric folds into the S2
|
| 6 |
+
kernel), S1 SharedKVPool (#370, gate PASS on Gemma-4 iSWA + 6.9Γ fan-out memory win), and S2
|
| 7 |
+
TurboQuant tier family + InnerQ + Level-A materialize + Level-B fused FA-VEC (#371/#374β396, 4/4
|
| 8 |
+
tiers logit-equiv PASS, decode ceiling broken) are all done, composed on the full F5 stack (S3 glue
|
| 9 |
+
gate), and **captured as patches `0072`/`0073`** (byte-identical proof). The remaining milestone is
|
| 10 |
+
**M6-S4** speculative MTP draft head (`gemma4_assistant`, #373) β a separate follow-on.
|
| 11 |
+
|
| 12 |
+
## What PolyKV is (reference)
|
| 13 |
+
|
| 14 |
+
Reference: arXiv 2604.24971, MIT β [github.com/ishan1410/PolyKV](https://github.com/ishan1410/PolyKV).
|
| 15 |
+
A document is prefilled **once**; its KV is extracted, **asymmetrically compressed**, and held
|
| 16 |
+
**once** in a host-resident `SharedKVPool`. N concurrent agents share that single compressed blob
|
| 17 |
+
(no per-agent dense copy of the shared prefix) β **O(1) memory in agent count** for the shared
|
| 18 |
+
context. Reference numbers: **2.91Γ compression**, 15 agents 19.8 GB β 0.45 GB, +1.59% perplexity
|
| 19 |
+
@2k (β +0.57% @4k).
|
| 20 |
+
|
| 21 |
+
**Asymmetric scheme** (the core idea β keys and values quantized differently because key errors
|
| 22 |
+
amplify exponentially through softmax while value errors scale linearly):
|
| 23 |
+
- **K β int8 (q8_0)** β higher precision.
|
| 24 |
+
- **V β 3-bit TurboQuant MSE** β FWHT (fast Walsh-Hadamard) rotation to spread outlier energy
|
| 25 |
+
across dims, then Lloyd-Max (MSE-optimal) quantization.
|
| 26 |
+
|
| 27 |
+
The reference is **HuggingFace Transformers / PyTorch**, framework-agnostic w.r.t. serving stacks
|
| 28 |
+
(no vLLM/SGLang dep). Crucially it does **decompress-then-attend** β each agent decompresses the
|
| 29 |
+
pool into a dense `DynamicCache` before attention, so its savings are at the **storage** layer, not
|
| 30 |
+
**in-attention**. No custom attention kernel.
|
| 31 |
+
|
| 32 |
+
## The opencoti angle β why M7 makes this better than the reference
|
| 33 |
+
|
| 34 |
+
M7 already ships **dequant-on-lift** (Stage 3d, `to_fp16_nc`): the streaming/window forward reads
|
| 35 |
+
host-resident KV and dequantizes it to a device f16 slot **per tile, inside the attention read
|
| 36 |
+
path**. That is exactly the "dequant-on-read" the PyTorch reference lacks. So opencoti can run
|
| 37 |
+
PolyKV's compression **in-attention** β the compressed pool never has to be densified, getting the
|
| 38 |
+
memory win at the attention layer, not just storage. The composition is already designed
|
| 39 |
+
(rolling_kv.md): *"M7 streams the compressed tiles, decompressing in-slot before FA; the auto-adapt
|
| 40 |
+
loop becomes `tile_bytes_max Γ compression_ratio`."*
|
| 41 |
+
|
| 42 |
+
Building blocks already in place: host-resident KV (M7-A pinned pool), quant-aware dequant-on-lift
|
| 43 |
+
(S3d), independent K/V cache types (`-ctk`/`-ctv`), session-keyed KV-reuse prefix (M0), the
|
| 44 |
+
iSWA two-cache split (shared-prefix scaffolding).
|
| 45 |
+
|
| 46 |
+
## Scope decision (from advanced_kv Β§M6)
|
| 47 |
+
|
| 48 |
+
**Single-tenant correctness first; multi-tenant memory bench follows when Tier-0 fan-out lands.**
|
| 49 |
+
PolyKV degenerates cleanly to a 1-tenant pool (= aggressive asymmetric KV-compression of one
|
| 50 |
+
sequence), so single-tenant is a real, shippable first milestone. The user directive is to
|
| 51 |
+
**validate shared-KV + rolling-KV functionality + performance together**.
|
| 52 |
+
|
| 53 |
+
## Staging β fast-first (bootstrap with stock types, then the hard kernel)
|
| 54 |
+
|
| 55 |
+
### S0 β Bootstrap: asymmetric KV compression via existing ggml types (ZERO new kernel)
|
| 56 |
+
Use llama.cpp's independent `-ctk q8_0 -ctv q4_0` (both existing KV types, both FA-supported, both
|
| 57 |
+
dequant-on-lift-able). This **is** the PolyKV K/V asymmetry (K precise, V aggressive) with stock
|
| 58 |
+
types, routed through M7's existing host-residency + dequant-on-lift. Proves the
|
| 59 |
+
asymmetric-compressed-KV + M7 composition architecture end-to-end with no new code.
|
| 60 |
+
- Compression: q8_0 K (0.53Γ) + q4_0 V (0.28Γ vs f16) β **~2.4Γ KV** overall.
|
| 61 |
+
- **Feasibility: PROVEN 2026-06-04.** `-ctk q8_0 -ctv q4_0` boots, attends, and recalls the needle
|
| 62 |
+
on Gemma head_dim 512 (q4_0 V dequants to f16 in-path; coherent output). KV 186.9 MiB (q4_0 V) vs
|
| 63 |
+
244.4 (q8_0 V) β asymmetric compression confirmed. The head_dim-512 q4_0-V risk is retired.
|
| 64 |
+
- **Correctness gate (RUNNING 2026-06-05, dir `baselines/20260604-2346`):** RULER vt+niah, asymmetric
|
| 65 |
+
ours (`KVTYPE_OVERRIDE=q8_0 CTV_OVERRIDE=q4_0 FLASH_ATTN=on BINARIES=ours`) vs cached controls.
|
| 66 |
+
`FLASH_ATTN=on` is **mandatory** β at head_dim 512 the auto-resolver disables FA for q4_0 V (no
|
| 67 |
+
native kernel), producing empty cells; forcing it on keeps FA + dequant-on-lift engaged.
|
| 68 |
+
- **The control matrix isolates V-quant exactly** (q8/q8 β q8/q4 is a V-only change):
|
| 69 |
+
|
| 70 |
+
| task | ctx | vanilla f16 | vanilla q8/q8 | ours q8/q8 | **ours q8/q4 (S0)** |
|
| 71 |
+
|------|-----|-----|-----|-----|-----|
|
| 72 |
+
| vt | 4096 | 100 | 100 | 100 | **80** |
|
| 73 |
+
| vt | 32768 | 100 | 80* | 96 | 0β |
|
| 74 |
+
| niah_single_1 | 4096 | 100 | 100 | 100 | **100** |
|
| 75 |
+
| niah_single_1 | 32768 | 100 | 100 | 100 | n/aβ |
|
| 76 |
+
|
| 77 |
+
\*vanilla q8/q8 vt@32k=80 is a known vanilla-specific prompt fragility (ours q8/q8 there = 96).
|
| 78 |
+
β **ours q8/q4 at 32k is a CPU-FA timeout, not a quality result** (re-confirmed 2026-06-18 on the
|
| 79 |
+
current post-upstream-align binary, sha `5e1b3a53`): q4_0-V has no GPU FA kernel at head_dim 512,
|
| 80 |
+
so resident-mode prefill runs on CPU-FA (~22 tok/s, ~1440s for 32k) and exceeds the harness's
|
| 81 |
+
1200s/sample budget β vt 5/5 ReadTimeout, niah the 2 naturally-completing samples both ReadTimeout
|
| 82 |
+
@1200s (the other 3 were ConnectionError from the reap, not data). This is the **S0 WALL below**,
|
| 83 |
+
not a retrieval failure. niah@4k=100 (0/5 empty) is the decisive correctness signal and it holds.
|
| 84 |
+
- **vt@4096 = 80**: both f16 and q8/q8 controls hold 100, so the 20pp drop isolates to V f16βq4_0 β
|
| 85 |
+
**one flipped sample at NS=5** ("direction real, magnitude not robust" per the baseline's own
|
| 86 |
+
NS-sizing note). This is the textbook PolyKV signature: aggressive V-quant nicks hard multi-hop
|
| 87 |
+
(vt) while single-needle (niah) should stay robust β and the exact motivation for TBQ3_0 (S2,
|
| 88 |
+
MSE-optimal) over naive q4_0. **niah@4k = 100 (re-confirmed 2026-06-18) is the decisive correctness
|
| 89 |
+
signal β q4_0-V keeps the single needle, and the upstream-align rename did not regress plain gemma4
|
| 90 |
+
decode**; niah@32k could not be measured on naive q4_0-V (CPU-FA timeout β see β and the S0 WALL).
|
| 91 |
+
- **Perf note (resident regime):** at 4k/32k the KV is FULLY RESIDENT (no host tail, POSITION_WINDOW=0)
|
| 92 |
+
so M7's dequant-on-lift does **not** engage; q8/q4 pays stock llama.cpp's head_dim-512 dequant tax
|
| 93 |
+
(MMA kernel is f16-only β full-KV dequant before FA, every batch) β ~3β4Γ slower prefill than f16.
|
| 94 |
+
**The performance win lives in the OVERFLOW regime** (256k cell), where M7 streams compressed tiles
|
| 95 |
+
and amortizes dequant per-tile in-attention. **Attempted 2026-06-18** (`m6-s0-overflow-tps.sh`, current
|
| 96 |
+
binary `5e1b3a53`): the 256k composition cell is NOT obtainable as milestone evidence on the current
|
| 97 |
+
binary, for two independent reasons β (1) the harness's raw `/completion` prompt at 256k greedily emits
|
| 98 |
+
an immediate end-token (RES_q8q8: GPU-fast prefill 222k tok @1122 tok/s, but `eval = 1 token`,
|
| 99 |
+
needle=NO), so decode-tps is unmeasurable without a generation-capable 256k prompt; and (2) the q8/q4
|
| 100 |
+
*overflow* win is gated on the **deferred q4_0-V GPU overflow kernel** (S2 TBQ3_0) β the same head_dim-512
|
| 101 |
+
gap as the resident wall. The q8/q8 overflow path itself is already proven GPU-fast at 256k (M7 ship,
|
| 102 |
+
bug-226: vt=96/niah=100). So the rolling-KV + *asymmetric* milestone evidence **awaits the S2 kernel**
|
| 103 |
+
(and a generation-capable 256k harness), consistent with the finalized S0 verdict below. (The perf script
|
| 104 |
+
was hardened this session: `--max-time 900` on the completion curl + the resident-q4_0-V@256k config
|
| 105 |
+
skipped, since it is the CPU-FA multi-hour path β see [[bug-564]].)
|
| 106 |
+
- Pass = retrieval holds (β₯ vanilla β 2pp on the robust niah signal; vt characterized at NS=5).
|
| 107 |
+
|
| 108 |
+
#### S0 WALL (2026-06-05): stock q4_0-V has NO fast GPU FA path at head_dim 512
|
| 109 |
+
The correctness gate surfaced a hard performance wall, not a quality result:
|
| 110 |
+
- **vt@4096** = 80, **316s/5 = 63s/sample, 0/5 empty** β fast, valid (real q4_0-V quality at 4k).
|
| 111 |
+
- **vt@32768** = 0 is a **TIMEOUT ARTIFACT, not a collapse**: `elapsed_secs=6001 = exactly 5Γ1200s`
|
| 112 |
+
(the ruler_runner `--http-timeout 1200`); server log shows `srv stop: cancel task` +
|
| 113 |
+
`stop processing: n_tokens=β¦, truncated=0` for every sample β **cancelled mid-prefill, never
|
| 114 |
+
decoded** (no error/NaN/abort). The 32k q8/q4 prefill does **not finish in 20 min**.
|
| 115 |
+
- **GPU utilization ~5% during prefill** (14 GB used = weights resident) β the quant-V attention runs
|
| 116 |
+
on **CPU** (no GPU FA kernel for q4_0 V at head_dim 512). ~50Γ slower than f16. niah@32k (same slow
|
| 117 |
+
prefill) is equally doomed β killed to stop wasting GPU.
|
| 118 |
+
- **Consequence:** at RESIDENT (4k/32k/256k all fit compressed), stock FA handles quant-V on CPU β
|
| 119 |
+
prohibitively slow at large ctx; the perf premise (smaller tail β higher tps) is swamped by the
|
| 120 |
+
CPU-FA tax, and a 256k RULER/needle cell is infeasible (hours/timeout).
|
| 121 |
+
- **Salvage hypothesis (to test):** M7's dequant-on-lift (S3d, `to_fp16_nc`) dequants q4_0βf16 **on
|
| 122 |
+
GPU per-tile**, but ONLY engages when the **window** is active (overflow). Forcing
|
| 123 |
+
`--kv-residency-mode window` / a tight `--vram-target` may route q4_0-V through the **GPU**
|
| 124 |
+
dequant-on-lift β potentially *faster* than the resident CPU-FA path, which would make the
|
| 125 |
+
composition both correct AND the perf win. Diagnostic: 32k forced-window q8/q4, compare prefill
|
| 126 |
+
wall-time + GPU-util vs resident. If GPU-fast β composition viable; if still CPU/slow β naive q4_0-V
|
| 127 |
+
is a dead end on Gemma-4 head_dim 512, and S0 needs V=q8_0 (memory ~1.9Γ not 2.4Γ) or S2's TBQ3_0
|
| 128 |
+
must ship a fused GPU dequant-FA kernel (raising S2 scope/risk).
|
| 129 |
+
|
| 130 |
+
#### S0 SALVAGE RESULT (2026-06-05): forced-window q4_0-V CRASHES β both paths blocked
|
| 131 |
+
Ran `.opencoti/m6-s0-window-gpupath-probe.sh` (32k, `--kv-residency-mode window --vram-target 1700`,
|
| 132 |
+
q8/q4). The window **engaged** correctly (POSITION_WINDOW=5, window 20480 / host tail 20480 cells)
|
| 133 |
+
but then **aborted at `fattn.cu:509: GGML_ABORT("fatal error")`** β the `BEST_FATTN_KERNEL_NONE`
|
| 134 |
+
branch: `ggml_cuda_get_best_fattn_kernel` returns NONE for q4_0-V at head_dim 512 (MMA is f16-only;
|
| 135 |
+
VEC needs head_dim β€256; WMMA excluded at 512; TILE didn't apply to this tile shape). **S3d's
|
| 136 |
+
dequant-on-lift was validated only with q8_0** (bug-226 RULER vt@262144); q4_0-V tiles reach the
|
| 137 |
+
kernel selector still-quantized β NONE β abort. So:
|
| 138 |
+
- **Resident q8/q4:** stock TILE-kernel fallback, ~50Γ slower than f16, RULER-timeout at β₯32k.
|
| 139 |
+
- **Window q8/q4 (M7 GPU path):** crashes at `fattn.cu:509` (q4_0-V not handled by the streaming
|
| 140 |
+
kernel selector / dequant-on-lift).
|
| 141 |
+
**Verdict:** the "zero-new-kernel" naive-q4_0-V bootstrap is **perf-blocked on Gemma-4 head_dim 512**
|
| 142 |
+
β correct at 4k (niah=100, vt=80) but cannot be made GPU-fast at scale without kernel work. Note:
|
| 143 |
+
rolling-KV itself is already proven GPU-fast at 256k with **q8_0** (M7 ship, bug-226: vt=96/niah=100).
|
| 144 |
+
The blocked piece is specifically the **asymmetric q4_0/3-bit V**. FORK (user decision pending):
|
| 145 |
+
(A) fix the streaming kernel/dequant-on-lift for q4_0-V (DSO debug + rebuild) β proves asymmetric
|
| 146 |
+
composition GPU-fast; (B) descope S0 to V=q8_0 (validated, GPU-fast; ~1.9Γ not 2.4Γ; loses asymmetry);
|
| 147 |
+
(C) skip to S2 TBQ3_0 with a proper fused GPU dequant-FA kernel (the real PolyKV V-quant);
|
| 148 |
+
(D) bank S0 correctness (4k proven) + proceed to **S1 SharedKVPool** (the "shared KV" core, host-side,
|
| 149 |
+
orthogonal to the V-quant kernel gap; delivers an O(1)-in-agents memory-perf story independent of it).
|
| 150 |
+
|
| 151 |
+
**DECISION (2026-06-05, user):** q4_0-V GPU kernel **DEFERRED** ("we'll see later about the q4_0
|
| 152 |
+
kernel") β it folds into S2 TBQ3_0, which needs a custom fused dequant-FA kernel regardless.
|
| 153 |
+
**Proceeding to S1 SharedKVPool now** (path D). S0 banked as: asymmetric architecture correct at 4k
|
| 154 |
+
(niah=100, vt=80); large-ctx GPU-fast asymmetric compression awaits the S2 kernel. S1 uses the
|
| 155 |
+
validated q8_0 KV (no kernel gap) and demonstrates the headline O(1)-in-agents sharing win.
|
| 156 |
+
|
| 157 |
+
### S1 β SharedKVPool: shared read-only prefix across sequences (the multi-tenant core) β
DONE (2026-06-05)
|
| 158 |
+
N slots/seq_ids attend ONE physical copy of a common document prefix + a per-slot private suffix β
|
| 159 |
+
O(1) prefix memory in agent count. Builds on M0 (prefix reuse), M7 (host residency), iSWA
|
| 160 |
+
(two-cache). Single agent degenerates to S0.
|
| 161 |
+
|
| 162 |
+
**Design (minimal-additive, rides native unified-mode sharing).** llama.cpp already lets one KV
|
| 163 |
+
cell belong to many `seq_id`s (`cells.seq[i]` is a `bitset<LLAMA_MAX_SEQ>`). In **unified mode**
|
| 164 |
+
(`--kv-unified`, `n_stream=1`) all slots live on stream 0, so `llama_memory_seq_cp(pool, slot, 0,
|
| 165 |
+
P)` takes the same-stream **zero-copy** branch (`cells.seq_add` β just OR the destination bit into
|
| 166 |
+
the existing prefix cells; no buffer copy, no new allocation). Attention isolation is automatic:
|
| 167 |
+
`set_input_kq_mask_impl` masks a cell for a seq unless `cells.seq_has(j, seq_id)`, so shared-prefix
|
| 168 |
+
cells (both bits) are attended by both agents while private-suffix cells (one bit) stay isolated.
|
| 169 |
+
S1 is therefore a tagged, **default-off** server hook (`shared_pool_slot`/`shared_prefix_n_tokens`
|
| 170 |
+
request params; gated prefill branch that `seq_cp`'s the pool's `[0,P)` prefix into the sharer's seq
|
| 171 |
+
and sets `n_past=P` to skip re-processing it) β **no new storage**. Counted-once is *structural*
|
| 172 |
+
(one boot-fixed KV buffer, prefix cells bit-shared), proven by cell occupancy, not a MiB delta.
|
| 173 |
+
|
| 174 |
+
**RUNTIME PREREQUISITE β `--no-clear-idle` (MANDATORY).** Upstream `clear_idle` defaults **on**
|
| 175 |
+
(with default `cache_ram_mib=8192`), so `--kv-unified` alone auto-enables the idle-slot prompt-cache
|
| 176 |
+
offload (PR #16391): when the pool slot goes idle, the next task launch serializes its KV to a host
|
| 177 |
+
blob **and frees the device cells**, evicting the prefix before any sharer's `seq_cp` runs β silent
|
| 178 |
+
prefix miss (suffix/isolation still work; only the shared prefix is lost). Booting `--no-clear-idle`
|
| 179 |
+
keeps the pool slot device-resident. The hook also self-guards: if `seq_pos_max(pool) < P-1` it
|
| 180 |
+
warns and falls through to a normal full reprocess (correct, just slower) instead of sharing nothing.
|
| 181 |
+
Root cause: [[bug-367]] β it masqueraded as a cross-seq attention regression; the seq_cp/mask
|
| 182 |
+
machinery was never at fault.
|
| 183 |
+
|
| 184 |
+
**Gate (`.opencoti/m6-s1-shared-pool-gate.sh`) β PASS on Gemma-4 A4B iSWA, q8/q8, `--parallel 3
|
| 185 |
+
--kv-unified --no-clear-idle`:** pool prefills an 874-token doc with needle `QUASAR-4417` on slot 0;
|
| 186 |
+
agents on slots 1/2 POST `shared_pool_slot:0` + a distinct private officer (Alice/Bob). Results:
|
| 187 |
+
both agents recall the needle (shared); each recalls only its own officer (isolation, R1 crux);
|
| 188 |
+
`cache_n=874βP` & `prompt_n=39βsuffix` per agent; Ξ£ prompt_n `{pool,A,B}` = 953 β `P+sufA+sufB`
|
| 189 |
+
(950), **not** `3Β·P` (2622) β prefix enters KV exactly once; pool slot intact after both sharers;
|
| 190 |
+
`shared_pool_slot:-1` default-off = full reprocess (`cache_n=0`) and still correct. iSWA two-cache
|
| 191 |
+
`seq_cp` delegation (the R2 risk) confirmed across the 5-layer SWA + 25-layer global caches.
|
| 192 |
+
Also verified on the non-iSWA path (Qwen2.5-1.5B-instruct).
|
| 193 |
+
|
| 194 |
+
**Fan-out MEMORY win β measured (`.opencoti/m6-s1-fanout-mem.sh`, Qwen, N=8, fixed 8192-cell unified
|
| 195 |
+
buffer).** Physical KV-cell occupancy (read from the `find_slot ... used =` debug counter) for the
|
| 196 |
+
SAME 8 agents over an 874-token shared doc: **naive (sharing off) = 7183 cells** (β 8ΓP, one full
|
| 197 |
+
copy per seq) vs **shared (S1) = 1041 cells** (874 prefix counted ONCE + 167 cells for the 7 private
|
| 198 |
+
suffixes β 24 cells/agent). That's a **6.9Γ reduction at N=8**, and projected onto the fixed buffer:
|
| 199 |
+
the naive path fits **~9 agents** before exhaustion while S1 fits **~306** β the O(1)-prefix headline
|
| 200 |
+
(prefix amortized once; per-agent cost is just the private suffix). This is the *memory* counterpart
|
| 201 |
+
to the gate's *compute* counted-once, so S1 is validated on both axes.
|
| 202 |
+
|
| 203 |
+
### S2 β TurboQuant tier family (TBQ8_0 / TBQ4_0 / TBQ3_0): the MSE-optimal KV-quant ladder (HIGH RISK)
|
| 204 |
+
TurboQuant is **one** scheme β FWHT (fast Walsh-Hadamard) rotation to spread outlier energy across
|
| 205 |
+
dims + Lloyd-Max (MSE-optimal) codebook β *parameterized by bit-width*. S2 ships the **full tier
|
| 206 |
+
ladder**, not a one-off 3-bit kernel: new ggml types **TBQ8_0, TBQ4_0, TBQ3_0** that share the same
|
| 207 |
+
rotation + codebook machinery, each with its own dequant-on-lift entry. This lets the asymmetric
|
| 208 |
+
scheme mix tiers freely (e.g. KβTBQ8_0, VβTBQ3_0) and supplies MSE-optimal replacements for the
|
| 209 |
+
naive q8_0/q4_0 KV used in S0/S1. **TBQ3_0 is the headline** (replaces V=q4_0 β 3-bit at reference
|
| 210 |
+
quality, +1.59% ppl @2k); **TBQ8_0/TBQ4_0** are the precision/mid rungs *and* the lower-risk
|
| 211 |
+
bring-up path β validate the shared rotation+codebook at 8-bit (near-lossless, easy cosine gate)
|
| 212 |
+
first, then descend to 4-bit and 3-bit. Highest-risk milestone (new quant kernels + dequant-on-lift
|
| 213 |
+
entries + ggml type registration + `GGML_TYPE_COUNT` bump + CUDA dequant).
|
| 214 |
+
|
| 215 |
+
**Adoption source (2026-06-05): the AtomicBot-ai TurboQuant fork, not a from-scratch kernel.**
|
| 216 |
+
[`AtomicBot-ai/atomic-llama-cpp-turboquant`](https://github.com/AtomicBot-ai/atomic-llama-cpp-turboquant)
|
| 217 |
+
(MIT, fork of `TheTom/llama-cpp-turboquant`) already ships a working, multi-platform (CUDA + Metal +
|
| 218 |
+
CPU) TurboQuant family: runtime KV types **`turbo2`/`turbo3`/`turbo4`** β `TURBO2_0`/`TURBO3_0`/
|
| 219 |
+
`TURBO4_0` (their wording: **"WHT + PolarQuant"**; `turbo3` = 3-bit ~4.3Γ, the recommended default,
|
| 220 |
+
with a Metal **"TurboFlash"** flash-attention decode kernel) selected via `-ctk/-ctv`; plus
|
| 221 |
+
quantize-time **weight** types `TQ3_1S`/`TQ4_1S` ("WHT-rotated Lloyd-Max", `block_size=32`) chosen as
|
| 222 |
+
`llama-quantize --type` with masks under `scripts/quantize-masks/`. **S2 becomes a study-and-adapt of
|
| 223 |
+
that fork** rather than a clean-room build: port its WHT rotation + low-bit codebook + the
|
| 224 |
+
CUDA/Metal dequant/`TurboFlash` decode path into our vendored `llama.cpp`, mapped onto our
|
| 225 |
+
dequant-on-lift (S3d) machinery and ggml-type registration. Reconcile naming on the way in (our
|
| 226 |
+
`TBQ3_0` β‘ their `TURBO3_0`/`turbo3`); decide whether to keep their `turbo*` spelling for CLI
|
| 227 |
+
compatibility with the fork's `-ctk turbo3` (and the draft `-ctkd/-ctvd turbo3`) so the published
|
| 228 |
+
Gemma-4 GGUFs run unmodified. **Caveat:** the fork's base differs from our vendored tree + we build
|
| 229 |
+
under **cosmocc** (no CMake/CUDA-nvcc in the host path; CUDA lives in the DSO) β this is a *port*,
|
| 230 |
+
not a cherry-pick, and pairs with S4 (both adopted from the same fork, both use `turbo3` KV).
|
| 231 |
+
- **Gate:** cosine/RULER holds at **each** tier (TBQ8_0 near-lossless, TBQ4_0 β q4_0-or-better,
|
| 232 |
+
TBQ3_0 at reference); TBQ3_0 V compression β ~2.9Γ (matches reference 2.91Γ); the fork's published
|
| 233 |
+
`-ctk turbo3` Gemma-4 path reproduces "no measurable quality regression". DSO rebuild.
|
| 234 |
+
|
| 235 |
+
#### S2 InnerQ β per-channel equalization, PER-LAYER head_dim β
DONE + ACCEPTED (2026-06-05)
|
| 236 |
+
turbo3 as a **K**-quant (not just V) needs the missing correctness ingredient: **InnerQ** per-channel
|
| 237 |
+
K-variance equalization *before* the WHT (`x[ch] *= scale[ch]` at K-write; dequant `Γscale_inv[ch]`
|
| 238 |
+
after the inverse WHT). Without it turbo3-K is ~0.983 cosine β too lossy for softmax (Kβ«V asymmetry).
|
| 239 |
+
opencoti uses **calibrate-then-activate-from-token-0**; `scale_inv` crosses the hostβDSO boundary as a
|
| 240 |
+
**file artifact** (the turbo3 forward is GPU-only in the DSO; our active dequant is the CPU `to_float`
|
| 241 |
+
on the lift path β both load the file at boot).
|
| 242 |
+
|
| 243 |
+
**The mixed-head_dim problem (Gemma-4).** `ne00` of the K set-rows view is `n_embd_gqa = head_dim Γ
|
| 244 |
+
n_head_kv`, **not** head_dim β and head_dim is NOT derivable from `ne00` alone. Gemma-4 mixes head_dim
|
| 245 |
+
**per layer**: SWA layers `head_dim 256` (`n_head_kv 8` β `n_embd_gqa 2048`) and global layers
|
| 246 |
+
`head_dim 512` (`n_head_kv 2` β `1024`), ~5:1. A single head_dim env mis-folds the minority layer-type
|
| 247 |
+
and contaminates the shared `scale_inv`. **Fix = per-WIDTH slots** keyed by `n_embd_gqa`: the host
|
| 248 |
+
supplies `TURBO_INNERQ_HEADDIM_MAP="2048:256,1024:512"`; the DSO carries per-slot
|
| 249 |
+
`scale/scale_inv/sq_accum[MAX_SLOTS=4][512]` + `slot_hd[]`, the dispatch maps `ne00βslot` and passes it
|
| 250 |
+
as a **kernel arg** (no per-launch H2D), the kernel folds `ch=(i_grp*128+j) % head_dim[slot]` into that
|
| 251 |
+
slot's accumulator (no cross-width contamination). Finalize writes a **v2 file** (`TIQ2`: `nslots`, per
|
| 252 |
+
slot `{n_embd_gqa, head_dim, float[head_dim]}`); v1 (`TIQ1`) still loads as one wildcard slot. The CPU
|
| 253 |
+
dequant selects its slot by `k == n_embd_gqa`. Empty map β one wildcard slot (uniform models, e.g. Qwen
|
| 254 |
+
head_dim 128 β identity fold `ch==j`, byte-compatible). Files: `ggml-cuda/set-rows.cu`,
|
| 255 |
+
`ggml-cuda/turbo-innerq.cuh`, `ggml-turbo-quant.c`; gate `.opencoti/s2-innerq-gate.sh` (+`HEADDIM_MAP`).
|
| 256 |
+
|
| 257 |
+
**Fused-path invariant (S2-next).** InnerQ's forward-equalize (`Γscale` at K-write) is **path-shared**;
|
| 258 |
+
the un-equalize is the only path-specific half and lives **only** in the lift's `to_float` (`Γscale_inv`).
|
| 259 |
+
The fused kernel keeps K equalized and moves the correction to a **Q pre-scale** (`Q[c]*=scale_inv[c]`,
|
| 260 |
+
`Q_preΒ·K_eq = QΒ·K`), reusing the **same per-width table** β so it must NOT route through `to_float`
|
| 261 |
+
(no double-correction). Per-layer InnerQ is therefore a prerequisite the fused path consumes wholesale.
|
| 262 |
+
|
| 263 |
+
**Acceptance (format-robust, NEVER greedy).** A greedy short-coherence probe is invalid here β the
|
| 264 |
+
`...-it` coder model is thinking-tuned (`<|channel>thought`) and degenerates on raw short prompts *even
|
| 265 |
+
at f16*. The valid gate is **logit-equivalence vs f16** (`.opencoti/s2-logit-equiv-gate.sh` over the
|
| 266 |
+
#355 harness). **VERDICT PASS** on Gemma-4 A4B, turbo3/turbo3 + per-layer InnerQ vs f16/f16, 12 needle
|
| 267 |
+
prompts: `real_frac=0.0` (β€0.15), answer-scoped `recall_mismatch=0` (12/12 both recall), all 10
|
| 268 |
+
divergences `post_answer`-benign, `mean_tv=0.0102`/`p95=0.0255` (catastrophe 0.30). turbo3-K +
|
| 269 |
+
per-layer InnerQ is distributionally faithful to f16. Validated non-regressive on Qwen3-4B (uniform
|
| 270 |
+
head_dim 128, identity fold). (#379)
|
| 271 |
+
|
| 272 |
+
#### S2 tier-family overview β quality Γ speed Γ memory, all 7 KV modes β
(2026-06-05)
|
| 273 |
+
|
| 274 |
+
Full sweep over `{f16, q8_0, q4_0, turbo8, turbo4, turbo3, turbo2}` on one rig (Gemma-4 A4B
|
| 275 |
+
Q4_K_M, RTX 3090, 6025-tok prompt, 256-tok forced decode `ignore_eos`, ctx 8192, single stream).
|
| 276 |
+
Harness: `.opencoti/s2-tier-overview.sh` + `s2-overview-assemble.py`. Quality = logit-equiv vs f16
|
| 277 |
+
(#355, n=12 needle); turbo verdicts from the tier-family gate, q8/q4 captured in the same pass.
|
| 278 |
+
|
| 279 |
+
```
|
| 280 |
+
mode | verdict real rec meanTV p95TV | pre tps dec tps util% | KV MiB bpv vsF16
|
| 281 |
+
--------------------------------------------------------------------------------------------
|
| 282 |
+
f16 | REF 0.000 0 0.0000 0.0000 | 2751 73.2 97 | 460.0 2.000 1.00x
|
| 283 |
+
q8_0 | PASS 0.000 0 0.0122 0.0785 | 2610 65.7 94 | 244.4 1.062 0.53x
|
| 284 |
+
q4_0 | PASS 0.000 0 0.0094 0.0007 | 2654 64.9 93 | 129.4 0.562 0.28x
|
| 285 |
+
turbo8 | PASS 0.000 0 0.0098 0.0157 | 994 2.7 54 | 233.6 1.016 0.51x
|
| 286 |
+
turbo4 | PASS 0.000 0 0.0152 0.0768 | 1069 3.2 58 | 122.2 0.531 0.27x
|
| 287 |
+
turbo3 | PASS 0.000 0 0.0075 0.0090 | 1003 3.1 53 | 89.8 0.391 0.20x
|
| 288 |
+
turbo2 | PASS 0.000 0 0.0228 0.1304 | 1129 3.3 58 | 61.1 0.266 0.13x
|
| 289 |
+
```
|
| 290 |
+
|
| 291 |
+
- **Quality:** all 6 quant modes PASS (real_frac 0, recall_mismatch 0). turbo2's p95 TV 0.13 is the
|
| 292 |
+
loosest but far under the 0.30 catastrophe line. Quality is **not** the differentiator β the whole
|
| 293 |
+
ladder is usable.
|
| 294 |
+
- **Memory (the win):** 460β61 MiB, a 7.5Γ span. turbo8 (234) and turbo4 (122) **Pareto-beat** q8_0
|
| 295 |
+
(244)/q4_0 (129) at matched bit-width β turbo carries one `norm`/128 vals (1.6% overhead) vs stock's
|
| 296 |
+
scale/32 (6.25%). turbo3 (90) and turbo2 (61) reach footprints stock has no q3/q2 KV type for.
|
| 297 |
+
- **Speed (the catch β RESOLVED 2026-06-06 by the perf-path below):** in this validation-path snapshot,
|
| 298 |
+
native quants decoded 65β73 tps / 93β97% util while **turbo decoded 2.7β3.3 tps / 53β58% util (~25Γ
|
| 299 |
+
slower).** The fingerprint: prefill dropped ~2.7Γ but decode collapsed ~25Γ β the CPU dequant-on-lift
|
| 300 |
+
reconstructed the WHOLE KV cache on the host every step (prefill amortizes over the query batch, decode
|
| 301 |
+
pays per single token). Root cause: the `ggml_cast(turboNβf16)` lift emitted a `GGML_OP_CPY` that
|
| 302 |
+
CUDA's CPY `supports_op` rejected (no turbo src), so the scheduler spilled it to the CPU backend β
|
| 303 |
+
whole compressed cache D2H + host dequant + H2D every token. **Fixed by the GPU-resident dequant-on-lift
|
| 304 |
+
(Level A, next subsection).** (#371)
|
| 305 |
+
|
| 306 |
+
#### S2 perf-path Level A β GPU-resident InnerQ-correct turbo dequant-on-lift β
DONE + GATED (2026-06-06)
|
| 307 |
+
|
| 308 |
+
> **β SUPERSEDED β historical.** The ~41 tps plateau here is the whole-cache `ggml_cast(turboNβF16)`
|
| 309 |
+
> materialize path; it is broken open by **Level B** (in-register fused FA-VEC, next subsection) and
|
| 310 |
+
> again by the **WS1/WS2 in-register turbo-V** work (context.md Β§5x). The current turbo decode story
|
| 311 |
+
> lives there β treat these Level-A numbers as a milestone record, not the shipping figures.
|
| 312 |
+
|
| 313 |
+
The "Speed (the catch)" spill above is fixed. The lift stays a graph `ggml_cast(turboNβF16)` but now
|
| 314 |
+
executes **entirely on CUDA**: a new block-cooperative kernel does the inverse WHT **once** per
|
| 315 |
+
128-element group in shared memory (vs the per-output-pair recompute in `turbo-dequant.cuh`), applies
|
| 316 |
+
InnerQ `Γscale_inv[ch]` in true-channel space, and writes f16 in VRAM β stock flash-attention then runs
|
| 317 |
+
unchanged. No host round-trip. Materialize-f16-then-stock-FA is preserved by design, so the ceiling is
|
| 318 |
+
the f16 path (Level B = fused FA-VEC reading turbo in-register, a later milestone).
|
| 319 |
+
|
| 320 |
+
**Edit surface (4 hook sites + 3 new files, all `// opencoti-hook: turboquant perf-lift`; 10 markers):**
|
| 321 |
+
- `ggml/src/ggml-cuda/turbo-cpy.{cu,cuh}` (NEW) β `cpy_turbo{2,3,4,8}_f16` kernels + `ggml_cpy_turbo_f16_cuda`
|
| 322 |
+
launcher (WHT-once cooperative inverse + InnerQ un-equalize + strided f16 write).
|
| 323 |
+
- `ggml/src/ggml-cuda/turbo-innerq-dev.cuh` (NEW) β cross-TU bridge declaring
|
| 324 |
+
`turbo_innerq_active_device_scale(ne00, &d_scale_inv, &head_dim)`.
|
| 325 |
+
- `ggml-cuda/set-rows.cu` β implements that accessor (owns the InnerQ device symbols; selects slot via
|
| 326 |
+
the existing `innerq_slot_for((int)ne00)`, returns a `cudaGetSymbolAddress` pointer + `head_dim`).
|
| 327 |
+
- `ggml-cuda/ggml-cuda.cu` β CPY `supports_op` now accepts `turbo{2,3,4,8}_0 β F16`.
|
| 328 |
+
- `ggml-cuda/cpy.cu` β `ggml_cuda_cpy` dispatches turboβF16 to the launcher (InnerQ pointer from the bridge,
|
| 329 |
+
or `nullptr` β identity).
|
| 330 |
+
- `src/llama-graph.cpp` β collapses the old two-step `turboβF32βF16` cast to a single `turboβF16` (removes
|
| 331 |
+
the f32 intermediate whose `turboβF32` leg was the CPU-spilling op).
|
| 332 |
+
|
| 333 |
+
**Correctness model (R2, resolved empirically):** the GPU lift is a faithful numeric replica of the CPU
|
| 334 |
+
dequant β same graph tensor, same `ne00`, same `innerq_slot_for`/`head_dim` table, same inverse-WHT order
|
| 335 |
+
(signs2 β butterfly β 1/β128Β·signs1) β so it makes the *identical* InnerQ decision as the CPU lift it
|
| 336 |
+
replaces. The gate passes iff the CPU gate passed (it does). No K/V flag needed.
|
| 337 |
+
|
| 338 |
+
**Build:** `build:llamafile:cuda` (DSO, ~24min; restamp `/root/.llamafile/v/0.10.1/ggml-cuda.so` **and**
|
| 339 |
+
`/root/ggml-cuda.so` byte-identical, bug-224) + `build:llamafile:make` (host, llama-graph.cpp). Scheduler
|
| 340 |
+
proof: `GGML_SCHED_DEBUG=1` shows `graph splits = 2` (no CPU-spill fragmentation; the per-node dump is
|
| 341 |
+
compiled out under `-DNDEBUG`).
|
| 342 |
+
|
| 343 |
+
**Gates (both GREEN, RTX 3090, Gemma-4 A4B Q4_K_M, 6025-tok prompt, 256-tok forced decode `ignore_eos`):**
|
| 344 |
+
|
| 345 |
+
```
|
| 346 |
+
mode | verdict real rec | dec tps was speedup util% was | KV MiB (unchanged)
|
| 347 |
+
-------------------------------------------------------------------------------------
|
| 348 |
+
f16 | REF 0.00 0 | 73.8 73.2 1.0x 95 97 | 460.0
|
| 349 |
+
q8_0 | PASS 0.00 0 | 65.9 65.7 1.0x 95 94 | 244.4
|
| 350 |
+
q4_0 | PASS 0.00 0 | 65.9 64.9 1.0x 96 93 | 129.4
|
| 351 |
+
turbo8 | PASS 0.00 0 | 42.5 2.7 15.7x 96 54 | 233.6
|
| 352 |
+
turbo4 | PASS 0.00 0 | 40.6 3.2 12.7x 98 58 | 122.2
|
| 353 |
+
turbo3 | PASS 0.00 0 | 41.6 3.1 13.4x 96 53 | 89.8
|
| 354 |
+
turbo2 | PASS 0.00 0 | 42.2 3.3 12.8x 95 58 | 61.1
|
| 355 |
+
```
|
| 356 |
+
|
| 357 |
+
- **Decode:** all turbo tiers **12.7β15.7Γ** faster (3β~41 tps), every tier clears the β₯10Γ bar. Prefill
|
| 358 |
+
also recovered (turbo8 994β2513 tps β the spill taxed prefill too); util 53β58% β 95β98%.
|
| 359 |
+
- **Quality:** all four tiers stay **logit-equiv PASS** (real_frac 0, recall_mismatch 0) vs the SAME f16
|
| 360 |
+
ref β the GPU dequant reproduces the CPU dequant's InnerQ output distributionally (R1 validated).
|
| 361 |
+
- **Memory + regression:** KV MiB identical to the validation-path table; f16/q8_0/q4_0 byte-unchanged.
|
| 362 |
+
- **Residual (root-caused 2026-06-06, #389):** turbo plateaus at a uniform ~41 tps **regardless of tier**
|
| 363 |
+
(turbo8/4/3/2 all ~41 despite 234β61 MiB KV) β the tell-tale of a **tier-independent** bottleneck. It is
|
| 364 |
+
the Level-A lift itself: `build_attn_mha` casts the **whole** turbo KV view to a full **460 MiB f16
|
| 365 |
+
buffer every decode step** (`ggml_cast(turboNβF16)` over all n_kv, redundantly re-dequantizing unchanged
|
| 366 |
+
positions), then runs stock f16 FA on it. The f16 output is the same size for every tier β flat ~41 tps;
|
| 367 |
+
the turbo read (61β234 MiB) is not the bottleneck. This **throws away** turbo's bandwidth advantage β
|
| 368 |
+
turbo decodes *below* q8/q4 (which the FA kernel reads in-register, no materialize). **The AtomicBot fork
|
| 369 |
+
proves the real ceiling: its CUDA path is the fused FA-VEC (`fattn-vec.cuh` reads `TURBO{2,3,4}_0`
|
| 370 |
+
in-register as "unquantized" K/V + sparse-V skip; full template-instance set).** Fork benchmark (M4 Max
|
| 371 |
+
Metal, fused): turbo3 = **88β92 % of f16** (61.84 vs 70.09 tps MoE; 19.71 vs 21.34 dense), 78.5 tps
|
| 372 |
+
tg128 on M5 β i.e. β q8/q4-class, **not** halved. Ratio-extrapolated to our rig (f16=74) the fused target
|
| 373 |
+
is **~63β66 tps turbo3**. Closing the gap = **Level B = port the fork's fused FA-VEC turbo path** (#390),
|
| 374 |
+
existing vendored code, the next milestone. (A cheaper "incremental materialize into a persistent f16
|
| 375 |
+
shadow" would remove the per-step redundancy but costs a full f16 shadow buffer β negates the runtime
|
| 376 |
+
memory win and is strictly dominated by the fused path; go straight to Level B.)
|
| 377 |
+
- Patch capture: the 6-file Level-A edit surface is captured in **`0073-turboquant-kv`** at S3 (#372),
|
| 378 |
+
registered via the patch README row (same snapshot-diff mechanism as `0070`). (S3pA / #383β388)
|
| 379 |
+
|
| 380 |
+
#### S2 perf-path Level B β fused FA-VEC turbo (in-register K/V, no f16 materialize) β
DONE + GATED (2026-06-06)
|
| 381 |
+
|
| 382 |
+
Level A's flat ~41 tps ceiling is **broken**. The whole-cache `ggml_cast(turboNβF16)` per decode step is
|
| 383 |
+
gone for the value tiers: flash-attention now reads turbo2/3/4 K/V **in-register** (centroidΓnorm, in the
|
| 384 |
+
WHT-rotated domain), and the expensive WalshβHadamard rotation runs only on the n_q-sized **Q** (forward)
|
| 385 |
+
and the small FA **output** (inverse) β never on the KV cache. Acceptance = logit-equivalence, never
|
| 386 |
+
greedy byte-equality.
|
| 387 |
+
|
| 388 |
+
**New op `GGML_OP_TURBO_WHT`** (`ggml/include/ggml.h`, `ggml/src/ggml.c`, `ggml-cpu/ggml-cpu.c` abort-stub,
|
| 389 |
+
`ggml-cuda/turbo-wht.{cu,cuh}` + `turbo-wht-core.cuh`): direction 0 forward-rotates Q
|
| 390 |
+
(`Γscale_inv β signsβ β WHT β 1/βGΒ·signsβ`); direction 1 inverse-rotates the output. InnerQ folds into
|
| 391 |
+
**both** WHT kernels via the device symbol `turbo_innerq_active_device_scale(ne[0]=head_dim)` (B-adapted β
|
| 392 |
+
no graph-tensor scale src, unlike the fork's mctx tensor). **Parseval:** `WHT(Qβs)Β·WHT(Kβs) = QΒ·K`, so the
|
| 393 |
+
InnerQ scale cancels and the turbo K/V reads are InnerQ-**free** (pure centroidΓnorm).
|
| 394 |
+
|
| 395 |
+
**Edit surface (all `// opencoti-hook: turboquant perf-lift (β¦ Level B)`):** `fattn-turbo.cuh` (NEW β
|
| 396 |
+
in-register `vec_dot_fattn_vec_KQ_turbo{2,3,4}` + `dequantize_V_turbo{2,3,4}`), `fattn-common.cuh`
|
| 397 |
+
(`get_vec_dot_KQ`/`get_dequantize_V` turbo cases), `fattn-vec.cuh` (`is_turbo_K` config; turbo rides the
|
| 398 |
+
**float** Q path), `fattn.cu` (turboβVEC gating, Dβ{128,256}), `ggml-cuda.cu` (TURBO_WHT dispatch +
|
| 399 |
+
supports_op), `template-instances/fattn-vec-instance-turbo{2,3,4}_0` (+ generator), `src/llama-graph.cpp`
|
| 400 |
+
(`build_attn_mha` fused gate). turbo8 + head_dim-512 global layers keep the Level-A materialize fallback
|
| 401 |
+
(no vec instance / no fork reference).
|
| 402 |
+
|
| 403 |
+
**Graph gate (`build_attn_mha`):** fuse iff `flash_attn && turbo2/3/4 && K==V && head_dimβ{128,256}` **and**
|
| 404 |
+
(L4.5 prefill-hybrid) per-stream `n_q β€ TBV_FUSE_MAX_NQ` (default 8) β DECODE fuses, large-n_q PREFILL falls
|
| 405 |
+
to the Level-A materializeβMMA path (turbo prefill on the vec kernel is ~2β3Γ slower than f16-MMA).
|
| 406 |
+
|
| 407 |
+
**Gate (GREEN, RTX 3090, Gemma-4 A4B Q4_K_M, 6025-tok prompt, 256-tok forced decode):**
|
| 408 |
+
|
| 409 |
+
```
|
| 410 |
+
mode | verdict mean_tv | dec@6k LevelA low-ctx | prefill | KV MiB | path
|
| 411 |
+
----------------------------------------------------------------------------------
|
| 412 |
+
f16 | REF β | 72.4 73.8 77.6 | 2788 | 460.0 | native
|
| 413 |
+
q8_0 | PASS 0.0122 | 65.4 65.9 β | 2617 | 244.4 | native
|
| 414 |
+
q4_0 | PASS 0.0094 | 64.6 65.9 70.2 | 2656 | 129.4 | native
|
| 415 |
+
turbo8 | PASS 0.0181 | 42.6 42.5 β | 2502 | 233.6 | materialize (fallback)
|
| 416 |
+
turbo4 | PASS 0.0136 | 48.5 40.6 β | 817* | 122.2 | FUSED
|
| 417 |
+
turbo3 | PASS 0.0067 | 51.4 41.6 64.1 | 1113* | 89.8 | FUSED
|
| 418 |
+
turbo2 | PASS 0.0179 | 56.0 42.2 β | 1537* | 61.1 | FUSED
|
| 419 |
+
```
|
| 420 |
+
`*` always-fuse prefill; the prefill-hybrid (default) restores turbo prefill to ~2474 tps (= materialize)
|
| 421 |
+
while keeping 51.8 decode (A/B turbo3: T=0 2544/41.4, T=8 2474/51.8, T=huge 1099/52.2; hybrid logit-equiv
|
| 422 |
+
PASS, mean_tv 0.0063).
|
| 423 |
+
|
| 424 |
+
- **Decode ceiling broken + scales with compression:** turbo2 56 > turbo3 51 > turbo4 48 > turbo8-materialize
|
| 425 |
+
43 tps β the more compressed the tier, the less KV bandwidth/step, the faster decode (the in-register
|
| 426 |
+
payoff). turbo2 = **86 % of q8_0**, 1.31Γ the Level-A ceiling. GPU util 96β98 % (Level-A ~53 %).
|
| 427 |
+
- **Quality:** 4/4 logit-equiv PASS (real_frac 0, recall_mismatch 0) vs the same f16 ref; **turbo3 mean_tv
|
| 428 |
+
0.0067 is the tightest of any tier** β a 3-bit cache beating 4-bit q4 (0.0094) on quality.
|
| 429 |
+
- **Decode-tps is ctx-dependent** (turbo3 64.1 low-ctx β 51.4 @6k). Report the ctx.
|
| 430 |
+
|
| 431 |
+
**Tier positioning (decided 2026-06-06, user):** at matched bits, native **q8_0/q4_0 dominate turbo8/turbo4**
|
| 432 |
+
on speed AND quality at ~equal VRAM β TurboQuant's MSE centroids buy nothing at 4β8 bit (scalar quant is
|
| 433 |
+
already near-lossless). **Use q8_0/q4_0 for 8/4-bit KV.** TurboQuant earns its place at **3-bit and below**,
|
| 434 |
+
where native has no KV type: **turbo3** Pareto-beats q4 (90 vs 129 MiB, TV 0.0067 vs 0.0094) at ~80 % of q4
|
| 435 |
+
long-ctx decode; **turbo2** is the smallest logit-equiv KV that exists (61 MiB, 56 tps) β the 256k play.
|
| 436 |
+
turbo8/turbo4 stay as ladder-rung / materialize fallbacks; turbo8 is **not** fused.
|
| 437 |
+
|
| 438 |
+
**Reference (`animehacker/llama-turboquant`, investigated #395):** its headline "tb3 β q4_0" is a **regime
|
| 439 |
+
artifact** (Qwen-0.8B, tg128, Strix-Halo UMA APU β KV cost negligible); reproduced on our rig at low ctx
|
| 440 |
+
(turbo3 64.1 vs q4 70.2 = 91 %). Its kernel does the **same float centroid-LUT dot** we do; the gap at long
|
| 441 |
+
ctx is **intrinsic centroid-decode compute** (turbo3 reads *less* KV than q4 yet decodes slower). No kernel
|
| 442 |
+
unlocks q4-parity at long ctx.
|
| 443 |
+
|
| 444 |
+
**L4.5 optimization round:** **prefill-hybrid = WINNER, shipped** (default `TBV_FUSE_MAX_NQ=8`). **int8-dp4a**
|
| 445 |
+
(restructure the KQ dot into integer `__dp4a` + int8 centroid codebook) β correctness **de-risked viable**
|
| 446 |
+
(`.opencoti/ab-dp4a-unit2.cu`: softmax TV 0.0005), but gather-bound (dp4a speeds the MAC, not the centroid
|
| 447 |
+
LUT lookups, nor the float V path) and needs re-enabling the q8_1 Q path through the L4-green kernel β
|
| 448 |
+
deferred to **#397** (flag-gated follow-on, correctness already proven). AB1 (fold-WHT-into-FA) costed at
|
| 449 |
+
~1β2 % (60 tiny launches/step) β subsumed by #397's q8_1 path; AB2/AB4/AB5 dropped (centroid-LUT is the right
|
| 450 |
+
primitive per the reference). β οΈ **Correction (user, 2026-06-07):** the original AB4 write-off β
|
| 451 |
+
"turbo8/4 demoted so D=512 turbo-vec moot" β was a **non-sequitur**. Head_dim-512 coverage is orthogonal
|
| 452 |
+
to *which tiers* survive: turbo8/4 being demoted says nothing about whether the 512-dim global layers need a
|
| 453 |
+
fused turbo path. They do β for turbo2/turbo3 (see below). AB4 should never have been dropped on that basis.
|
| 454 |
+
|
| 455 |
+
#### S2 perf-path D=512 fused turbo-vec for turbo2/turbo3 β
DONE + GATED (2026-06-07, #411) β ships as `0075-d512-turbo-vec`
|
| 456 |
+
|
| 457 |
+
**Done.** The "D=512 turbo-vec moot" verdict above was scoped to turbo8/4 (demoted in favour of native
|
| 458 |
+
q8/q4). It did **not** hold for the sub-4-bit tiers: **turbo3/turbo2 are the 256k play**, and at head_dim
|
| 459 |
+
512 (Gemma-4 global layers, `n_head_kv=2`, ~1 in 6 layers) they fell back to the **Level-A materializeβMMA**
|
| 460 |
+
path β the fused gate was `head_dimβ{128,256}` only. At long ctx that whole-cache `ggml_cast(turboNβF16)`
|
| 461 |
+
per step is the decode tax that scales linearly with `n_kv`, biting hardest at 256k (our primary case).
|
| 462 |
+
|
| 463 |
+
**The assumption that a flat D=512 would blow the VEC register budget (the original reason upstream caps
|
| 464 |
+
`can_use_vector_kernel` at β€256) was WRONG for the turbo path** β no split / tiling was needed. The VEC
|
| 465 |
+
kernel is already **D-generic** (`nthreads`/`nthreads_V` derive from D arithmetically; at D=512, ncols=1:
|
| 466 |
+
`nthreads_V=min(D/4,32)=32`β`V_cols_per_iter=1`, KQ smem `max(ne_KQ, nwarpsΒ·1Β·512)` halfs β 4β8 KB on the
|
| 467 |
+
Ampere half2 path β well under the 48 KB static-smem cap β and VKQ β 8 half2 β 16 regs). A plain
|
| 468 |
+
`DECL_FATTN_VEC_CASE(512, turbo2/3)` instance compiles + runs clean. Ships as **`0075-d512-turbo-vec`**:
|
| 469 |
+
`fattn-vec.cuh` extern decls + the two turbo template instances (+ `generate_cu_files.py` autogen line),
|
| 470 |
+
`fattn.cu` kernel-selection relax (`Q->ne[0]==512`βVEC) + dispatch, `llama-graph.cpp` `fused_turbo_512`
|
| 471 |
+
fuse-gate extension. turbo4/turbo8 @ 512 keep Level-A (no D=512 instance).
|
| 472 |
+
|
| 473 |
+
**Gate (`.opencoti/p411-d512-gate.sh` + `p411-perf.sh`):** correctness = **Parseval-EXACT** (the WHT is
|
| 474 |
+
block-diagonal over 128-blocks β D=512 = 4 independent Parseval-exact 128-blocks): fused-vs-materialize
|
| 475 |
+
logit-equivalence **`real_frac=0.0` for BOTH turbo3 and turbo2** (bit-equivalent, not merely close). Perf:
|
| 476 |
+
turbo3 @ ctx 27.7k, **fused 41.0 vs materialize 29.7 tok/s = +38%**, scaling with `n_kv` (larger at 256k).
|
| 477 |
+
No new ABI / flag (auto-engages via the existing `TBV_FUSE_MAX_NQ`=8 decode gate). This unblocks the long
|
| 478 |
+
RULER re-run.
|
| 479 |
+
|
| 480 |
+
- Patch capture: the Level-B edit surface joins **`0073-turboquant-kv`** at S3 (#372) β captured +
|
| 481 |
+
byte-identical-proven (vendored source stays uncommitted per policy; DSO sha `19168f6a`, both `/root`
|
| 482 |
+
paths restamped byte-identical).
|
| 483 |
+
|
| 484 |
+
### S4 β Speculative MTP draft head (`gemma4_assistant`) for Gemma-4 A4B β adopt AtomicBot fork (HIGH RISK, NEW 2026-06-05)
|
| 485 |
+
Pairs with S2 (same fork, same `turbo3` KV). Gives **ours** llamafile a working **Multi-Token
|
| 486 |
+
Prediction** speculative-decode path for Gemma-4 A4B: a ~0.4B **assistant draft head** predicts
|
| 487 |
+
`B-1` future tokens from the target's last hidden state, the 27B-MoE target verifies them in
|
| 488 |
+
parallel β **same output distribution** (lossless), ~85-88% accept / claimed +30-50% short-prompt
|
| 489 |
+
throughput on dense targets. The user frames the head as **"EAGLE3 weight layout with low/mid/high
|
| 490 |
+
feature taps"**; the fork ships it as the custom **`gemma4_assistant`** architecture (EAGLE-family;
|
| 491 |
+
`eagle3` is a sibling `--spec-type`).
|
| 492 |
+
|
| 493 |
+
**Draft artifact (published, ready to consume):**
|
| 494 |
+
[`AtomicChat/gemma-4-26B-A4B-it-assistant-GGUF`](https://huggingface.co/AtomicChat/gemma-4-26B-A4B-it-assistant-GGUF)
|
| 495 |
+
β F16 (816 MB) / Q8_0 (440) / Q5_K_M / **Q4_K_M (310, recommended draft)** / Q4_K_S. Target = any
|
| 496 |
+
Gemma-4 26B-A4B-it GGUF (our `gemma-4-A4B-98e-v6-coder-it-Q4_K_M` or unsloth's). These GGUFs use the
|
| 497 |
+
`gemma4_assistant` arch and **will NOT load in stock llama.cpp** β that's exactly the gap S4 closes.
|
| 498 |
+
|
| 499 |
+
**Fork usage we must reproduce (CUDA/Linux):**
|
| 500 |
+
```
|
| 501 |
+
llama-server -m TARGET.gguf --mtp-head ASSISTANT.gguf --spec-type mtp \
|
| 502 |
+
--draft-block-size 3 --draft-max 8 --draft-min 0 -ngl 99 -ngld 99 \
|
| 503 |
+
-ctk turbo3 -ctv turbo3 -ctkd turbo3 -ctvd turbo3 -fa on -c 16384
|
| 504 |
+
```
|
| 505 |
+
|
| 506 |
+
**What S4 must adopt/port into our vendored cosmocc-built `llama.cpp` (study-and-adapt, not
|
| 507 |
+
cherry-pick β fork base β our base, and CUDA lives in our DSO):**
|
| 508 |
+
1. **`gemma4_assistant` architecture** β arch enum + hparams + `models/gemma4-assistant.cpp` graph
|
| 509 |
+
builder (the EAGLE3 low/mid/high feature-tap head + centroid LM head for E2B/E4B; dense tied head
|
| 510 |
+
for 26B-A4B). Loader for the assistant GGUF's tensors.
|
| 511 |
+
2. **In-context MTP scheduler** β the fork loads the draft **into the target context** (no second
|
| 512 |
+
`llama_context`, **no second KV cache**), on a dedicated scheduler; the head consumes the target's
|
| 513 |
+
last hidden state, drafts a block, the target batch-verifies. This composes with M7 residency and
|
| 514 |
+
S1 unified KV β design the seam carefully (one cache, one slot, draft tokens appended/verified).
|
| 515 |
+
3. **Runtime flags** (`common/arg.cpp`): `--mtp-head` (+ `-md` alias), `--spec-type mtp`,
|
| 516 |
+
`--draft-block-size`, `--draft-max`, `--draft-min`, `-ngld`, and draft KV `-ctkd/-ctvd`
|
| 517 |
+
(depends on S2 for `turbo3`; until S2 lands, draft KV can ride q8_0/f16).
|
| 518 |
+
4. **TS adapter / inference-engine surface** β expose the draft-head path through the opencoti
|
| 519 |
+
tiered-inference config (later; not S4 core).
|
| 520 |
+
- **Gate:** with the assistant draft loaded, decode is **distribution-identical** to no-draft greedy
|
| 521 |
+
(teacher-forced KL β 0 / top-1 agreement β reuse the #355 logit-equivalence harness), measured
|
| 522 |
+
accept-rate in the published 70-88% band, and net **throughput up** vs no-draft on Gemma-4 A4B at
|
| 523 |
+
RTX-3090. Draft-off byte-identical. NEVER greedy-needle alone.
|
| 524 |
+
- **Risks:** (R-S4a) fork base divergence β the arch/scheduler may touch files heavily rewritten by
|
| 525 |
+
our 0030-0042/0070 patches; budget a real port. (R-S4b) "no second KV cache" claim must be
|
| 526 |
+
reconciled with our M7/S1 single-unified-cache model β verify the draft's appended tokens don't
|
| 527 |
+
corrupt the shared/rolling cache. (R-S4c) cosmocc has no nvcc in the host path; any new CUDA kernel
|
| 528 |
+
(TurboFlash for the draft) goes in the DSO build, not `:make`.
|
| 529 |
+
|
| 530 |
+
### S3 β Compose-with-glue + bench + patches + docs β
DONE + SHIPPED (2026-06-06, #372)
|
| 531 |
+
The ship: prove PolyKV composes on the M5 advanced-KV stack, capture the vendored delta into
|
| 532 |
+
reproducible patches, wire the user-facing TS knob, and land bookkeeping. **S4 (MTP draft) is split
|
| 533 |
+
out as its own future milestone β its `0062` patch is NOT part of this ship.**
|
| 534 |
+
|
| 535 |
+
**Adapter knob (P1).** The PolyKV user-facing KV-type selector is `-ctk`/`-ctv`, surfaced on the
|
| 536 |
+
`opencoti-llamafile` adapter as `config.ctypeK`/`ctypeV` (env `OPENCOTI_LLAMAFILE_CTYPE_K/_V`,
|
| 537 |
+
validated against `KNOWN_KV_TYPES` incl. `turbo{2,3,4,8}`), emitted in `buildServerArgs` after
|
| 538 |
+
`--kv-residency-mode` / before `--neo-pipeline`. Default-unset β no flag emitted (byte-identical to
|
| 539 |
+
prior arg vector). S1's pool selection is a **per-request** JSON param (`shared_pool_slot` /
|
| 540 |
+
`shared_prefix_n_tokens`), server-side, not a CLI flag. 98 adapter tests PASS + typecheck clean.
|
| 541 |
+
|
| 542 |
+
**Compose bench (P1, `perf/llamafile/advanced-kv-stack.bench.ts`).** Added config **A4** = baseline
|
| 543 |
+
+ `-ctk q8_0 -ctv q4_0` (S0 native asymmetric, greedy-needle-safe) and **Sβ²** = the full F5 stack +
|
| 544 |
+
asymmetric KV. Live (Qwen 0.5B) result **11/12 hard PASS**: A4 boots + recovers needle, A4 CUDA KV
|
| 545 |
+
4.89 vs baseline 12.00 MiB (0.41Γ β€ 0.6Γ gate), Sβ² `cached_n>0` β§ needle_recovered, Sβ² per-cell mem <
|
| 546 |
+
S per-cell (soft W3 PASS). The sole hard FAIL (C9, S 51.92 vs B 336.05 tps) is the **pre-existing M2
|
| 547 |
+
head-split perf floor**, not PolyKV β A4-alone decodes 128 tps, proving the asymmetric KV path itself
|
| 548 |
+
is fast; C3/C7 SKIPPED (workload forced no shift this run).
|
| 549 |
+
|
| 550 |
+
**Live turbo3 compose gate (P1b, `.opencoti/m6-s3-glue-gate.sh`) β the PolyKV β HeadInfer-moot
|
| 551 |
+
evidence.** turbo3 **and** turbo2 K/V under the live F5 stack flags (`--kv-unified --cache-reuse 256
|
| 552 |
+
--rest-kv-eviction --rest-kv-recent 256 --rest-kv-layer -1`, `--parallel 2`, Gemma-4 A4B,
|
| 553 |
+
InnerQ-calibrated) vs the **same-stack f16 reference** (isolates the KV-type effect, not the stack
|
| 554 |
+
effect). Both PASS: turbo3 `real_frac=0.0` mean_tv 0.0164, turbo2 `real_frac=0.0` mean_tv 0.01784,
|
| 555 |
+
`recall_mismatch=0`. **PolyKV β HeadInfer conflict is MOOT:** M7 retired the head-split default
|
| 556 |
+
(`kvResidencyMode auto` = *window*, not the M2 head axis), so TurboQuant's Level-B fused FA-VEC never
|
| 557 |
+
collides with the M2 head-axis split β they cannot both be active. The gate is the proof: turbo fused
|
| 558 |
+
decode composes cleanly with window residency + multi-slot reuse + retention eviction.
|
| 559 |
+
|
| 560 |
+
**Patches (P2/P3) β captured + byte-identical-proven.** The vendored PolyKV delta over the
|
| 561 |
+
`0001-0042 + 0070 + 0071` baseline (snapshot-diff of a faithful `reset β bootstrap β apply` baseline
|
| 562 |
+
vs the live tree) splits into two patches:
|
| 563 |
+
|
| 564 |
+
| Patch | Stage | Touches | Acceptance |
|
| 565 |
+
|-------|-------|---------|------------|
|
| 566 |
+
| `0072-poly-kv-pool` | S1 SharedKVPool (#370) | 3 server files (`tools/server/server-{task.h,task.cpp,context.cpp}`) | default-off byte-identical; gate `m6-s1-shared-pool-gate.sh` |
|
| 567 |
+
| `0073-turboquant-kv` | S2 TurboQuant (tiers+InnerQ+Level-A+Level-B, #371/#374β396) | 32 files (13 NEW); `GGML_OP_TURBO_WHT` op; carries the outer `llamafile/build-functions.sh` host-source add | logit-equiv vs f16 (#355); gates `s2-tier-family-gate.sh` + `m6-s3-glue-gate.sh` |
|
| 568 |
+
|
| 569 |
+
Both numbered **`0072`/`0073` (NOT the plan's stale `0060`/`0061`)** because the build pipeline applies
|
| 570 |
+
patches in **pure lexical sort** order (`build-pipeline.ts`, `.sort()`), and the TurboQuant FA-VEC edits
|
| 571 |
+
sit **on top of** `0070`'s `fattn.cu`/`llama-graph.cpp`/`ggml-cuda.cu` rewrite β `0060/0061` would sort
|
| 572 |
+
*before* 0070 and break the chain (M7 shipped before M6, so the original labels predate it). S0
|
| 573 |
+
asymmetric q8/q4 needs **no patch** (rides M7 dequant-on-lift natively). **Proof:** both reverse-apply
|
| 574 |
+
clean against live (`git apply --check -R`), and a forward-apply onto a fresh baseline copy
|
| 575 |
+
**reproduces the live tree byte-for-byte** (`diff -r` = 0 differing / 0 only-in-live / 0 only-in-fwd,
|
| 576 |
+
outer `build-functions.sh` identical). The full `0001-0042+0070+0071+0072+0073` chain is strict-apply
|
| 577 |
+
proven by composition (#366 + the baseline `:apply` + the `0072/0073` `--check`). Vendored source stays
|
| 578 |
+
uncommitted per policy β only the `.patch` files commit. (DSO sha `19168f6a`; both `/root` paths
|
| 579 |
+
restamped byte-identical.)
|
| 580 |
+
|
| 581 |
+
#### S2 perf-path β turbo-MMA verify kernel + scalar-K/turbo-V fused decode β
SHIPPED OPT-IN default-OFF (2026-07-09, #628/#630, WS1, `0105-0107`)
|
| 582 |
+
|
| 583 |
+
WorkStream-1 = the **asymmetric strong scalar-K (q8_0/q6_0/q5_0/q4_0) + cheap turbo/TCQ-V** decode
|
| 584 |
+
family β K read as scalar (true-space), V read in-register as rotated turbo/TCQ (no f16 materialize);
|
| 585 |
+
the graph `build_attn_mha` `wht_q`/`wht_o` split applies the OUTPUT inverse-WHT only (K is true-space,
|
| 586 |
+
V is FWHT-rotated). Two decode regimes:
|
| 587 |
+
|
| 588 |
+
- **Fused-VEC decode (n_q=1)** β the foundation, unconditional. 16 new FA-VEC instances
|
| 589 |
+
(`fattn-vec-instance-q{4,5,6,8}_0-turbo{2,3}_{0,tcq}.cu`) + `FATTN_VEC_CASES_ALL_D` dispatch + Kβ V
|
| 590 |
+
selector admission + `build-functions.sh` 3e default-build inclusion. Invariant K bit-width > V;
|
| 591 |
+
reverse (turbo-K/scalar-V) not built. Ships in `0107`.
|
| 592 |
+
- **turbo-MMA verify (n_q>1)** β a turbo-aware tensor-core kernel that dequants q8_0-K + turbo2-V into
|
| 593 |
+
the MMA smem tile (rotated V; graph `wht_o` keeps it exact) for MTP verify batches at head_dim **128**
|
| 594 |
+
(Qwen full-attn), **256** (Gemma SWA-local), **512** (Gemma global). Three per-head-dim knobs
|
| 595 |
+
`WS1_TURBO_MMA_D128/D256/D512`, **all default-OFF** β off β verify keeps the fused-VEC routing (the
|
| 596 |
+
pre-#628 default + logit-equiv reference). `WS1_DEROTATE` (inverse-FWHT V in-load β graph skips
|
| 597 |
+
`wht_o`) is a separate default-OFF experiment. bug-2130: `!is_quant_kv` for `need_f16_K/V` so
|
| 598 |
+
`launch_fattn` reads raw quant blocks (f16 keeps true,true = byte-identical). Ships in `0107`.
|
| 599 |
+
|
| 600 |
+
**VERDICT (#630 β 3-model crossover map on bs2, A4B / omni27 / qw35a3, decode-tps + teacher-forced TV +
|
| 601 |
+
niah, ctx 32k/128k/256k):** turbo-MMA is **mid-ctx-specialized and earns NO default-on anywhere.**
|
| 602 |
+
|
| 603 |
+
| model (attn) | 32k | 128k | 256k | fidelity |
|
| 604 |
+
|---|---|---|---|---|
|
| 605 |
+
| **A4B** Gemma iSWA, D256-local + D512-global | +4.2% (241.4 vs 231.6) | **+6.4%** (249.1 vs 234.1) | **β14.3%** (190.3 vs 222.0) | niah 100 |
|
| 606 |
+
| **omni27** Qwen3.6-27B dense, D128 full | β0.1% (99.1 vs 99.2) | +0.3% (92.2 vs 91.9) | +0.1% (87.2 vs 87.1) | niah 100, real_frac 0 |
|
| 607 |
+
| **qw35a3** Qwen3.6-35B-A3B MoE, D128 full | +1.2% (204.8 vs 202.4) | +0.5% (187.7 vs 186.7) | +0.4% (168.0 vs 167.3) | niah 100, real_frac 0 |
|
| 608 |
+
|
| 609 |
+
**Named mechanism (A4B, nsys `--cuda-graph-trace=node`):** *windowed-D256 cast-saving swamped by the
|
| 610 |
+
ctx-growing D512 global-cast tail.* turbo-MMA eliminates the `cpy_turbo2_f16` materialize cast on the
|
| 611 |
+
D256 **SWA-windowed** V-cache, paying instead ~2Γ slower D256 attention (in-kernel re-dequant) β both
|
| 612 |
+
ctx-INDEPENDENT constants (windowed). The saving it must beat is the **D512 GLOBAL** materialize
|
| 613 |
+
(`cpy_turbo2_f16`, still paid β D512 knob off), whose per-cast cost doubles with ctx (123Β΅s@128k β
|
| 614 |
+
242Β΅s@256k). So the eliminated (windowed) cast stays fixed while the total step balloons with the D512
|
| 615 |
+
tail: net-win @128k (cast eliminated β 2744ms), net-loss @256k (only β 211ms left to eliminate). And it
|
| 616 |
+
is **un-gatable** β a per-op `k->ne[1]` context-gate sees the SWA WINDOW length, not session ctx
|
| 617 |
+
(bug-2133). On **Qwen D128 full-attention** the per-op gate WOULD work (uniform n_kv), but there is
|
| 618 |
+
nothing to gate for: turbo-MMA is bit-exact yet tps-flat because the dense/MoE **FFN dominates the
|
| 619 |
+
verify step** β attention acceleration has no leverage.
|
| 620 |
+
|
| 621 |
+
**Ship:** OPT-IN, **default-OFF** (`0107`); the D128/D256/D512 instances are additive/correct/harmless.
|
| 622 |
+
WS1's shipped decode wins are `0106` (bug-2126 boot correctness) + **`0105` bug-2127 DCA per-tensor
|
| 623 |
+
defer = the ~2Γ decode win** β independent of turbo-MMA. Chain 0001..0107 strict-applies from clean
|
| 624 |
+
0.10.3 and reproduces the FINAL tree byte-identically (verified 2026-07-09).
|
| 625 |
+
|
| 626 |
+
> **This WS1 null does NOT transfer to DCA-on β see WS2 (#629).** The turbo-MMA "no default-on
|
| 627 |
+
> anywhere" verdict is a **noDCA, full-attention verify-batch** result: the D512-global
|
| 628 |
+
> `cpy_turbo2_f16` cast it had to beat was *still paid* (D512 knob off), and on Qwen the FFN
|
| 629 |
+
> swamps the attention win. **WS2 (#629) is the opposite verdict on the DCA-on decode path:** reading
|
| 630 |
+
> the rotated turbo/TCQ-V **in-register inside the multichunk DCA fused flash-attn kernel** (n_qβ€16
|
| 631 |
+
> gate) *eliminates* that context-growing whole-cache cast, and is a **validated decode WIN of
|
| 632 |
+
> +68β95% @256k** across A4B / 31B (D512) and omni27 / qw35a3 (D256), every turbo/TCQ tier,
|
| 633 |
+
> retrieval-lossless (niah 100) β now **DEFAULT-ON**. Full matrix + caveats:
|
| 634 |
+
> [context.md Β§5x](../evaluations/context.md). WS1 turbo-MMA (verify-batch, noDCA) and WS2
|
| 635 |
+
> in-register turbo-V (decode, DCA-on-fused) are distinct kernels with distinct verdicts.
|
| 636 |
+
|
| 637 |
+
### S4 β Speculative MTP draft head β
DONE + SHIPPED (2026-06-07, #373/#404β410, `0074`)
|
| 638 |
+
The ship: the Gemma-4 A4B `gemma4_assistant` Multi-Token-Prediction draft head, loaded **into** the
|
| 639 |
+
target (no second `llama_context` / KV cache), proven a **decode-throughput win** AND logit-equivalent,
|
| 640 |
+
captured as `0074-speculative-mtp.patch`, surfaced on the adapter, bookkept.
|
| 641 |
+
|
| 642 |
+
**The binding requirement was throughput, and it is MET.** MTP-on must BEAT MTP-off in decode tok/s β
|
| 643 |
+
not just be correct. Final gate on the capture-candidate binary (`.opencoti/p4-fused-gate.sh`,
|
| 644 |
+
Gemma-4 A4B v6-coder Q4_K_M @ RTX 3090, 200 tok Γ 2 prompts, temp 0):
|
| 645 |
+
|
| 646 |
+
| mode | decode tok/s | vs off | draft accept |
|
| 647 |
+
|------|-------------|--------|--------------|
|
| 648 |
+
| off (plain greedy) | 78.34 | β | β |
|
| 649 |
+
| seq (per-step MTP) | 113.49 | **1.45Γ** | 0.769 / 0.606 |
|
| 650 |
+
| fused (single-graph MTP) | 114.03 | **1.46Γ** | 0.769 / 0.606 |
|
| 651 |
+
|
| 652 |
+
`seq == fused` byte-identical (the permitted refactor-equivalence use of byte-equality β two
|
| 653 |
+
implementations of the same MTP computation). `off != seq` text divergence is **NOT** a regression: it
|
| 654 |
+
is the documented benign **batch-vs-sequential near-tie** numerical artifact (bug-270) β plain greedy is
|
| 655 |
+
bit-deterministic (verified off-vs-off A==B, 761 B identical), so the divergence is the deterministic
|
| 656 |
+
float difference between the spec-decode's batched target-verify and sequential decode flipping an
|
| 657 |
+
argmax within tolerance. The directive-mandated correctness metric is **logit-equivalence**, not greedy
|
| 658 |
+
byte-equality: `.opencoti/s4-p3-logit-equiv.sh` (8 needle prompts, top-20 teacher-forced) gives
|
| 659 |
+
`real_frac=0.0`, `frac_full_agree=1.0`, 100% draft acceptance (the `VERDICT:FAIL` string is the known
|
| 660 |
+
`mean_tv` artifact for spec-accepted empty-`top:[]` tokens β judge on `real_frac`, per
|
| 661 |
+
[[project_mtp_s4_p3]]).
|
| 662 |
+
|
| 663 |
+
**Root cause of the early "MTP is ~19% SLOWER" result (bug-419) was weight placement, not
|
| 664 |
+
cuda-graph.** Stage-0 measurement (phase-split timers: submit=22 ms, sync=0.01 ms; `GGML_SCHED_DEBUG=2`)
|
| 665 |
+
showed the assistant's **dense LM head ran as a CPU island** mid-graph, stalling the GPU ~95% idle. The
|
| 666 |
+
`gemma4_assistant` `tok_embd` (~277 MiB) is the weight-tied dense output head, but llama.cpp classifies
|
| 667 |
+
`tok_embd` as an INPUT tensor and `dev_input` is hard-pinned to the CPU buft β so the per-draft-step
|
| 668 |
+
full-vocab (262144) output mul_mat executed on CPU. **The fix is one loader edit:** route the
|
| 669 |
+
`gemma4_assistant` `tok_embd` through the OUTPUT buft (`dev_output`, GPU when the output layer is
|
| 670 |
+
offloaded). cuda-graph replay was MOOT (sync already free); the originally-planned custom fused MTP-head
|
| 671 |
+
kernel was unnecessary. See [[project_dso_cache_poisoning]] for the parallel DSO gotcha class.
|
| 672 |
+
|
| 673 |
+
**Adapter knob.** `config.mtpHead` / `specType` / `draftNGpuLayers` (env
|
| 674 |
+
`OPENCOTI_LLAMAFILE_MTP_HEAD` / `_SPEC_TYPE` / `_DRAFT_NGL`), emitted in `buildServerArgs` as
|
| 675 |
+
`--mtp-head <path>` / `--spec-type mtp` / `-ngld <n>` after `--fused-moe-up-gate`, before `extraArgs`.
|
| 676 |
+
`-ngld` is REQUIRED for the win (a CPU-resident draft head re-seeds the stall). Default-unset β no draft
|
| 677 |
+
(plain decode, byte-identical arg vector). 31 adapter tests PASS + typecheck clean.
|
| 678 |
+
|
| 679 |
+
**Patch (`0074-speculative-mtp`) β captured + byte-identical-proven both directions.** Snapshot-diff of
|
| 680 |
+
the live tree vs a faithful `base-mtp` (= `base-poly` + `apply(0072,0073)` = `0001-0042+0070+0071+0072+
|
| 681 |
+
0073`, no MTP): 24 files, 1 NEW (`src/models/gemma4-assistant.cpp`), 0 outer-vendor delta. Forward-apply
|
| 682 |
+
onto a fresh base-mtp reproduces the live tree byte-for-byte (0/24 differ); reverse-apply onto live
|
| 683 |
+
reproduces base-mtp (0/23 differ + new file deleted). Three surgical upstream edits (marker
|
| 684 |
+
`opencoti F5 M6-S4 mtp`) + additive surface β registered in `docs/protocols/UPSTREAM_SYNC.md`. The
|
| 685 |
+
reverted sparse-argmax centroid fast path (dead for this dense `use_ordered_embeddings=false` GGUF) is
|
| 686 |
+
banked at `.opencoti/banked/mtp-sparse-argmax.md`, not shipped (verification discipline: never ship an
|
| 687 |
+
unexercised path). Vendored source stays uncommitted per policy β only the `.patch` commits.
|
| 688 |
+
|
| 689 |
+
**P5 (#409) β POSITION_WINDOW MTP perf path: characterized, NOT implemented (route inapplicable).**
|
| 690 |
+
The task hypothesized routing the MTP cross-read through `build_attn_mha_position_window` if the
|
| 691 |
+
`get_k` streamback under window residency is a bottleneck. Measurement (forced `--vram-target 600`,
|
| 692 |
+
n_kv~2919) shows Gemma-4 A4B's iSWA cache is **POSITION_WINDOW-ineligible** (`swa_type=1`; eligibility
|
| 693 |
+
requires `swa_type==NONE`) β overflow relief is **GPU_STREAM**, so the proposed window route never
|
| 694 |
+
engages. Under GPU_STREAM overflow MTP is **throughput-negative** (MTP-on 30.3 vs off 46.3 tok/s;
|
| 695 |
+
correct β needle recovered, accept 0.786). Refused to ship a dead position-window MTP route (same
|
| 696 |
+
discipline as the reverted sparse-argmax). DISPOSITION: the supported long-context MTP path is
|
| 697 |
+
**turbo-KV-resident** β S2 Level-B + the D=512 fused turbo instances (#411) keep the 256k cache in
|
| 698 |
+
VRAM β `GPU_RESIDENT` β MTP keeps its 1.46Γ win; f16-overflow-streaming is a config to avoid (f16 KV
|
| 699 |
+
doesn't fit 256k anyway). See [[bug-419]], bug-426.
|
| 700 |
+
|
| 701 |
+
### Phase-0 fork recon (2026-06-05) β findings that shape S2 + S4
|
| 702 |
+
Shallow-cloned the fork (`/srv/dev-disk-by-label-opt/dev/_recon/atomic-llama-cpp-turboquant`) and
|
| 703 |
+
diffed its TurboQuant + MTP surfaces against OURS. **Headline: the fork's base is contemporaneous
|
| 704 |
+
with ours, not years apart** β both end the ggml type table at `MXFP4=39`, `GGML_TYPE_COUNT=42`; both
|
| 705 |
+
carry the same recent model set (`gemma4-iswa`, `minimax-m2`, `qwen35*`); OURS already has
|
| 706 |
+
`LLM_ARCH_GEMMA4` and `COMMON_SPECULATIVE_TYPE_EAGLE3` + a full ngram/draft speculative family. So
|
| 707 |
+
both milestones are *additive onto a near-identical base*, far cheaper than a cross-era port.
|
| 708 |
+
|
| 709 |
+
**S2 (TurboQuant) edit surface:**
|
| 710 |
+
- ggml types append cleanly at 42-46 (`TURBO2_0/3_0/4_0`, `TQ3_1S/TQ4_1S`), `COUNT 42β47` β fork
|
| 711 |
+
`ggml.h:432`, traits `ggml.c:737`. Block structs in `ggml-common.h` (TURBO3_0 = 14B/128-blk:
|
| 712 |
+
fp16 norm + 2-bit qs[32] + 1-bit signs[16]).
|
| 713 |
+
- CPU quant/dequant is ONE new file `ggml/src/ggml-turbo-quant.c` (~1026 lines: seeded WHT/Hadamard
|
| 714 |
+
rotation `TURBO_SEED_ROTATION=42` + PolarQuant/Lloyd-Max codebook) + dispatcher cases in `ggml.c`.
|
| 715 |
+
**Self-contained, low-conflict.**
|
| 716 |
+
- CUDA (DSO): `turbo-wht.cu`, `turbo-quant.cuh`, `mmvq-tq.cu`, `vecdotq.cuh`, + **12 `template-
|
| 717 |
+
instances/fattn-vec-instance-turbo*.cu`**. The fattn hook is the **only CRITICAL conflict** (OURS
|
| 718 |
+
0070 heavily rewrote `fattn.cu`).
|
| 719 |
+
- CLI: `kv_cache_types[]` in `common/arg.cpp` maps `"turbo3"` β enum (keep the spelling for fork-GGUF
|
| 720 |
+
compat).
|
| 721 |
+
- **Bring-up order:** Gate-1 = TURBO3_0 **CPU-only** (enum + struct + `ggml-turbo-quant.c` + traits +
|
| 722 |
+
dispatcher + CLI, NO CUDA, NO fattn) β validates WHT/codebook + type registration with zero
|
| 723 |
+
fattn.cu risk. Gate-2 = CUDA dequant w/ dequant-then-compute fallback (reuse our S3d lift). Gate-3
|
| 724 |
+
= fused TurboFlash templates (the 0070 rebase).
|
| 725 |
+
|
| 726 |
+
- **Gate-2 design (2026-06-05, decided).** The fork's `convert.cu` turbo3βf16 emits **rotated** f16
|
| 727 |
+
(it relies on the fused kernel to rotate q on-the-fly). Our S3d "dequant-on-lift β standard f16
|
| 728 |
+
FA" path does NOT rotate q, so the correct fallback un-rotates K/V instead:
|
| 729 |
+
**dequant turbo3 β rotated f16 β `ggml_cuda_turbo_wht` (direction=1, inverse) β true-space f16
|
| 730 |
+
K/V β unmodified standard f16 FA.** Minimal additive port set: `turbo-quant.cuh` + `turbo-wht.cu`
|
| 731 |
+
/`.cuh` (verbatim) β `dequantize.cuh` turbo3 accessor β `convert.cu` to_fp16 cases β graph
|
| 732 |
+
insertion of the inverse-WHT pass on the lifted K/V β `supports_op`/type registration in
|
| 733 |
+
`ggml-cuda.cu` β CUDA DSO rebuild β GPU cosine/needle gate vs vanilla f16.
|
| 734 |
+
- **InnerQ stub-disabled for Gate-2 (decided, user).** `turbo-quant.cuh` couples to InnerQ
|
| 735 |
+
device globals (`d_innerq_scale*`, per-channel Q/V equalization) defined in `turbo-innerq.cu`.
|
| 736 |
+
For the Gate-2 fallback we compile with **identity InnerQ** (scales = 1, `d_innerq_active = 0`)
|
| 737 |
+
β either by porting `turbo-innerq.cu` in its dormant/identity state or by stubbing the globals
|
| 738 |
+
β so K/V un-rotation is the plain inverse WHT. **InnerQ is re-enabled in Gate-3** (the fused
|
| 739 |
+
TurboFlash path), where the q-rotation + channel-scale fusion actually pays off. Identity
|
| 740 |
+
InnerQ is numerically exact for the un-rotation (it only rescales channels pre-quant; with
|
| 741 |
+
scale=1 the round-trip is the bare WHT codebook path Gate-1 already validated at cosine 0.983).
|
| 742 |
+
- **Gate-2 is two halves (both mechanism-matched to our tree):**
|
| 743 |
+
- **READ (dequant β true f16) β source DONE 2026-06-05.** New `ggml-cuda/turbo-dequant.cuh`
|
| 744 |
+
(InnerQ-free; folds the inverse WHT into the per-block dequant since turbo3 block=128=WHT
|
| 745 |
+
group) + `convert.cu` `to_fp16_cuda` & `to_fp16_nc_cuda` TURBO3_0 cases (use
|
| 746 |
+
`dequantize_turbo3_0_inv`) + `QR_TURBO3` define in ggml-common.h. Routes through our
|
| 747 |
+
**type-agnostic `GGML_OP_STREAMING_FLASH_ATTN`** (supports_op gates head_dim only; the op
|
| 748 |
+
dequants K/V to f16 on the S3d lift at fattn.cu:748) β **no standard-FA supports_op change,
|
| 749 |
+
no graph node added.** The gate must force the streaming path (residency-mode / low
|
| 750 |
+
`--vram-target`) so turbo3 never hits the stock `FLASH_ATTN_EXT` (which lacks a turbo3 vec
|
| 751 |
+
kernel until Gate-3).
|
| 752 |
+
- **WRITE (f32 β turbo3) β NEXT.** Our KV store uses `ggml_set_rows` (cpy_k/cpy_v β
|
| 753 |
+
`ggml_set_rows`, llama-kv-cache.cpp:3091/3188/3204) β the SAME mechanism as the fork's
|
| 754 |
+
`set-rows.cu`. Port `k_set_rows_turbo3` + `k_set_rows_turbo3_tail` + `set_rows_cuda_turbo3`
|
| 755 |
+
(~300 lines: group forward-WHT via warp shuffle, `__ballot_sync` sign pack, corrected-norm)
|
| 756 |
+
into our `set-rows.cu` (which today has 0 turbo refs), + `GGML_OP_SET_ROWS` supports_op /
|
| 757 |
+
dispatch for a turbo3 dst. Then ONE CUDA DSO rebuild + GPU cosine/needle gate vs vanilla f16.
|
| 758 |
+
|
| 759 |
+
- **Gate-1 β DONE (2026-06-05).** Host build clean (`ggml-turbo-quant.c.o` linked; two splice bugs
|
| 760 |
+
fixed: bug-333 truncated TQ4_1S `type_traits` entry, bug-334 decls trapped inside
|
| 761 |
+
`#ifdef __cplusplus`). Registration proven by the boot line
|
| 762 |
+
`llama_kv_cache: β¦ K (turbo3): 1.37 MiB, V (turbo3): 1.37 MiB` + `attn_rot_k = 1` on Qwen-1.5B
|
| 763 |
+
CPU (`-ngl 0 -ctk turbo3 -ctv turbo3`) β i.e. `-ctk turbo3` accepted, type name resolves, cache
|
| 764 |
+
allocates at the compressed block size (~5Γ vs f16's ~7 MiB). CPU then **segfaults at warmup** β
|
| 765 |
+
EXPECTED: `dequantize_row_turbo3_0` is a rotated-space stub (no inverse WHT; the GPU FA kernel
|
| 766 |
+
rotates q so `(Rq)Β·(Rk)=qΒ·k`), so turbo3 *attention compute* is Gate-2/3 (CUDA) scope, not a
|
| 767 |
+
Gate-1 fail. Gate-1c math validated out-of-band: vendored kernel primitives
|
| 768 |
+
(`turbo_cpu_fwht`/`_inverse`/`nearest_centroid_3bit`/`CENTROIDS_3BIT`) are byte-identical (modulo
|
| 769 |
+
`GGML_API` macro + a comment) to `_recon/turbo3_selftest` (cosine 0.983 round-trip). Gate script
|
| 770 |
+
`.opencoti/s2-gate1.sh`. **Next: Gate-2 (CUDA dequant-then-FA via S3d lift) β requires DSO rebuild.**
|
| 771 |
+
|
| 772 |
+
**S4 (MTP draft head) edit surface:**
|
| 773 |
+
- `gemma4_assistant` arch = new file `src/models/gemma4-assistant.cpp` (clean) + enum/hparams in
|
| 774 |
+
`llama-arch.{h,cpp}` (`n_embd_backbone`, `n_centroids`, `centroid_top_k`, `use_ordered_embeddings`,
|
| 775 |
+
`requires_target_arch`). EAGLE3 feature taps = target token-emb + prev backbone hidden β
|
| 776 |
+
`mtp_pre_projection` β transformer β backbone hidden β (centroid head for E2B/E4B | dense head for
|
| 777 |
+
A4B). Greedy argmax on device.
|
| 778 |
+
- **"No second KV cache" CONFIRMED in code:** the assistant is loaded INTO the target via
|
| 779 |
+
`llama_model_load_mtp_from_file()` (tensors prefixed `mtp.`, stored as `target->mtp_assistant`);
|
| 780 |
+
there is no second `llama_context`. MTP cross-attn reads the target's existing KV at the **last
|
| 781 |
+
full/SWA target layer** (`build_attn_mtp()` β `gemma4_mtp_kv_layer_last_in_range`). Scheduler =
|
| 782 |
+
`common_speculative_state_mtp` (`common/speculative.cpp`) + `llama_decode_mtp_async/wait`
|
| 783 |
+
(`llama-context.cpp`), async depth-2 with lazy-wait overlap; MTP only READS target KV, never writes.
|
| 784 |
+
- Flags additive in `common/arg.cpp`: `--mtp-head`/`-md`, `--spec-type mtp`, `--draft-block-size`
|
| 785 |
+
(default 3), `--n-draft-max/min`, `-ngld`, `-ctkd/-ctvd`.
|
| 786 |
+
- **#1 integration risk (R-S4b, confirmed):** `build_attn_mtp()` must read target KV through OUR M7
|
| 787 |
+
rolling-KV (POSITION_WINDOW) + S1 accessor dispatch, not the fork's direct `cache_k/cache_v`; and
|
| 788 |
+
S1/M7 must not mutate the target prefix mid-MTP-snapshot. Mitigation: route MTP's attn through the
|
| 789 |
+
same accessor as the regular forward; keep the slot in decode-state (no compress) while an MTP
|
| 790 |
+
draft is pending. Fallback: dequant-on-lift the window into a temp buffer for the MTP read.
|
| 791 |
+
- **Bring-up order:** P1 arch+loader (smoke-load the assistant GGUF, no decode) β P2 flags/params β
|
| 792 |
+
P3 scheduler skeleton (async no-ops) β P4 KV-accessor merge (single-token MTP forward, cosine vs
|
| 793 |
+
naive) β P5 async + S1 race-guard (turn-2 latency + logit-equivalence) β P6 docs/patch 0062.
|
| 794 |
+
|
| 795 |
+
## Critical files (anticipated)
|
| 796 |
+
- `src/llama-kv-cache.{cpp,h}` β pool/shared-prefix storage, type-asymmetric K/V, compression-ratio
|
| 797 |
+
into the M7 tile-sizing.
|
| 798 |
+
- `ggml/src/ggml-cuda/fattn.cu` + S3d dequant-on-lift β read compressed tiles (S0 reuses; S2 adds
|
| 799 |
+
the TBQ8_0 / TBQ4_0 / TBQ3_0 dequant kernels).
|
| 800 |
+
- `ggml` core β `TBQ8_0` / `TBQ4_0` / `TBQ3_0` type registration (S2).
|
| 801 |
+
- `common/arg.cpp` β pool/agent + (later) `--poly-kv` knobs; `packages/opencoti-llamafile` adapter.
|
| 802 |
+
- Patches `0072-poly-kv-pool` (S1) / `0073-turboquant-kv` (S2); gates `.opencoti/m6-s*-*.sh`;
|
| 803 |
+
`perf/llamafile/advanced-kv-stack.bench.ts`.
|
| 804 |
+
|
| 805 |
+
## Verification
|
| 806 |
+
1. S0: single-tenant cosine/RULER == vanilla at asymmetric q8_0/q4_0; ~2.4Γ KV; M7 overflow path OK.
|
| 807 |
+
2. S1: 2-agent shared-prefix recall; O(1) shared-prefix memory.
|
| 808 |
+
3. S2: cosine/RULER holds per TurboQuant tier (TBQ8_0 / TBQ4_0 / TBQ3_0); TBQ3_0 ~2.9Γ compression.
|
| 809 |
+
4. S3: glue bench (A4/Sβ² + turbo3/turbo2 logit-equiv compose); patches `0072`/`0073` strict-apply in
|
| 810 |
+
series after `0071`, reverse-apply clean + forward-reproduce byte-identical; hooks registered; reap/DSO hygiene. β
|
| 811 |
+
|
| 812 |
+
## Standing ops
|
| 813 |
+
`OPENCOTI_NO_CCACHE=1`; `:cuda` rebuilds DSO (~24min) only when S2 lands its kernels β S0/S1 are
|
| 814 |
+
host-side; vendor-backup before any destructive vendor op; reap `llamafile.*--server` (never
|
| 815 |
+
embedder 38092 / SSH); DSO restamp before each ours boot; GPU β₯10 GiB; commit only when asked;
|
| 816 |
+
soft-fork additive + `opencoti-hook:` markers registered; RULER acceptance via cosine/semantic/RULER,
|
| 817 |
+
never greedy byte-equality.
|
docs/features/rolling_kv.md
ADDED
|
@@ -0,0 +1,1004 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# F5 M7 β Rolling KV: position-windowed KV residency
|
| 2 |
+
|
| 3 |
+
**Status:** SHIPPED (2026-06-04, `dev`).
|
| 4 |
+
> The design below (locked 2026-05-29) describes the head-axis streaming
|
| 5 |
+
> double-buffer pipeline. **That approach was superseded mid-flight** β on
|
| 6 |
+
> Gemma-4's 2-KV-head global layers the head axis can only express a binary
|
| 7 |
+
> 0/50/100 spill, and the streaming op was inert under attention softcap. The
|
| 8 |
+
> **shipped** design decomposes residency by **KEY POSITION**: a device-resident
|
| 9 |
+
> window `[0, wc)` + a pinned-host tail `[wc, n_kv)` on plain flash-attention,
|
| 10 |
+
> merged by an online-softmax combine (keystone: LSE-emitting `launch_fattn`).
|
| 11 |
+
> The tactic auto-selects at cache construction (eligibility predicate +
|
| 12 |
+
> measured PCIe-bandwidth/compute crossover; host-agnostic, nothing hardcoded)
|
| 13 |
+
> and is driven by **`--kv-residency-mode {auto,head,window}`** (default `auto`).
|
| 14 |
+
> The dormant CPU_FA_TAIL tactic stays in-tree for sub-x1 PCIe links but is
|
| 15 |
+
> gated out of auto-select. Acceptance is **logit-distribution equivalence vs
|
| 16 |
+
> vanilla** (`.opencoti/rolling-kv-equiv-gate.sh`), not greedy byte-equality.
|
| 17 |
+
> Ships as `0070-rolling-kv` (29 files); the orthogonal cosmocc state-IO fix
|
| 18 |
+
> (bug-269) ships as `0071-state-io-cosmocc`. See `STATE_SUMMARY.md` ("W4 β M7
|
| 19 |
+
> Rolling KV β¦ SHIPPED") and the `0070`/`0071` rows in
|
| 20 |
+
> `vendors/patches/llamafile/README.md` for the as-shipped record.
|
| 21 |
+
|
| 22 |
+
**Patch slot:** `0070-rolling-kv.patch` (was LMCache, repurposed β
|
| 23 |
+
LMCache stays deferred under a future slot if it returns).
|
| 24 |
+
**Prerequisites:** #289 pinned host buffer **(SHIPPED, 0034)**, #290 CPU
|
| 25 |
+
FA TG threading, #293 rebar / PCIe probe **(SHIPPED, 0036 β runtime
|
| 26 |
+
consumption + PCIe-link autodetect; see M7-B)**.
|
| 27 |
+
**Replaces:** the static GPU/CPU partition in F5 M2
|
| 28 |
+
(`--headinfer-gpu-heads-frac`) β M2 becomes a runtime tactic the
|
| 29 |
+
scheduler picks per-layer, not a user-facing flag.
|
| 30 |
+
**Sister docs:** [`advanced_kv.md`](advanced_kv.md) (the M0-M6 series
|
| 31 |
+
this builds on).
|
| 32 |
+
|
| 33 |
+
## Why this exists
|
| 34 |
+
|
| 35 |
+
opencoti's M2 puts a static fraction of attention heads on GPU and the
|
| 36 |
+
rest on CPU. At Gemma 4 A4B + Q4_K_M + 256k + q8_0 KV that pins ~19 GB
|
| 37 |
+
on a 24 GB card and leaves 4-5 GB GPU idle for the entire decode while
|
| 38 |
+
half the KV computes on CPU through a slow CPU-FA path. The card has
|
| 39 |
+
spare capacity it can't reach because the policy is static, the CPU
|
| 40 |
+
side never returns to GPU compute, and there is no streaming staging
|
| 41 |
+
between host and device. The result observed today (RULER vt@262144,
|
| 42 |
+
F5 stack): 0.0 score, semantic collapse β task tracked separately as
|
| 43 |
+
bug-226, but the deeper architectural gap is what M7 fixes.
|
| 44 |
+
|
| 45 |
+
The right design for sequential, memory-bandwidth-bound attention is
|
| 46 |
+
classic double-buffered streaming: hold all KV in pinned host memory,
|
| 47 |
+
keep a small pool of GPU ping-pong slots, run compute on one slot
|
| 48 |
+
while DMA loads the next. Both PCIe and SMs are always active. VRAM is
|
| 49 |
+
always hot.
|
| 50 |
+
|
| 51 |
+
## Decisions locked (2026-05-29)
|
| 52 |
+
|
| 53 |
+
1. **Tile axis = composite (PCIe-auto-adapted).**
|
| 54 |
+
Per-layer would have been the cleanest mapping, but on solidPC
|
| 55 |
+
(PCIe 3.0 x8, ~6.5 GB/s effective) a single layer's KV at 256k
|
| 56 |
+
q8_0 is ~530 MB, transferring in ~80 ms while per-layer compute is
|
| 57 |
+
~5-15 ms. Per-layer pipeline is PCIe-bound 6-15Γ. The auto-adapt
|
| 58 |
+
loop at server boot picks the tile axis: per-layer when compute
|
| 59 |
+
β₯ transfer; position-window sub-tile otherwise. solidPC will land
|
| 60 |
+
on position-window; a future PCIe 4.0 x16 host falls into
|
| 61 |
+
per-layer automatically with no flag changes.
|
| 62 |
+
|
| 63 |
+
2. **M2 retires as a flag, lives on as a runtime tactic.**
|
| 64 |
+
`--headinfer-gpu-heads-frac` is removed. The pipeline scheduler
|
| 65 |
+
picks per-layer between two tactics:
|
| 66 |
+
- **GPU-stream tactic** (new) β fetch this layer's tile via the
|
| 67 |
+
ping-pong pool, compute FA on GPU
|
| 68 |
+
- **CPU-spill tactic** (M2-style) β keep this layer's KV pinned
|
| 69 |
+
on host, run CPU FA inline
|
| 70 |
+
User sets `--vram-target` (e.g. `--vram-target 22000` for 22 GB);
|
| 71 |
+
the scheduler aims to saturate that without exceeding it.
|
| 72 |
+
|
| 73 |
+
3. **PCIe 3.0 x8 is permanent on this host class.**
|
| 74 |
+
solidPC has a Ryzen 5600G; the iGPU absorbs 8 lanes and the board
|
| 75 |
+
caps the discrete slot at gen3. This is common (5600G/5700G class).
|
| 76 |
+
Auto-adapt is permanent infrastructure, not edge-case handling.
|
| 77 |
+
|
| 78 |
+
4. **Default tactic is GPU_RESIDENT; relief is load-adaptive and reversible.**
|
| 79 |
+
*(locked 2026-05-30 β supersedes the boot-immutable tactic model below;
|
| 80 |
+
motivated by the W2 RULER ON-vs-OFF run, where iqk speeding the CPU-half FA
|
| 81 |
+
moved wall-time ~0% because the CPU split should not have been engaged at all
|
| 82 |
+
with 23 GiB VRAM free.)*
|
| 83 |
+
The engine **maximizes VRAM by default** and only sheds load to slower
|
| 84 |
+
tactics when it must:
|
| 85 |
+
- The server starts a fixed pool of N parallel slots (`--parallel N`, e.g.
|
| 86 |
+
4/8/β¦) but allocates KV **dynamically**, never at a fixed CPU/GPU split.
|
| 87 |
+
- With 1β2 active requests whose KV fits, **every layer is GPU_RESIDENT** β
|
| 88 |
+
KV lives on the card, zero DMA, zero CPU FA: vanilla's fast path, using all
|
| 89 |
+
available VRAM natively.
|
| 90 |
+
- As concurrency rises and the working set approaches the VRAM budget, the
|
| 91 |
+
engine engages graduated **relief efforts, in order**, only for the
|
| 92 |
+
layers/streams that no longer fit: (a) tile streaming (ping-pong
|
| 93 |
+
double-buffer, `GPU_STREAM`), then (b) sub-tile streaming
|
| 94 |
+
(`GPU_STREAM_SUBTILE`), then (c) CPU/GPU KV split (`CPU_SPILL`, M2-style,
|
| 95 |
+
consuming the #290 iqk engine).
|
| 96 |
+
- As requests drain, the engine **transitions back** toward GPU_RESIDENT,
|
| 97 |
+
reclaiming VRAM for the survivors.
|
| 98 |
+
Residency is therefore a runtime function of *(free VRAM, context size,
|
| 99 |
+
active-request count)*, re-evaluated as load changes β **not** a boot-time
|
| 100 |
+
immutable table. `--vram-target` is a **cap** (leave headroom for siblings),
|
| 101 |
+
defaulting to "all free VRAM β reserve" (maximize). The fixed
|
| 102 |
+
`--headinfer-gpu-heads-frac` split is never the default; CPU spill is the
|
| 103 |
+
last resort, reached only under genuine VRAM pressure.
|
| 104 |
+
|
| 105 |
+
## Architecture
|
| 106 |
+
|
| 107 |
+
### Storage layer
|
| 108 |
+
|
| 109 |
+
All per-layer KV lives in **pinned host memory**
|
| 110 |
+
(`ggml_backend_dev_host_buffer_type(cuda_dev)` β
|
| 111 |
+
`cudaHostAlloc(cudaHostAllocPortable)`). Pinning unlocks full PCIe
|
| 112 |
+
bandwidth on async DMA and is the foundation #289 builds. With M7 in
|
| 113 |
+
place, M2's CPU-half pinning generalizes to *all* KV: the GPU view of
|
| 114 |
+
each layer is a transient slot, never a permanent home.
|
| 115 |
+
|
| 116 |
+
A small "hot-tile" reserve at the GPU side can hold the most-recently-
|
| 117 |
+
streamed tiles across tokens (M1-retention-style affinity), avoiding
|
| 118 |
+
redundant DMA when consecutive tokens both attend to the same layer
|
| 119 |
+
range. Default reserve = 0 (start simple), tunable.
|
| 120 |
+
|
| 121 |
+
### Slot pool
|
| 122 |
+
|
| 123 |
+
Boot-time computed:
|
| 124 |
+
|
| 125 |
+
```
|
| 126 |
+
free_vram = nvidia-smi free at server start, minus user --vram-target slack
|
| 127 |
+
reserve = model weights + compute scratch + cuBLAS workspace (~1-2 GiB)
|
| 128 |
+
slot_budget = free_vram - reserve - hot_tile_reserve
|
| 129 |
+
tile_bytes_max = (compute_ms_per_layer Γ effective_bw_GBps Γ safety_0.8)
|
| 130 |
+
n_slots = clamp(slot_budget / tile_bytes_max, 2, 4)
|
| 131 |
+
```
|
| 132 |
+
|
| 133 |
+
Two slots is the minimum for a ping-pong; three lets the scheduler
|
| 134 |
+
prefetch one ahead of compute; four covers the case where compute and
|
| 135 |
+
copy are unbalanced enough that staggered prefetch helps. Above four
|
| 136 |
+
gives diminishing returns since each extra slot just delays
|
| 137 |
+
host-buffer reuse.
|
| 138 |
+
|
| 139 |
+
### CUDA stream graph
|
| 140 |
+
|
| 141 |
+
Two dedicated streams per slot pair:
|
| 142 |
+
- `copy_stream` β issues `cudaMemcpyAsync(slot_i, host_kv_layer_L, ...)`
|
| 143 |
+
- `compute_stream` β runs FA over `slot_i`
|
| 144 |
+
|
| 145 |
+
Event sync:
|
| 146 |
+
- `copy_done[i]` signals after copy into slot i β `compute_stream`
|
| 147 |
+
waits before consuming slot i
|
| 148 |
+
- `compute_done[i]` signals after FA over slot i β `copy_stream` waits
|
| 149 |
+
before overwriting slot i
|
| 150 |
+
|
| 151 |
+
Forward pass per token (sketch):
|
| 152 |
+
|
| 153 |
+
```
|
| 154 |
+
for layer L in 0..n_layers:
|
| 155 |
+
slot = next_slot()
|
| 156 |
+
if not slot.holds(L): # hot-tile miss
|
| 157 |
+
copy_stream.memcpy_async(slot, host_kv[L]) # ~tile_ms
|
| 158 |
+
record(copy_done[slot])
|
| 159 |
+
compute_stream.wait(copy_done[slot])
|
| 160 |
+
compute_stream.fa(Q[L], slot, V_slot) # ~tile_ms
|
| 161 |
+
record(compute_done[slot])
|
| 162 |
+
# next iteration: prefetch L+1 while compute on L+0 finishes
|
| 163 |
+
```
|
| 164 |
+
|
| 165 |
+
When `tile_bytes_max < per_layer_kv_bytes`, the inner loop runs N
|
| 166 |
+
position-window sub-tiles per layer with online-softmax accumulation
|
| 167 |
+
across them. FA's running max + denominator carry between sub-tiles
|
| 168 |
+
naturally β flash-attention's exact math, just with the K/V chunk
|
| 169 |
+
source coming from a rotating slot instead of a contiguous tensor.
|
| 170 |
+
|
| 171 |
+
### M2 as runtime tactic
|
| 172 |
+
|
| 173 |
+
The scheduler holds a `layer_tactic[L]` table picked at server boot:
|
| 174 |
+
|
| 175 |
+
```
|
| 176 |
+
for layer L in 0..n_layers:
|
| 177 |
+
if total_resident_kv + kv_bytes(L) β€ slot_budget: # it fits β keep it hot
|
| 178 |
+
layer_tactic[L] = GPU_RESIDENT # zero DMA, zero CPU β vanilla's fast path
|
| 179 |
+
else if tile_bytes_max β₯ kv_bytes_per_layer(L) AND has_slot_headroom():
|
| 180 |
+
layer_tactic[L] = GPU_STREAM # ping-pong double-buffer
|
| 181 |
+
else if cpu_fa_throughput(L) > pcie_throughput(L):
|
| 182 |
+
layer_tactic[L] = CPU_SPILL # M2-style β KV pinned, CPU computes (#290)
|
| 183 |
+
else:
|
| 184 |
+
layer_tactic[L] = GPU_STREAM_SUBTILE
|
| 185 |
+
```
|
| 186 |
+
|
| 187 |
+
The table is **re-evaluated as load changes** (active-request count,
|
| 188 |
+
working-set vs VRAM budget) β *not* immutable for the server lifetime
|
| 189 |
+
(superseding the original boot-immutable model, per Decision 4,
|
| 190 |
+
2026-05-30). With 1β2 requests that fit, every layer is `GPU_RESIDENT`
|
| 191 |
+
and the engine runs at vanilla speed on all available VRAM; as
|
| 192 |
+
concurrency grows it demotes the layers/streams that no longer fit
|
| 193 |
+
`GPU_RESIDENT β GPU_STREAM β GPU_STREAM_SUBTILE β CPU_SPILL` in that
|
| 194 |
+
order; as requests drain it promotes them back toward `GPU_RESIDENT`.
|
| 195 |
+
Slot-level adaptivity (which tile in which slot, prefetch ordering)
|
| 196 |
+
rides underneath the tactic-level transitions.
|
| 197 |
+
|
| 198 |
+
`GPU_RESIDENT` is a first-class tactic (it also absorbs the iswa-window
|
| 199 |
+
layers noted in Open Questions). CPU-spill consumes the work from #290
|
| 200 |
+
(CPU FA TG threading port) and is the **last-resort** tactic, reached
|
| 201 |
+
only under genuine VRAM pressure.
|
| 202 |
+
|
| 203 |
+
## Auto-adapt loop (server boot, ~100 ms total)
|
| 204 |
+
|
| 205 |
+
1. **PCIe topology** β read
|
| 206 |
+
`/sys/bus/pci/devices/<bus>/{current,max}_link_{width,speed}`,
|
| 207 |
+
compute `effective_bw_GBps = width Γ gtps Γ 0.8 / 8`. Cache the
|
| 208 |
+
value for the server's lifetime.
|
| 209 |
+
2. **Compute microbench** β synthesize one layer's worth of K/V
|
| 210 |
+
tensors at f16, run a 1-step `ggml_flash_attn_ext` with `seq_len =
|
| 211 |
+
target_ctx`, measure wall time on `compute_stream`. Yields
|
| 212 |
+
`ms_per_layer_at_target_ctx`.
|
| 213 |
+
3. **Tile sizing** β apply the formula above.
|
| 214 |
+
4. **Tactic table** β populate `layer_tactic[L]` from per-layer KV
|
| 215 |
+
sizes (Gemma 4 iswa has uniform `n_kv_heads`; trivial. Mixture
|
| 216 |
+
architectures with variable per-layer KV are the only case where
|
| 217 |
+
per-L matters).
|
| 218 |
+
5. **Log** β print the chosen tile axis, size, n_slots, and tactic
|
| 219 |
+
histogram (`N_layers_gpu_stream, N_layers_gpu_subtile,
|
| 220 |
+
N_layers_cpu_spill`) so operators can sanity-check.
|
| 221 |
+
|
| 222 |
+
The probe binary (`perf/llamafile/rebar-probe.sh`) handles step 1's
|
| 223 |
+
discovery once at install / first-boot; the server reads its cached
|
| 224 |
+
JSON. The compute microbench (step 2) runs inside the server itself,
|
| 225 |
+
no external tool. This keeps the boot-time cost bounded and avoids
|
| 226 |
+
shelling out.
|
| 227 |
+
|
| 228 |
+
## Phase breakdown
|
| 229 |
+
|
| 230 |
+
### M7-A β Pinned-host KV residency
|
| 231 |
+
Consume #289's pinned-host-buffer-type swap + staging pool. Generalize
|
| 232 |
+
M2's CPU-half buffer to all KV: every layer's K/V starts in pinned
|
| 233 |
+
host memory regardless of tactic.
|
| 234 |
+
|
| 235 |
+
### M7-B β Auto-adapt loop
|
| 236 |
+
Implements PCIe topology read + compute microbench + tile sizing
|
| 237 |
+
formula + tactic table population at server boot. Logs decisions.
|
| 238 |
+
|
| 239 |
+
> **Step 1 (PCIe topology read) is already shipped** by W1 / patch 0036
|
| 240 |
+
> (#293). At server boot `pcie_profile_init()` (`common/pcie-profile.{h,cpp}`)
|
| 241 |
+
> resolves `effective_bw_gbps` via the 4-tier cascade (manual override β
|
| 242 |
+
> measured probe JSON β `nvidia-smi` β conservative PCIe3 x8 default) and
|
| 243 |
+
> logs it. M7-B consumes the resolved `pcie_profile_get().effective_bw_gbps`
|
| 244 |
+
> directly β no new topology code needed; M7-B adds only the compute
|
| 245 |
+
> microbench (step 2), tile sizing (step 3), and tactic table (step 4).
|
| 246 |
+
|
| 247 |
+
### M7-C β Tile streamer + slot pool
|
| 248 |
+
Allocates `n_slots` GPU buffers of `tile_bytes_max`. Owns the slot
|
| 249 |
+
rotation policy (round-robin with hot-tile affinity if reserve > 0).
|
| 250 |
+
|
| 251 |
+
### M7-D β Compute/copy CUDA stream graph
|
| 252 |
+
Two CUDA streams per slot pair, event-based sync, async DMA enqueue,
|
| 253 |
+
graph captured into ggml's existing compute backend so FA consumers
|
| 254 |
+
see no change.
|
| 255 |
+
|
| 256 |
+
### M7-E β M2 retirement / runtime-tactic scheduler
|
| 257 |
+
Removes `--headinfer-gpu-heads-frac` from `common/arg.cpp`. Adds
|
| 258 |
+
`--vram-target` (number, MiB). Plumbs the tactic table through
|
| 259 |
+
`build_attn_mha` so per-layer kernel dispatch picks GPU stream vs CPU
|
| 260 |
+
spill from the table, not from a flag.
|
| 261 |
+
|
| 262 |
+
### M7-F β TS adapter + config schema
|
| 263 |
+
`packages/opencoti-llamafile/src/config.ts`: drop `headinferGpuHeadsFrac`,
|
| 264 |
+
add `vramTargetMiB`. `buildServerArgs` emits the new flag. Update
|
| 265 |
+
adapter tests.
|
| 266 |
+
|
| 267 |
+
### M7-G β Verification
|
| 268 |
+
1. **Cosine β₯ 0.999 unified-mode equivalence** vs pre-M7 binary at
|
| 269 |
+
small ctx where tile sizing doesn't activate (M5 stack bench unchanged).
|
| 270 |
+
2. **256k+q8_0+Gemma 4 quality β₯ vanillaβ2pp** β RULER vt@262144,
|
| 271 |
+
`-ctk q8_0 -ctv q8_0`, ours-streaming vs vanilla. NOTE (2026-06-01,
|
| 272 |
+
corrected): this is a *vanilla-q8_0 parity* gate, NOT a "bug-226 fix"
|
| 273 |
+
gate. bug-226's real defect (concat_q block-scrambling) was already fixed
|
| 274 |
+
by 0035; q8_0 is confirmed coherent on this Gemma at small ctx (vanilla,
|
| 275 |
+
resident, AND S3d streaming n_head_cpu=4 all = 100% on RULER vt@4k,
|
| 276 |
+
templated). So this cell tests that the streaming pipeline holds q8_0
|
| 277 |
+
parity *at 256k* β the only unconfirmed cell β and is gated behind a 256k
|
| 278 |
+
f16 canary per the standing "no 256k re-run until M7 lands + canary"
|
| 279 |
+
instruction. A reference vanilla-q8_0@256k baseline must be captured here
|
| 280 |
+
(it was never run β the kvtype-split confound). Do NOT treat oursβvanilla
|
| 281 |
+
as a tautology: vanilla q8_0 scores ~100% at coherence-checkable ctx.
|
| 282 |
+
3. **VRAM target enforced** β server holds within `--vram-target` Β±
|
| 283 |
+
200 MiB across the run.
|
| 284 |
+
4. **Throughput β₯ vanilla at 256k** β at concurrency=1, M7 should at
|
| 285 |
+
least match vanilla (PCIe-bound floor) and ideally beat it through
|
| 286 |
+
better VRAM utilization (more GPU compute, less CPU spill).
|
| 287 |
+
5. **Hook count audit**: removes M2's hook surface (frac flag);
|
| 288 |
+
adds M7's vram-target hook. Net delta tracked in
|
| 289 |
+
`docs/protocols/UPSTREAM_SYNC.md` Registry.
|
| 290 |
+
6. **Dynamic residency transition** (Decision 4) β drive a load ramp:
|
| 291 |
+
1 request (expect all-`GPU_RESIDENT`, throughput β vanilla, VRAM near
|
| 292 |
+
`--vram-target`), ramp to `--parallel N` saturation (expect graduated
|
| 293 |
+
relief: stream β subtile β cpu-spill engages, VRAM held within target),
|
| 294 |
+
then drain back to 1 (expect promotion back to `GPU_RESIDENT`,
|
| 295 |
+
throughput recovers). Assert the tactic histogram shifts with load in
|
| 296 |
+
both directions and never exceeds the VRAM cap. This is the core test
|
| 297 |
+
of the maximize-VRAM-by-default behavior.
|
| 298 |
+
|
| 299 |
+
### M7-H β Substance commit + docs ship
|
| 300 |
+
Snapshot-diff `0070-rolling-kv.patch`. Update
|
| 301 |
+
`advanced_kv.md` M7 section (was LMCache β now Rolling KV). Move
|
| 302 |
+
`headinferGpuHeadsFrac` to a deprecated config field with a clear
|
| 303 |
+
upgrade note. Update STATE_SUMMARY, `.wolf/anatomy.md`,
|
| 304 |
+
`.wolf/memory.md`. Task #296 β completed.
|
| 305 |
+
|
| 306 |
+
## Rung 2 implementation blueprint (code-grounded, 2026-05-30)
|
| 307 |
+
|
| 308 |
+
Rungs 0 (M7-A auto-residency) and 1 (M7-B tactic table) have shipped on
|
| 309 |
+
`dev` (commits `7fe88b8`, `8957fc9`; patch `0070-rolling-kv.patch`). The
|
| 310 |
+
inert per-layer `headinfer_tactic` table + `rolling_kv_plan` + `get_layer_tactic(il)`
|
| 311 |
+
are in place. Rung 2 wires the **GPU_STREAM** tactic β and the exploration
|
| 312 |
+
below pins down what that actually requires before any kernel is written.
|
| 313 |
+
|
| 314 |
+
### The load-bearing finding: GPU_STREAM is inseparable from online-softmax
|
| 315 |
+
|
| 316 |
+
A "single-tile GPU_STREAM" (copy the whole layer's KV to one slot, run a
|
| 317 |
+
standard `ggml_flash_attn_ext`, no running-max accumulation) **does not
|
| 318 |
+
exist as a useful tactic**. GPU_STREAM only has value when a layer's KV
|
| 319 |
+
*exceeds* what fits resident on the card β if it fit one slot, the correct
|
| 320 |
+
tactic is GPU_RESIDENT (keep it; don't re-DMA every step). So GPU_STREAM
|
| 321 |
+
*always* means KV > slot β multiple tiles β **cross-tile online-softmax**
|
| 322 |
+
(running max + denominator in F32). There is no low-risk subset of the
|
| 323 |
+
streaming win; the precision-critical kernel is the whole deliverable.
|
| 324 |
+
This is why Rung 2 is gated separately and why the plan flags it HIGH-risk.
|
| 325 |
+
|
| 326 |
+
### The discrete-GPU linchpin β a custom op, not graph nodes
|
| 327 |
+
|
| 328 |
+
On the RTX 3090 (non-integrated), CUDA `supports_buft` returns FALSE for
|
| 329 |
+
pinned-host buffers (`ggml-cuda.cu` ~5144), so if GPU_STREAM were expressed
|
| 330 |
+
as ordinary graph nodes (KV on a pinned-host CPU buffer feeding
|
| 331 |
+
`ggml_flash_attn_ext`), the ggml scheduler inserts a **sequential**
|
| 332 |
+
hostβdevice copy (`ggml-backend.cpp` ~1269) β no overlap, no win over
|
| 333 |
+
CPU_SPILL. Therefore M7-D must be a **custom streaming-FA op** that owns its
|
| 334 |
+
own `cudaMemcpyAsync` on a dedicated copy stream. It cannot be plain nodes.
|
| 335 |
+
|
| 336 |
+
### Integration points (mapped)
|
| 337 |
+
|
| 338 |
+
- **Dispatch site:** `src/llama-graph.cpp:2306`. Today:
|
| 339 |
+
`if (cparams.neo_pipeline_mode != 0 && mctx_cur->headinfer_split_active(il))`
|
| 340 |
+
β `build_attn_mha_neo` (two-FA GPU+CPU); else β `build_attn_mha` (single
|
| 341 |
+
FA). Rung 2 reads `mctx_cur->get_layer_tactic(il)` here and adds a third
|
| 342 |
+
branch for GPU_STREAM. **Requires** exposing `get_layer_tactic` on
|
| 343 |
+
`llama_kv_cache_context` (delegating to the cache, like
|
| 344 |
+
`headinfer_split_active` at `llama-kv-cache.h:514`).
|
| 345 |
+
- **Stream/event mechanism to extend:** the M3 NEO orchestrator
|
| 346 |
+
(`ggml/include/ggml-neo-pipeline.h` + the CUDA `graph_compute` hook at
|
| 347 |
+
`ggml-cuda.cu:4067-4191`) is the exact precedent. It already: registers an
|
| 348 |
+
op (`register_pair`), routes it to a non-default stream
|
| 349 |
+
(`curr_stream_no=1`, `cuda_ctx->stream(device,1)`), records a
|
| 350 |
+
`cudaEvent` (`cudaEventRecord`, 4189), and makes downstream consumers
|
| 351 |
+
`cudaStreamWaitEvent` on it (4099-4101). M7-D needs the *copy* analogue:
|
| 352 |
+
a `copy_stream`, `cudaMemcpyAsync(slot[i], host_kv_tile)`, a `copy_done[i]`
|
| 353 |
+
event the `compute_stream` waits on, and a `compute_done[i]` event so the
|
| 354 |
+
next tile's copy can reuse the slot β i.e. a per-slot ping-pong of the
|
| 355 |
+
same `cudaEventRecord`/`cudaStreamWaitEvent` primitives already in the file.
|
| 356 |
+
- **Bandwidth for tile sizing:** the graph context already holds `cparams`
|
| 357 |
+
(it reads `cparams.neo_pipeline_mode` at 2306). So `cparams.pcie_bw_gbps`
|
| 358 |
+
(resolved from `pcie_profile_get().effective_bw_gbps` in
|
| 359 |
+
`common.cpp:common_context_params_to_llama`, threaded `cparams.h` β
|
| 360 |
+
`llama.h` β `llama-context.cpp` like `vram_target_mib`) is readable at
|
| 361 |
+
dispatch β **no kv-cache ctor plumbing needed**, the kernel sizes tiles
|
| 362 |
+
itself. This 4-file ABI thread lands *with* the kernel, not before
|
| 363 |
+
(cohesion; nothing reads it until the kernel exists).
|
| 364 |
+
- **New op:** add `GGML_OP_STREAMING_FLASH_ATTN` to `ggml.h` + a CUDA
|
| 365 |
+
forward impl (the tile loop + online-softmax) + `supports_op` (CUDA-only;
|
| 366 |
+
CPU aborts, like 0042's fused-MoE). Slot pool (M7-C) allocates `n_slots`
|
| 367 |
+
GPU buffers of `tile_bytes_max` in the CUDA backend context, lifecycle
|
| 368 |
+
paired with the events.
|
| 369 |
+
|
| 370 |
+
### Decomposition (each its own DSO rebuild + cosine gate)
|
| 371 |
+
|
| 372 |
+
1. **R2-a β op + slot pool + dispatch, NO overlap.** New op, single
|
| 373 |
+
copy_stream, sequential tile loop with online-softmax, dispatched from
|
| 374 |
+
`get_layer_tactic(il)==GPU_STREAM`. Assign GPU_STREAM in
|
| 375 |
+
`build_rolling_kv_plan` when KV doesn't fit but `--vram-target` allows a
|
| 376 |
+
slot pool (else CPU_SPILL). Gate: cosine β₯0.999 at small ctx (GPU_STREAM
|
| 377 |
+
never assigned β inactive), and **correctness of the online-softmax at
|
| 378 |
+
large ctx** (the precision gate β vs CPU_SPILL output on the same prompt).
|
| 379 |
+
2. **R2-b β double-buffer.** Add `copy_done`/`compute_done` events so tile
|
| 380 |
+
L+1's DMA overlaps tile L's compute. Pure latency; output must stay
|
| 381 |
+
bit-stable vs R2-a. Gate: same cosine + throughput delta.
|
| 382 |
+
3. **R2-c β M7-E scheduler + load ramp.** Promote/demote tactics as
|
| 383 |
+
`--parallel` load ramps (Decision-4 dynamic residency). Gate: M7-G load
|
| 384 |
+
ramp + RULER vt@262144 (bug-226) + VRAM cap.
|
| 385 |
+
|
| 386 |
+
The cparams `pcie_bw_gbps` thread + `get_layer_tactic` on the context land
|
| 387 |
+
in R2-a (smallest cohesive unit that compiles + runs). M7-C's slot pool is
|
| 388 |
+
folded into R2-a (it has nothing to hold before the op exists β see task #303).
|
| 389 |
+
|
| 390 |
+
### R2-a actual-state + streaming-kernel refinement (2026-05-31)
|
| 391 |
+
|
| 392 |
+
What **actually shipped** as R2-a (commits `7f73270`, `2489317`; canonical
|
| 393 |
+
`0070`) is only the **op-dispatch shell**: `GGML_OP_STREAMING_FLASH_ATTN` exists,
|
| 394 |
+
dispatches at `get_layer_tactic(il)==GPU_STREAM`, and its CUDA forward is
|
| 395 |
+
`ggml_cuda_flash_attn_ext(ctx, dst)` **verbatim** β `n_slots == 0`, KV still
|
| 396 |
+
device-resident, **no tile loop, no copy stream, no inter-tile online-softmax.**
|
| 397 |
+
It is byte-identical to GPU_RESIDENT precisely because it is monolithic FA on a
|
| 398 |
+
resident tensor; the "online-softmax retired by reuse" insight covers only
|
| 399 |
+
`launch_fattn`'s **intra-tensor** `parallel_blocks` combine, *not* the inter-tile
|
| 400 |
+
combine across host-streamed slots. So the precision core is **unwritten**.
|
| 401 |
+
|
| 402 |
+
**The load-bearing architectural finding** (grounded: `build_rolling_kv_plan`
|
| 403 |
+
`llama-kv-cache.cpp:2197`; dispatch `llama-graph.cpp:2372-2389`):
|
| 404 |
+
|
| 405 |
+
- GPU_STREAM is assigned **iff `layers[j].gpu_heads > 0`** β i.e. exactly when the
|
| 406 |
+
layer already has an **M2 head-split**, so its spilled heads' K/V are **already
|
| 407 |
+
in pinned host** (`k_cpu`/`v_cpu`, allocated with the pinned `cpu_buft` at
|
| 408 |
+
`llama-kv-cache.cpp:461-528`). **There is no whole-layer-host residency reorder**
|
| 409 |
+
β Rolling KV *rides the existing M2 split*. The ctor-ordering issue
|
| 410 |
+
(`build_rolling_kv_plan` runs at `:598`, after buffer alloc) is therefore moot
|
| 411 |
+
for residency: the pinned-host half already exists before the plan runs.
|
| 412 |
+
- Two combine axes, do not conflate them:
|
| 413 |
+
- **Head axis (M2):** device head-group vs pinned-host head-group are
|
| 414 |
+
*independent* outputs β combined by a plain **`ggml_concat`** (exactly what
|
| 415 |
+
`build_attn_mha_neo` already does, `llama-graph.cpp:2382-2389`). **No
|
| 416 |
+
online-softmax.**
|
| 417 |
+
- **Key axis (Rolling KV):** the host head-group's long-context KV is streamed
|
| 418 |
+
in **key-dimension tiles** to a device slot β FA per tile β merged with
|
| 419 |
+
**running max + denom in F32** (`flash_attn_combine_results`). **This** is the
|
| 420 |
+
precision-critical online-softmax; it engages only when the host head-group's
|
| 421 |
+
KV exceeds one slot (large ctx). One tile β inactive β byte-identical.
|
| 422 |
+
|
| 423 |
+
So the real M7-D kernel is **`build_attn_mha_neo` with the CPU-half replaced by an
|
| 424 |
+
op-owned, key-axis-tiled, async-DMA GPU FA**: stream the spilled head-group's
|
| 425 |
+
host KV tiles into the slot pool (own `cudaMemcpyAsync` on a `copy_stream`),
|
| 426 |
+
FA each tile on the `compute_stream`, online-softmax-combine across key tiles,
|
| 427 |
+
then head-axis-concat with the device head-group's FA. This supersedes the R2-a
|
| 428 |
+
"feed the full concatenated K/V to one streaming op" framing β that path only
|
| 429 |
+
ever reproduced monolithic FA (the scheduler's sequential hostβdevice copy of the
|
| 430 |
+
concat is the very thing M7-D must replace).
|
| 431 |
+
|
| 432 |
+
### Implementation decomposition, refined (S1 β S2 β S3, each a DSO rebuild + gate)
|
| 433 |
+
|
| 434 |
+
- **S1 β structural split, byte-identical. β
DONE 2026-05-31.** Routed GPU_STREAM
|
| 435 |
+
to the **split** accessors (`get_k_gpu`/`get_v_gpu`/`get_k_cpu`/`get_v_cpu` +
|
| 436 |
+
`headinfer_gpu_heads`, like NEO), not `get_k`/`get_v`. `build_attn_mha_streaming`
|
| 437 |
+
restructured to the NEO shape: device head-group via resident `ggml_flash_attn_ext`
|
| 438 |
+
+ host head-group via the `GGML_OP_STREAMING_FLASH_ATTN` op (forward still plain FA
|
| 439 |
+
at S1) + head-axis `ggml_concat`; no NEO pair-registration. **Slot pool + op-owned
|
| 440 |
+
DMA deferred to S2** β they have no correctness purpose at a single tile and belong
|
| 441 |
+
with key-tiling, which keeps S2 as pure precision math. Binary-only `:make` (op
|
| 442 |
+
forward untouched β no DSO rebuild). Gate **PASS**: GPU_STREAM
|
| 443 |
+
(`OPENCOTI_M7_STREAMING=1`) β‘ NEO byte-identical greedy decode on Qwen2.5-1.5B at
|
| 444 |
+
frac 0.5 (`GPU_STREAM=28` layers engaged; A==B==C all byte-identical). Owning the
|
| 445 |
+
path surfaced + fixed **bug-167**: the `--flash-attn auto` resolver
|
| 446 |
+
(`llama-context.cpp`) `GGML_ASSERT`ed on the M2/NEO/M7 split FA naming
|
| 447 |
+
`__fattn___g-<il>` (latent since M3 NEO) β rewritten to skip non-plain FA ops.
|
| 448 |
+
- **S2 β key-axis tile loop + inter-tile online-softmax. β
DONE 2026-05-31
|
| 449 |
+
(byte-identical).** The open sub-design was resolved by a hard CUDA finding:
|
| 450 |
+
the **mma kernel our 3090 selects does NOT use `flash_attn_combine_results`** β
|
| 451 |
+
it normalises in-kernel and combines via the `stream_k` `dstk_fixup` path
|
| 452 |
+
(only tile/vec/wmma use the `parallel_blocks`/`dst_tmp_meta` machinery; fattn.cu
|
| 453 |
+
dispatches head_dim=128 to MMA on any `turing_mma_available` GPU, VEC fast-path
|
| 454 |
+
gated `cc β₯ ADA_LOVELACE`). So extending `launch_fattn` to reuse its combine was
|
| 455 |
+
invalid for our path. **Resolution (user-approved): LSE-weighted host-tile
|
| 456 |
+
combine.** Per key-tile, run the STOCK `ggml_cuda_flash_attn_ext` (untouched β
|
| 457 |
+
resident path byte-identical, every kernel family covered) for normalised
|
| 458 |
+
`O_t`, plus a small `streaming_lse_kernel` for per-row `lse_t = m_t + log d_t`;
|
| 459 |
+
merge in F32 via `O = Ξ£_t O_tΒ·exp(lse_t β M)/Ξ£_t exp(lse_t β M)`, `M = max_t
|
| 460 |
+
lse_t` (exact online-softmax). Single-tile short-circuits to plain FA (= S1).
|
| 461 |
+
Lives in `fattn.cu` (`ggml_cuda_streaming_flash_attn` + 2 kernels), dispatched
|
| 462 |
+
from `ggml-cuda.cu`; env `OPENCOTI_M7_STREAM_TILES` (default 1). Gate
|
| 463 |
+
`.opencoti/s2-gate.sh`: T2(2-tile)/T4(4-tile) **byte-identical** to NEO ref at a
|
| 464 |
+
1969-token prompt (n_blk=8, genuine cross-tile), needle recalled, reap clean β
|
| 465 |
+
stronger than the cosineβ₯0.999 bar. Speed: zero penalty on any working path
|
| 466 |
+
(single-tile short-circuits; multi-tile only runs where resident can't hold KV;
|
| 467 |
+
the LSE pass reuses the already-streamed slot K β no extra DMA). NB: scoring
|
| 468 |
+
coverage is scale+mask only (max_bias==0, softcap==0, no sinks) β resident
|
| 469 |
+
fallback otherwise; gate model qwen2.5-1.5b satisfies all three.
|
| 470 |
+
- **S3 (= blueprint R2-b) β double-buffer overlap.** Staged S3a β S3b β S3c so
|
| 471 |
+
correctness is gated before throughput, throughput before the memory win.
|
| 472 |
+
- **S3a β slot-pool machinery (DONE, 2026-05-31, byte-stable).** Per-key-tile
|
| 473 |
+
round-robin GPU slot pool (`OPENCOTI_M7_STREAM_SLOTS=N`); each tile's K/V is
|
| 474 |
+
lifted into a packed slot and FA reads the slot, proving the slot path is a
|
| 475 |
+
numeric no-op vs S2. **Single-stream by design**: copies issue inline as
|
| 476 |
+
`cudaMemcpyAsync(β¦, cudaMemcpyDefault, ctx.stream())` on the op's assigned
|
| 477 |
+
stream β NO dedicated copy_stream, NO events. This sidesteps the M2
|
| 478 |
+
`concurrent_events` stream collision (bug-253): at frac<1.0 under eager exec
|
| 479 |
+
the head-split forks work onto streams 1..n, and a hardcoded second stream
|
| 480 |
+
raced it, scrambling output; `ctx.stream()` auto-rides whatever stream the
|
| 481 |
+
op was forked onto. Zero events β CUDA-graph-capturable β the bug-251
|
| 482 |
+
graph-disable was reverted. Gate `.opencoti/s3a-gate.sh`: T2s1/T2s2/T4s2 all
|
| 483 |
+
**byte-identical** to T2(S2)==REF(NEO), needle recalled in all 5, reap clean.
|
| 484 |
+
- **S3b β double-buffer overlap (DONE, 2026-05-31, byte-stable).** Dedicated
|
| 485 |
+
copy_stream at fixed index `GGML_CUDA_MAX_STREAMS-1` (=7) carries tile L+1's
|
| 486 |
+
K/V DMA while FA/lse for tile L run on `ctx.stream()`, gated by a
|
| 487 |
+
pre-created `copy_done[]`/`compute_done[]` event pool (created ONCE with
|
| 488 |
+
`cudaEventDisableTiming`, never during capture β record/wait ARE legal during
|
| 489 |
+
capture, so NO graph-disable needed; the audit corrected bug-251 here). Index
|
| 490 |
+
7 avoids the QKV `concurrent_events` fan-out (streams 1-3, which JOIN before
|
| 491 |
+
our op runs) and NEO's stream 1 β hardcoding stream 1 was bug-253. Two latent
|
| 492 |
+
bugs surfaced and were fixed during S3b: **bug-255** (cross-op WAR hazard β
|
| 493 |
+
slot pool buffers reused across layers; copy_stream not ordered vs the compute
|
| 494 |
+
stream across op invocations β layer L+1's copy clobbered a slot layer L's FA
|
| 495 |
+
still read β prefill garbage; fixed with a per-op-entry `cs_sync` event
|
| 496 |
+
ordering copy_stream behind the compute stream) and **bug-254** (the shared
|
| 497 |
+
`streaming_lse_kernel` hard-casts K to `half*`; an S3b-0 stop-gap f16 guard
|
| 498 |
+
keeps any non-f16 K resident until S3d's dequant-on-lift lands β S3b stays
|
| 499 |
+
**f16-pure**). Gate `.opencoti/s3b-gate.sh`: B2s2 (overlap) and B4s2
|
| 500 |
+
(overlap + slot-reuse + prefetch) **byte-identical** to S3a==REF(NEO), needle
|
| 501 |
+
recalled; the `--parallel 2` P2 collision-stress is SOFT (warn-only) β its
|
| 502 |
+
collision axis is already covered by B2s2/B4s2 byte-identity, and its boot-
|
| 503 |
+
fail is a pre-existing llamafile graph-node-pool ceiling under `--parallel`
|
| 504 |
+
(bug-256), gated separately at M7-G. Regression shield green: NEO 4/4 hard,
|
| 505 |
+
advanced-kv-stack 8/9 (sole fail = pre-existing non-S3b C5, task #280).
|
| 506 |
+
- **S3c β scheduler bypass (the memory win, β
DONE 2026-05-31, byte-stable).**
|
| 507 |
+
On a discrete GPU the ggml scheduler can't run a `cuda_host` buffer as an op
|
| 508 |
+
input (`ggml_backend_cuda_device_supports_buft` β false), so it auto-inserts a
|
| 509 |
+
full sequential hostβdevice copy of the pinned CPU-half KV before the streaming
|
| 510 |
+
op every step (`ggml-backend.cpp`), then rewrites the op's `src` to the device
|
| 511 |
+
dup β no memory win, and the S3a/S3b lift was deviceβdevice. S3c adds an
|
| 512 |
+
**op-scoped, env-gated** suppression of that copy (`OPENCOTI_M7_STREAM_BYPASS=1`,
|
| 513 |
+
keyed on `GGML_OP_STREAMING_FLASH_ATTN` + K/V src + host buffer) so the op's
|
| 514 |
+
`src` stays the pinned-host **permuted** view, and `fattn.cu` detects the host
|
| 515 |
+
pointer (`cudaPointerGetAttributes`), forces the slot path, and lifts each tile
|
| 516 |
+
with a **strided `cudaMemcpy2DAsync`** (`spitch = nb[1] = n_head_cpuΒ·row`, since
|
| 517 |
+
the permuted host view's key rows are NOT contiguous, unlike the scheduler's
|
| 518 |
+
device dup). Device srcs (bypass=0) keep the exact 1-D lift β byte-identical
|
| 519 |
+
bisection lever. **The streaming op never drifts to CPU** (CUDA-only op type).
|
| 520 |
+
Gate (`.opencoti/s3c-gate.sh`): S3a==REF, B2s2==S3a, B4s2==S3a, **S3c==B4s2
|
| 521 |
+
byte-identical**, and the **no-copy-node proof** β under bypass=1 the scheduler
|
| 522 |
+
copy is suppressed for every layer's K/V (instrumentation count >0, all
|
| 523 |
+
`host=1`), under bypass=0 never (==0). NOTE: the CUDA0 compute-buffer PEAK is
|
| 524 |
+
insensitive to the suppressed ~CPU-half MiB at 4k (FA softmax scratch dominates
|
| 525 |
+
+ allocator reuse β bug-257), so the steady-state VRAM win is measured at 256k
|
| 526 |
+
in M7-G, not at this gate. Regression shield: neo-pipeline 4/4, advanced-kv-stack
|
| 527 |
+
8/9 (sole fail = owned non-S3c C5, task #280 β the bench enables streaming on
|
| 528 |
+
zero configs, so the entire S3c path is structurally inert there). Still f16
|
| 529 |
+
(the bug-254 guard stays; dequant is S3d).
|
| 530 |
+
- **S3d β dequant-on-lift. β
DONE 2026-06-01, validated incl. n_head_cpu>1.**
|
| 531 |
+
The slot is now ALWAYS f16: a quantized K/V is dequantized into the slot on the
|
| 532 |
+
lift via `ggml_get_to_fp16_nc_cuda` (the exact primitive fattn-common.cuh uses;
|
| 533 |
+
strides in elements = nb/type_size, packed f16 output), keeping
|
| 534 |
+
`streaming_lse_kernel` type-agnostic and removing the bug-254 f16 stop-gap. Two
|
| 535 |
+
safety guards: an unsupported quant type (no nc converter) and a quant type with
|
| 536 |
+
no slot both stay resident. Gate (`.opencoti/s3d-gate.sh`, Qwen2.5-1.5B): f16
|
| 537 |
+
anchors byte-identical (S3a==REF, B2s2==S3a, B4s2==S3a, S3c==B4s2 β refactor inert
|
| 538 |
+
on f16), NOCOPY (896/0), **DEQUANT byte-identical to resident-q8_0** (streaming
|
| 539 |
+
q8_0 == resident q8_0). Council audit (7-agent) cleared the dequant stride math
|
| 540 |
+
against the stock resident reference. **n_head_cpu>1 validated** on Gemma-4 A4B
|
| 541 |
+
(8 KV heads β frac 0.5 = 4 GPU/4 CPU on 25/30 layers, all streaming): RULER vt@4k
|
| 542 |
+
q8_0 = **100%**, equal to resident-q8_0 ground truth and vanilla β so the
|
| 543 |
+
latent-stride fix (`Kt.nb[1]=k_row`, lse `k_nb1=k_row`) is load-bearing and
|
| 544 |
+
correct, not inert (`.opencoti/s3d-gemma-nhead-validate.sh`). **bug-226 reframed:**
|
| 545 |
+
the real opencoti defect was the concat_q block-scrambling, already fixed by 0035;
|
| 546 |
+
q8_0 works on this Gemma at coherence-checkable ctx (vanilla, resident, AND S3d
|
| 547 |
+
streaming all = 100% templated). The earlier "vanilla q8_0 collapse" was an
|
| 548 |
+
off-template (raw `/completion`) confound, retracted. The only open cell is **256k
|
| 549 |
+
q8_0 quality β M7-G** (deferred per the post-M7 + 256k-canary instruction); S3d is
|
| 550 |
+
NOT blocked on it.
|
| 551 |
+
|
| 552 |
+
- **E1 β stream-by-default auto-engage. β
DONE 2026-06-01 (E1a+E1b+E1c).**
|
| 553 |
+
Turns the dev-env-gated S-ladder into the product's auto-selected relief
|
| 554 |
+
tactic. **E1a (host):** `OPENCOTI_M7_STREAMING` is now tri-state β `"1"`βforce
|
| 555 |
+
GPU_STREAM, `"0"`βforce CPU_SPILL, **unset β GPU_STREAM** (stream-by-default for
|
| 556 |
+
any spilled layer); `frac=1.0` β no spill β all GPU_RESIDENT β vanilla
|
| 557 |
+
byte-identical (flip inert). **E1b (CUDA DSO + host):** the streaming op
|
| 558 |
+
self-configures β tile count `n_tiles=clamp(ceil(n_kv/2048),1,n_blk)` auto from
|
| 559 |
+
n_kv, `slots=2` default, overlap default-on at slotsβ₯2, S3c scheduler-copy
|
| 560 |
+
bypass default-on (host-buffer-guarded). All knobs keep their env override as
|
| 561 |
+
bisection levers. **E1c (adapter):** `launch.ts` default-emits
|
| 562 |
+
`--headinfer-gpu-heads-frac auto` + `--vram-target`; config gains
|
| 563 |
+
`vramTargetMiB` (#332). Gate (`.opencoti/m7e-b-gate.sh`, Qwen2.5-1.5B):
|
| 564 |
+
RESIDENT==VANILLA + KAUTO==BASE + **FIDELITY AUTO==RESIDENT byte-identical**
|
| 565 |
+
(the auto streaming faithfully reproduces the no-split reference β the
|
| 566 |
+
pathological-prompt "degeneration" is correct, matching vanilla's own
|
| 567 |
+
filler-echo; the old "AUTO must diverge" gate premise was retracted) +
|
| 568 |
+
MULTI-TILE-ENGAGED (auto n_tiles up to 4) + DETERMINISM + NOCOPY (1400/0) +
|
| 569 |
+
needles incl. q8_0. Shields held: advanced-kv 8/9 (C9 = the #287 M2 slowdown
|
| 570 |
+
M7 exists to fix, not a regression), neo 3/4 (C2 = M3 "no observable win"
|
| 571 |
+
perf-noise; C4 off-path byte-identical passes). **Gemma-4 A4B n_head_cpu=4
|
| 572 |
+
auto path** (`.opencoti/m7e-gemma-auto.sh`): RULER vt@4k q8_0 = **100%**, equal
|
| 573 |
+
to vanilla + resident β the full-auto product path is coherent on the
|
| 574 |
+
bug-226-class multi-CPU-head split. bug-259 (streaming-op warmup/-fit abort
|
| 575 |
+
under stream-by-default) fixed: CUDA `supports_op` for STREAMING_FLASH_ATTN
|
| 576 |
+
validates head_dim only (the op repacks K/V to contiguous f16 slots, so raw-src
|
| 577 |
+
stride-sensitivity was wrong); host-side CPU-supports-op=false + pass-4 CUDA pin
|
| 578 |
+
kept as inert defense-in-depth. **E2 (dynamic load-ramp under `--parallel`) is
|
| 579 |
+
next** β deferred behind the M7-G 256k canary (needs runtime active-occupancy
|
| 580 |
+
infra the codebase lacks; const-after-ctor `layer_tactic[]`).
|
| 581 |
+
|
| 582 |
+
## Open questions (resolution path during impl)
|
| 583 |
+
|
| 584 |
+
- **Hot-tile reserve default** β does keeping 1-2 recently-used tiles
|
| 585 |
+
GPU-resident across tokens give measurable gain, or is the DMA cost
|
| 586 |
+
already negligible vs compute under the auto-tune? Decide at M7-C
|
| 587 |
+
first bench.
|
| 588 |
+
- **Sub-tile online-softmax precision** β when running N
|
| 589 |
+
position-window sub-tiles per layer, the running max + denominator
|
| 590 |
+
accumulate in F32. Verify f32 accumulator precision holds at 256k
|
| 591 |
+
(probably fine; FA already does this internally).
|
| 592 |
+
- **iswa interaction** β Gemma 4 uses interleaved sliding-window
|
| 593 |
+
attention. Some layers attend to a small recent window only. Those
|
| 594 |
+
layers may not need streaming at all (the working set fits in one
|
| 595 |
+
slot). Tactic table should detect and route iswa-window layers to a
|
| 596 |
+
trivial `GPU_RESIDENT` tactic β a third tactic alongside
|
| 597 |
+
`GPU_STREAM` and `CPU_SPILL`.
|
| 598 |
+
- **PolyKV M6 composition** β if M6 ships first, M7 streams the
|
| 599 |
+
*compressed* tiles, decompressing in-slot before FA. Tile size math
|
| 600 |
+
changes (smaller bytes per slot, same compute time β bigger
|
| 601 |
+
effective tiles). M7's auto-adapt loop becomes
|
| 602 |
+
`tile_bytes_max Γ compression_ratio`. Plan to land M7 first if
|
| 603 |
+
bug-226 forces it; otherwise M6 β M7 is natural.
|
| 604 |
+
|
| 605 |
+
## Risks
|
| 606 |
+
|
| 607 |
+
- **Online-softmax across sub-tiles** is the trickiest math. FA
|
| 608 |
+
already does it within a kernel; we extend it across multiple
|
| 609 |
+
kernel invocations using the saved max + denom. One precision bug
|
| 610 |
+
here destroys output quality. Mitigation: keep the M0/M1/M2/M3/M5
|
| 611 |
+
regression-shield benches; gate ship on cosine β₯ 0.999 at small ctx
|
| 612 |
+
where the sub-tile path doesn't activate.
|
| 613 |
+
- **Async DMA + ggml scheduler** integration β ggml's existing
|
| 614 |
+
scheduler doesn't expect "this tensor's data arrives mid-graph from
|
| 615 |
+
another stream." Need to register events as scheduler dependencies
|
| 616 |
+
or pre-stage at graph-build time. Risk: race conditions invisible
|
| 617 |
+
in single-tenant tests but visible under `--parallel`.
|
| 618 |
+
- **Probe + boot ordering** β the auto-adapt loop reads PCIe topology
|
| 619 |
+
from the cached probe JSON. If the probe hasn't run (fresh install),
|
| 620 |
+
fall back to a `current_link_speed` read inline; if that fails, use
|
| 621 |
+
a conservative tile size (1 MiB) and warn. Never fail boot on
|
| 622 |
+
topology read.
|
| 623 |
+
- **Re-tuning when VRAM availability changes mid-run** β if a sibling
|
| 624 |
+
process grabs VRAM, M7's slot allocations may need to shrink. v1
|
| 625 |
+
doesn't handle this; the existing `--vram-target` assumption is
|
| 626 |
+
static. Document as a known limitation.
|
| 627 |
+
- **M2 flag removal is a user-visible break.** Anyone scripting
|
| 628 |
+
`--headinfer-gpu-heads-frac` will break. Ship a deprecation cycle:
|
| 629 |
+
M7 lands accepting both flags (frac β vram-target translation), a
|
| 630 |
+
release later the old flag warns, a release after that it errors.
|
| 631 |
+
|
| 632 |
+
## Why land this above PolyKV M6
|
| 633 |
+
|
| 634 |
+
M7 is the foundation for "KV doesn't have to fit on GPU." PolyKV
|
| 635 |
+
adds compression to that. Without M7, PolyKV's compressed pool still
|
| 636 |
+
has to fit on GPU; with M7, even an uncompressed pool can stream
|
| 637 |
+
arbitrarily large KV. **M7 first lets PolyKV's compression be pure
|
| 638 |
+
gravy on top of unlocked KV capacity, rather than a hard requirement
|
| 639 |
+
to fit.** Sequence: bug-226 root cause β M7 design β M7 impl β
|
| 640 |
+
PolyKV M6 (composed atop M7). Adjust if bug-226 turns out to need M7
|
| 641 |
+
urgently; otherwise the optimization round (#287, tasks #289-#293)
|
| 642 |
+
ships first as M7 prerequisites.
|
| 643 |
+
|
| 644 |
+
## S0 cliff-mechanism verdict (2026-07-01) β graphs are NOT the cause; redirect #586/#587/#588
|
| 645 |
+
|
| 646 |
+
Before building the pending rolling-KV perf work (#586 shared staging ring / #587
|
| 647 |
+
cross-layer run-ahead / #588 perf gate), an S0 investigation nailed **what actually
|
| 648 |
+
causes the overflow decode cliff** (resident ~30β46 tps β spill ~0.3β1.6 tps). The
|
| 649 |
+
working hypothesis had been "a host-sourced tail tile disables CUDA-graph capture for
|
| 650 |
+
the whole decode step β all `n_kv/32768` streaming tiles run as eager host-issued
|
| 651 |
+
launches." **That hypothesis is REFUTED.**
|
| 652 |
+
|
| 653 |
+
**Method:** the binary already emits a native per-slot metric `graphs reused = N`
|
| 654 |
+
(no rebuild needed β this is the "read the target's native diagnostics first" lesson).
|
| 655 |
+
A/B on the 3090 (Qwen3-8B-Q8_0, ctx 16384, ~15k-token prompt so the spill genuinely
|
| 656 |
+
pushes occupied cells into the tail):
|
| 657 |
+
|
| 658 |
+
| config | split | **graphs reused** | decode tps |
|
| 659 |
+
| --- | --- | --- | --- |
|
| 660 |
+
| resident (vt 20000) | FULLY RESIDENT | **23** | 41.99 |
|
| 661 |
+
| spill (vt 2000) | 3072 resident / 13312 tail (1872 MiB) | **23** | 1.56 (27Γ collapse) |
|
| 662 |
+
|
| 663 |
+
CUDA graphs are captured and **replayed 23Γ in BOTH cases** β the graph is *not*
|
| 664 |
+
disabled by a host tail. So the ~640 ms/token penalty happens **inside the replayed
|
| 665 |
+
graph**: it is real streaming-path *work*, not launch/eager overhead.
|
| 666 |
+
|
| 667 |
+
**Mechanism (grounded):** a host tail flips the *entire* attention β including the
|
| 668 |
+
device-resident window β from the fast dense-FA path onto the slow per-tile streaming
|
| 669 |
+
path (`two_region` re-tiles window *and* tail, fattn.cu). Each tile pays a strided
|
| 670 |
+
per-head copy (`cudaMemcpy2DAsync` Γ`n_head_kv`Γ2) + the `cs_sync` barrier
|
| 671 |
+
(fattn.cu:~1375) which serializes the copy stream against all prior compute β so tiles
|
| 672 |
+
execute serially even under graph replay. Cost β tile count (matches the earlier
|
| 673 |
+
2-tile 632 ms vs 7-tile 3146 ms scaling), and the device-resident window pays the
|
| 674 |
+
streaming tax it would NOT pay when fully resident.
|
| 675 |
+
|
| 676 |
+
**Redirect (the levers are structural, none is "graph capture"):**
|
| 677 |
+
- **#586 shared staging ring** β collapse the per-head `n_head_kvΓ2` strided copies
|
| 678 |
+
into one bulk transfer (cuts node/copy count).
|
| 679 |
+
- **#587 cross-layer run-ahead** β drop/loosen the per-op `cs_sync` barrier so the
|
| 680 |
+
tail DMA overlaps compute instead of serializing (was mis-framed as "restore graph
|
| 681 |
+
capture"; the real target is the barrier).
|
| 682 |
+
- **structural** β when only a tail spills, run the device-resident window as ONE
|
| 683 |
+
dense FA (the same fast kernel the fully-resident case uses) emitting its lse, tile
|
| 684 |
+
ONLY the tail, and do one online-softmax combine. Removes the streaming tax from the
|
| 685 |
+
window (the bulk of the KV); benefit β window/tail ratio (largest when occupied
|
| 686 |
+
barely exceeds VRAM β the real serving regime; deep overflow stays bandwidth-bound).
|
| 687 |
+
- **#588 perf gate** β re-run the 3090 cliff curve; success = spill decode within
|
| 688 |
+
~10β15% of resident at small-tail (occupied β VRAM) configs.
|
| 689 |
+
|
| 690 |
+
See bug-1342 (0089, the boot OOM that unblocked this curve) and bug-2094 (0090, the
|
| 691 |
+
`--parallel β₯2` multi-session boot fix found while probing tail amortization).
|
| 692 |
+
|
| 693 |
+
### Multi-session amortization (2026-07-01) β the cliff does NOT amortize under --parallel
|
| 694 |
+
|
| 695 |
+
With bug-2094 (0090) unblocking `--parallel β₯2` boot, the original question β does batching
|
| 696 |
+
hide the tail penalty? β was answered on bs2 (RTX PRO 6000, 14B-1M-Q8_0, controlled per-slot
|
| 697 |
+
window: p2 vt=2Γp1 vt so each slot's window β equal):
|
| 698 |
+
|
| 699 |
+
| config | host tail | per-slot decode tps | aggregate |
|
| 700 |
+
| --- | --- | --- | --- |
|
| 701 |
+
| p1 (1 session, vt 8000) | 31232 cells | 1.08 | **1.08** |
|
| 702 |
+
| p2 (2 sessions, vt 16000) | 27136 cells/slot | 0.28 + 0.66 | **0.94** |
|
| 703 |
+
|
| 704 |
+
Two concurrent spilled sessions aggregate to **less** than one (0.94 < 1.08 tps) β batching
|
| 705 |
+
does not recover throughput; the sessions **contend on the same serialized streaming path**
|
| 706 |
+
(shared copy stream + the per-op `cs_sync` barrier), and per-slot tps is asymmetric (0.28 vs
|
| 707 |
+
0.66) from uneven interleave on that one copy stream. **Consequence:** for the multi-session
|
| 708 |
+
serving target ("more users per card"), spilling collapses per-card throughput regardless of
|
| 709 |
+
concurrency, so the #586/#587 streaming-path rework (kill the serialization) is **critical**,
|
| 710 |
+
not a nice-to-have.
|
| 711 |
+
|
| 712 |
+
## 2026-07-05 β bug-1843 shipped (patch 0098): the spill-decode recompute is dead
|
| 713 |
+
|
| 714 |
+
The S0-cliff redirect (#586 ring / #587 barrier-drop) proved perf-inert
|
| 715 |
+
(bug-1838); the real lever was the **per-tile `streaming_lse_kernel`
|
| 716 |
+
recompute** that every `head_dim<=256` decode tile paid because `decode_lse`
|
| 717 |
+
was gated on `head_dim > 256` (bug-1843, ~80% of spill-decode wall β 1.14 vs
|
| 718 |
+
6.05 tps NOOP A/B, Qwen3-8B ctx40960/vt14000, 3090). Patch
|
| 719 |
+
`0098-rolling-kv-lse-decode` arms the Stage-3a LSE channel for Dβ€256 at all
|
| 720 |
+
three streaming-FA sites and recomputes only when
|
| 721 |
+
`opencoti_fattn_dst_lse_written` says no finalize ran.
|
| 722 |
+
|
| 723 |
+
Shipped gates (3090, DSO df46dc36): spill decode **1.14 β 6.04 tps (5.3Γ, ==
|
| 724 |
+
NOOP ceiling)** with needle PRESENT; resident 42.5 tps unchanged;
|
| 725 |
+
`OPENCOTI_LSE_NOOP` now speed-inert (6.02 β recompute structurally off the
|
| 726 |
+
path). Correctness: teacher-forced logit-equiv (Qwen3-8B D128,
|
| 727 |
+
ours-resident REF vs POSITION_WINDOW at 256/16384 cells resident):
|
| 728 |
+
real_frac=0.0, frac_full_agree=1.0, mean_tv 0.0068 β PASS. Follow-up: #588
|
| 729 |
+
re-runs the 3090 overflowβtps cliff curve on the fixed binary (go/no-go for
|
| 730 |
+
further rolling-KV investment). Note bug-2115 while gating: bs2-built DSOs
|
| 731 |
+
do NOT load on solidPC (glibc β₯2.38 vs Debian 11) β build the 3090 DSO
|
| 732 |
+
locally or the run silently falls back to CPU.
|
| 733 |
+
|
| 734 |
+
## 2026-07-06 β #588 perf gate: post-fix cliff curve β spill decode is at the PCIe floor (rolling-KV perf line CLOSED)
|
| 735 |
+
|
| 736 |
+
Curve on the bug-1843-fixed DSO (`df46dc36`, 3090, Qwen3-8B-Q8_0, ctx 16384,
|
| 737 |
+
~13.1k-token prompt = 2304 MiB f16 KV, needle PRESENT in every cell):
|
| 738 |
+
|
| 739 |
+
| vt (MiB) | window split | occupied tail | decode tps | ms/token |
|
| 740 |
+
| --- | --- | --- | --- | --- |
|
| 741 |
+
| 22000/16000/14000/13000 | FULLY RESIDENT | 0 | 42.3β42.5 | 23.6 |
|
| 742 |
+
| 12000 | 11008 / 16384 cells | ~2.1k cells β 295 MiB | 15.5 | 64.6 |
|
| 743 |
+
| 8000/4000/2000 | 256 / 16384 (floor) | ~12.9k cells β 1815 MiB | 3.33 | 300 |
|
| 744 |
+
|
| 745 |
+
**The PCIe model now fits within ~5%:** `t(token) β 23.6 ms + occupied_tail_bytes
|
| 746 |
+
/ 6.5 GB/s` (vt12000 predicted 67 ms vs 64.6 measured; floor predicted 303 ms vs
|
| 747 |
+
300). The S0 anchor cell (vt 2000) improved 1.56 β 3.33 tps and the former
|
| 748 |
+
compute-side ceiling (streaming_lse recompute) is gone β spill decode is now
|
| 749 |
+
purely bandwidth-bound, i.e. at the structural floor for a design that re-streams
|
| 750 |
+
the tail every token.
|
| 751 |
+
|
| 752 |
+
**Gate verdict: NO-GO for further re-stream perf work.** The "within 10β15% of
|
| 753 |
+
resident at small tails" bar is only reachable for tails β€ ~25 MiB (~170 cells)
|
| 754 |
+
on a 6.5 GB/s 3090 (~8Γ more on bs2's 50 GB/s link) β no copy-plumbing lever
|
| 755 |
+
(#586-class) can beat the link itself. What remains valuable:
|
| 756 |
+
|
| 757 |
+
- **Window mode as graceful-overflow fallback** (shipping default): linear
|
| 758 |
+
degradation β spilled bytes, correctness clean (needle + logit-equiv
|
| 759 |
+
real_frac=0), no cliff pathology left.
|
| 760 |
+
- **The real capacity levers are residency-side**, per #582: quant-KV decode
|
| 761 |
+
reclaim (#620), auto KV-tier boot policy (#621), and position-axis mixed KV
|
| 762 |
+
with a *compressed VRAM tail* (#622) β which avoids the per-token re-stream
|
| 763 |
+
entirely instead of optimizing it.
|
| 764 |
+
|
| 765 |
+
## 2026-07-06 β bs2 tail-spill curve (the real-bandwidth one): bug-2116 bulk-H2D unlocks the 50 GB/s link
|
| 766 |
+
|
| 767 |
+
The #588 verdict above is on the **3090's 6.5 GB/s link** β where the tail is so
|
| 768 |
+
bandwidth-starved that no plumbing lever can help. bs2 (RTX PRO 6000, ~50 GB/s
|
| 769 |
+
link) is the host where window-mode spill is actually *usable*, and it is also the
|
| 770 |
+
host where the **bug-2116 bulk-H2D fix** (batched staging vs per-tile serial
|
| 771 |
+
`cudaMemcpyAsync`) matters most. Curve on **Qwen2.5-14B-Instruct-1M Q8_0**, ctx
|
| 772 |
+
131072, ~106k-token prompt (KV 9984 MiB f16-resident), needle PRESENT in every
|
| 773 |
+
cell. Both curves share the same VTβwindow-split mapping, so they are directly
|
| 774 |
+
comparable against the real host-tail byte count:
|
| 775 |
+
|
| 776 |
+
| host tail | BEFORE (per-tile H2D) | AFTER (bug-2116 bulk-H2D) |
|
| 777 |
+
| --- | --- | --- |
|
| 778 |
+
| 0 (fully resident) | 15.88 tps / 63.0 ms | 15.91 tps / 62.85 ms |
|
| 779 |
+
| 1443 MiB | 13.19 / 75.8 | 13.17 / 75.95 |
|
| 780 |
+
| 2028 MiB | **9.05** / 110 | **12.23** / 81.79 |
|
| 781 |
+
| 2828 MiB | **3.96** / 253 | **10.28** / 97.31 |
|
| 782 |
+
| 7624 MiB | (already collapsed by 2.8 GiB) | 5.53 / 180.84 |
|
| 783 |
+
|
| 784 |
+
**The fix converts launch-bound into link-bound.** BEFORE, the deep-tail slope is
|
| 785 |
+
~5.5 GB/s effective (2028β2828 MiB costs +142 ms) β i.e. even on bs2's 50 GB/s
|
| 786 |
+
hardware the *serial per-tile launches* throttle spill to 3090-class bandwidth,
|
| 787 |
+
and the curve cliffs at ~2 GiB. AFTER, the deep slope is **~56 GB/s** (2828β7624
|
| 788 |
+
MiB costs +83.5 ms for 4.68 GB) β matching bs2's physical link β so at a 2.8 GiB
|
| 789 |
+
tail you keep **10.28 tps = 65% of resident** (vs 3.96 = 25% before), and a 7.6 GiB
|
| 790 |
+
tail still decodes at 5.53 tps. This is the measured confirmation of the "~8Γ on
|
| 791 |
+
bs2's link" projection in the #588 section, and it makes window mode a genuinely
|
| 792 |
+
usable graceful-overflow tier on the production host, not just a non-cliff fallback.
|
| 793 |
+
|
| 794 |
+
Data: `bs2:/srv/ml/opencoti-c1/tailcurve-out/` (BEFORE) and `tailcurve-after-out/`
|
| 795 |
+
(AFTER); decode tps read from `slot print_timing β¦ eval time` in the per-cell
|
| 796 |
+
`tailN.log` server logs (the AFTER driver never aggregated a `run.log` beyond
|
| 797 |
+
tail=200 β the numbers above are recovered from the server logs). **Two AFTER
|
| 798 |
+
cells are truncated** (host tail 1638 MiB / 4426 MiB β run cut off; the log has
|
| 799 |
+
prompt-processing but no eval line); they will be re-run to complete the curve
|
| 800 |
+
when GPU1 frees from the bug-2121 window-mode correctness gate (which exercises
|
| 801 |
+
this same spill path). The AFTER slope is already pinned by the 2828β7624 MiB
|
| 802 |
+
segment, so the verdict does not depend on the two missing points.
|
| 803 |
+
|
| 804 |
+
### 2026-07-06 β multi-model spill sweep: production KV is compact at 128k (spill barely triggers)
|
| 805 |
+
|
| 806 |
+
Extending the bs2 curve to four production targets (Gemma-4-31B Q6_K, Gemma-4-A4B-128e
|
| 807 |
+
Q4_K_M, Qwen3.6-27B-Omnimerge Q6_K, Qwen3.6-35B-A3B Q6_K), q8_0-K/q4_0-V, ctx 131072,
|
| 808 |
+
surfaced a physical finding **before** any bandwidth number: at 128k these models simply
|
| 809 |
+
do not have enough KV to spill. Measured resident KV (fully on-GPU) and fully-resident
|
| 810 |
+
decode tps:
|
| 811 |
+
|
| 812 |
+
| model | attention | resident KV @131072 | resident decode tps |
|
| 813 |
+
| --- | --- | --- | --- |
|
| 814 |
+
| Qwen2.5-14B-1M Q8_0 | full | **9584 MiB** | 15.9 |
|
| 815 |
+
| Qwen3.6-27B-Omnimerge Q6_K | full | 3328 MiB | 29.8 |
|
| 816 |
+
| Qwen3.6-35B-A3B Q6_K | full (MoE) | 1040 MiB | 89.4 |
|
| 817 |
+
| Gemma-4-31B Q6_K | **iSWA** | **488 MiB** (global only) | 25.6 |
|
| 818 |
+
| Gemma-4-A4B-128e Q4_K_M | **iSWA** | (small global) | β |
|
| 819 |
+
|
| 820 |
+
Two structural reasons the production models barely spill at 128k, both of which the
|
| 821 |
+
14B-1M (the original curve) side-steps: **(1) GQA compactness** β the Qwen production
|
| 822 |
+
quants have few KV heads, so full-attention KV is only 1β3.3 GB at 128k vs the 14B-1M's
|
| 823 |
+
9.6 GB (Q8, more KV heads); **(2) iSWA** β Gemma-4 (both A4B and 31B) keeps only ~1/6 of
|
| 824 |
+
layers global, so the cache that *can* spill is tiny (31B global = 488 MiB). On a 96 GB
|
| 825 |
+
card none of these overflow at 128k, so a natural spill only appears at far longer
|
| 826 |
+
context (256kβ1M for the Qwen full-attn models; iSWA Gemma effectively never spills its
|
| 827 |
+
global cache at usable context).
|
| 828 |
+
|
| 829 |
+
A follow-up sweep (`tailcurve-multi2`) forced spill by capping `--vram-target` below
|
| 830 |
+
(weights + KV), weights measured from `nvidia-smi`, tail ladder = 0/25/50/75/90 % of each
|
| 831 |
+
model's KV. Results (bs2, DSO `8b23afbf`, q8_0-K/q4_0-V, ctx 131072, needle PRESENT in
|
| 832 |
+
**every** cell):
|
| 833 |
+
|
| 834 |
+
**14B-1M β the one clean forced-spill curve** (full-attn, 9584 MiB KV):
|
| 835 |
+
|
| 836 |
+
| host tail | decode tps | ms/tok |
|
| 837 |
+
| --- | --- | --- |
|
| 838 |
+
| 1019 MiB | 13.16 | 75.97 |
|
| 839 |
+
| 3515 MiB | 9.36 | 106.87 |
|
| 840 |
+
| 6011 MiB | 6.46 | 154.83 |
|
| 841 |
+
| 8507 MiB | 5.03 | 198.80 |
|
| 842 |
+
| 9984 MiB (all KV on host) | 4.35 | 229.62 |
|
| 843 |
+
|
| 844 |
+
Deep slope (6011β9984 MiB) = **51.9 GB/s**, i.e. the bs2 host link β the same result as the
|
| 845 |
+
`tailcurve-after` curve above, reproduced by an independent forced-spill trigger. Window mode
|
| 846 |
+
degrades linearly and holds retrieval to the fully-spilled floor.
|
| 847 |
+
|
| 848 |
+
**The four production targets do not give a spill curve at 128k**, for two distinct reasons:
|
| 849 |
+
|
| 850 |
+
- **iSWA (Gemma-31B, A4B):** the *global* cache β the only part window mode spills β is just
|
| 851 |
+
**1536 cells (~5 MiB)**. Every forced-spill cell shows `256 / 1536 resident`, decode stays
|
| 852 |
+
flat (31B ~21 tps, A4B ~38 tps), needle PRESENT. Spill is a non-event; the sliding-window
|
| 853 |
+
layers never leave VRAM. **iSWA models effectively cannot be made to spill** at any usable
|
| 854 |
+
context.
|
| 855 |
+
- **Compact-GQA full-attn (Qwen-27B 3328 MiB, 35B-A3B 1040 MiB):** KV fits so easily that the
|
| 856 |
+
calibrated low-VT still didn't drop below (weights+KV) β all cells stayed FULLY RESIDENT
|
| 857 |
+
(flat 29.8 / 89.4 tps). Forcing them to spill would need a hardcoded sub-weights VT; at 128k
|
| 858 |
+
they simply have no overflow. A genuine spill on these needs 256kβ512k context (bigger KV).
|
| 859 |
+
|
| 860 |
+
(A first attempt, `tailcurve-multi`, was discarded β its calibration read the high-VT budget
|
| 861 |
+
as free-VRAM β negative targets β BOOT-FAILs; only the resident-tps column above survived it.)
|
| 862 |
+
|
| 863 |
+
**Bottom line for the four requested models:** at 128k only the 14B-1M (large Q8 KV) spills,
|
| 864 |
+
and it hits the ~52 GB/s link exactly like the reference curve. The production targets either
|
| 865 |
+
*can't* spill (iSWA global cache is ~5 MiB) or *don't* (compact GQA KV fits) β so window mode
|
| 866 |
+
is a no-op safety-net for them at this context, and real spill characterization would require
|
| 867 |
+
256kβ512k runs.
|
| 868 |
+
|
| 869 |
+
## 2026-07-12 β #582-P2 SHIPPED: position-axis mixed KV (compressed tail, `-ctkt`/`-ctvt`)
|
| 870 |
+
|
| 871 |
+
The window-mode spill above re-streams the tail every token at *the boot KV type*, so a big
|
| 872 |
+
Q8 tail is both slow (bytes) and redundant (older tokens are attention-light). **P2 makes the
|
| 873 |
+
spilled tail carry a MORE-compressed type than the resident window** β the position-axis
|
| 874 |
+
complement to P0 (head-axis, #620/#643) and P1 (boot-tier auto-select, #621). Recent tokens
|
| 875 |
+
in the window `[0,wc)` keep their high-fidelity type on-device; the tail `[wc,n_kv)` on pinned
|
| 876 |
+
host is quantized further, so each decode step ships fewer H2D bytes without touching the part
|
| 877 |
+
of the cache attention actually leans on.
|
| 878 |
+
|
| 879 |
+
**Interface.** Two new CLI flags set the *tail* K/V type independently of the window:
|
| 880 |
+
`-ctkt <type>` / `-ctvt <type>` (`--cache-type-k-tail` / `--cache-type-v-tail`). Unset β
|
| 881 |
+
sentinel `GGML_TYPE_COUNT` β "tail == window", and every code path is byte-identical to the
|
| 882 |
+
shipped uniform-window behaviour (regression leg of the gate, `REG==U8` proven byte-identical).
|
| 883 |
+
Typical use: `-ctk q8_0 -ctv q8_0 -ctkt q4_0 -ctvt q4_0` (q8 window β q4 tail).
|
| 884 |
+
|
| 885 |
+
**Where the win lands (decode).** The POSITION_WINDOW streaming FA op (`fattn.cu`) already
|
| 886 |
+
reads window and tail *each in-register at its own type* (the #620/#629 reader path) β there is
|
| 887 |
+
no whole-cache f16 materialise, so a q4 tail is genuinely ~half the H2D bytes of a q8 tail.
|
| 888 |
+
|
| 889 |
+
**Two correctness-fallback fixes** were needed for the paths that *don't* go through the
|
| 890 |
+
in-register decode op (both host-only, both byte-identical when tail type == window type):
|
| 891 |
+
|
| 892 |
+
- **bug-2161** β prefill / graph-reserve (n_q > `WS2_NQ_MAX`) reassembles window+tail via
|
| 893 |
+
`ggml_concat`, which asserts `a->type == b->type`. Fixed with a file-local
|
| 894 |
+
`poswin_lift_to_f16` (mirror of `dca_lift_to_f16`: scalar quants β `ggml_cast` F32 β F16,
|
| 895 |
+
byte-exact) applied to both region views only when their types differ.
|
| 896 |
+
- **bug-2162** β an in-memory **context checkpoint** (llama.cpp PR16391, on by default) hits
|
| 897 |
+
the Stage 3c-6 state-IO window paths, which had assumed the tail row size equals the window
|
| 898 |
+
row size β so a mixed config asserted on the 2nd prompt. Fixed with a per-region
|
| 899 |
+
`ggml_row_size(k_cpu->type / v_cpu->type, β¦)`, symmetric across writer and reader.
|
| 900 |
+
|
| 901 |
+
**Gate (S7, bs2 Qwen2.5-14B-Instruct-1M-Q8_0, `-c 24576` `--vram-target 18000`, ~21k-token
|
| 902 |
+
prompts β wc β 13312 resident, ~11264-cell host tail; logit-equiv vs the model's own
|
| 903 |
+
full-resident gold β NEVER greedy needle):**
|
| 904 |
+
|
| 905 |
+
| config | tail type | correctness (real_frac) | advisory mean-TV vs gold | decode tps |
|
| 906 |
+
| --- | --- | --- | --- | --- |
|
| 907 |
+
| U8 (uniform q8 tail) | q8_0 | 0.0 | 0.0069 | 26.4 |
|
| 908 |
+
| **MIX (q8 window β q4 tail)** | q4_0 | **0.0** | **0.0110** | **28.1** |
|
| 909 |
+
| U4 (uniform q4) | q4_0 | 0.0 | 0.0118 | β |
|
| 910 |
+
|
| 911 |
+
Mixed sits **strictly between** uniform-q8 and uniform-q4 on fidelity (closer to gold than
|
| 912 |
+
uniform-q4) at **+6.4% decode tps** over uniform-q8 οΏ½οΏ½ the tail's fewer H2D bytes convert
|
| 913 |
+
directly to throughput, and the recent window keeps quality above the uniform-q4 floor. The
|
| 914 |
+
gate exercises checkpoints, so it doubles as a bug-2162 checkpoint round-trip test.
|
| 915 |
+
|
| 916 |
+
Shipped as patch `0120-p2-mixed-kv` (marker `opencoti-hook: P2 mixed-kv`, 12 nested-llama.cpp
|
| 917 |
+
files, additive; chain state β 66 patches). See UPSTREAM_SYNC.md registry entry for the full
|
| 918 |
+
5-point plumbing surface.
|
| 919 |
+
|
| 920 |
+
### 2026-07-12 β #653 P2 productized: TS adapter flags + P2-auto boot policy
|
| 921 |
+
|
| 922 |
+
Two follow-ons turn the raw `-ctkt`/`-ctvt` mechanism into a first-class, opt-in
|
| 923 |
+
opencoti feature.
|
| 924 |
+
|
| 925 |
+
**Part 1 β adapter (`packages/opencoti-llamafile`).** The mixed-tail flags are now
|
| 926 |
+
typed config fields, so a caller sets them like any other KV knob instead of hand-passing
|
| 927 |
+
`extraArgs`:
|
| 928 |
+
|
| 929 |
+
- `config.ts` β `ctypeKTail` / `ctypeVTail: string | undefined` (env
|
| 930 |
+
`OPENCOTI_LLAMAFILE_CTYPE_K_TAIL` / `_V_TAIL`), defaulting to `undefined` = server
|
| 931 |
+
default = "tail == window".
|
| 932 |
+
- `launch.ts` β `buildServerArgs` emits `-ctkt`/`-ctvt` only when set, ordered right
|
| 933 |
+
after `-ctk`/`-ctv` (the window types they refine), before `--neo-pipeline`. Unset β
|
| 934 |
+
nothing emitted β byte-identical argv.
|
| 935 |
+
- `test/launch-args.test.ts` β 4 new cases (omit-when-unset, symmetric q4_0 tail,
|
| 936 |
+
independent side, ordering); suite 49 pass / 0 fail.
|
| 937 |
+
|
| 938 |
+
**Part 2 β P2-auto boot policy (patch `0121-p2-auto-tail`).** Extends the P1 boot
|
| 939 |
+
auto-tier so that when it is *forced to spill* (the auto-selected window still overflows
|
| 940 |
+
the VRAM budget), it can auto-pick a compressed tail rather than re-streaming the whole
|
| 941 |
+
tail at the window type. Policy is **"q4_0 floor when window > q4_0"**: per axis, a
|
| 942 |
+
window higher-bit than q4_0 gets a q4_0 tail; a window already at/below q4_0 keeps
|
| 943 |
+
tail == window. It fires only on the two spill exits of `opencoti_auto_select_kv_tier`
|
| 944 |
+
(never on the fully-resident fast path), and an explicit `-ctkt`/`-ctvt` is always honored.
|
| 945 |
+
|
| 946 |
+
Gate opt-in is **separate from P1** β `OPENCOTI_KV_AUTO_TIER_TAIL=1` on top of
|
| 947 |
+
`OPENCOTI_KV_AUTO_TIER=1` β so a P1-only boot stays byte-identical:
|
| 948 |
+
|
| 949 |
+
| leg | envs | tail line | correctness |
|
| 950 |
+
| --- | --- | --- | --- |
|
| 951 |
+
| P1AUTO | `OPENCOTI_KV_AUTO_TIER=1` | *(none β tail == window)* | `real_frac 0.0` |
|
| 952 |
+
| **P2AUTO** | `+ OPENCOTI_KV_AUTO_TIER_TAIL=1` | `auto KV tail = q4_0/q4_0` (~189 MiB / 3072-cell spill) | `real_frac 0.0`, mean-TV 0.0087 |
|
| 953 |
+
|
| 954 |
+
The window was `q5_1/q4_0` at the **second-pass binding budget** β the bug-1342 two-pass
|
| 955 |
+
compute reserve runs the auto-tier twice; the first (looser) pass fits, the binding pass
|
| 956 |
+
spills, and the tail policy fires there. HOST-ONLY (`src/llama-kv-cache.cpp` is libllama,
|
| 957 |
+
not the CUDA DSO). Marker: reuses the 0116 `opencoti-hook: kv-auto-tier` site (like 0118,
|
| 958 |
+
no new hook). Chain state β 67 patches.
|
| 959 |
+
|
| 960 |
+
## 2026-07-14 β bug-2172 SHIPPED (patch 0125): rolling-KV window on a HYBRID attention sub-cache
|
| 961 |
+
|
| 962 |
+
Until now the position-window armed cleanly only on caches whose cell placement is
|
| 963 |
+
**append-only** β dest cell idx == key position, monotone in batch order (every dense / iSWA
|
| 964 |
+
/ unified `llama_kv_cache`). A **hybrid** model (qwen35moe / qwen3next: gated-delta-net
|
| 965 |
+
recurrent β full-attention β MoE β NextN) routes its attention sub-cache (`mem_attn` inside
|
| 966 |
+
`llama_memory_hybrid`) through the **recurrent** batch pipeline (`split_seq` +
|
| 967 |
+
`[TAG_RECURRENT_ROLLBACK_SPLITS]` + `find_slot` cell-reuse/wrap), which hands the scatter
|
| 968 |
+
**non-monotone** destination idxs. Under tight VRAM / large ctx the window armed and the
|
| 969 |
+
append-only scatter's monotone `GGML_ASSERT` in `cpy_k` fired at boot.
|
| 970 |
+
|
| 971 |
+
**Root cause** β not a bandwidth or a residency bug (those were #2142 / #2144); the append-only
|
| 972 |
+
window scatter *assumed* a contiguous window-prefix / tail-suffix partition of the batch, which
|
| 973 |
+
a recurrent-rollback batch violates.
|
| 974 |
+
|
| 975 |
+
**Fix (host-only, one TU + two one-liners; NO CUDA / NO DSO rebuild):**
|
| 976 |
+
|
| 977 |
+
- New `llama_kv_cache::append_only` flag (default **true** β every existing cache is
|
| 978 |
+
byte-identical). Set **false** only by `llama_memory_hybrid` on its `mem_attn`.
|
| 979 |
+
- `!append_only && windowed`: window + tail tensors get **+1 trash sink cell**; the idx
|
| 980 |
+
tensors become **2-row `[n_tokens,2]`** (row 0 window-route, row 1 tail-route, off-target β
|
| 981 |
+
trash); `cpy_k`/`cpy_v` scatter the full per-stream K/V into **both** regions via two
|
| 982 |
+
arbitrary-dest `ggml_set_rows` (drops the monotone assert). `ne[0]` stays `n_tokens`
|
| 983 |
+
(graph guards hold); fixed 2-set_rows/stream topology (reserve == live).
|
| 984 |
+
- **Read side unchanged** β `get_k/v_window` / `get_k/v_tail` already cover every active
|
| 985 |
+
scattered cell, and the streaming-FA `KQ_mask` `-INF`-masks the rollback holes by absolute
|
| 986 |
+
key position. **Mask contract verified**: `GGML_OP_STREAMING_FLASH_ATTN` is per-cell,
|
| 987 |
+
absolute-key-indexed, softmax-denominator-normalized β no kernel change.
|
| 988 |
+
- **Scope**: iSWA-hybrid out of scope (no current model is both hybrid AND SWA; the window
|
| 989 |
+
never arms on `swa_type!=NONE`).
|
| 990 |
+
|
| 991 |
+
**Validated** (solidPC host `make`): qwen35moe auto-spill boots with **no assert** + correct
|
| 992 |
+
output; dense (Qwen) + iSWA (Gemma) regression shields **byte-identical** (append_only path
|
| 993 |
+
untouched). Marker: `opencoti-hook: hybrid-window-orderagnostic` (3 sites). Chain state β 71
|
| 994 |
+
patches.
|
| 995 |
+
|
| 996 |
+
**RYS Γ rolling-KV-window composition confirmed** (2026-07-14, solidPC 3090,
|
| 997 |
+
Qwen_Qwen3.6-35B-A3B-Q3_K_M, `--repeat-layers 5,9 --kv-residency-mode window
|
| 998 |
+
--vram-target 18000 -c 40960 -fa on`): RYS ENGAGED (40 β **44 effective layers**, +4
|
| 999 |
+
duplicated) **and** the window armed on the hybrid attention sub-cache (256 resident / 40704
|
| 1000 |
+
host-tail = 874 MiB) **simultaneously**. A 946-token needle prompt (needle sitting in the
|
| 1001 |
+
~690-cell spilled tail) β greedy-correct retrieval (`74-ALPHA-purple-9931`), **no assert / no
|
| 1002 |
+
NaN**, 16 graphs reused, clean slot release (pp 1452 tps / tg 57.5 tps). The two features that
|
| 1003 |
+
each stress the hybrid attention cache β RYS's extra duplicated KV layers and the
|
| 1004 |
+
order-agnostic window scatter β compose without interference.
|
docs/features/rolling_kv_compute_reserve.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Rolling-KV: two-pass compute-buffer reserve (fix the resident-allocation OOM)
|
| 2 |
+
|
| 3 |
+
**Status:** SHIPPED 2026-07-01 as `0089-rolling-kv-compute-reserve.patch` (9 files,
|
| 4 |
+
byte-identical-verified). Host-only change (`llama-kv-cache*`, `llama-context.cpp`,
|
| 5 |
+
`llama-model.cpp`, `llama-cparams.h`, `llama-memory-hybrid{,-iswa}.cpp`) β
|
| 6 |
+
`build:llamafile:make` rebuild ONLY (no CUDA DSO, no restamp). Vendor backup:
|
| 7 |
+
`.opencoti/vendor-backups/20260701-123044-two-pass-compute-reserve`.
|
| 8 |
+
|
| 9 |
+
**Gate result (bs2, RTX PRO 6000 96 GB, 2026-07-01):** the exact repro config below
|
| 10 |
+
(`-c 524288 --kv-residency-mode window --vram-target 88000 -ctk f16 -ctv f16`, KV
|
| 11 |
+
~96 GB) that **OOM'd the pre-0089 binary at `graph_reserve` now BOOTS** and prefills
|
| 12 |
+
with the streaming window engaged β prefill tps falls 4000β~450 tok/s as the host tail
|
| 13 |
+
grows (the expected cliff, per `docs/features/rolling_kv.md`). Two-pass gate conditions
|
| 14 |
+
(`offload && flash_attn && !devices.empty() && kv_residency_mode != head`) are all met on
|
| 15 |
+
this path, so boot-success β the measured reserve replaced the too-small 1536 MiB fixed
|
| 16 |
+
default. Streaming-window FA math is unchanged (size-only patch), correctness carried by
|
| 17 |
+
the `0070` M7 logit-equiv/RULER gates.
|
| 18 |
+
|
| 19 |
+
## The bug (bug-1342)
|
| 20 |
+
|
| 21 |
+
The POSITION_WINDOW resident-window sizer
|
| 22 |
+
(`opencoti_compute_resident_window_cells`, llama-kv-cache.cpp:227) reserves a
|
| 23 |
+
**fixed** `OPENCOTI_HEADINFER_AUTO_RESERVE_BYTES = 1536 MiB` (line 120) for the
|
| 24 |
+
compute scratch that `graph_reserve` allocates AFTER the KV cache. But the
|
| 25 |
+
compute buffer **scales with context** β measured **3.63 GiB (3 800 045 568 B)
|
| 26 |
+
at 262 144β524 288 ctx** on the RTX PRO 6000. So the sizer keeps ~2.1 GiB too
|
| 27 |
+
much KV resident and `graph_reserve` OOMs:
|
| 28 |
+
|
| 29 |
+
```
|
| 30 |
+
ggml_gallocr_reserve_n_impl: failed to allocate CUDA0 buffer of size 3800045568
|
| 31 |
+
graph_reserve: failed to allocate compute buffers
|
| 32 |
+
llama_init_from_model: failed to initialize the context: failed to allocate compute pp buffers
|
| 33 |
+
```
|
| 34 |
+
|
| 35 |
+
Repro: RTX6000, Qwen2.5-14B-1M Q8_0, `-c 524288 --kv-residency-mode window
|
| 36 |
+
--vram-target 88000 -ctk f16 -ctv f16`. 128k boots (KV ~25.7 GiB fits); 512k
|
| 37 |
+
(KV ~96 GiB) does not.
|
| 38 |
+
|
| 39 |
+
The author KNEW (comment at llama-kv-cache.cpp:114-119): "this reserve only
|
| 40 |
+
covers the not-yet-allocated compute buffers. --vram-target gives the operator
|
| 41 |
+
a tighter knob when this default is wrong." The fix removes the manual-guess
|
| 42 |
+
dependency.
|
| 43 |
+
|
| 44 |
+
## Fix: two-pass pre-measure (user-chosen 2026-07-01)
|
| 45 |
+
|
| 46 |
+
Model-agnostic: MEASURE the real compute buffer, don't estimate it.
|
| 47 |
+
|
| 48 |
+
- **Pass 1 (measure):** `create_memory` with a *measurement flag* β the sizer
|
| 49 |
+
returns a MINIMAL window (`walign` cells resident, rest host) β tiny device
|
| 50 |
+
KV β boots for ANY compute size. Run `sched_reserve()` (creates the sched at
|
| 51 |
+
llama-context.cpp:525, runs the pp worst-case reserve at :697). Then read the
|
| 52 |
+
actual GPU compute buffer: sum `ggml_backend_sched_get_buffer_size(sched, b)`
|
| 53 |
+
over backends whose `ggml_backend_dev_type(...)==GPU`.
|
| 54 |
+
- **Pass 2 (real):** re-`create_memory` with `kv_compute_reserve_mib = measured
|
| 55 |
+
+ safety(256 MiB)`. Sizer now sizes the window against `budget β model β
|
| 56 |
+
measured_compute`. Set `sched_need_reserve=true`; `sched_reserve()` again.
|
| 57 |
+
- **Gate the two-pass** so the common resident case stays SINGLE-pass /
|
| 58 |
+
byte-identical: only engage when `offload && kv_residency_mode != head(1) &&
|
| 59 |
+
full-KV-demand + a generous compute margin > free_vram` (overflow possible).
|
| 60 |
+
Reuse the `kv_demand` vs `free_vram` compare already in the sizer β expose a
|
| 61 |
+
cheap `bool opencoti_kv_may_overflow(...)` helper. If it can't overflow β
|
| 62 |
+
today's single pass, no behavior change.
|
| 63 |
+
|
| 64 |
+
Compute buffer size is INDEPENDENT of the window/tail split (it depends on
|
| 65 |
+
n_ubatch Γ n_kv Γ n_embd, and n_kv = full ctx regardless of resident split), so
|
| 66 |
+
the Pass-1 minimal-window measurement equals the Pass-2 compute β one
|
| 67 |
+
measurement suffices.
|
| 68 |
+
|
| 69 |
+
## Exact edit surface (threading map, verified 2026-07-01)
|
| 70 |
+
|
| 71 |
+
1. **llama-cparams.h** (struct at :9): add
|
| 72 |
+
`uint32_t kv_compute_reserve_mib;` (0 = use 1536 default) and
|
| 73 |
+
`bool kv_window_measure_pass;` (default false).
|
| 74 |
+
2. **llama-context.cpp**:
|
| 75 |
+
- Init defaults near :104-108 (beside `cparams.vram_target_mib` /
|
| 76 |
+
`cparams.kv_residency_mode`): `kv_compute_reserve_mib=0`,
|
| 77 |
+
`kv_window_measure_pass=false`.
|
| 78 |
+
- Two-pass orchestration wrapping the `memory.reset(model.create_memory(...))`
|
| 79 |
+
at :385 + the `sched_reserve()` at :451. Backend-init block (:388-449) is
|
| 80 |
+
memory-independent β keep it before the first `sched_reserve`. After the
|
| 81 |
+
Pass-1 reserve, sum GPU compute buffer, set
|
| 82 |
+
`cparams.kv_compute_reserve_mib`, clear `kv_window_measure_pass`,
|
| 83 |
+
`memory.reset(create_memory(...))` again, `sched_need_reserve=true`,
|
| 84 |
+
`sched_reserve()`. Guard whole block behind `opencoti_kv_may_overflow`.
|
| 85 |
+
3. **llama-kv-cache.h** (ctor decl :96-145): add `uint32_t
|
| 86 |
+
compute_reserve_mib` and `bool window_measure_pass` args (beside
|
| 87 |
+
`vram_target_mib` :123 / `kv_residency_mode` :133).
|
| 88 |
+
4. **llama-kv-cache.cpp**:
|
| 89 |
+
- `opencoti_compute_resident_window_cells` (:227): take `compute_reserve_mib`
|
| 90 |
+
+ `window_measure_pass`. Replace the fixed
|
| 91 |
+
`OPENCOTI_HEADINFER_AUTO_RESERVE_BYTES` at :271-273 with `compute_reserve_mib
|
| 92 |
+
? (size_t)compute_reserve_mib<<20 : OPENCOTI_HEADINFER_AUTO_RESERVE_BYTES`.
|
| 93 |
+
If `window_measure_pass` β early-return the minimal window (`walign`, using
|
| 94 |
+
the same align math at :296-298) so Pass-1 device KV is ~0.5 MiB.
|
| 95 |
+
- ctor (:316): accept + forward the 2 args to the sizer call at :396.
|
| 96 |
+
- (optional) expose `opencoti_kv_may_overflow` (reuse the auto-frac budget
|
| 97 |
+
math) for the context-level gate.
|
| 98 |
+
5. **llama-kv-cache-iswa.{h,cpp}**: thread the same 2 args through the wrapper
|
| 99 |
+
to both inner `llama_kv_cache` ctors.
|
| 100 |
+
6. **llama-model.cpp** create_memory (:2082 iswa, :2113 plain): pass
|
| 101 |
+
`cparams.kv_compute_reserve_mib`, `cparams.kv_window_measure_pass` alongside
|
| 102 |
+
`cparams.vram_target_mib` (:2098/:2128) and `cparams.kv_residency_mode`
|
| 103 |
+
(:2102/:2132).
|
| 104 |
+
|
| 105 |
+
## Gates
|
| 106 |
+
|
| 107 |
+
- **Boot repro (the fix):** the exact 512k config above now BOOTS with a host
|
| 108 |
+
tail (was OOM). Read the logged `position window = C / kv_size cells resident`
|
| 109 |
+
+ host tail; decode tps > 0, needle held.
|
| 110 |
+
- **Resident unchanged (no regression):** a config that fits resident (e.g. 128k
|
| 111 |
+
or `--vram-target` high) still logs FULLY RESIDENT, single-pass, and decodes
|
| 112 |
+
at the same ~38 tps β byte-identical path (measure gate not tripped).
|
| 113 |
+
- **Curve enabled:** with boot fixed, the tps-vs-host-tail curve (#583/#584 on
|
| 114 |
+
3090) is runnable on RTX6000: fix a ctx, sweep `--vram-target` down to grow
|
| 115 |
+
the tail, read (host-tail MiB, decode tps). NOTE the binary must LOG the
|
| 116 |
+
device/host split β the `position window = ...` INFO line at
|
| 117 |
+
llama-kv-cache.cpp:310 is the measurement source (the bench's grep must match
|
| 118 |
+
THAT line, not a non-existent `CUDA_Host KV buffer` line).
|
| 119 |
+
|
| 120 |
+
## Constraints (standing)
|
| 121 |
+
|
| 122 |
+
Host rebuild only (`bun run build:llamafile:make` from repo, or the make path) β
|
| 123 |
+
NOT a CUDA DSO rebuild, NO restamp (llama-kv-cache/llama-context are host TUs).
|
| 124 |
+
Additive soft-fork + `opencoti-hook:` markers (two-pass reserve) + UPSTREAM_SYNC
|
| 125 |
+
registry. Correctness via boot + niah, never greedy byte-equality. Commit only
|
| 126 |
+
when asked (dev; trailers). Vendor-backup already taken (above).
|
docs/features/rolling_kv_step_prefetch.md
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Rolling-KV: step/graph-level tail prefetch (design)
|
| 2 |
+
|
| 3 |
+
**Status:** DESIGN (2026-07-01). Supersedes the op-level double-buffer's overlap
|
| 4 |
+
ambition. Gates on user go-ahead β this is an M7-scale CUDA effort (full host
|
| 5 |
+
rebuild + dual-DSO restamp + logit-equiv/RULER-niah gate).
|
| 6 |
+
|
| 7 |
+
## Problem (measured, 3090)
|
| 8 |
+
|
| 9 |
+
POSITION_WINDOW streams the host KV tail over PCIe **every decode step**, and the
|
| 10 |
+
tail DMA is ~100% un-hidden β decode collapses on a smooth hyperbola at the raw
|
| 11 |
+
PCIe rate (~6.4 GB/s, `+0.156 ms/MiB`). Confirmed on both model classes:
|
| 12 |
+
|
| 13 |
+
| model | attn | streaming layers | overflow decode | note |
|
| 14 |
+
|---|---|---|---|---|
|
| 15 |
+
| Gemma-4-A4B-128e | iSWA | ~5 global | 71β19.5 tps @236 MiB tail | graceful hyperbola |
|
| 16 |
+
| Qwen2.5-14B-1M | full | 48 | 26.9β0.86 tps @~1.2 GB tail | ~10Γ worse (48 streaming layers) |
|
| 17 |
+
|
| 18 |
+
Qwen is **not** CPU_SPILL and **not** ineligible β it engages POSITION_WINDOW
|
| 19 |
+
(GPU 60β84% busy, CPU ~2.7 cores during overflow decode). It's slow purely
|
| 20 |
+
because full-attention streams a tail on *every* layer.
|
| 21 |
+
|
| 22 |
+
## Root cause (code)
|
| 23 |
+
|
| 24 |
+
`fattn.cu` streaming forward runs a per-op, 1-tile-ahead ping-pong: `lift(t+1)`
|
| 25 |
+
on `copy_stream` while tile `t` computes on the main stream. At decode (`n_q=1`)
|
| 26 |
+
one attention op's compute is a sub-ms VRAM read; its tail tile DMA is ~30Γ that.
|
| 27 |
+
So each tile's DMA can only hide behind the *previous tile's* tiny compute β
|
| 28 |
+
DMA-bound from MiB 1.
|
| 29 |
+
|
| 30 |
+
The **specific barrier** that prevents cross-layer overlap is the per-op WAR
|
| 31 |
+
resync at `fattn.cu:1379`:
|
| 32 |
+
|
| 33 |
+
```cpp
|
| 34 |
+
CUDA_CHECK(cudaEventRecord(cs_sync, stream)); // record ALL prior compute
|
| 35 |
+
CUDA_CHECK(cudaStreamWaitEvent(cs, cs_sync, 0)); // copy_stream waits on it
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
This orders `copy_stream` *after all prior compute-stream work at every op entry*
|
| 39 |
+
(needed because the slot pool is re-handed per op β WAR hazard). It means the copy
|
| 40 |
+
stream can **never run ahead of the current attention op** β so layer L+1's tail
|
| 41 |
+
cannot begin loading during layer L's FFN. The recovery budget is therefore one
|
| 42 |
+
op's compute (~0.1 ms β ~0.7 MiB), not the whole step's (~14 ms β ~90 MiB Gemma).
|
| 43 |
+
|
| 44 |
+
## The fix: a persistent, step-spanning tail-prefetch ring
|
| 45 |
+
|
| 46 |
+
Flatten the per-layer tile loop into **one tile stream across the whole step's
|
| 47 |
+
attention layers**, kept `B` tiles ahead on a dedicated copy stream, hiding tail
|
| 48 |
+
DMA behind the *sum* of all intervening compute (attention + FFN of the layers in
|
| 49 |
+
between), not one op's.
|
| 50 |
+
|
| 51 |
+
Components:
|
| 52 |
+
|
| 53 |
+
1. **Shared device staging ring** (`B` = 2β4 buffers, each `tile_kv_full` Γ f16,
|
| 54 |
+
sized once). Replaces the per-op `ggml_cuda_pool_alloc` slot pool. VRAM cost =
|
| 55 |
+
`B Γ k_slot+v_slot` β a few hundred MiB, independent of context length.
|
| 56 |
+
2. **Flat prefetch scheduler** over the ordered sequence of (layer, tile) tail
|
| 57 |
+
reads for the step. Issues H2D into ring slots on `copy_stream`, staying `B`
|
| 58 |
+
ahead. A ring slot is refilled only after its `compute_done[slot]` fires (WAR
|
| 59 |
+
discipline **replaces** the per-op `cs_sync` barrier β that's the edit that
|
| 60 |
+
unblocks cross-layer run-ahead).
|
| 61 |
+
3. **Op consumes, does not copy.** Each layer's streaming FA waits on its tiles'
|
| 62 |
+
`copy_done`, reads the already-resident staging tile, records `compute_done`.
|
| 63 |
+
The window (resident) region stays a plain FA, unchanged.
|
| 64 |
+
4. **Scheduling home.** The scheduler must span op boundaries, so it lives one
|
| 65 |
+
level up from the op β either (a) a context/graph-level KV-prefetch pass that
|
| 66 |
+
walks the step's attention nodes and their `k_tail/v_tail` sources, or (b) a
|
| 67 |
+
persistent per-context streamer object the ops register their tiles with. (a)
|
| 68 |
+
is cleaner for CUDA-graph capture; (b) is less invasive. Decide in S1.
|
| 69 |
+
|
| 70 |
+
## Bounded win (be honest)
|
| 71 |
+
|
| 72 |
+
Perfect overlap gives `decode_ms = max(step_compute, total_tail_DMA)` instead of
|
| 73 |
+
their sum. Consequences:
|
| 74 |
+
|
| 75 |
+
- **Below ~`step_compute Γ PCIe_BW`** (~90 MiB on Gemma-A4B, less on the 14B):
|
| 76 |
+
tail fully hidden β **near-1.0Γ (resident speed)**. This is the "free zone" the
|
| 77 |
+
op-level path fails to deliver (currently ~0 MiB).
|
| 78 |
+
- **Above it:** PCIe-bound at `1000/(tail/BW)` regardless β physics. E.g. Gemma
|
| 79 |
+
@236 MiB β `max(14, 35) = 35 ms` β 28 tps vs today's 19.5 (~1.4Γ).
|
| 80 |
+
- **Does NOT rescue 1M full-attention** (14B tail = GBs, step_compute tiny β floor
|
| 81 |
+
stays low). Route (b) low-bit-resident remains the only long-ctx full-attn
|
| 82 |
+
ceiling-raiser (Β§5b-14B turbo-KLD, #581).
|
| 83 |
+
|
| 84 |
+
So this widens the usable-overflow zone and roughly doubles the mid-tail regime;
|
| 85 |
+
it does not defeat PCIe for extreme context. Worth it for the iSWA/short-overflow
|
| 86 |
+
serving band; not a substitute for compression.
|
| 87 |
+
|
| 88 |
+
## Companion lever: per-layer residency budget (stream fewer layers)
|
| 89 |
+
|
| 90 |
+
Orthogonal to *how well* each tail is hidden is *how many* tails exist. Today the
|
| 91 |
+
window/tail split is one global `window_cells` applied to every layer. But layers
|
| 92 |
+
differ enormously in how far back they attend:
|
| 93 |
+
|
| 94 |
+
- **Gemma (iSWA):** already per-layer β sliding-window layers carry NO tail, only
|
| 95 |
+
the ~5 global layers stream. This lever is fully banked; it's the 10Γ gap vs the
|
| 96 |
+
14B. Nothing to do.
|
| 97 |
+
- **Qwen (full-attn):** HAL probe (needle ~1k in 12k niah) β only layers 0β4
|
| 98 |
+
(+~11, ~47) are truly local; ~13 layers attend the needle at β₯0.9, ~22 more at
|
| 99 |
+
0.45β0.9 β retrieval is **distributed across ~35/48 layers**. So the safely
|
| 100 |
+
droppable set is small.
|
| 101 |
+
|
| 102 |
+
**Bounded win (measured):** window-only the provably-local layers β 48β~41
|
| 103 |
+
streaming β **1.17Γ**; pushing into the mid-band trades retrieval (StreamingLLM
|
| 104 |
+
wall β the needle drops on the majority; see [[project_qwen_retrieval_distributed_hal]]).
|
| 105 |
+
Hard ceiling if you could window all but the 13 strong retrievers β 3.7Γ, but
|
| 106 |
+
unreachable without breaking niah. Treat as a **~1.2β1.5Γ correctness-gated
|
| 107 |
+
refinement that STACKS on the prefetch** (fewer streaming layers Γ each better
|
| 108 |
+
hidden), NOT a Gemma-style restructure. Cross-layer cache SHARING stays dead
|
| 109 |
+
(orthogonal caches, cosβ0.001) β this only skips a layer's OWN tail when that
|
| 110 |
+
layer is local.
|
| 111 |
+
|
| 112 |
+
Mechanism: replace the scalar `window_cells` with a per-layer `window_cells[il]`
|
| 113 |
+
(a layer whose profile is local gets `window_cells[il]==kv_size` β no tail =
|
| 114 |
+
resident-cheap). Drive `window_cells[il]` from an attention-locality profile
|
| 115 |
+
(reuse the HAL probe offline, or a cheap online out-of-window-mass estimate at
|
| 116 |
+
prefill). HARD GATE: per-layer niah must hold β never widen a layer's window past
|
| 117 |
+
the point its retrieval mass survives.
|
| 118 |
+
|
| 119 |
+
## Staged plan (de-risk correctness FIRST)
|
| 120 |
+
|
| 121 |
+
- **S0 β per-layer residency budget β SHELVED 2026-07-01 (mechanism proven, not
|
| 122 |
+
shippable as scoped; user pivoted to S1/S2).** Implemented via env-gated
|
| 123 |
+
`OPENCOTI_KV_LOCAL_LAYERS` in `llama-kv-cache.cpp` (per-layer `window_cells`;
|
| 124 |
+
a local layer β `window_cells=0` resident sentinel, `layer_window` gates the
|
| 125 |
+
tail machinery). **Measured on 3090, 14B-1M @20k overflow, `--kv-residency-mode
|
| 126 |
+
window`:** the *throughput* lever is real and scales with resident count β
|
| 127 |
+
baseline (0 resident) 0.97 tps, local1 0.96, local5 1.05, **local10 1.24 tps
|
| 128 |
+
(1.28Γ)**, matching the 48/38-streaming ratio. **But correctness breaks from
|
| 129 |
+
N=1:** baseline needle=YES, but local1/5/10 all needle=NO. Root cause
|
| 130 |
+
(bug-1341): the READ dispatch is per-layer (llama-graph.cpp:3274,
|
| 131 |
+
`get_layer_tactic==POSITION_WINDOW`; `window_cells==0`βplain FA, correct) but
|
| 132 |
+
the KV **WRITE** path splits every layer window/tail on a context-uniform
|
| 133 |
+
assumption β a resident layer routes its whole write to a non-existent tail
|
| 134 |
+
(tail_c=0) β device KV stays zero β attends zeros β poisons the forward from
|
| 135 |
+
layer 0. A prior alloc assert was bug-1340 (fixed: `layer_window` gating).
|
| 136 |
+
**Why shelved:** (a) the fix is write-path surgery in llama-graph.cpp + rebuild
|
| 137 |
+
+ re-gate, not the "single-file host-only cheap win" it was scoped as; (b) even
|
| 138 |
+
fixed, only the ~5β6 truly-local layers (0β4, needle_max<0.1 per the HAL
|
| 139 |
+
profile) are safe β realistic ceiling ~1.12Γ, still needing bs2 256k
|
| 140 |
+
validation. The step-prefetch below is the bigger, uniform, correctness-free
|
| 141 |
+
lever β do that first. Resurrect S0 only if a larger justifying win appears;
|
| 142 |
+
the exact edit recipe is in buglog bug-1340/1341.
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
- **S1 β foundation (low-risk, no math change):** shared staging ring + flat
|
| 146 |
+
scheduler skeleton that still runs **1-deep** (B forced to current behaviour).
|
| 147 |
+
Prove logit-equiv byte-identical to today's op-level path. Establishes the new
|
| 148 |
+
ownership without changing timing.
|
| 149 |
+
- **S2 β cross-layer run-ahead:** drop the per-op `cs_sync` barrier, replace with
|
| 150 |
+
ring WAR discipline; let the scheduler run `B` ahead across layers. Re-prove
|
| 151 |
+
logit-equiv (this is the risky reorder β the online-softmax combine must be
|
| 152 |
+
unaffected; ordering is data-independent so it *should* be identical, gate it).
|
| 153 |
+
- **S3 β perf gate:** re-run the 3090 cliff curve (both models). PASS = the
|
| 154 |
+
free-zone extends to ~`step_computeΓBW` and the mid-tail regime β `max(compute,
|
| 155 |
+
DMA)`. Quantify vs the current hyperbola.
|
| 156 |
+
- **S4 β tune B + tile size** for the staging-VRAM/overlap tradeoff; auto-size B
|
| 157 |
+
from `vram_target` headroom. bs2 96 GB validation.
|
| 158 |
+
- **S5 β ship:** additive patch(es) + `opencoti-hook:` markers + UPSTREAM_SYNC +
|
| 159 |
+
this doc's results + .wolf + pgvector.
|
| 160 |
+
|
| 161 |
+
## Constraints (standing)
|
| 162 |
+
|
| 163 |
+
vendor-backup WHOLE tree before any vendor mutation; new kernel/reorder = full host
|
| 164 |
+
`rm -rf o` + CUDA DSO rebuild + restamp BOTH DSO paths byte-identical + `nm -D`;
|
| 165 |
+
correctness via logit-equiv / RULER-niah, NEVER greedy needle; additive soft-fork;
|
| 166 |
+
CUDA-graph capturability preserved (no `cudaEventCreate` inside the op β reuse the
|
| 167 |
+
pre-created event pool pattern from #315). Commit only when asked (dev).
|
| 168 |
+
|
| 169 |
+
## Postscript (2026-07-05)
|
| 170 |
+
|
| 171 |
+
S1 (staging ring) and S2 (barrier drop) landed but measured perf-inert on the
|
| 172 |
+
3090 (bug-1838: the PCIe link is saturated; there is no copy-side slack to
|
| 173 |
+
reclaim). The actual spill-decode ceiling was compute-side: the
|
| 174 |
+
`streaming_lse_kernel` recompute on Dβ€256 decode tiles β fixed by bug-1843 /
|
| 175 |
+
patch `0098-rolling-kv-lse-decode` (5.3Γ spill decode, see
|
| 176 |
+
`rolling_kv.md`). S3 (this doc's perf gate) proceeds as #588 on the fixed
|
| 177 |
+
binary.
|
| 178 |
+
|
| 179 |
+
## #638 / bug-2148 β context-shift guard on a spilled window
|
| 180 |
+
|
| 181 |
+
The spilled position window (GPU window `[0,wc)` β CPU tail `[wc,kv_size)`) is
|
| 182 |
+
**incompatible with in-place KV re-roping** (server `--context-shift`, self-extend).
|
| 183 |
+
The k_shift graph (`llm_graph_input_k_shift` / `build_rope_shift`) views `k_per_stream`
|
| 184 |
+
over `get_size()` cells, but a windowed layer's `k_per_stream` holds only `window_cells`,
|
| 185 |
+
so a shift over-reads it and asserts (`ggml.c:1840`); the host tail is never re-roped.
|
| 186 |
+
`llama_kv_cache::get_can_shift()` now returns `false` whenever a spilled window is active
|
| 187 |
+
(`window_cells > 0` && populated `k_cpu_per_stream`), so the server disables `ctx_shift` +
|
| 188 |
+
cache-reuse at init and **bounds the request at `n_ctx`** instead of crashing. Fully-resident
|
| 189 |
+
windows and non-window caches are byte-identical and still context-shift; iSWA/hybrid
|
| 190 |
+
wrappers propagate the leaf guard. The no-degradation alternative β a host-side rope-by-delta
|
| 191 |
+
pass over the CPU tail on every shift β is **deferred** (out of scope for the multi-session
|
| 192 |
+
prefix-shared serving target). PolyKV shared-prefix (`seq_cp`/`seq_add`, does NOT set
|
| 193 |
+
`is_fragmented`) composes with spill cleanly and needs no guard β validated in #638.
|
| 194 |
+
Patch `0115-rolling-kv-shift-guard-bug2148`.
|
docs/features/rys_layer_duplication.md
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# RYS runtime layer duplication (weight-shared "Repeat-Your-Self")
|
| 2 |
+
|
| 3 |
+
> Status: **DESIGN + PROTOTYPE** (2026-07-12). Parser/expander prototyped and
|
| 4 |
+
> validated; edit surface mapped; MVP scoped. No source landed yet.
|
| 5 |
+
>
|
| 6 |
+
> Child of [MASTER_PLAN.md](../MASTER_PLAN.md). Additive soft-fork feature β
|
| 7 |
+
> new load-time flag, per-arch surgical hooks in the graph loop + KV alloc.
|
| 8 |
+
|
| 9 |
+
## 1. What this is
|
| 10 |
+
|
| 11 |
+
David Noel Ng's **RYS** ("Repeat Your Self", [rys-ii][rys]) shows that
|
| 12 |
+
re-running a contiguous block of *middle* transformer layers β **no weight
|
| 13 |
+
changes, no training** β measurably improves a model (the layers duplicated are
|
| 14 |
+
the ones operating in the format-agnostic "reasoning" band; encoding/decoding
|
| 15 |
+
boundary layers must not be touched). For Qwen3.5-27B the Pareto-optimal blocks
|
| 16 |
+
are tiny: `(33,34)` (+1 layer) already captures most of the EQ gain, up through
|
| 17 |
+
`(26,34)` (+8) for the best absolute score.
|
| 18 |
+
|
| 19 |
+
The naive way to ship a RYS variant is a **physically merged GGUF** (mergekit
|
| 20 |
+
passthrough) β but that *doubles the duplicated layers' weight bytes* on disk
|
| 21 |
+
and in VRAM. dnhkng is instead working with TurboDerp on **pointer-based**
|
| 22 |
+
duplication in ExLlamaV3: the repeated layers **share the weight tensors** with
|
| 23 |
+
their originals, so *"no additional VRAM is consumed for the parameters
|
| 24 |
+
themselves β you only pay extra for the compute time and KV cache of the
|
| 25 |
+
additional forward passes."*
|
| 26 |
+
|
| 27 |
+
**This feature ports that pointer-shared idea to opencoti llamafile, driven
|
| 28 |
+
purely by a load-time flag.** No new GGUF, no re-quant, quant-agnostic:
|
| 29 |
+
|
| 30 |
+
```
|
| 31 |
+
llamafile --server -m Qwen3.5-27B-*.gguf --repeat-layers 26,34 # RYS-XL, +8 layers
|
| 32 |
+
llamafile --server -m Qwen3.5-27B-*.gguf --repeat-layers 33,34 # RYS-S, +1 layer
|
| 33 |
+
```
|
| 34 |
+
|
| 35 |
+
Stock llama.cpp has **no** runtime layer-repeat; this is a genuine opencoti
|
| 36 |
+
differentiator and fits our "additive, load-time" ethos.
|
| 37 |
+
|
| 38 |
+
## 2. Mechanism: a layer execution plan (`eff β src`)
|
| 39 |
+
|
| 40 |
+
llama.cpp's per-architecture graph builder runs a residual loop
|
| 41 |
+
`for (il = 0; il < n_layer; ++il)` that (a) reads weights from
|
| 42 |
+
`model.layers[il]` and (b) uses `il` to index the KV cache, RoPE, and per-layer
|
| 43 |
+
hparams. We replace the identity iteration with a **plan**: a vector of *source*
|
| 44 |
+
layer indices to execute in order.
|
| 45 |
+
|
| 46 |
+
- **`layer_plan[eff] = src`** β for each *effective* position `eff`, which
|
| 47 |
+
source layer's weights to run. Default (no flag) = identity `[0,1,β¦,n_layer-1]`.
|
| 48 |
+
- A RYS block `(i,j)` (half-open, matching the article) inserts source indices
|
| 49 |
+
`iβ¦j-1` a second time **right after** their first pass.
|
| 50 |
+
- **`n_layer_eff = layer_plan.size()`** β the *effective* layer count. This is
|
| 51 |
+
what the KV cache, RoPE loop, and residual loop use. The GGUF's `n_layer`
|
| 52 |
+
(the *source* weight count) is unchanged β weight loading is untouched.
|
| 53 |
+
|
| 54 |
+
Forward loop, conceptually:
|
| 55 |
+
|
| 56 |
+
```cpp
|
| 57 |
+
for (int eff = 0; eff < n_layer_eff; ++eff) {
|
| 58 |
+
const int src = layer_plan[eff];
|
| 59 |
+
const auto & layer = model.layers[src]; // WEIGHTS: shared by pointer, zero extra VRAM
|
| 60 |
+
cur = build_norm(inpL, layer.attn_norm, β¦, eff);
|
| 61 |
+
cur = build_layer_attn(inp, cur, inp_pos, β¦, /*kv_il=*/eff); // KV/cache index: eff (distinct slot)
|
| 62 |
+
β¦
|
| 63 |
+
inpL = cur;
|
| 64 |
+
}
|
| 65 |
+
```
|
| 66 |
+
|
| 67 |
+
The duplicated pass reads the *same* weights but writes its *own* K/V at the
|
| 68 |
+
same token positions β exactly the RYS semantics.
|
| 69 |
+
|
| 70 |
+
### 2a. Prototype (validated)
|
| 71 |
+
|
| 72 |
+
The plan expander is prototyped and unit-tested against every config in the
|
| 73 |
+
article's Qwen3.5-27B Pareto table (reference:
|
| 74 |
+
`scratchpad/rys_plan.py`; the C++ will mirror it):
|
| 75 |
+
|
| 76 |
+
| spec | blocks | `n_eff` (from 64) | extra | article |
|
| 77 |
+
|------------|--------------|-------------------|-------|---------|
|
| 78 |
+
| `33,34` | (33,34) | 65 | +1 | +1 β |
|
| 79 |
+
| `31,34` | (31,34) | 67 | +3 | +3 β |
|
| 80 |
+
| `30,35` | (30,35) | 69 | +5 | +5 β |
|
| 81 |
+
| `26,34` | (26,34) | 72 | +8 | +8 β |
|
| 82 |
+
| `24,35` | (24,35) | 75 | +11 | +11 β |
|
| 83 |
+
| `29,34` | (29,34) | 69 | +5 | +5 β |
|
| 84 |
+
|
| 85 |
+
`(26,34)` expands to `β¦,24,25,[26,27,28,29,30,31,32,33],[26,27,28,29,30,31,32,33],34,35,β¦`
|
| 86 |
+
β the seam is the duplicated `26..33`. Overlapping blocks are rejected in the
|
| 87 |
+
MVP (disjoint-ascending only; the Pareto winners are all single blocks).
|
| 88 |
+
|
| 89 |
+
## 3. Parameter grammar
|
| 90 |
+
|
| 91 |
+
```
|
| 92 |
+
--repeat-layers <spec>
|
| 93 |
+
```
|
| 94 |
+
- `<spec>` = one or more blocks separated by `;`.
|
| 95 |
+
- Each block is `i,j` **or** `i-j` = the half-open range `[i,j)` of **source**
|
| 96 |
+
transformer-layer indices to duplicate in place. `i,j` matches the article's
|
| 97 |
+
`(i,j)` notation for copy-paste from the HF configs.
|
| 98 |
+
- Examples: `33,34` Β· `26,34` Β· `43,45;28,34` (compose two disjoint blocks).
|
| 99 |
+
- Validation (fail-fast at load): `0 β€ i < j β€ n_transformer_layers`; blocks
|
| 100 |
+
disjoint; the MTP/`nextn_predict_layers` head is **never** duplicable.
|
| 101 |
+
|
| 102 |
+
Also plumbed through the opencoti TS adapter (`buildServerArgs`) as
|
| 103 |
+
`repeatLayers?: string` so opencode configs can set it.
|
| 104 |
+
|
| 105 |
+
## 4. Memory / cost model
|
| 106 |
+
|
| 107 |
+
- **Params: zero extra VRAM** β duplicated layers point at the same
|
| 108 |
+
`model.layers[src]` tensors.
|
| 109 |
+
- **KV cache: grows by the extra passes** β `n_layer_eff/n_layer`. E.g. `(26,34)`
|
| 110 |
+
on a 64-layer model = +8 β **+12.5%** KV. Modest; the article calls it out
|
| 111 |
+
explicitly. (This is why RYS-S `(33,34)`, +1.5% KV, is the low-overhead pick.)
|
| 112 |
+
- **Compute: +one forward pass per duplicated layer** per token β same ratio as
|
| 113 |
+
KV. The win is quality-per-token, paid in tokens/s.
|
| 114 |
+
|
| 115 |
+
## 5. What is provably unaffected
|
| 116 |
+
|
| 117 |
+
- **Weight loading** β untouched; `n_layer` (source) drives `create_tensor`.
|
| 118 |
+
- **RoPE / positions** β positions come from the batch, not the layer. A source
|
| 119 |
+
layer run at two `eff` positions sees the same token positions; only its KV
|
| 120 |
+
slot differs. No self-extend-style position hacking.
|
| 121 |
+
- **Off (no flag)** β `layer_plan` = identity β byte-identical to stock. This is
|
| 122 |
+
the primary regression gate.
|
| 123 |
+
|
| 124 |
+
## 6. Composition with opencoti per-layer features
|
| 125 |
+
|
| 126 |
+
Everything opencoti indexes by layer must consume the **effective** index (or map
|
| 127 |
+
`effβsrc` for source-keyed attributes). Scoped incrementally:
|
| 128 |
+
|
| 129 |
+
| feature | keyed by | MVP posture |
|
| 130 |
+
|---|---|---|
|
| 131 |
+
| plain f16 / scalar-quant KV | `eff` slot | **MVP** β works directly |
|
| 132 |
+
| RoPE / positions | batch pos | unaffected |
|
| 133 |
+
| iSWA (Gemma sliding/global per layer) | **src** (`is_swa(il)`) | Gemma phase β map `effβsrc` |
|
| 134 |
+
| DCA (all-KV) | per-layer | later β gate off under `--repeat-layers` first, then make plan-aware |
|
| 135 |
+
| rolling-KV (`window_cells[il]`) | `eff` budget | later β size by `n_layer_eff` |
|
| 136 |
+
| PolyKV / sparse-attn | per-layer | later |
|
| 137 |
+
| MTP / NextN head | excluded | head never duplicated; spec path unchanged |
|
| 138 |
+
|
| 139 |
+
**Survey nuance (important):** DCA, rolling-KV (`window_cells[il]`), PolyKV, and
|
| 140 |
+
sparse-attn are all keyed through `map_layer_ids` / per-**physical**-layer
|
| 141 |
+
structs β so they **auto-inherit eff-correctness** the moment the `:927` alloc
|
| 142 |
+
loop (Β§8b#4) is eff-length. The residual risk is *not* the accessors; it's the
|
| 143 |
+
**sizing loops that still say `hparams.n_layer`** and would under-allocate for
|
| 144 |
+
eff slots: `n_layer_kv()`, `opencoti_compute_resident_window_cells`
|
| 145 |
+
(`kv-cache:249/:772`), and the sparse block-sel reserve `8*n_layer_kv`
|
| 146 |
+
(`kv-cache:879`). Plus DCA's `is_swa(il)` in `gemma4:217` needs `src`.
|
| 147 |
+
|
| 148 |
+
**MVP rule:** ship `--repeat-layers` composing only with plain/quant KV on the
|
| 149 |
+
dense arch first; **hard-error at load** if combined with DCA / rolling-KV /
|
| 150 |
+
sparse / MTP until each sizing loop is made eff-aware (explicit follow-ups). This
|
| 151 |
+
prevents silent under-allocation, which would corrupt rather than error.
|
| 152 |
+
|
| 153 |
+
## 7. Per-architecture scope
|
| 154 |
+
|
| 155 |
+
### Phase 1 β Qwen3.5 / Qwen3.6 **dense** (MVP target)
|
| 156 |
+
Uniform layers (no iSWA, uniform head counts/rope) β `effβsrc` is trivial for
|
| 157 |
+
everything except KV-slot allocation. `nextn_predict_layers` (MTP head) excluded
|
| 158 |
+
from the plan. Target models: Qwen3.5-27B dense, Qwen3.6 dense variants.
|
| 159 |
+
Forward loop: `models/qwen35.cpp` (also covers the dense path), `models/qwen3.cpp`.
|
| 160 |
+
|
| 161 |
+
### Phase 2 β Gemma-4 **dense** 12B and 31B
|
| 162 |
+
Adds the iSWA wrinkle: alternating sliding-window / global layers, per-layer
|
| 163 |
+
query scaling / attn logit-softcap. A duplicated layer must inherit its **source**
|
| 164 |
+
layer's SWA type and scaling β per-layer hparam lookups map `effβsrc`. The KV
|
| 165 |
+
cache's SWA/global split (`kv_local_layer`, the iSWA cache) must size by
|
| 166 |
+
`n_layer_eff` with the src-derived SWA pattern. Forward loop: `models/gemma4.cpp`.
|
| 167 |
+
Targets: `gemma-4-12B-it` dense, `gemma-4-31B-it` dense.
|
| 168 |
+
|
| 169 |
+
(Gemma-4 A4B is MoE β out of scope for the dense MVP; RYS on MoE is a separate
|
| 170 |
+
question about whether per-expert routing survives block re-traversal.)
|
| 171 |
+
|
| 172 |
+
## 8. Edit surface
|
| 173 |
+
|
| 174 |
+
> Line-anchored from the source survey (2026-07-12). All paths under
|
| 175 |
+
> `vendors/sources/llamafile/llama.cpp/`.
|
| 176 |
+
|
| 177 |
+
**Governing finding:** in this fork the `il` handed to `build_attn(...)` is used
|
| 178 |
+
for BOTH weight lookup (`model.layers[il]` in the model `.cpp`) AND KV-slot
|
| 179 |
+
lookup (`map_layer_ids[il]`). RYS **decouples** them: **weights + architecture
|
| 180 |
+
props (`is_swa`, rope-freq, `n_head`, `n_rot`) index by `src`; the KV-slot index
|
| 181 |
+
handed to `build_attn` is `eff`.** For Qwen **dense** this is a no-op distinction
|
| 182 |
+
outside KV (uniform layers, `is_swa` always false), so `build_attn(eff)` is
|
| 183 |
+
clean. For Gemma the iSWA sub-cache choice must be `src`-driven (see Β§8b).
|
| 184 |
+
|
| 185 |
+
### 8a. C-ABI plumbing caveat (load-bearing)
|
| 186 |
+
`struct llama_context_params` (`include/llama.h:336`) crosses the **C ABI** β it
|
| 187 |
+
cannot carry a `std::vector`. So:
|
| 188 |
+
- `include/llama.h:336` β add a **compact int spec** (`uint32_t repeat_start,
|
| 189 |
+
repeat_end, repeat_count`, or a small CSV string).
|
| 190 |
+
- `src/llama-cparams.h:9` β add `std::vector<int> layer_plan;` to `llama_cparams`
|
| 191 |
+
(plain C++ struct, vector OK). Insert near the opencoti extension fields (69-80).
|
| 192 |
+
- `src/llama-context.cpp:~133` β **expand** the compact spec into
|
| 193 |
+
`cparams.layer_plan` here (where `hparams.n_layer` is known); default unset =
|
| 194 |
+
identity `0..n_layer-1`; set `n_layer_eff = layer_plan.size()`.
|
| 195 |
+
|
| 196 |
+
### 8b. Minimal MVP edit set β Qwen dense (plain KV)
|
| 197 |
+
| # | site | change | invasiveness |
|
| 198 |
+
|---|------|--------|--------------|
|
| 199 |
+
| 1 | `llama-cparams.h:9` | add `layer_plan` vector | trivial |
|
| 200 |
+
| 2 | `include/llama.h:336` | compact int spec (C-ABI) | trivial |
|
| 201 |
+
| 3 | `llama-context.cpp:~133` | expand spec β `layer_plan`; `n_layer_eff` | trivial |
|
| 202 |
+
| 4 | `llama-kv-cache.cpp:927` (+ sizing `:165/:278/:451/:478`) | iterate `n_layer_eff`; size each eff slot from `src=plan[eff]` (`n_embd_k_gqa(src)`, `has_kv(src)`); `map_layer_ids[eff]=layers.size()` | moderate |
|
| 203 |
+
| 5 | `models/qwen3.cpp:76` (Β·qwen2.cpp:76Β·qwen3moe.cpp:86) | loop over `eff`; `src=plan[eff]`; `model.layers[il]β[src]`; `build_attn(β¦,eff)`; out-ids guard `eff==n_layer_eff-1` | moderate |
|
| 204 |
+
| 6 | `common/arg.cpp:~2780` | `add_opt("--repeat-layers")` (mirror `--override-kv` CSV) | trivial |
|
| 205 |
+
| 7 | `common/common.cpp:~1660` | map `common_params` range β `llama_context_params` spec | trivial |
|
| 206 |
+
|
| 207 |
+
`qwen35.cpp:169` β the plan is built over `[0, n_transformer_layers)` where
|
| 208 |
+
`n_transformer_layers = n_layer - nextn_predict_layers` (`:167`), so the **MTP
|
| 209 |
+
head is already excluded** by construction; never duplicate a `nextn` block.
|
| 210 |
+
|
| 211 |
+
**KV precedent:** `llama-kv-cache.cpp` already tolerates a non-identity
|
| 212 |
+
`map_layer_ids` β the `il_share` path (`:946-963`, bug-858 dual-ctx MTP) and
|
| 213 |
+
`il_reuse` (`:1265-1283`, SWA reuse). Both *share* physical slots; RYS is the
|
| 214 |
+
inverse (allocate **more, distinct** slots), so no new indirection type is
|
| 215 |
+
needed β just an **eff-length domain** on the `:927` alloc loop. The 30+ read
|
| 216 |
+
accessors (`get_k`/`cpy_k` via `mli_at_checked(map_layer_ids, il, β¦)`) are
|
| 217 |
+
unchanged as long as the graph passes `eff` and every `eff` has a map entry.
|
| 218 |
+
|
| 219 |
+
### 8c. Additional edits β Gemma-4 dense (iSWA), the invasive part
|
| 220 |
+
| site | change |
|
| 221 |
+
|------|--------|
|
| 222 |
+
| `models/gemma4.cpp:194` | loop rewrite splitting `src`/`eff`: `is_swa(src)`, `get_rope_freq_base/scale(cparams,src)` (`:201-202`), `n_rot(src)`, `layers[src].rope_freqs`, `f_attention_scale` by `src`; feed `eff` only to `build_attn` (`:269-283`) + KV routing |
|
| 223 |
+
| `gemma4.cpp:164` iSWA dual-cache | `build_attn_inp_kv_iswa()` + kv-cache filter/reuse must assign each **eff** slot to base-vs-swa sub-cache by `is_swa(plan[eff])` β pass a `plan`-aware filter into the iSWA cache ctor (the invasive step: it currently reads `swa_layers[il]`) |
|
| 224 |
+
| `llama-model.cpp:1929/1933/1937` | `get_rope_freq_base/scale/factors` only ever called with `src` (they branch on `is_swa`) |
|
| 225 |
+
|
| 226 |
+
### 8d. hparams helpers (index by `src`)
|
| 227 |
+
`llama-hparams.cpp`: `is_swa(il)` `:208`, `n_head(il)` `:30`, `n_head_kv(il)`
|
| 228 |
+
`:38`, `n_rot(il)` `:65`, `n_embd_k_gqa/v_gqa(il)` `:103/:109`, `has_kv(il)`
|
| 229 |
+
`:231`, `n_layer_kv()` `:250`, `swa_layers[]` (`hparams.h:141`). No signature
|
| 230 |
+
changes β the rule is "call these with `src` in the loop." RoPE positions come
|
| 231 |
+
from the batch (`llama-graph.cpp:161`), not the layer, so a `src` layer run at
|
| 232 |
+
two `eff` positions is correct with no position hacking.
|
| 233 |
+
|
| 234 |
+
## 9. Validation plan
|
| 235 |
+
|
| 236 |
+
Correctness is *mechanism* correctness (the plan re-traverses exactly the
|
| 237 |
+
intended layers) β RYS quality is the user's block choice, not ours to prove.
|
| 238 |
+
|
| 239 |
+
1. **Off = identity** β `--repeat-layers` absent β logits byte-identical to
|
| 240 |
+
stock (teacher-forced, real_frac=0). Primary regression gate.
|
| 241 |
+
2. **Plan structural** β boot log dumps `n_layer_eff` + the `effβsrc` plan;
|
| 242 |
+
assert it matches the spec (e.g. `(26,34)` β 72 layers, seam at 34/42).
|
| 243 |
+
3. **Deterministic divergence** β `(33,34)` vs base produces a *deterministic,
|
| 244 |
+
non-degenerate* logit change (greedy, same GGUF) β proves the duplicated pass
|
| 245 |
+
actually runs and feeds forward, not a no-op.
|
| 246 |
+
4. **KV integrity** β niah retrieval stays 100 at a short ctx with a duplicated
|
| 247 |
+
block (the extra KV slots are wired correctly; no cross-slot aliasing).
|
| 248 |
+
5. **Known-win spot check** β reproduce the article's *direction*: a small EQ/math
|
| 249 |
+
probe should move in the reported direction for `(33,34)` / `(26,34)` on
|
| 250 |
+
Qwen3.5-27B (sanity that we duplicated the reasoning band, not garbage).
|
| 251 |
+
6. **Gemma iSWA** β the src-derived SWA pattern is honored (duplicated global
|
| 252 |
+
layer stays global); niah 100 + coherent decode.
|
| 253 |
+
|
| 254 |
+
Correctness always via logit-equiv / niah β never greedy-needle-as-proof.
|
| 255 |
+
|
| 256 |
+
## 10. Risks / caveats
|
| 257 |
+
|
| 258 |
+
- **KV growth is real** (Β§4) β surface it in the boot log; the TS adapter should
|
| 259 |
+
account for it in VRAM budgeting.
|
| 260 |
+
- **iSWA mapping** β feared to be the main Gemma risk, but **verified correct**
|
| 261 |
+
(see Β§10a): a middle SWA layer duplicates coherently, so the dual-cache effβsrc
|
| 262 |
+
wiring is sound. What actually breaks is *boundary* layers, independent of SWA.
|
| 263 |
+
- **MoE out of scope** for dense MVP β but the iSWA path was validated on **both**
|
| 264 |
+
dense (31B, 60 layers) and MoE (A4B, 26B-A4B-128e) with identical behaviour.
|
| 265 |
+
- **Upstream syncability** β new flag + per-arch loop hooks tagged
|
| 266 |
+
`opencoti-hook: rys-layer-dup` and registered in `UPSTREAM_SYNC.md`; the
|
| 267 |
+
`layer_plan` indirection is small and localized to keep future bumps cheap.
|
| 268 |
+
|
| 269 |
+
## 10a. Boundary-layer fragility (bug-2164) β a MODEL property, not an engine bug
|
| 270 |
+
|
| 271 |
+
Duplicating the **first or last** transformer layers reliably produces incoherent
|
| 272 |
+
output. This is the well-documented franken-merge / passthrough self-merge
|
| 273 |
+
fragility (mergekit `passthrough`, SOLAR depth-upscaling all DROP the first/last
|
| 274 |
+
*m* layers), NOT an engine defect. The engine duplicates *any* layer faithfully.
|
| 275 |
+
|
| 276 |
+
**Decisive 2Γ2** (2026-07-13, native `google_gemma-4-26B-A4B-it-Q4_K_M`, 3090,
|
| 277 |
+
CHAT endpoint β the raw `/completion` greedy path is INVALID on this thinking
|
| 278 |
+
model: its answer lands in `reasoning_content`, so even RYS-off looks like garbage
|
| 279 |
+
there). Garbage is determined **entirely by boundary-ness, independent of
|
| 280 |
+
SWA/global**:
|
| 281 |
+
|
| 282 |
+
| | SWA layer | GLOBAL layer |
|
| 283 |
+
|---|---|---|
|
| 284 |
+
| **middle** (L4, L10 / L5, L17) | coherent β | coherent β |
|
| 285 |
+
| **boundary** (L0, L28 / L29) | garbage β | garbage β |
|
| 286 |
+
|
| 287 |
+
- Gemma-4 A4B coherent band β **L4..L24**; L0βL3 (early) and L25βL29 (late) break.
|
| 288 |
+
- Qwen3-8B has a wider tolerant band β only its **last** layer (L35) breaks.
|
| 289 |
+
- Every attention output + final logit stays **finite** (no NaN) even under the
|
| 290 |
+
garbage config β finite-but-wrong = model-level, not a numeric/cache corruption.
|
| 291 |
+
|
| 292 |
+
**Guidance: duplicate MIDDLE layers.** The engine emits a boot-time **advisory
|
| 293 |
+
WARNING** (`llama-context.cpp`, RYS engage block) whenever a plan duplicates a
|
| 294 |
+
layer within the first/last `max(3, n_transformer/8)`; it warns but does not block
|
| 295 |
+
(the exact usable band is model-dependent). Correctness gate = coherence smoke via
|
| 296 |
+
the CHAT endpoint (RYS intentionally changes the model, so logit-equivalence vs
|
| 297 |
+
baseline is the wrong bar). Historical note: an earlier pass mis-labelled this an
|
| 298 |
+
"SWA sub-cache engine bug" by testing only L0 (which is *both* SWA *and* first) on
|
| 299 |
+
the noisy raw-greedy path β see [[project_rys_swa_dup_garbage]].
|
| 300 |
+
|
| 301 |
+
## 10b. RYS Γ MTP self-spec β the draft context must NOT inherit the plan (bug-2171, 0124)
|
| 302 |
+
|
| 303 |
+
RYS is a **target-only** capability. With RYS active *and* self-speculation on
|
| 304 |
+
(`--spec-type draft-mtp`, Qwen NextN or the gemma4 assistant), the MTP draft
|
| 305 |
+
`llama_context` is built from `common_context_params_to_llama(params_base)`, so it
|
| 306 |
+
inherited the target's `params.repeat_layers` β and its `cparams.layer_plan` got
|
| 307 |
+
RYS-duplicated too. But the NextN head lives at the canonical `il = n_transformer`
|
| 308 |
+
(`n_layer β nextn_predict_layers`), which the RYS effβsrc map never contains, so
|
| 309 |
+
draft-context init aborts with `cpy_k: map_layer_ids MISS il=n_transformer` β
|
| 310 |
+
`failed to create MTP context`. Fix (`0124`, `src/llama-context.cpp`): the RYS
|
| 311 |
+
plan-expansion block gates the spec on context type β
|
| 312 |
+
`rys_spec = (cparams.ctx_type == LLAMA_CONTEXT_TYPE_MTP) ? nullptr : params.repeat_layers`
|
| 313 |
+
(mirrored in `sched_reserve()`), so the **draft runs the plain base stack** while the
|
| 314 |
+
**target keeps RYS**. This is **lossless**: greedy self-spec verifies every drafted
|
| 315 |
+
token against the target, so the drafter's stack depth only moves acceptance/tps.
|
| 316 |
+
In self-spec the NextN head consumes the target's *already-RYS* hidden state via
|
| 317 |
+
shared memory β running the drafter plain is the intended composition, not a
|
| 318 |
+
limitation.
|
| 319 |
+
|
| 320 |
+
## 11. References
|
| 321 |
+
|
| 322 |
+
- [dnhkng, *LLM Neuroanatomy II*][rys] β the RYS-II study, Pareto table, pointer-based note ("The Models").
|
| 323 |
+
- [dnhkng/RYS on GitHub](https://github.com/dnhkng/RYS) β scanner, probes, model-builder config grammar.
|
| 324 |
+
- mergekit `passthrough` β the physical-merge convention `(i,j)` mirrors.
|
| 325 |
+
|
| 326 |
+
[rys]: https://dnhkng.github.io/posts/rys-ii/
|
docs/features/rys_probe.md
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# RYS template probe (`--rys-probe`) β design + implementation
|
| 2 |
+
|
| 3 |
+
Status: **IMPLEMENTED (v1.1 = ΞPPL + graded task-probe β 2 templates + external
|
| 4 |
+
eval hook), host-only, validated on Qwen3-4B** (2026-07-13, #669/#670). Not yet
|
| 5 |
+
captured as a patch / committed β ships on request. Companion to the shipped RYS
|
| 6 |
+
execution engine,
|
| 7 |
+
[rys_layer_duplication.md](rys_layer_duplication.md) (`--repeat-layers`, patch 0122).
|
| 8 |
+
|
| 9 |
+
## 0. What shipped (v1.1)
|
| 10 |
+
|
| 11 |
+
A new `PROBE` program mode in `llamafile/rys_probe.cpp` (dispatched from
|
| 12 |
+
`llamafile/main.cpp`; flag detected in `llamafile/args.cpp`). It:
|
| 13 |
+
|
| 14 |
+
1. Parses args as the `PERPLEXITY` example, then **disables `-fit`** (the probe
|
| 15 |
+
sizes its own small `n_ctx`; the fit path builds a trial context that we don't
|
| 16 |
+
want mutating our cparams β it aborted the flash-attn print otherwise).
|
| 17 |
+
2. Loads the model **once** (`common_init_from_params(params, /*model_only*/true)`).
|
| 18 |
+
3. Enumerates contiguous mid-stack blocks `[i,j)` in the band (default
|
| 19 |
+
`[max(3,n/8) .. n-max(3,n/8))`) Γ `--rys-probe-widths`. Default widths =
|
| 20 |
+
**`auto`** = every width `1 .. (band)` (an explicit `W1,W2,β¦` list overrides).
|
| 21 |
+
4. Scores **base first** (empty plan = reference), then each candidate by
|
| 22 |
+
rebuilding **only the context** (`llama_init_from_model` with
|
| 23 |
+
`cp.repeat_layers = "i,j"`; the model/weights are plan-independent), computing
|
| 24 |
+
a compact self-contained **ΞPPL** over a small corpus (`-f`/`-p`, else a
|
| 25 |
+
built-in slice) **+ a graded built-in task-probe** (24 arithmetic / word-problem
|
| 26 |
+
/ factual / sequence items, greedy decode with substring-match early-stop; the
|
| 27 |
+
fraction correct is the `task` score, used both as a rank signal and a guardrail
|
| 28 |
+
β a candidate that drops below base `task` is discarded).
|
| 29 |
+
5. Prints a ranked table of the surviving wins, then **exactly two ready-to-paste
|
| 30 |
+
templates**:
|
| 31 |
+
- **MOST EFFICIENT** β best PPL-drop *per added layer* (largest `ΞPPL/(#dup layers)`).
|
| 32 |
+
- **MAX GAIN** β largest absolute PPL-drop / lowest PPL.
|
| 33 |
+
When no wider block beats the efficient one, the two coincide and the probe
|
| 34 |
+
says so honestly ("the same block is both").
|
| 35 |
+
|
| 36 |
+
**Flags:** `--rys-probe` (enter probe mode) Β· `--rys-probe-widths auto|W1,W2,β¦`
|
| 37 |
+
(default `auto`) Β· `--rys-probe-band auto|i:j` Β· `--rys-probe-topk N` (default 10,
|
| 38 |
+
rows in the table). Corpus comes from the standard `-f`/`-p`; `-ngl`, `-c`, `-fa`
|
| 39 |
+
flow through.
|
| 40 |
+
|
| 41 |
+
**The last mile β external eval `(c)`.** ΞPPL + the built-in task-probe are cheap
|
| 42 |
+
*proxies*, not a verdict. The probe's two templates feed
|
| 43 |
+
[`perf/llamafile/rys-probe-eval.sh`](../../perf/llamafile/rys-probe-eval.sh),
|
| 44 |
+
which boots `llama-server` for BASE + each template (only `--repeat-layers`
|
| 45 |
+
changes between arms) and runs a user-supplied `EVAL_CMD` (e.g. a GSM8K / MMLU
|
| 46 |
+
slice via the omk harness) against each, printing a real task-score comparison
|
| 47 |
+
with deltas-vs-base. That is the analogue of dnhkng's graded benchmark, minus the
|
| 48 |
+
days-long search β the final pick is a downstream score, not a proxy.
|
| 49 |
+
|
| 50 |
+
**Validation (Qwen3-4B-Q4_K_M, CPU):**
|
| 51 |
+
- band [8,16), widths {2,4}, 12 candidates: base ppl 30.15; wins `9,13`
|
| 52 |
+
(pplβ24.95, Ξβ5.20, 11.1% over) and `10,12` (pplβ27.38, Ξβ2.76, 5.6% over) β
|
| 53 |
+
MAX GAIN = `9,13`, MOST EFFICIENT = `10,12` (β1.38/layer), two distinct frontier
|
| 54 |
+
points.
|
| 55 |
+
- band [10,16), widths `auto` (all 1..6), 14 candidates: base ppl 30.15 / task
|
| 56 |
+
0.917; winner `11,13` (pplβ28.03, Ξβ2.12, 5.56% over, task 0.958) β sole survivor
|
| 57 |
+
wide enough, so MOST EFFICIENT == MAX GAIN (honest "same block is both").
|
| 58 |
+
|
| 59 |
+
Contiguous mid-stack winners, no boundary layers, consistent with Β§1's structural
|
| 60 |
+
prior. Build gotcha: a `common_params` struct **field addition** needs a **clean
|
| 61 |
+
host rebuild** (`rm -rf o` + make) β incremental `build:llamafile:make` left
|
| 62 |
+
ABI-skewed `common_params` consumers (bug-2160/2166 class); see BUILD_CYCLE.md. A
|
| 63 |
+
later default-**value** change to an existing field (e.g. widths default
|
| 64 |
+
`1,2,4`β`auto`) is ABI-safe and rebuilds incrementally.
|
| 65 |
+
|
| 66 |
+
Question (user, 2026-07-13): can opencoti-llamafile grow a flag that *probes* a
|
| 67 |
+
GGUF for good RYS layer-duplication templates, without reproducing dnhkng's
|
| 68 |
+
multi-**day** search?
|
| 69 |
+
|
| 70 |
+
Short answer: **yes, cheaply** β because we can replace the two expensive halves
|
| 71 |
+
of the reference method (a huge candidate space + a downstream-benchmark score)
|
| 72 |
+
with (a) a **pruned** candidate space we already know the shape of, and (b) a
|
| 73 |
+
**perplexity scan** the binary can already run. The result is a *shortlist
|
| 74 |
+
generator* (minutesβ~1 h), not a full Pareto search β the user still validates
|
| 75 |
+
the top-K on the real retrieval/benchmark harness.
|
| 76 |
+
|
| 77 |
+
## 1. What the reference method (dnhkng RYS-II) actually does, and why it's slow
|
| 78 |
+
|
| 79 |
+
From <https://dnhkng.github.io/posts/rys-ii/> (64-layer stack):
|
| 80 |
+
|
| 81 |
+
| Stage | Mechanism | Cost |
|
| 82 |
+
|---|---|---|
|
| 83 |
+
| Full scan | every contiguous `(i,j)` block, duplicate `[i,j)`, measure Ξ | many single-block evals |
|
| 84 |
+
| Beam search | width 24, depth 3β6, greedily stack blocks | **3,024 candidates, ~14 h** |
|
| 85 |
+
| Surrogate | XGBoost trained on **4,411 measured** configs ranks **~2,000,000** candidates | top **100** fully benchmarked |
|
| 86 |
+
| Validate | Pareto front (better *and* smaller) β 4 optimal configs | large val sets |
|
| 87 |
+
|
| 88 |
+
Two things make it days-long:
|
| 89 |
+
|
| 90 |
+
1. **The score is a downstream benchmark** β `Math120` (16β120 math Qs) + `EQ140`
|
| 91 |
+
(EQ-Bench scenarios). Every candidate needs generation + grading. Small
|
| 92 |
+
"probe" sets are used for the search, larger sets for finalists, but it is
|
| 93 |
+
still an accuracy eval per candidate.
|
| 94 |
+
2. **The space is ~2 M** contiguous+stacked blocks, so a surrogate (XGBoost on
|
| 95 |
+
4.4 k measured points) is needed just to *rank* it.
|
| 96 |
+
|
| 97 |
+
**Key structural finding we can exploit:** every Pareto-optimal template was a
|
| 98 |
+
**single contiguous mid-stack block**. The cheapest winner was `(33,34)`
|
| 99 |
+
(+1.56% params); the widest useful one `(26,34)` (+12.5%). Nothing near the
|
| 100 |
+
first/last layers ever won β which is exactly our own boundary-layer result
|
| 101 |
+
([rys_layer_duplication.md Β§10a](rys_layer_duplication.md), bug-2164: duplicating
|
| 102 |
+
early/late layers yields incoherent output).
|
| 103 |
+
|
| 104 |
+
## 2. What opencoti already has
|
| 105 |
+
|
| 106 |
+
- The **execution engine**: `--repeat-layers "i,j"` sets `cparams.layer_plan` and
|
| 107 |
+
re-runs `[i,j)` weight-shared, load-time, all archs (patch 0122). An
|
| 108 |
+
in-process probe just calls this in a loop with different plans.
|
| 109 |
+
- A **perplexity** path in the vendored `llama.cpp` (used throughout the TCQ/KV
|
| 110 |
+
campaigns) β a forward pass over a fixed corpus returning mean NLL.
|
| 111 |
+
- **KLD-vs-reference** tooling (logit-equiv harness).
|
| 112 |
+
- The **boundary-layer advisory** logic that already computes the safe mid-stack
|
| 113 |
+
band `[max(3,n/8) .. n-max(3,n/8))`.
|
| 114 |
+
|
| 115 |
+
So the only missing piece is an *outer loop + a scorer + a ranked report*.
|
| 116 |
+
|
| 117 |
+
## 3. Proposed design β a PPL-scan shortlist, not a Pareto search
|
| 118 |
+
|
| 119 |
+
### 3a. Score: perplexity on a small fixed corpus (NOT KLD-to-base)
|
| 120 |
+
|
| 121 |
+
- **Use ΞPPL vs the base model on a small held-out corpus.** RYS improves the
|
| 122 |
+
model by *changing* it, so a candidate that lowers PPL below base on a
|
| 123 |
+
quality-correlated corpus is the cheap analogue of "Math/EQ went up". One
|
| 124 |
+
forward pass over a few-k-token corpus per candidate β seconds to a couple of
|
| 125 |
+
minutes on a served GPU, no generation/grading.
|
| 126 |
+
- **KLD-to-base is the wrong optimization target here** and we must not use it as
|
| 127 |
+
the score: RYS *intentionally* diverges from base, so minimizing KLD rewards
|
| 128 |
+
doing nothing (this is the same trap as "logit-equiv vs base" for RYS β
|
| 129 |
+
[rys_layer_duplication.md](rys_layer_duplication.md)). KLD is still useful as a
|
| 130 |
+
**guardrail**: a candidate whose KLD explodes (or whose PPL blows up) is the
|
| 131 |
+
boundary-fragility / broken-merge regime β flag and drop it, don't rank it.
|
| 132 |
+
- Caveat, stated honestly: **PPL is a weak proxy for reasoning/EQ gains.** dnhkng
|
| 133 |
+
used task accuracy precisely because self-merge gains are small and PPL-noisy.
|
| 134 |
+
So the probe's job is to *shortlist*, and the last mile stays a real eval.
|
| 135 |
+
|
| 136 |
+
### 3b. Space: prune to contiguous mid-stack blocks (no surrogate needed)
|
| 137 |
+
|
| 138 |
+
Because winners are always contiguous mid-stack, enumerate only:
|
| 139 |
+
|
| 140 |
+
- contiguous blocks `[i,j)` fully inside the safe band `[max(3,n/8) .. n-max(3,n/8))`,
|
| 141 |
+
- with width `1 .. w_max` (default `w_max β ceil(n/8)` β the +12.5% ceiling),
|
| 142 |
+
- optionally strided starts to cap the count.
|
| 143 |
+
|
| 144 |
+
For a 64-layer model that is a few hundred candidates, not 2 M β so a **direct
|
| 145 |
+
scan needs no XGBoost surrogate at all.** (An optional depth-2 greedy stack, Γ la
|
| 146 |
+
their beam search, can be a follow-up once single-block ranking is trusted.)
|
| 147 |
+
|
| 148 |
+
### 3c. Surface: `--rys-probe`
|
| 149 |
+
|
| 150 |
+
```
|
| 151 |
+
llamafile --rys-probe \
|
| 152 |
+
-m model.gguf \
|
| 153 |
+
[--rys-probe-corpus calib.txt] # default: bundled ~4k-token slice
|
| 154 |
+
[--rys-probe-widths 1,2,4,8] # block widths to try
|
| 155 |
+
[--rys-probe-band auto|i:j] # candidate band (default = advisory band)
|
| 156 |
+
[--rys-probe-topk 10] # how many to print / emit
|
| 157 |
+
```
|
| 158 |
+
|
| 159 |
+
Load the model once; for each candidate plan set `layer_plan`, run PPL over the
|
| 160 |
+
corpus (base = empty plan, run first as the reference), record
|
| 161 |
+
`(block, overhead_pct, ppl, dppl_vs_base, kld_guardrail)`. Emit a table sorted by
|
| 162 |
+
ΞPPL and a **Pareto view** (ΞPPL vs overhead%) β the same "better *and* smaller"
|
| 163 |
+
lens dnhkng used, so the user picks a point on the frontier. Output the winner as
|
| 164 |
+
a ready-to-paste `--repeat-layers` spec.
|
| 165 |
+
|
| 166 |
+
Reuses the 0122 engine + the existing perplexity path; **host-only, no CUDA, no
|
| 167 |
+
new kernels.**
|
| 168 |
+
|
| 169 |
+
### 3d. Cost
|
| 170 |
+
|
| 171 |
+
`N_candidates Γ one small-corpus PPL forward`. A few hundred candidates Γ a
|
| 172 |
+
few-k-token PPL β **minutes to ~1 h on one GPU** for a mid-size model β vs days.
|
| 173 |
+
The saving is entirely from (1) pruning 2 M β hundreds (structural prior) and
|
| 174 |
+
(2) PPL instead of graded generation.
|
| 175 |
+
|
| 176 |
+
## 4. Honest limitations
|
| 177 |
+
|
| 178 |
+
- **Shortlist, not verdict.** PPL can rank a candidate above one that would win
|
| 179 |
+
on reasoning/EQ. Ship it as "top-K to validate", and keep the real
|
| 180 |
+
RULER/benchmark run as the decider β exactly how dnhkng re-measured finalists.
|
| 181 |
+
- **PPL validity on franken-merges is model-dependent.** For some Gemma-4
|
| 182 |
+
omnimerge GGUFs raw PPL is meaningless
|
| 183 |
+
([project note: PPL/KLD invalid on Gemma merges]); the probe must sanity-check
|
| 184 |
+
the base PPL is finite/reasonable and warn (or fall back to a tiny task-probe)
|
| 185 |
+
when it isn't.
|
| 186 |
+
- **Single-block first.** Stacked (beam) templates are a phase-2 extension; the
|
| 187 |
+
single-contiguous-block scan already covers every dnhkng Pareto winner.
|
| 188 |
+
|
| 189 |
+
## 5. Recommendation
|
| 190 |
+
|
| 191 |
+
Feasible and cheap. Build it as an **opt-in `--rys-probe` subcommand** that
|
| 192 |
+
(1) enumerates contiguous mid-stack blocks, (2) scores each by ΞPPL on a small
|
| 193 |
+
fixed corpus with a KLD/PPL-blowup guardrail, (3) prints a ΞPPL-vs-overhead
|
| 194 |
+
Pareto shortlist and the best `--repeat-layers` spec. It is a thin wrapper over
|
| 195 |
+
the already-shipped 0122 engine + the existing perplexity path β no kernels, no
|
| 196 |
+
CUDA. It does **not** reproduce dnhkng's rigor (no 2 M-candidate surrogate, no
|
| 197 |
+
graded Math/EQ); it deliberately trades that for a minutes-scale shortlist the
|
| 198 |
+
user finishes with one real eval pass.
|
| 199 |
+
|
| 200 |
+
Decision (user, 2026-07-13, #669/#670): shipped **(b) ΞPPL + a graded
|
| 201 |
+
arithmetic/short-reasoning task-probe**, output narrowed to **two templates**
|
| 202 |
+
(MOST EFFICIENT + MAX GAIN), plus a **(c) external-eval hook**
|
| 203 |
+
(`perf/llamafile/rys-probe-eval.sh`) that runs a real downstream eval per template
|
| 204 |
+
so the final pick is a task score, not a proxy. See Β§0.
|
docs/features/sparse_attn.md
ADDED
|
@@ -0,0 +1,453 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Sparse Attention β Quest-style block-selector for long-context decode (#551)
|
| 2 |
+
|
| 3 |
+
> Status: BUILT + CHARACTERIZED on the 3090 (2026-06-27); 1M validation on bs2
|
| 4 |
+
> pending. The real 1M **decode** lever β companion to the DCA + quant-KV
|
| 5 |
+
> **prefill** work (#444/#445/#554). Default-OFF, byte-identical when off. Develops
|
| 6 |
+
> on the 3090 (A4B / Qwen3-4B / 27B-Q4 @ β€256k); 1M validation on bs2.
|
| 7 |
+
>
|
| 8 |
+
> **Headline result (Β§S4 results):** the sparse/dense **decode** ratio is
|
| 9 |
+
> *context-driven, not batch-driven*. Single-stream (B1), Qwen3-4B, f16 KV, 25%
|
| 10 |
+
> block coverage on the 3090, the ratio climbs monotonically and the gap to dense
|
| 11 |
+
> closes 9.2% β 0.8% from 16K to the model's 40K ceiling (0.908 β 0.992). Crossover
|
| 12 |
+
> (>1.0, a real decode win) extrapolates just past 40K β the 256k/1M regime on bs2.
|
| 13 |
+
> Batch is *not* our lever (unlike TheTom β see Β§S4 results for why).
|
| 14 |
+
|
| 15 |
+
## Why this, and why now
|
| 16 |
+
|
| 17 |
+
The 1M serving path has two halves. **Prefill** is handled by DCA's analytical
|
| 18 |
+
band + quantized KV (q5_0/q4_0 β f16 KV at 1M is 206 GB, impossible), and #554
|
| 19 |
+
removed the whole-cache-lift prefill cliff. **Decode is still O(n_kv) per token**:
|
| 20 |
+
at 1M every generated token attends to ~1M KV entries, memory-bound and slow. The
|
| 21 |
+
only thing that breaks that is *sparse attention* β attend to a small, query-chosen
|
| 22 |
+
subset of KV blocks instead of all of them.
|
| 23 |
+
|
| 24 |
+
## Method: Quest-style min/max block bounds (decided 2026-06-25)
|
| 25 |
+
|
| 26 |
+
Reference scouted: **TheTom/turboquant_plus** `docs/papers/block-selector-sparse-attention.md`.
|
| 27 |
+
Decisive finding for us: that work is **Apple Silicon / MLX**, where TheTom found
|
| 28 |
+
custom sparse kernels with *real* K-skipping LOSE (14Γ at B=8) to the tuned
|
| 29 |
+
`sdpa_vector_2pass`, so he uses a float mask that still streams the full K/V (saves
|
| 30 |
+
compute, **not bandwidth**). He explicitly notes the literature wins (Quest, NSA,
|
| 31 |
+
SeerAttention, DuoAttention) "all target **CUDA, where FlashAttention's sparse kernel
|
| 32 |
+
is mature**." **We are CUDA.** Our FA-VEC already has the `KV_max`/`KV_min` tile-trim
|
| 33 |
+
(`fattn-vec.cuh:363`) that lets us skip **whole KV blocks before the QK dot** β real
|
| 34 |
+
K-read + QK + V savings β which TheTom's Metal stack could not. So we build the thing
|
| 35 |
+
he couldn't, using the cleaner CUDA ancestor:
|
| 36 |
+
|
| 37 |
+
**Quest** (Tang 2024). Parameter-free, exact upper-bound:
|
| 38 |
+
- **Per-(KV-head, block) min/max key bounds.** Block size `B_SEL` (start 64; tune).
|
| 39 |
+
Store `kmin[head_dim]`, `kmax[head_dim]` per block β a tiny side-cache
|
| 40 |
+
(`n_blocks Γ head_dim Γ 2 Γ f16`). For quant-KV, compute min/max from the f16
|
| 41 |
+
values **before** quantizing (composes with q5_0/q4_0).
|
| 42 |
+
- **Per-query upper bound.** For query `q`, the max possible `qΒ·k` over a block is,
|
| 43 |
+
channel-wise, `Ξ£_d max(q_dΒ·kmin_d, q_dΒ·kmax_d)` (use kmax where q_d>0, kmin where
|
| 44 |
+
q_d<0). O(n_blocksΒ·head_dim) β negligible vs O(n_kvΒ·head_dim) dense attention.
|
| 45 |
+
- **Top-K block selection.** Keep the top-`K_SEL` blocks by upper bound **PLUS** the
|
| 46 |
+
recent window + attention-sink block (streaming-LLM safety so recency/retrieval is
|
| 47 |
+
never silently dropped). `K_SEL` and window are knobs; tune on RULER-niah.
|
| 48 |
+
- **Kernel skip.** Extend the FA-VEC tile loop to `continue` past unselected blocks β
|
| 49 |
+
the real K+QK+V skip.
|
| 50 |
+
|
| 51 |
+
Budget policy: **fixed top-K + recent/sink** (decided). TheTom found adaptive top-K
|
| 52 |
+
"never beat union top-K" and converged loosely at long ctx; fixed-K is predictable in
|
| 53 |
+
VRAM/compute and easy to gate.
|
| 54 |
+
|
| 55 |
+
## Compose (three sparsity levers, three pipeline stages)
|
| 56 |
+
|
| 57 |
+
1. **Block-selector (this work)** β skip K blocks *before* QK (saves K-read + QK + V).
|
| 58 |
+
2. **DCA analytical band** β the selector narrows *within* each INTRA/SUCC/INTER band.
|
| 59 |
+
3. **sparse-V (#546)** β within selected blocks, skip V dequant for negligible-weight
|
| 60 |
+
positions (the residual, post-softmax).
|
| 61 |
+
Plus **quant-KV** (min/max from dequant K at write). The three are independent gates at
|
| 62 |
+
the block / band / position granularities.
|
| 63 |
+
|
| 64 |
+
## Where it lives (insertion points, from the Explore map)
|
| 65 |
+
|
| 66 |
+
- **Write path (S1):** per-block min/max side-cache, filled as K is written
|
| 67 |
+
(KV-cache write in `llama-graph.cpp` / the cpy path). Additive tensor; off β not allocated.
|
| 68 |
+
- **Selector (S2):** host/graph pre-pass computing the per-query block mask from the
|
| 69 |
+
bounds; carried to the kernel by **extending the existing `KV_max`/`KV_min`
|
| 70 |
+
mechanism** (`fattn-common.cuh:27-28`, `fattn-vec.cuh:352-353`) with a block-select
|
| 71 |
+
bitmask/priority.
|
| 72 |
+
- **Kernel (S3):** block-skip `continue` in the FA-VEC tile loop at `fattn-vec.cuh:363`
|
| 73 |
+
(the long-ctx **decode** hot path is FA-VEC, not MMA).
|
| 74 |
+
|
| 75 |
+
## S1 integration anchors (mapped 2026-06-25, read-only)
|
| 76 |
+
|
| 77 |
+
All file:line in `vendors/sources/llamafile/llama.cpp/`:
|
| 78 |
+
- **K write (scatter):** `src/llama-graph.cpp:2771` β `mctx_cur->cpy_k(ctx0, k_cur, k_idxs, il)` (impl in `llama-kv-cache.cpp`). The per-block min/max must be derived from `k_cur` here (pre-quant) or right after, per write.
|
| 79 |
+
- **KV tensor alloc (add the side-tensor here):** `src/llama-kv-cache.cpp:637` (GPU `ggml_new_tensor_3d(ctx_s, type_k, n_embd_k_gpu, wc, 1)`) + `:686` (host); stored in the `layers` vector (`llama_kv_cache_layer`, pushed `:738`). Add a parallel per-(KV-head, block) `kmin/kmax` tensor alongside `k_l`.
|
| 80 |
+
- **Host-fill model:** `set_input_dca` `src/llama-kv-cache.cpp:3899-4040` (dispatch `:5250`) β the idiom for filling a per-forward input tensor (direct `tensor->data` writes / `ggml_backend_tensor_set`); mirror for the selector's per-block mask in S2.
|
| 81 |
+
- **Flag plumbing (mirror `--dca` for `--sparse-attn*`):** `common/arg.cpp:1516,1528` (add_opt) Β· `src/llama-cparams.h:56-57` (cparams fields) Β· `common/common.cpp:1649-1650` (paramsβcparams) Β· `tools/server/server-context.cpp:1102` (server gate).
|
| 82 |
+
- **Sizing accessors:** `llama-hparams.h:268` `n_head_kv(il)` Β· `:283` `n_embd_head_k(il)` Β· `cparams.n_ctx` (graph ctor `llama-graph.cpp:1009`).
|
| 83 |
+
- **S1 sequence:** vendor-backup WHOLE tree FIRST β add `--sparse-attn{,-topk,-block-size}` flags (default off) β alloc the kmin/kmax side-tensor (only when enabled) β fill it at/after the `cpy_k` write β build host + verify default-off path is byte-identical (no new tensor allocated, no graph change). Selector + kernel skip are S2/S3.
|
| 84 |
+
|
| 85 |
+
## Correctness gate (NEVER greedy needle β bug-263/270/354)
|
| 86 |
+
|
| 87 |
+
- **RULER-niah @256k** on the 3090 (A4B + Qwen3-4B): the needle's block MUST survive
|
| 88 |
+
selection β niah recall is the primary signal that block-selection didn't drop the
|
| 89 |
+
answer.
|
| 90 |
+
- **Logit-equivalence vs dense** (`real_frac`, top-k agreement) as the tolerance check.
|
| 91 |
+
- Perf: decode + prefill tps vs dense, sparsity-ratio sweep, at 32k/256k (3090) then
|
| 92 |
+
512k/1M (bs2).
|
| 93 |
+
|
| 94 |
+
## Knobs (default OFF, byte-identical)
|
| 95 |
+
|
| 96 |
+
- `--sparse-attn {off,on}` (or auto by ctx threshold)
|
| 97 |
+
- `--sparse-attn-block-size` (B_SEL, default 64)
|
| 98 |
+
- `--sparse-attn-topk` (K_SEL blocks) and `--sparse-attn-recent` (always-keep window)
|
| 99 |
+
- env mirrors for dev (e.g. `OPENCOTI_SPARSE_ATTN_*`).
|
| 100 |
+
|
| 101 |
+
## Staging
|
| 102 |
+
|
| 103 |
+
- **S1** β per-block min/max side-cache + write-path plumbing (host). Off β byte-identical.
|
| 104 |
+
- **S2** β Quest selector (queryΒ·bounds β top-K + recent/sink mask) + KV_max/KV_min carry.
|
| 105 |
+
- **S3** β FA-VEC kernel block-skip in the tile loop.
|
| 106 |
+
- **S4** β gate: RULER-niah @256k + logit-equiv vs dense + decode/prefill tps (3090).
|
| 107 |
+
- **S5** β compose with sparse-V #546 and DCA bands; re-gate.
|
| 108 |
+
- **S6** β capture patch (vendor-backup + snapshot-diff, byte-identical re-apply) +
|
| 109 |
+
this doc's measured table + UPSTREAM_SYNC markers + STATE_SUMMARY + .wolf.
|
| 110 |
+
|
| 111 |
+
## S2/S3 engineering design (mapped + decided 2026-06-25)
|
| 112 |
+
|
| 113 |
+
Integration reuses the proven `KV_max`/`KV_min` shape (pool-alloc + pre-pass fill +
|
| 114 |
+
kernel param), but with one decisive difference: the per-step fill **writes** kmin/kmax
|
| 115 |
+
and the selector **reads** them *in the same graph*, so ordering is enforced with real
|
| 116 |
+
ggml **src edges** (two new graph ops) β **not** the DCA thread-local side-channel
|
| 117 |
+
(`opencoti_fattn_dca_pos_q`, `ggml-cuda/fattn.cu:27-29`), which is safe only for
|
| 118 |
+
analytical inputs with no shared-buffer write-then-read.
|
| 119 |
+
|
| 120 |
+
**Three pieces:**
|
| 121 |
+
|
| 122 |
+
1. **Fill op** `sparse_attn_fill` (new GGML custom op; pattern = the `ggml_streaming_*`
|
| 123 |
+
/ `ggml_turbo_wht` ops #345/#391).
|
| 124 |
+
- Inserted in `build_attn` right after each `cpy_k` (`llama-graph.cpp:2771`; iSWA
|
| 125 |
+
`:2985`, `:3067`), gated on `cparams.sparse_attn_enabled`.
|
| 126 |
+
- srcs: `k_cur` (new tokens' K, **pre-quant f16**) + `k_idxs` (scatter positions).
|
| 127 |
+
dst-update: `kmin_l`/`kmax_l` cache tensors (`build_forward_expand`, like `cpy_k`;
|
| 128 |
+
the write into the persistent cache tensor + the selector's read edge needs the
|
| 129 |
+
`ggml_view_1d` dep-tie idiom β bug-225 precedent β so the scheduler sees fillβselect).
|
| 130 |
+
- CUDA kernel: per (KV-head, channel), reduce min/max over each affected block's
|
| 131 |
+
positions; **min/max-merge with the existing** kmin/kmax (blocks fill incrementally
|
| 132 |
+
across decode). Affected blocks = `[n_past/B, (n_past+n_new)/B]`.
|
| 133 |
+
- Off β op not added β byte-identical.
|
| 134 |
+
|
| 135 |
+
2. **Selector op** `sparse_attn_select` (new GGML custom op; pre-pass shape mirrors
|
| 136 |
+
`flash_attn_dca_to_KV_max`, `fattn-common.cuh:891`).
|
| 137 |
+
- Inserted after `get_k` (`:2813`) before the FA op.
|
| 138 |
+
- srcs: `Q` (rotated FA query) + `kmin_l` + `kmax_l` (the read edge orders it after the
|
| 139 |
+
fill). dst: `block_sel` bitmask (`n_blocks` bits per `(seq, KV-head)`, I32-packed).
|
| 140 |
+
- CUDA kernel: per block b, UB = `Ξ£_d max(q_dΒ·kmin_b[d], q_dΒ·kmax_b[d])`; **GQA** β max
|
| 141 |
+
UB over the query group per KV-head (one block set per KV-head, NSA co-load); top-`K_SEL`
|
| 142 |
+
by UB (smem partial-sort / threshold) **OR'd with** the recent-window + sink blocks;
|
| 143 |
+
write bits.
|
| 144 |
+
|
| 145 |
+
3. **FA-VEC block-skip** (S3).
|
| 146 |
+
- `block_sel` rides as an **extra src on the FA op** β set `result->src[5] = block_sel`
|
| 147 |
+
after `ggml_flash_attn_ext` (mirror of `sinks`=src[4]); the scheduler builds the
|
| 148 |
+
selectorβFA edge from the src slot regardless of the core op. `launch_fattn` passes
|
| 149 |
+
`block_sel->data` as a new kernel param `const int * __restrict__ block_sel` alongside
|
| 150 |
+
`KV_max`/`KV_min` (`fattn-vec.cuh:83-84`, `fattn-common.cuh:27-28`).
|
| 151 |
+
- Tile loop (`fattn-vec.cuh:363`, immediately after the existing `k_VKQ_min` skip):
|
| 152 |
+
`if (block_sel && !(block_sel[base + ((k_VKQ_0/B)>>5)] & (1u<<((k_VKQ_0/B)&31)))) continue;`.
|
| 153 |
+
B (= `B_SEL`) aligned to the FA tile stride (`nthreads`) and the quant blck (32) β
|
| 154 |
+
`B_SEL` a multiple of both for clean skipping.
|
| 155 |
+
- Threaded through every FA-VEC instance signature (exactly as `KV_max` was). The MMA
|
| 156 |
+
prefill path is a **separate later stage** (S1βS4 target FA-VEC decode).
|
| 157 |
+
|
| 158 |
+
**Gate ladder (NEVER greedy needle β bug-263/270/354):**
|
| 159 |
+
- **Plumbing anchor:** `K_SEL = n_blocks` (select-all) β sparse must equal dense β
|
| 160 |
+
logit-equiv `real_frac=0`. Proves the carry + skip are exact before any real sparsity.
|
| 161 |
+
- Then reduce `K_SEL`: RULER-niah@256k (needle's block must survive) + logit-equiv
|
| 162 |
+
tolerance + decode/prefill tps vs dense, sparsity-ratio sweep (3090 @ 32k/256k β bs2 1M).
|
| 163 |
+
|
| 164 |
+
**Build reality:** fill+selector+skip β CUDA DSO rebuild (~25 min/iter, restamp BOTH DSO
|
| 165 |
+
paths). Min 2 builds (B1: fill β verify kmin/kmax vs a CPU min/max reference via a debug
|
| 166 |
+
dump; B2: selector+skip β select-all anchor then sparsity sweep). Plan 2β4 iterations.
|
| 167 |
+
|
| 168 |
+
## Open questions (resolve during impl)
|
| 169 |
+
|
| 170 |
+
- Block size vs the FA-VEC 128-thread tile stride and the quant block size (q5_0/q4_0
|
| 171 |
+
blck=32) β align B_SEL to a multiple of both for clean skipping.
|
| 172 |
+
- GQA: bounds are per-KV-head; the selector reduces the GQA query group to a per-KV-head
|
| 173 |
+
representative (max over the group's per-block upper bounds) so all grouped Q-heads
|
| 174 |
+
share one block set (NSA-style co-load) β avoids per-Q-head mask divergence.
|
| 175 |
+
- Interaction with the DCA fused MMA path (prefill) β S1βS4 target FA-VEC decode; the
|
| 176 |
+
MMA/prefill block-selector (TheTom's 2.9Γ@128K prefill analogue) is a later stage.
|
| 177 |
+
- Where to compute the selector: a small dedicated CUDA op vs folding into the FA launch.
|
| 178 |
+
|
| 179 |
+
## Comparison vs TheTom (S4 β user-required, "test the same as theTom to compare")
|
| 180 |
+
|
| 181 |
+
Reproduce TheTom's exact block-selector benchmark so our numbers sit directly beside
|
| 182 |
+
theirs. Their config (from `turboquant_plus/docs/papers/block-selector-sparse-attention.md`,
|
| 183 |
+
fetched 2026-06-25):
|
| 184 |
+
|
| 185 |
+
- **Model:** `Qwen2.5-14B-Instruct-1M-4bit` β **we have this exact model on bs2** (the #550
|
| 186 |
+
DCA vehicle; native-1M, no-YaRN β see `project_qwen25_1m_native_no_yarn.md`). Same model β
|
| 187 |
+
direct apples-to-apples.
|
| 188 |
+
- **Grid:** batch `B β {1,2,4,8}` Γ ctx `{16K β¦ 131072}` (their 19-cell grid). B>1 uses our
|
| 189 |
+
PolyKV `--parallel`. Metric = **dense-vs-sparse decode + prefill tps ratio**.
|
| 190 |
+
- **Block size:** 64 β matches our locked `B_SEL=64`.
|
| 191 |
+
- **Their headline (M5 Max / MLX):** decode **1.19Γ (B4/16K) β 1.73Γ (B8/28K) β 1.48Γ (B4/48K)**;
|
| 192 |
+
prefill **2.49Γ (64K) β 2.90Γ (128K)**. These are the numbers to match/beat.
|
| 193 |
+
- **Their quality validation is INCOMPLETE** β *no* clean end-to-end retrieval benchmark;
|
| 194 |
+
only cosine-vs-dense (0.999 prefill / 0.99995 long-ctx adaptive-topK). **We add a real
|
| 195 |
+
RULER-niah@{256K,1M} retrieval gate they lack** β a strict improvement on their eval.
|
| 196 |
+
|
| 197 |
+
**Why ours should win the decode column:** TheTom's selector is a **JL-projection
|
| 198 |
+
float-additive mask** (`contentDim=32`, `-β` for non-selected) β *compute-only masking, no
|
| 199 |
+
bandwidth save* β because MLX/Metal couldn't do a real K-skip (cerebrum 2026-06-21). Ours is
|
| 200 |
+
**Quest min/max block-skip on CUDA FA-VEC** that `continue`s past unselected blocks BEFORE
|
| 201 |
+
the K-read + QK + V β a real bandwidth save β so our decode ratio should *exceed* their
|
| 202 |
+
1.19β1.73Γ at the same sparsity, not just match it. Prefill (MMA path) is a later stage
|
| 203 |
+
(their 2.49β2.90Γ is the prefill-selector analogue, S5+).
|
| 204 |
+
|
| 205 |
+
> **CORRECTION (2026-06-27), see Β§S4 results:** the "should *exceed* their 1.19β1.73Γ"
|
| 206 |
+
> claim was over-stated *for the batched column*. TheTom's wins are at **batch 4β8**;
|
| 207 |
+
> ours are at **context length** β different mechanisms (his fixed mask-cost is
|
| 208 |
+
> amortized across streams; our real K-skip is a *fixed per-stream* cost that batching
|
| 209 |
+
> can't amortize). So at his batch points we sit near break-even, not above. Where the
|
| 210 |
+
> prediction holds is the axis he does *not* measure β single-stream long-context β and
|
| 211 |
+
> the 3090 ladder (0.908β0.992, climbing) is the first hard evidence for it.
|
| 212 |
+
|
| 213 |
+
**S4 deliverable:** a side-by-side table β same model, same BΓctx grid, dense-vs-sparse
|
| 214 |
+
decode/prefill ratio (ours, on the 3090 β€128K then bs2 to 1M) next to TheTom's MLX column,
|
| 215 |
+
PLUS our RULER-niah column. Run on bs2 (the 14B-1M lives there) to mirror their long-ctx grid.
|
| 216 |
+
|
| 217 |
+
## S4 results β 3090 single-stream ctx-ladder + batch grid (2026-06-27)
|
| 218 |
+
|
| 219 |
+
The TheTom comparison ran in two parts on the 3090 (bs2/14B-1M busy; the same-model
|
| 220 |
+
1M grid is still pending). Model: **Qwen3-4B-Q4_K_M**, f16 KV, 25% block coverage,
|
| 221 |
+
block size 64, Quest selector (`--sparse-attn on`). Metric = dense-vs-sparse **decode**
|
| 222 |
+
throughput ratio. Decode tps read from the server log's authoritative
|
| 223 |
+
`eval time β¦ / 128 tokens β¦ tokens per second` line β *not* the HTTP JSON, which is
|
| 224 |
+
unreliable under the bug-743 parse-race (see Gotchas).
|
| 225 |
+
|
| 226 |
+
### Single-stream ctx-ladder (B1) β the decisive axis
|
| 227 |
+
|
| 228 |
+
| ctx (B1) | dense tps | sparse tps | ratio | gap to dense |
|
| 229 |
+
|---------:|----------:|-----------:|------:|-------------:|
|
| 230 |
+
| 16384 | 74.50 | 67.65 | 0.908 | β9.2% |
|
| 231 |
+
| 24576 | 61.54 | 59.37 | 0.965 | β3.5% |
|
| 232 |
+
| 32768 | 53.41 | 52.06 | 0.975 | β2.5% |
|
| 233 |
+
| 40000 | 47.46 | 47.06 | **0.992** | β0.8% |
|
| 234 |
+
|
| 235 |
+
Monotonic climb; the gap halves roughly every ~8β16K. At Qwen3-4B's native 40960
|
| 236 |
+
ceiling we are within measurement noise of dense, and the trend extrapolates to
|
| 237 |
+
crossover (>1.0) just beyond β i.e. the 256k/1M serving regime. This is the expected
|
| 238 |
+
signature of a *real bandwidth-saving K-skip*: saved K-reads grow with cache size
|
| 239 |
+
while the selector's fill + QK-estimate + `block_sel`-gather cost is ~fixed, so the
|
| 240 |
+
ratio rises with context. (Script: `.opencoti/sparse-ctx-ladder.sh`, gitignored.)
|
| 241 |
+
|
| 242 |
+
### Batch grid (B1/B2/B4) β confirms batch is NOT our lever
|
| 243 |
+
|
| 244 |
+
| ctx/seq | B | dense_agg | sparse_agg | ratio | note |
|
| 245 |
+
|--------:|--:|----------:|-----------:|------:|------|
|
| 246 |
+
| 16384 | 1 | 74.5 | 67.3 | 0.903 | clean |
|
| 247 |
+
| 16384 | 2 | 62.6 | 12.0 | 0.192 | bug-743 parse-race (one req died, n<B) |
|
| 248 |
+
| 16384 | 4 | 42.4 | 11.3 | 0.267 | bug-743 parse-race (n=3) |
|
| 249 |
+
| 24576 | 1 | 60.9 | 58.1 | 0.954 | clean |
|
| 250 |
+
| 24576 | 2 | 44.9 | 43.2 | 0.962 | clean β sparse ~3% slower on **both** slots |
|
| 251 |
+
| 24576 | 4 | 27.7 | 27.7 | 1.000 | clean β uniform ~3% penalty washed out by scheduler asymmetry |
|
| 252 |
+
|
| 253 |
+
Per-slot eval times on the clean 24K cells show sparse ~3% slower per stream (e.g.
|
| 254 |
+
B2 dense 39.6/5.3 tps vs sparse 38.0/5.2). The 24K "1.000" is *not* a batched win β it
|
| 255 |
+
is the uniform per-stream penalty hidden by one-fast-slot/three-starved scheduler
|
| 256 |
+
asymmetry. So batching gives our selector **no** multiplier. (Script:
|
| 257 |
+
`.opencoti/sparse-batch-grid.sh`.)
|
| 258 |
+
|
| 259 |
+
### Verdict β we win a different game than TheTom
|
| 260 |
+
|
| 261 |
+
| | TheTom (turboquant_plus) | ours (#551 Quest) |
|
| 262 |
+
|---|---|---|
|
| 263 |
+
| Selector | JL-projection float-additive mask (compute-only, no bandwidth save) | Quest min/max block-skip on CUDA FA-VEC (real K/V-read skip) |
|
| 264 |
+
| Winning axis | **batch** (1.19Γ B4/16K β 1.73Γ B8/28K β 1.48Γ B4/48K) | **context** (0.908@16K β 0.992@40K, B1) |
|
| 265 |
+
| B1 behaviour | structurally β€1.0 (mask adds work, saves nothing at B1) | climbs to ~1.0 and past it with ctx |
|
| 266 |
+
| Quality eval | cosine-vs-dense only (0.999) | RULER-niah retrieval gate (a real gate he lacks) |
|
| 267 |
+
| Hardware/model | M5 Max / MLX / 14B-1M-4bit | 3090 / CUDA / Qwen3-4B f16 |
|
| 268 |
+
|
| 269 |
+
His mask saves no bandwidth, so his win **needs** batch to amortize a fixed selector
|
| 270 |
+
cost across streams; remove the batch and his advantage disappears. Ours saves real
|
| 271 |
+
bandwidth per stream, so it scales with **context** and batching can't amortize a
|
| 272 |
+
per-stream cost. Consequences:
|
| 273 |
+
|
| 274 |
+
- **At B1 (single-stream serving) we should beat him** β we climb to ~1.0 with ctx via
|
| 275 |
+
real K-skip; his compute-only mask is structurally β€1.0 at B1 (he doesn't report B1).
|
| 276 |
+
- **At B8/28K (his sweet spot) he wins on his hardware and we get no batch multiplier** β
|
| 277 |
+
we sit near break-even there.
|
| 278 |
+
- **On validation we are strictly ahead** β RULER-niah vs cosine-0.999.
|
| 279 |
+
|
| 280 |
+
A true number-against-his-column comparison is still **not** apples-to-apples (different
|
| 281 |
+
HW/model/quant). The only direct side-by-side is the still-pending S4 deliverable: the
|
| 282 |
+
same 14B-1M-4bit, the same BΓctx grid, on bs2 to 1M, **plus** our RULER-niah column.
|
| 283 |
+
|
| 284 |
+
### Gotchas surfaced
|
| 285 |
+
|
| 286 |
+
- **bug-743 (parse-race):** under load the server can feed a request's *generated*
|
| 287 |
+
filler continuation back into the JSON parser β `500 "Failed to parse input at pos 0"`,
|
| 288 |
+
which `curl` receives as the response (0 tps). It fires intermittently even at
|
| 289 |
+
`--parallel 1`. **Always read decode tps from the server log, not the HTTP JSON.** A
|
| 290 |
+
reliability bug to fix before batched sparse can be trusted in production.
|
| 291 |
+
|
| 292 |
+
## DeepSeek Sparse Attention (DSA) β investigated, NOT portable (2026-06-27)
|
| 293 |
+
|
| 294 |
+
Investigated per user request (Reddit thread on a llama.cpp DSA effort). DSA
|
| 295 |
+
(arXiv:2512.02556) is **not transferable** to our Qwen/Gemma targets:
|
| 296 |
+
|
| 297 |
+
- It needs a **trained "lightning indexer" head** (FP8, ReLU-gated, KL-distilled from
|
| 298 |
+
dense attention) plus ~**946B tokens** of base-weight sparsity adaptation, and is
|
| 299 |
+
**MLA/MQA-coupled** β our models are GQA with no indexer weights.
|
| 300 |
+
- The llama.cpp implementation (fairydreaming/sszymczyk, fork branch `deepseek-dsa`,
|
| 301 |
+
merged upstream as **PR #23346**, 2026-05-29) is **correctness-only** β the author's
|
| 302 |
+
own note says it "doesn't improve long-context performance yet" (oversized compute
|
| 303 |
+
buffers).
|
| 304 |
+
- The one transferable idea β a small trained indexer head β is a **training project**,
|
| 305 |
+
not a port. Out of scope for the vendored-fork decode lever.
|
| 306 |
+
|
| 307 |
+
## All-KV-types composition (bug-747, 2026-06-27)
|
| 308 |
+
|
| 309 |
+
Sparse attention originally composed **only with f16 KV**: the B1 fill op
|
| 310 |
+
(`GGML_OP_SPARSE_ATTN_FILL`) read the *quantized cache* K view, and its CUDA
|
| 311 |
+
`supports_op` + launcher required `src[1]` to be `GGML_TYPE_F16`. With any quantized KV
|
| 312 |
+
cache (`q8_0`/`q4_0`/`q6_0`/turbo*/tcq*) the cache K is non-f16 β `supports_op=0` on
|
| 313 |
+
CUDA0 β the scheduler could not place the op on the buffer holding the pre-allocated
|
| 314 |
+
`kbounds` side-cache β **abort at graph-reserve** (`ggml-backend.cpp:919`,
|
| 315 |
+
`cannot run the operation`). (Sibling of bug-737, which was the I64 `k_idxs` variant of
|
| 316 |
+
the same abort.)
|
| 317 |
+
|
| 318 |
+
**Fix β k_cur incremental-merge fill.** The fill now reads **`k_cur`** (the *new* token K
|
| 319 |
+
projection, always f16/f32 *before* cache quantization) instead of the quantized cache
|
| 320 |
+
view. The kernel is templated on K type (`half`/`float` via `sparse_ld<>`), derives
|
| 321 |
+
`n_new = k_cur->ne[2]` and the column stride `nb1 = k_cur->nb[2]`, maps cache positions
|
| 322 |
+
via `p0 = k_idxs[0]`, and for each block either **MERGEs** (min/max) into the prior
|
| 323 |
+
`kbounds` (the straddling last block at decode / unaligned prefill chunk) or
|
| 324 |
+
**OVERWRITEs** (a fully-new block). Because min/max is exact, this reproduces the old
|
| 325 |
+
dense recompute **byte-identically on f16**, and because the cache is quantized *downstream*
|
| 326 |
+
(in `cpy_k`), the Quest bound is the exact pre-quant envelope β so sparse now composes
|
| 327 |
+
with **every** cache KV type for free. `supports_op` + launcher relaxed to `f16||f32`;
|
| 328 |
+
the graph passes `k_fill = ggml_is_contiguous(k_cur) ? k_cur : ggml_cont(k_cur)`.
|
| 329 |
+
|
| 330 |
+
### Gate (Qwen2.5-14B-Instruct-1M-Q6_K, RTX 3090, RULER niah_single_1, 12 samples)
|
| 331 |
+
|
| 332 |
+
| ctx | KV | mode | coverage | niah | notes |
|
| 333 |
+
|----:|----|------|---------:|-----:|-------|
|
| 334 |
+
| 24k | f16 | dense | 100% | 100.0 | baseline |
|
| 335 |
+
| 24k | f16 | sparse | 25% | 91.67 | lossy regime (below f16 lossless threshold) |
|
| 336 |
+
| 24k | f16 | sparse | 65% | **100.0** | f16 lossless reference |
|
| 337 |
+
| 24k | q8_0 | dense | 100% | 100.0 | q8 alone is clean |
|
| 338 |
+
| 24k | q8_0 | sparse | 25% | 75.0 | lossy regime |
|
| 339 |
+
| 24k | q8_0 | sparse | 65% | **100.0** | **composition lossless** |
|
| 340 |
+
| 24k | q8_0 | sparse | 80% | 100.0 | composition lossless |
|
| 341 |
+
| 48k | q8_0 | dense | 100% | 100.0 | |
|
| 342 |
+
| 48k | q8_0 | sparse | 25% | 83.33 | lossy regime (improves vs 24k: more absolute blocks) |
|
| 343 |
+
|
| 344 |
+
**Composition is lossless.** q8_0βsparse holds niah=100 at the same coverage (β₯65%) where
|
| 345 |
+
f16βsparse is lossless. The 25%-coverage gap (q8 75 vs f16 91.67) is *not* a composition
|
| 346 |
+
defect β the block *selection* is identical (both fill `kbounds` from the same f16 `k_cur`),
|
| 347 |
+
so the gap is q8 quantization noise on the attended needle block plus 12-sample variance in
|
| 348 |
+
the sub-threshold lossy regime. At shippable coverage the penalty vanishes.
|
| 349 |
+
|
| 350 |
+
### The decode win lives on quantized KV
|
| 351 |
+
|
| 352 |
+
Steady-state decode (max over niah gen samples), niah=100 configs:
|
| 353 |
+
|
| 354 |
+
| KV | dense | sparse@65% | ratio |
|
| 355 |
+
|----|------:|-----------:|------:|
|
| 356 |
+
| f16 | 27.4 tps | 20.2 tps | 0.74Γ (loss) |
|
| 357 |
+
| q8_0 | 15.2 tps | ~17.4 tps | ~1.15Γ (win) β cross-gate hint |
|
| 358 |
+
|
| 359 |
+
**Same-boot A/B confirmation (q8_0, Qwen2.5-14B-1M-Q6_K, 24k, `sparse-q8-win-confirm.sh`).**
|
| 360 |
+
To remove cross-run variance the dense baseline and every sparse coverage were run
|
| 361 |
+
back-to-back in one harness boot. The win is real and grows monotonically as coverage drops,
|
| 362 |
+
while niah stays 100 all the way down to 50% coverage:
|
| 363 |
+
|
| 364 |
+
| coverage | niah | steady tps | vs dense |
|
| 365 |
+
|---------:|-----:|-----------:|---------:|
|
| 366 |
+
| 100% (dense) | 100 | 13.71 | 1.00Γ |
|
| 367 |
+
| 80% | 100 | 15.15 | 1.10Γ |
|
| 368 |
+
| 65% | 100 | 17.63 | 1.29Γ |
|
| 369 |
+
| 50% | 100 | 18.33 | **1.34Γ** |
|
| 370 |
+
|
| 371 |
+
Best lossless operating point in this sweep: **50% coverage β 1.34Γ decode at full niah.**
|
| 372 |
+
(25% coverage is lossy β q8 75 β so the lossless band bottoms out between 25% and 50%.)
|
| 373 |
+
The same-boot dense baseline (13.71) ran ~10% below the cross-gate extraction (15.2),
|
| 374 |
+
which is exactly the run-to-run variance the same-boot A/B exists to eliminate β the
|
| 375 |
+
sparse/dense *ratio* is the trustworthy number, not the absolute tps.
|
| 376 |
+
|
| 377 |
+
On **f16**, sparse@lossless coverage is *slower* than dense: a 14B model at 24k is not
|
| 378 |
+
KV-bandwidth-bound enough for a 35%-block skip to overcome the per-stream selector cost.
|
| 379 |
+
On **q8_0** β the actual long-context serving config β sparse is a **lossless decode win**,
|
| 380 |
+
because the dense path must **dequantize every block** (`q8βf16` lift) before FA, and the
|
| 381 |
+
block-skip skips that dequant work too. That extra saved compute is what tips q8
|
| 382 |
+
positive where f16 stays negative. So bug-747 didn't just make q8βsparse *work* β quantized
|
| 383 |
+
KV is *where the win is*, even on a model that loses on f16.
|
| 384 |
+
|
| 385 |
+
### Stacking with sparse-V (#546) β orthogonal on speed, tau must be retuned per config (2026-06-27)
|
| 386 |
+
|
| 387 |
+
sparse-attn (#551, block-skip) and sparse-V (#546, attention-gated V-skip, `TURBO_SPARSE_V_TAU`)
|
| 388 |
+
both live in `fattn-vec.cuh`: the block-selector chooses which blocks to visit; the V-skip drops
|
| 389 |
+
negligible-weight positions *inside* visited blocks. Same-boot 4-cell A/B, q8_0 KV,
|
| 390 |
+
Qwen2.5-14B-1M-Q6_K, 24k, block coverage 50%, sparse-V tau 0.1 (`sparse-stack-ab.sh`):
|
| 391 |
+
|
| 392 |
+
| cell | niah | tps | vs dense |
|
| 393 |
+
|------|-----:|----:|---------:|
|
| 394 |
+
| dense | 100 | 17.91 | 1.00Γ |
|
| 395 |
+
| block-skip @50% | **100** | 19.87 | 1.11Γ (lossless) |
|
| 396 |
+
| sparse-V Ο=0.1 | **0** | 20.11 | 1.12Γ (lossy) |
|
| 397 |
+
| both | **0** | 23.49 | **1.31Γ** |
|
| 398 |
+
|
| 399 |
+
Two findings: (1) the levers **compound on throughput** β `both` (1.31Γ) is faster than either alone,
|
| 400 |
+
confirming they cut on orthogonal axes (which blocks vs which positions-within-block). (2) sparse-V's
|
| 401 |
+
**Ο=0.1 does not transfer**: it was tuned lossless on turbo/turbo_tcq KV on 27B and Gemma-A4B (#546,
|
| 402 |
+
niah 100), but on q8_0 KV / Qwen2.5-14B-1M it destroys the needle (niah 0) β Ο=0.1 is far too aggressive
|
| 403 |
+
for this model's q8 attention distribution. The lossless compound win is recoverable via a per-config
|
| 404 |
+
Ο-sweep (0.1 β 0.01 β 0.001) to find where V-skip rejoins niah=100; block-skip alone remains the
|
| 405 |
+
proven lossless lever here (1.11Γ @ 50%).
|
| 406 |
+
|
| 407 |
+
## Sparse-V auto-policy (#565, patch 0088) β distinct from the block-selector above
|
| 408 |
+
|
| 409 |
+
NB: this is the **sparse-V** decode lever (#546, quantized-V attention-gated skip in the FA-VEC
|
| 410 |
+
kernel), NOT the Quest/vslash block-selector that is the subject of the rest of this doc. They are
|
| 411 |
+
independent. Sparse-V is shipped + validated; this section documents only its **auto-policy**.
|
| 412 |
+
|
| 413 |
+
The win is **arch-dependent**: lossless on interleaved-SWA (Gemma-4 β small concentrated global
|
| 414 |
+
V-cache), no-win on full-attention (Qwen β diffuse weights near the mean). Rather than make every
|
| 415 |
+
standalone `llamafile --server` user discover `TURBO_SPARSE_V_TAU`, the **binary self-configures**:
|
| 416 |
+
|
| 417 |
+
- **Where:** `src/llama-context.cpp`, `llama_context` ctor (`// opencoti-hook: sparse-v auto-policy (#565)`).
|
| 418 |
+
- **Rule:** if `getenv("TURBO_SPARSE_V_TAU")==NULL` AND `hparams.swa_type != LLAMA_SWA_TYPE_NONE`
|
| 419 |
+
(iSWA) AND `params.type_v` is quantized (not f16/bf16/f32) β `setenv("TURBO_SPARSE_V_TAU","0.05",0)`.
|
| 420 |
+
- **Why host-side env (not a DSO setter):** ggml-cuda is a runtime-loaded DSO; the kernel already
|
| 421 |
+
reads the threshold from `TURBO_SPARSE_V_TAU`. Pre-populating that env is host-only (no DSO ABI
|
| 422 |
+
change, DSO byte-identical), and is the only channel that also reaches a standalone CLI run.
|
| 423 |
+
- **Override:** `overwrite=0` β an explicit `TURBO_SPARSE_V_TAU` (or `_EPS`/`_SINK`/`_RECENT`) wins.
|
| 424 |
+
- **Safety:** full-attn models leave the env unset β kernel Ο=0 β dense, byte-identical. f16/bf16 V is
|
| 425 |
+
inert via the kernel's own `type_V != F16/BF16` guard even if the env were set.
|
| 426 |
+
- **Visibility:** announced once at `LLAMA_LOG_WARN` (llamafile suppresses the model-load INFO block;
|
| 427 |
+
a silent INFO-only auto-change is invisible β bug-2090).
|
| 428 |
+
- **Sampling:** non-gating. Sparse-V is a KV-bandwidth decode lever; correctness is proven at greedy
|
| 429 |
+
(temp 0, the strictest case), so the policy is sampling-agnostic.
|
| 430 |
+
|
| 431 |
+
Verified (3090): decision-table 4/4 + A/B @ 24k lossless (niah 100==100) and non-slower; the
|
| 432 |
+
measurable decode win lands at 64k+ (production 50-sample: A4B/31B niah 100, +2.7/3.8% @ 64k β
|
| 433 |
+
+1.3/3.1% @ 256k). See docs/evaluations/context.md (2026-06-29).
|
| 434 |
+
|
| 435 |
+
### Adaptive Ο (the `eps` mode) β built and characterized, NOT the shipped default (#565 verdict)
|
| 436 |
+
|
| 437 |
+
#565's literal goal was a *runtime weight-distribution-driven* threshold (not a fixed peak floor). It
|
| 438 |
+
**exists in the binary** as the `eps` mode: `fattn-vec.cuh:515/566` computes the per-query threshold
|
| 439 |
+
`thr = eps Β· d_inv_nproc Β· KQ_sum[jj]` β recomputed from the running softmax denominator so it adapts
|
| 440 |
+
to each token's actual attention mass (skip iff the dropped *normalized mass* < `eps`). Selectable via
|
| 441 |
+
`TURBO_SPARSE_V_EPS`; `eps=0` β falls back to the static `d_sparse_v_tau`.
|
| 442 |
+
|
| 443 |
+
**It is NOT what the auto-policy regulates** β the policy above only sets the *static* `TURBO_SPARSE_V_TAU`;
|
| 444 |
+
it never sets `TURBO_SPARSE_V_EPS`. That is deliberate, not a gap: the adaptive `eps` mode was measured
|
| 445 |
+
**uniformly lossless but ~zero speedup** (Qwen-14B-1M full-attn: `eps` 0.005β0.10 all niah=100, tps flat
|
| 446 |
+
7.8β7.9 vs dense 7.83). The reason is structural β a mass-relative threshold can only harvest the
|
| 447 |
+
*sub-mean* tail, and on diffuse full-attention weights cluster near the mean, so almost nothing is
|
| 448 |
+
skippable regardless of `eps`; on iSWA the static Ο=0.05 already captures the concentrated-global-cache
|
| 449 |
+
win more aggressively. So adaptive Ο ships as a **safe opt-in knob** (never slower/lossy, `eps=0`
|
| 450 |
+
byte-identical) while the static Ο via auto-policy is the production win. **Verdict:** the question "does a
|
| 451 |
+
runtime-adaptive threshold beat static Ο on the architectures we have?" is answered β *no*. Reopen only for
|
| 452 |
+
a fundamentally different full-attention adaptive scheme (which is the content-selection problem #551, itself
|
| 453 |
+
a closed dead-end). #565 closed as **built + characterized**, 2026-06-30.
|
docs/features/tiered_inference.md
ADDED
|
@@ -0,0 +1,805 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# F1 β Tiered Inference Engine
|
| 2 |
+
|
| 3 |
+
> Parent: [../MASTER_PLAN.md](../MASTER_PLAN.md)
|
| 4 |
+
> Status: **planning**
|
| 5 |
+
> Owner: TBD
|
| 6 |
+
|
| 7 |
+
## Problem
|
| 8 |
+
|
| 9 |
+
A single provider is never the right answer for a real coding agent:
|
| 10 |
+
|
| 11 |
+
- A small local model is cheap and private but stalls on hard tasks.
|
| 12 |
+
- A subscription provider (Ollama Cloud Gemma 4 31B, Claude Max,
|
| 13 |
+
ChatGPT Plus, etc.) has caps; you don't want every token to count
|
| 14 |
+
against them.
|
| 15 |
+
- A per-token provider (Anthropic API, OpenAI API, etc.) works but
|
| 16 |
+
charges for everything it sees.
|
| 17 |
+
|
| 18 |
+
We want opencoti to start work on the cheapest provider that can
|
| 19 |
+
plausibly handle it, and **escalate only when needed**.
|
| 20 |
+
|
| 21 |
+
## Tier 0 vision β primary local execution, two paths
|
| 22 |
+
|
| 23 |
+
Tier 0 is the role-label for "the cheap-and-fast tier we start every
|
| 24 |
+
turn on." It is *not* hard-wired to a specific runtime. opencoti
|
| 25 |
+
supports two first-class Tier-0 realizations and the user picks one
|
| 26 |
+
explicitly at first-run setup (no silent auto-fallback):
|
| 27 |
+
|
| 28 |
+
- **GPU path β the advanced one.**
|
| 29 |
+
Patched llamafile + `ManniX-ITA/gemma-4-A4B-98e-v5-coder-it`
|
| 30 |
+
(Gemma 4 26B-A4B base, 98-expert prune of the original 128, ~4 B
|
| 31 |
+
active per token, 256 K context max, IQ3_M β 9.82 GB, runs end-to-end
|
| 32 |
+
on a 24 GB GPU with full offload). The patched llamafile is what
|
| 33 |
+
opens up the advanced features below.
|
| 34 |
+
- **No-GPU path β the simple one.**
|
| 35 |
+
Ollama Cloud as an OpenAI-compatible Tier-0 provider with the
|
| 36 |
+
Gemma 4 31B dense model (free BF16 on Ollama Cloud at time of
|
| 37 |
+
writing). Plain HTTP request flow, no local compute, no advanced
|
| 38 |
+
features. Equally first-class for users without a GPU.
|
| 39 |
+
|
| 40 |
+
Anything else the OpenAI-compatible provider interface accepts works
|
| 41 |
+
too (a remote vLLM, a different Ollama endpoint, etc.). Tier 0 is the
|
| 42 |
+
abstraction; the realizations are config-driven.
|
| 43 |
+
|
| 44 |
+
Smaller Gemma 4 variants (E2B, E4B) and the stock Gemma 3 lineage are
|
| 45 |
+
explicitly *not* primary targets β evaluated much later if at all.
|
| 46 |
+
|
| 47 |
+
### Advanced features tied to the GPU path
|
| 48 |
+
|
| 49 |
+
The patched-llamafile path unlocks features the no-GPU path cannot
|
| 50 |
+
offer. opencoti **does not** expose this distinction in user-facing
|
| 51 |
+
configuration: features that need the patched llamafile are present
|
| 52 |
+
automatically when the GPU path is selected, and silently absent on
|
| 53 |
+
the no-GPU path. From the user's perspective Tier 0 is Tier 0;
|
| 54 |
+
internally the engine queries provider capabilities and degrades
|
| 55 |
+
gracefully.
|
| 56 |
+
|
| 57 |
+
Concretely (see [local_llamafile.md](local_llamafile.md) for detail):
|
| 58 |
+
|
| 59 |
+
- **KV-cache reuse across agentic turns** β turn N+1 reuses turn N's
|
| 60 |
+
prefix, dramatic latency drop on long contexts.
|
| 61 |
+
- **Big-context throughput patches** β keep tokens/sec usable at
|
| 62 |
+
64K+ contexts.
|
| 63 |
+
- **Single-download Ollama-blob reuse** β if Ollama is installed
|
| 64 |
+
locally, the patched llamafile can mount the existing
|
| 65 |
+
`~/.ollama/models/blobs/sha256-β¦` GGUF directly. No duplicate
|
| 66 |
+
download.
|
| 67 |
+
|
| 68 |
+
## Goals
|
| 69 |
+
|
| 70 |
+
- **G1.** Configure an ordered set of tiers (Tier 0, Tier 1, ...),
|
| 71 |
+
each with one or more providers.
|
| 72 |
+
- **G2.** Run a request on the lowest-numbered tier first; escalate
|
| 73 |
+
on failure-to-progress, hard error, or explicit escalation signal.
|
| 74 |
+
- **G3.** Support two tier topologies:
|
| 75 |
+
- **Linear** β try a single provider in this tier, fall through on
|
| 76 |
+
failure.
|
| 77 |
+
- **Fan-out** β try multiple providers in this tier in parallel
|
| 78 |
+
(or as a quorum), pick the best response.
|
| 79 |
+
- **G4.** Within a tier, prefer subscription providers; fall back to
|
| 80 |
+
metered providers when subs are exhausted or rate-limited.
|
| 81 |
+
- **G5.** Make the escalation policy a **user-visible config**, not
|
| 82 |
+
a hidden heuristic.
|
| 83 |
+
|
| 84 |
+
## Non-goals (for now)
|
| 85 |
+
|
| 86 |
+
- Reordering tiers automatically based on observed quality.
|
| 87 |
+
- Cross-session learning of provider quality.
|
| 88 |
+
- Coordinating tiers across multiple concurrent sessions.
|
| 89 |
+
|
| 90 |
+
## Design sketch
|
| 91 |
+
|
| 92 |
+
### Where it lives
|
| 93 |
+
|
| 94 |
+
- `packages/opencoti-tiers/` β new package. Holds the tier schema,
|
| 95 |
+
the router, the escalation policy, the provider-pool primitives.
|
| 96 |
+
- **Surgical hook** in opencode's model-call site: when an opencoti
|
| 97 |
+
tier config is present and enabled, the call is delegated to the
|
| 98 |
+
router from `opencoti-tiers`. Otherwise opencode behaves as
|
| 99 |
+
upstream.
|
| 100 |
+
|
| 101 |
+
### Concepts
|
| 102 |
+
|
| 103 |
+
- **Tier** = an ordered slot in the escalation chain. Tier 0 is the
|
| 104 |
+
cheapest, tried first.
|
| 105 |
+
- **Provider Pool** inside a tier = one of:
|
| 106 |
+
- `linear: [Provider, Provider, ...]` β tried in order
|
| 107 |
+
- `fanout: { mode: "first-good" | "best-of-n" | "quorum", members: [...] }`
|
| 108 |
+
- **Billing class** per provider:
|
| 109 |
+
- `subscription` β has a cap; depleted state is observable.
|
| 110 |
+
- `metered` β pay per token, no cap.
|
| 111 |
+
- **Escalation triggers** (configurable, ORed):
|
| 112 |
+
- hard-fail (provider error, timeout, refused)
|
| 113 |
+
- stuck-progress (no tool calls / no diff after N turns)
|
| 114 |
+
- explicit (user keypress, or model emits an "escalate" tool call)
|
| 115 |
+
- structured-quality (output fails a structured validator β e.g.,
|
| 116 |
+
code did not parse, tests still failing after N attempts)
|
| 117 |
+
|
| 118 |
+
### Config shape (illustrative, not final)
|
| 119 |
+
|
| 120 |
+
GPU-path example:
|
| 121 |
+
|
| 122 |
+
```yaml
|
| 123 |
+
opencoti:
|
| 124 |
+
tiers:
|
| 125 |
+
enabled: true
|
| 126 |
+
effort: balanced # low | balanced | high | max
|
| 127 |
+
models:
|
| 128 |
+
# Small local registry β names β entries. Same idea as
|
| 129 |
+
# claude-hooks' provider registry. Entries are matched by name
|
| 130 |
+
# from the `chain` below.
|
| 131 |
+
gemma4-a4b-coder:
|
| 132 |
+
kind: llamafile
|
| 133 |
+
gguf: ~/.opencoti/models/gemma-4-A4B-98e-v5-coder-it-IQ3_M.gguf
|
| 134 |
+
contextLength: 131072 # capped; model supports up to 256K
|
| 135 |
+
gemma4-31b-cloud:
|
| 136 |
+
kind: openai-compat
|
| 137 |
+
baseURL: https://ollama.com/api
|
| 138 |
+
apiKeyEnv: OLLAMA_CLOUD_API_KEY
|
| 139 |
+
modelId: gemma4:31b
|
| 140 |
+
billing: subscription
|
| 141 |
+
chain:
|
| 142 |
+
- id: tier0
|
| 143 |
+
topology: linear
|
| 144 |
+
providers:
|
| 145 |
+
- gemma4-a4b-coder # patched llamafile, advanced features
|
| 146 |
+
- id: tier1
|
| 147 |
+
topology: linear
|
| 148 |
+
providers:
|
| 149 |
+
- gemma4-31b-cloud # subscription (Ollama Cloud)
|
| 150 |
+
- groq/llama-3.3-70b # metered, fallback
|
| 151 |
+
- id: tier2
|
| 152 |
+
topology: fanout
|
| 153 |
+
mode: first-good
|
| 154 |
+
providers:
|
| 155 |
+
- anthropic/claude-sonnet-4-6 # subscription via Max
|
| 156 |
+
- anthropic-api/claude-sonnet-4-6 # metered, fallback
|
| 157 |
+
escalation:
|
| 158 |
+
on: [hard-fail, stuck-progress, structured-quality]
|
| 159 |
+
stuck-progress:
|
| 160 |
+
no-progress-turns: 3
|
| 161 |
+
structured-quality:
|
| 162 |
+
validators: [code-parses, tests-pass-or-improve]
|
| 163 |
+
```
|
| 164 |
+
|
| 165 |
+
No-GPU example (Tier 0 is the cloud provider; no llamafile present):
|
| 166 |
+
|
| 167 |
+
```yaml
|
| 168 |
+
opencoti:
|
| 169 |
+
tiers:
|
| 170 |
+
enabled: true
|
| 171 |
+
effort: balanced
|
| 172 |
+
models:
|
| 173 |
+
gemma4-31b-cloud:
|
| 174 |
+
kind: openai-compat
|
| 175 |
+
baseURL: https://ollama.com/api
|
| 176 |
+
apiKeyEnv: OLLAMA_CLOUD_API_KEY
|
| 177 |
+
modelId: gemma4:31b
|
| 178 |
+
billing: subscription
|
| 179 |
+
chain:
|
| 180 |
+
- id: tier0
|
| 181 |
+
topology: linear
|
| 182 |
+
providers:
|
| 183 |
+
- gemma4-31b-cloud
|
| 184 |
+
- id: tier1
|
| 185 |
+
# ... user's preferred escalation provider ...
|
| 186 |
+
```
|
| 187 |
+
|
| 188 |
+
### Tool surface
|
| 189 |
+
|
| 190 |
+
opencode already injects a strong default tool set into every
|
| 191 |
+
`streamText` call (15 built-in: `shell`, `read`, `write`, `edit`,
|
| 192 |
+
`glob`, `grep`, `task`, `fetch`, `search`, `todo`, `skill`, `patch`,
|
| 193 |
+
`question`, `lsp`, `invalid`), plus full MCP server integration, plus
|
| 194 |
+
plugin / `.opencode/tool/*` user-defined tools, **all gated by the
|
| 195 |
+
same permission flow**. That surface already meets-or-exceeds what
|
| 196 |
+
claude-hooks' caliber-grounding-proxy + consultants skill expose
|
| 197 |
+
(`survey_project`, `list_files`, `read_file`, `glob`, `grep`,
|
| 198 |
+
`recall_memory`, and the sandboxed `write_file` for the coder role).
|
| 199 |
+
|
| 200 |
+
What opencode does NOT have today, and that opencoti will port from
|
| 201 |
+
claude-hooks in a later phase (target: F4 or F5 β out of scope for
|
| 202 |
+
F1):
|
| 203 |
+
|
| 204 |
+
- **`recall_memory` as a project-injected tool.** Opencode requires
|
| 205 |
+
the user to wire pgvector/sqlite_vec via MCP. claude-hooks injects
|
| 206 |
+
recall_memory directly into the tool list so the model can query
|
| 207 |
+
memory without an MCP roundtrip. The opencoti tiers package already
|
| 208 |
+
injects synthetic `__tier_*` tools (M5); the same mechanism extends
|
| 209 |
+
to `__opencoti_recall_memory` cleanly.
|
| 210 |
+
- **`survey_project` β hierarchical project map.** Composable from
|
| 211 |
+
`glob` + `read`, but having one shot of "tell me the project shape"
|
| 212 |
+
saves the model several turns.
|
| 213 |
+
- **Grounding system-prompt prepend.** A short paragraph that frames
|
| 214 |
+
the model's task in terms of "you're working in this project at
|
| 215 |
+
cwd β¦". The M5 prompt-augmentation primitive (`augmentSystem`) is
|
| 216 |
+
the right seam.
|
| 217 |
+
- **Multi-role consultant orchestration.** opencode has `task`
|
| 218 |
+
(launch one subagent), claude-hooks consultants has a 6-role
|
| 219 |
+
LangGraph council (planner / researcher / critic / synthesizer /
|
| 220 |
+
tool_executor / coder). The structured orchestration is the part
|
| 221 |
+
worth porting; opencode's `task` is the underlying primitive it
|
| 222 |
+
would build on. Likely a `opencoti-consultants` package consuming
|
| 223 |
+
opencode's existing subagent + permission infrastructure.
|
| 224 |
+
|
| 225 |
+
These ports are documented here only as the **direction** for later
|
| 226 |
+
phases. F1 M5.7 (below) covers the configuration plumbing; F4/F5
|
| 227 |
+
plans, when written, will own the actual ports.
|
| 228 |
+
|
| 229 |
+
### "Effort" presets
|
| 230 |
+
|
| 231 |
+
The `effort` knob is a one-line way for the user to pick how willing
|
| 232 |
+
opencoti is to spend tokens. Conceptual mapping:
|
| 233 |
+
|
| 234 |
+
| Effort | Behavior |
|
| 235 |
+
| --- | --- |
|
| 236 |
+
| `low` | Stay on Tier 0 unless hard-fail. Never fan-out. |
|
| 237 |
+
| `balanced` | Default. Escalate on stuck-progress and structured-quality. Fan-out only on tier-config that explicitly opts in. |
|
| 238 |
+
| `high` | More aggressive escalation, allows fan-out on Tier 1+. |
|
| 239 |
+
| `max` | Fan-out on multiple tiers when configured; never silently degrade. |
|
| 240 |
+
|
| 241 |
+
### Subscription accounting
|
| 242 |
+
|
| 243 |
+
Subscription depletion is observed per-provider:
|
| 244 |
+
|
| 245 |
+
- Provider adapters expose a `subscriptionState()` returning
|
| 246 |
+
`{ available: bool, resetsAt?: Date, reason?: string }`.
|
| 247 |
+
- The router consults it before picking a sub-class provider in a
|
| 248 |
+
pool. On `available: false`, it falls through to the next member
|
| 249 |
+
(typically metered) until reset.
|
| 250 |
+
- Adapters are responsible for parsing rate-limit / quota signals
|
| 251 |
+
from their provider's responses.
|
| 252 |
+
|
| 253 |
+
## Milestones
|
| 254 |
+
|
| 255 |
+
### M1 β Schema + dry-run router *(done β 2026-05-21)*
|
| 256 |
+
|
| 257 |
+
- [x] Tier config Zod schema in
|
| 258 |
+
`packages/opencoti-tiers/src/config.ts` (`Effort`, `Billing`,
|
| 259 |
+
`FanoutMode`, `EscalationTrigger`, `Provider`, `Tier`,
|
| 260 |
+
`Escalation`, `TierConfig`). Strings normalize to providers via
|
| 261 |
+
`z.preprocess`; defaults: `enabled=false`, `effort=balanced`,
|
| 262 |
+
`escalation.on=[hard-fail]`, fanout `mode=first-good`.
|
| 263 |
+
- [x] Provider-state model in `src/provider-state.ts`
|
| 264 |
+
(`ProviderState`, `AVAILABLE`, `exhausted()`).
|
| 265 |
+
- [x] Effort presets in `src/effort.ts` mapping `low|balanced|high|max`
|
| 266 |
+
to `{ maxTiersToTry, allowFanout, defaultEscalationTriggers }`.
|
| 267 |
+
- [x] Pure `plan()` function in `src/router.ts` that produces a
|
| 268 |
+
`Plan` (ordered `PlanStep`s with `attempts`, `skipped`,
|
| 269 |
+
`blockedByEffort`, `notes`). No real provider calls.
|
| 270 |
+
- [x] 17 unit tests (`bun test`) cover: schema parsing + rejects
|
| 271 |
+
(empty chain, unknown effort, unknown fanout mode), provider
|
| 272 |
+
normalization (string β object, billing preserved), linear
|
| 273 |
+
escalation order with subscription-first reordering,
|
| 274 |
+
subscription-exhausted fallback (single and all-exhausted),
|
| 275 |
+
fanout modes (`best-of-n`, `quorum`) preserving config order,
|
| 276 |
+
effort presets (`low` caps to tier 0; `balanced` downgrades fanout
|
| 277 |
+
to linear; `high` preserves fanout; explicit effort overrides
|
| 278 |
+
config).
|
| 279 |
+
- Typecheck (`tsc --noEmit`) clean.
|
| 280 |
+
|
| 281 |
+
### M2 β Surgical hook into opencode model call *(done β 2026-05-21)*
|
| 282 |
+
|
| 283 |
+
- [x] Mapped the call site to
|
| 284 |
+
`packages/opencode/src/session/llm.ts` (`run` Effect.fn around the
|
| 285 |
+
`streamText({...})` call). The file already has a sister
|
| 286 |
+
experimental-native runtime seam β opencoti's hook is its
|
| 287 |
+
immediate neighbor, mirroring the same shape (one Effect.fn yield,
|
| 288 |
+
one conditional early return).
|
| 289 |
+
- [x] Added `packages/opencoti-tiers/src/runtime.ts` exposing
|
| 290 |
+
`TierRuntime.maybeRoute(input): Effect<TierRunResult | undefined>`
|
| 291 |
+
and `TierRuntime.isEnabledIn(cfg)`. M2 implementation is a
|
| 292 |
+
passthrough (`Effect.succeed(undefined)`); M3 swaps it for the
|
| 293 |
+
real router β adapter flow without touching upstream again.
|
| 294 |
+
- [x] `TierRunResult` is typed to match opencode's `run` return
|
| 295 |
+
shape (`{type: "ai-sdk"; result: ReturnType<typeof streamText>}`
|
| 296 |
+
| `{type: "native"; stream: never}`). The `never` on the native
|
| 297 |
+
stream keeps the inferred return-type union at the call site
|
| 298 |
+
identical to upstream's β `never` is the absorbed element in
|
| 299 |
+
discriminated-union narrowing.
|
| 300 |
+
- [x] Surgical hook in `packages/opencode/src/session/llm.ts`:
|
| 301 |
+
- import block (line ~29): `import * as TierRuntime from "@opencoti/tiers/runtime"`
|
| 302 |
+
tagged with the `// opencoti-hook: tiered-inference β see docs/features/tiered_inference.md` marker.
|
| 303 |
+
- call site (just before the `flags.experimentalNativeLlm` seam):
|
| 304 |
+
`const tier = yield* TierRuntime.maybeRoute({ input, prepared, cfg }); if (tier) return tier`
|
| 305 |
+
tagged with the same marker.
|
| 306 |
+
- [x] `packages/opencode/package.json` gains
|
| 307 |
+
`"@opencoti/tiers": "workspace:*"`. No comment marker is possible
|
| 308 |
+
in JSON; the dep is registered in `docs/protocols/UPSTREAM_SYNC.md`.
|
| 309 |
+
- [x] All three hook entries registered in the UPSTREAM_SYNC.md
|
| 310 |
+
hook registry.
|
| 311 |
+
- [x] `bun typecheck` from repo root: 17/17 packages green (including
|
| 312 |
+
`opencode` with the new hook).
|
| 313 |
+
- [x] `bun test` in `packages/opencoti-tiers`: 23/23 green, including
|
| 314 |
+
6 new tests for `TierRuntime.maybeRoute` + `TierRuntime.isEnabledIn`.
|
| 315 |
+
|
| 316 |
+
Net hook footprint in upstream source: **one import line + a
|
| 317 |
+
two-line conditional + one JSON dep entry** β the minimum that lets
|
| 318 |
+
M3 wire real routing without ever editing `llm.ts` again.
|
| 319 |
+
|
| 320 |
+
### M3 β Tier 0 = llamafile end-to-end *(done β 2026-05-21)*
|
| 321 |
+
|
| 322 |
+
- [x] `TierProvider` interface defined in
|
| 323 |
+
`packages/opencoti-tiers/src/provider.ts` (`TierProvider`,
|
| 324 |
+
`OpenedTierProvider`, `TierStreamRequest`). Types-only; no
|
| 325 |
+
dependency on any concrete provider package β the dep direction is
|
| 326 |
+
llamafile β tiers, not the other way around.
|
| 327 |
+
- [x] Llamafile implementation in
|
| 328 |
+
`packages/opencoti-llamafile/src/tier-provider.ts`
|
| 329 |
+
(`tierProvider()` factory). Singleton process per host (lazy
|
| 330 |
+
promise cache); uses `@ai-sdk/openai-compatible` to build a
|
| 331 |
+
`LanguageModel` against the running llamafile's
|
| 332 |
+
`/v1/chat/completions` endpoint.
|
| 333 |
+
- [x] `TierRuntime.maybeRoute` replaced with real routing logic in
|
| 334 |
+
`packages/opencoti-tiers/src/runtime.ts`:
|
| 335 |
+
- Env gate (`OPENCOTI_TIERS_ENABLED=true`) β opt-in by design at
|
| 336 |
+
M3.
|
| 337 |
+
- Dynamic-imports `@opencoti/llamafile/tier-provider` via a string
|
| 338 |
+
variable, so `@opencoti/tiers` has no static dep on llamafile
|
| 339 |
+
(would be circular otherwise β llamafile already depends on
|
| 340 |
+
tiers for the interface).
|
| 341 |
+
- All failure modes (provider not installed, launch fails, open
|
| 342 |
+
fails) fall through to upstream by returning `undefined`. The
|
| 343 |
+
surgical hook never throws β it's forgiving by design.
|
| 344 |
+
- Constructs `streamText` with the opened `LanguageModel` and the
|
| 345 |
+
upstream `prepared` request (messages, system, params, abort).
|
| 346 |
+
- [x] **No further edits to `packages/opencode/src/session/llm.ts`.**
|
| 347 |
+
The M2 surgical hook is the last upstream touch needed for this
|
| 348 |
+
feature. M3 lands entirely inside opencoti packages.
|
| 349 |
+
- [x] Tests: 24 unit tests passing in `@opencoti/tiers`
|
| 350 |
+
(env-gate-off, env-gate-on-but-no-llamafile, `isEnabledIn`
|
| 351 |
+
variants); 2 unit tests in `@opencoti/llamafile` covering
|
| 352 |
+
`tierProvider()` factory shape. Live `open()` covered by the
|
| 353 |
+
smoke script (manual).
|
| 354 |
+
|
| 355 |
+
**M3 known limitations** (intentional β addressed by later
|
| 356 |
+
milestones):
|
| 357 |
+
- Tool calling not wired through. M3 calls `streamText({model,
|
| 358 |
+
messages, system, params})` without forwarding `prepared.tools`.
|
| 359 |
+
Many local llamafile models can't tool-call usefully anyway; tool
|
| 360 |
+
routing comes when M5 brings escalation triggers and tools
|
| 361 |
+
matter.
|
| 362 |
+
- Config is env-var-driven, not opencoti-config-driven. The
|
| 363 |
+
user-facing tier config (effort, chain, escalation) defined in
|
| 364 |
+
`@opencoti/tiers/config` is **not yet read** by M3's
|
| 365 |
+
`maybeRoute` β M4 wires it in (and removes the env gate in favor
|
| 366 |
+
of `opencoti.tiers.enabled` in the config file).
|
| 367 |
+
- Single hardcoded llamafile tier. No escalation, no fan-out β those
|
| 368 |
+
are M4, M5, M6.
|
| 369 |
+
- Singleton llamafile process for host lifetime. If the user
|
| 370 |
+
reconfigures the model mid-session, M3 ignores the new model. M3+
|
| 371 |
+
refines lifecycle.
|
| 372 |
+
|
| 373 |
+
### M4 β Tier 1 cloud fallback (linear) **[DONE 2026-05-21]**
|
| 374 |
+
|
| 375 |
+
- One subscription provider + one metered fallback work end-to-end
|
| 376 |
+
with subscription exhaustion observed correctly.
|
| 377 |
+
- Implementation:
|
| 378 |
+
- `cloud-provider.ts` β generic `TierProvider` over any
|
| 379 |
+
OpenAI-compatible HTTP endpoint. Preflight GET `${baseURL}/models`
|
| 380 |
+
classifies the response: 200 β AVAILABLE; 401/403 β exhausted
|
| 381 |
+
(unauthorized); 402 β exhausted (payment-required); 429 β
|
| 382 |
+
exhausted with `resetsAt` parsed from `Retry-After` (both numeric
|
| 383 |
+
seconds and HTTP-date forms). State lives in the provider's
|
| 384 |
+
closure and is exposed via `getState()`.
|
| 385 |
+
- `registry.ts` β pure resolver mapping a config `Provider` entry
|
| 386 |
+
to a concrete `TierProvider`. `kind: "llamafile"` (or string
|
| 387 |
+
shorthand `"llamafile"` / `"llamafile/β¦"`) dynamic-imports
|
| 388 |
+
`@opencoti/llamafile/tier-provider`; `kind: "openai-compat"` with
|
| 389 |
+
`baseURL` + `modelId` builds a `cloudProvider`.
|
| 390 |
+
- `executor.ts` β walks the `plan()` output across tiers and
|
| 391 |
+
providers. On each iteration it collects state from each
|
| 392 |
+
provider's `getState()` plus a module-level overlay (for
|
| 393 |
+
`open()` failures that don't self-mark) and calls `plan()` again.
|
| 394 |
+
A bounded loop (`totalProviders + 1`) prevents runaway replanning.
|
| 395 |
+
- `runtime.ts` β env-var gate **removed**; replaced by
|
| 396 |
+
`isEnabledIn(cfg)` reading `cfg.opencoti.tiers.enabled === true`,
|
| 397 |
+
plus `safeParseConfig` on `cfg.opencoti.tiers`. The hook then
|
| 398 |
+
delegates to `execute()` and wraps the returned `languageModel`
|
| 399 |
+
in `streamText`. `maybeRouteWith` is a test seam letting tests
|
| 400 |
+
inject an in-memory resolver.
|
| 401 |
+
- Schema additions (in `config.ts`): provider entries now accept
|
| 402 |
+
`kind: "llamafile" | "openai-compat"`, `baseURL`, `apiKey`,
|
| 403 |
+
`apiKeyEnv` (preferred β read at runtime), `modelId`. Backwards
|
| 404 |
+
compatible: existing string-shorthand entries (`"llamafile/β¦"`)
|
| 405 |
+
still parse.
|
| 406 |
+
- Test coverage: 47 tests across `config.test.ts`, `router.test.ts`,
|
| 407 |
+
`runtime.test.ts`, `cloud-provider.test.ts`, `executor.test.ts`.
|
| 408 |
+
Cloud preflight tests use `fetchImpl` injection β no live HTTP.
|
| 409 |
+
- Zero upstream edits β M4 is entirely additive.
|
| 410 |
+
- Out of scope for M4 (deferred to M5+):
|
| 411 |
+
- **Cross-tier** escalation triggers (hard-fail, stuck-progress,
|
| 412 |
+
structured-quality). M4 only covers within-tier subβmetered.
|
| 413 |
+
- Forwarding `prepared.tools` through `streamText`.
|
| 414 |
+
|
| 415 |
+
### M5 β Escalation triggers v1 **[DONE 2026-05-21]**
|
| 416 |
+
|
| 417 |
+
In/post-stream escalation observer wraps the M4 pre-stream router so
|
| 418 |
+
the engine can react to bad turns and route the *next* turn to a
|
| 419 |
+
higher tier. All four configured triggers (`hard-fail`,
|
| 420 |
+
`stuck-progress`, `structured-quality`, `explicit`) work end-to-end.
|
| 421 |
+
|
| 422 |
+
- Architecture (locked from planning conversation):
|
| 423 |
+
- **Model-as-judge structured-quality**: the model's in-stream
|
| 424 |
+
behavior IS the quality verdict β tool errors, no progress,
|
| 425 |
+
explicit bail-out β NOT a parallel validator registry. The user's
|
| 426 |
+
tools (test runners, language servers) are already the validators.
|
| 427 |
+
- **Per-turn escalation unit**: one `streamText` invocation = one
|
| 428 |
+
chance. Per-session counters survive across turns. When a
|
| 429 |
+
threshold is met, the provider is overlay-marked; the next turn
|
| 430 |
+
picks the next tier via the existing executor machinery.
|
| 431 |
+
- **Distinct executor + escalator primitives** sharing the
|
| 432 |
+
`stateOverlay`. Executor stays pre-stream (M4). Escalator wraps
|
| 433 |
+
`streamText` and is in/post-stream.
|
| 434 |
+
- **No mid-stream stream-swap** β mid-stream errors mark the
|
| 435 |
+
provider exhausted and let the next turn re-route. Only
|
| 436 |
+
pre-first-chunk hard-fail triggers a same-turn retry via a
|
| 437 |
+
`restartStream` callback the runtime supplies.
|
| 438 |
+
- Implementation:
|
| 439 |
+
- `escalator.ts` β `wrapWithEscalation({result, opened, cfg,
|
| 440 |
+
sessionID, telemetry, restartStream?}) β StreamTextResult`.
|
| 441 |
+
Returns a `Proxy<StreamTextResult>` that overrides only
|
| 442 |
+
`fullStream`; the wrapped `fullStream` is an async generator that
|
| 443 |
+
tees events into a `TurnObserver` and re-yields them. The
|
| 444 |
+
`TurnObserver` counts tool-call / tool-result / tool-error / error
|
| 445 |
+
events, evaluates triggers at stream finish, calls
|
| 446 |
+
`_markEscalated(providerName, "trigger:reason")` from `executor.ts`
|
| 447 |
+
when a threshold is hit, and emits telemetry.
|
| 448 |
+
- `session-state.ts` β `Map<sessionID, SessionTierState>` with 30-min
|
| 449 |
+
TTL eviction on read. Fields: `stuckTurns`, `qualityFails`,
|
| 450 |
+
`lastDifficulty`, `lastTier`, `lastAccess`,
|
| 451 |
+
`explicitEscalationPending`.
|
| 452 |
+
- `synthetic-tools.ts` β `syntheticTools(sessionID, telemetry?)`
|
| 453 |
+
returns `{ __tier_report_difficulty, __tier_request_escalation }`
|
| 454 |
+
as AI-SDK `Tool` objects with `inputSchema` (zod) and `execute`
|
| 455 |
+
handlers that record into session state and emit telemetry. Both
|
| 456 |
+
names start with `__tier_` so any consumer can filter them.
|
| 457 |
+
- `prompt-augmentation.ts` β `augmentSystem(systemLines, enabled)`
|
| 458 |
+
appends one narrow paragraph telling the model about the two
|
| 459 |
+
synthetic tools when `enabled` is true.
|
| 460 |
+
- `telemetry.ts` β `Telemetry` interface with 7 methods
|
| 461 |
+
(`attemptStart`, `attemptSuccess`, `exhaustionObserved`,
|
| 462 |
+
`escalationTriggered`, `difficultyReported`,
|
| 463 |
+
`requestEscalationCalled`, `sessionStateUpdated`) + a
|
| 464 |
+
`noopTelemetry` default. F3 (`opencoti-server`) will eventually
|
| 465 |
+
implement this interface; M5 ships the schema only.
|
| 466 |
+
- `executor.ts` extensions: `ExecuteResult.tierId` (so the escalator
|
| 467 |
+
knows which tier opened the stream); `telemetry?` in
|
| 468 |
+
`ExecutorOptions`; `_markEscalated(name, reason)` exported
|
| 469 |
+
package-internal; telemetry calls at `attemptStart` /
|
| 470 |
+
`exhaustionObserved` branch points.
|
| 471 |
+
- `runtime.ts` extensions: merges `prepared.tools` with
|
| 472 |
+
`syntheticTools(sessionID, telemetry)` (the M3/M4 known limitation
|
| 473 |
+
is gone); replaces `system: ...join("\n")` with
|
| 474 |
+
`augmentSystem(...).join("\n")`; provides a `restartStream`
|
| 475 |
+
callback to the escalator that re-calls `execute()` (the just-marked
|
| 476 |
+
provider is skipped automatically).
|
| 477 |
+
- Schema additions in `config.ts`:
|
| 478 |
+
`escalation.qualityFails.threshold` (defaults to 2).
|
| 479 |
+
- Trigger semantics:
|
| 480 |
+
- `hard-fail`: pre-first-chunk error β silently restart THIS turn
|
| 481 |
+
once via `restartStream`. Mid-stream errors β mark exhausted,
|
| 482 |
+
escalate NEXT turn. User-AbortSignal abort is excluded.
|
| 483 |
+
- `stuck-progress`: turn finishes with zero tool calls AND
|
| 484 |
+
`lastDifficulty != "low"` β increments `stuckTurns`. Escalates
|
| 485 |
+
when `stuckTurns >= cfg.escalation.stuckProgress.noProgressTurns`.
|
| 486 |
+
A productive turn (β₯1 tool call) resets the counter.
|
| 487 |
+
- `structured-quality`: any `tool-error` event β increments
|
| 488 |
+
`qualityFails`. Escalates when `qualityFails >=
|
| 489 |
+
cfg.escalation.qualityFails.threshold`. A clean turn with β₯1
|
| 490 |
+
successful tool result resets the counter.
|
| 491 |
+
- `explicit`: any call to `__tier_request_escalation` (model bailout)
|
| 492 |
+
OR an `explicitEscalationPending` flag from a prior turn β
|
| 493 |
+
immediate escalation regardless of threshold.
|
| 494 |
+
- Test coverage: 33 new M5 tests (`escalator.test.ts`,
|
| 495 |
+
`session-state.test.ts`, `synthetic-tools.test.ts`,
|
| 496 |
+
`prompt-augmentation.test.ts`, `telemetry.test.ts`, + 3 extensions
|
| 497 |
+
to `runtime.test.ts`). Total `@opencoti/tiers`: 80 tests, all green.
|
| 498 |
+
Workspace typecheck clean.
|
| 499 |
+
- Zero upstream edits β M5 is entirely additive in
|
| 500 |
+
`packages/opencoti-tiers/`. Hook footprint unchanged: still the 2
|
| 501 |
+
M2 markers in `packages/opencode/src/session/llm.ts`.
|
| 502 |
+
|
| 503 |
+
### M5.5 β Effort-level + tiermode schema expansion **[DONE 2026-05-21]**
|
| 504 |
+
|
| 505 |
+
Pure-schema milestone, no runtime change beyond what the new config
|
| 506 |
+
shape brings.
|
| 507 |
+
|
| 508 |
+
- **Effort enum**: replaced `low|balanced|high|max` with a 5-base set
|
| 509 |
+
`low|normal|medium|high|max` plus 5 x-prefixed fanout opt-ins
|
| 510 |
+
`xlow|xnormal|xmedium|xhigh|xmax`. The `x` prefix marks "fan-out at
|
| 511 |
+
this tier"; aggression is the base level, fanout is the orthogonal
|
| 512 |
+
bit. No `balanced` legacy alias β pre-1.0, follow CLAUDE.md's
|
| 513 |
+
"don't keep backwards-compatibility shims" rule.
|
| 514 |
+
- **`effort.ts` constraints table** rebuilt as `5 Γ 2` grid:
|
| 515 |
+
- `low` / `xlow`: cap at one tier; `hard-fail` trigger only.
|
| 516 |
+
- `normal` / `xnormal`: walk all tiers; `+ stuck-progress`.
|
| 517 |
+
- `medium` / `xmedium`: walk all tiers; `+ structured-quality`.
|
| 518 |
+
- `high` / `xhigh`: same trigger set as medium; reserved for later
|
| 519 |
+
threshold-tuning differentiation.
|
| 520 |
+
- `max` / `xmax`: same trigger set, most-aggressive thresholds.
|
| 521 |
+
- `allowFanout` is `false` for the bare base levels, `true` for the
|
| 522 |
+
`x*` opt-ins. (Behavior change: old `high`/`max` enabled fanout;
|
| 523 |
+
that capability moved to `xhigh`/`xmax`.)
|
| 524 |
+
- **Tiermode**: new top-level config field. `tiermodes:` is a map of
|
| 525 |
+
named profiles; each profile may override `effort`, per-tier
|
| 526 |
+
`topologyOverrides`, or a partial `escalation` overlay. The
|
| 527 |
+
top-level `tiermode: string` field points into the map and is
|
| 528 |
+
validated at parse time (refinement: name must exist).
|
| 529 |
+
- **`tiermode.ts` resolver**: `resolveTiermode(cfg) β ResolvedTierConfig`
|
| 530 |
+
pure function. Returns the effective `{ effort, topologyOverrides,
|
| 531 |
+
escalation, config }` after profile merge. Router and runtime do
|
| 532 |
+
NOT consume it yet (M5.5 is pure schema); the resolver is package-
|
| 533 |
+
exported so M6/M7 wiring is trivial.
|
| 534 |
+
- **Tests**: total `@opencoti/tiers` test count up from 80 to 107
|
| 535 |
+
(+27 net: 10 new + parameterized parsing across all 10 effort
|
| 536 |
+
values + 5 tiermode parsing + 6 resolver tests; 2 existing fanout-
|
| 537 |
+
tests migrated from `high`/`max` to `xhigh`/`xmax`). Workspace
|
| 538 |
+
typecheck clean. Zero new upstream surgical hooks.
|
| 539 |
+
|
| 540 |
+
### M5.7 β Tier-0 provider configuration + model registry **[DONE 2026-05-21]**
|
| 541 |
+
|
| 542 |
+
User-facing first-run setup and a small project-local model registry.
|
| 543 |
+
Lands after M5.5 (schema is final by then) and before M6 (fan-out
|
| 544 |
+
needs a working multi-provider Tier 0 to test against). No runtime
|
| 545 |
+
change beyond what config-read needs.
|
| 546 |
+
|
| 547 |
+
- **Model registry.** A `models:` map in the config (see Config-shape
|
| 548 |
+
example above) maps short names (`gemma4-a4b-coder`,
|
| 549 |
+
`gemma4-31b-cloud`, β¦) to provider entries. The `chain[].providers`
|
| 550 |
+
field references those names. Same idea as claude-hooks' provider
|
| 551 |
+
registry. Schema lives in `packages/opencoti-tiers/src/config.ts`.
|
| 552 |
+
- **First-run setup CLI** (`opencoti tier setup` or equivalent β
|
| 553 |
+
exact subcommand TBD, runs from the standard opencode CLI surface):
|
| 554 |
+
1. GPU detection. Reads `nvidia-smi`, `rocm-smi`, or an equivalent
|
| 555 |
+
probe. Reports detected VRAM.
|
| 556 |
+
2. **GPU path branch.** Prompts:
|
| 557 |
+
- Confirm Gemma 4 A4B-98e-v5-coder as the Tier-0 model (yes by
|
| 558 |
+
default, with the option to point to a different GGUF).
|
| 559 |
+
- Quant pick β IQ3_M default for β₯10 GB VRAM, IQ4_XS or CD-IQ4_K_M
|
| 560 |
+
for ~11 GB, Q4_K_S for ~12 GB, Q5_K_L for ~15 GB, Q8_0 for 24 GB.
|
| 561 |
+
Recommended pick is computed from detected VRAM and shown as
|
| 562 |
+
the default.
|
| 563 |
+
- GGUF source β three options:
|
| 564 |
+
- **Download from HuggingFace.** Downloads to
|
| 565 |
+
`$XDG_DATA_HOME/opencoti/models/` (`~/.local/share/opencoti/models/`
|
| 566 |
+
on Linux default) using the user's HF auth if any.
|
| 567 |
+
- **Use existing file.** User points at a GGUF path. Validated
|
| 568 |
+
(magic bytes + metadata read) before being recorded.
|
| 569 |
+
- **Import from local Ollama.** If `~/.ollama/models/` is
|
| 570 |
+
detected, parse manifests under
|
| 571 |
+
`~/.ollama/models/manifests/registry.ollama.ai/library/<model>/<tag>`
|
| 572 |
+
to find the blob digest; the patched llamafile mounts
|
| 573 |
+
`~/.ollama/models/blobs/sha256-<digest>` directly. **Single
|
| 574 |
+
download, zero duplication.**
|
| 575 |
+
3. **No-GPU path branch.** Prompts:
|
| 576 |
+
- Confirm Ollama Cloud as the Tier-0 provider.
|
| 577 |
+
- Recommend `gemma4:31b` (free BF16 on Ollama Cloud at time of
|
| 578 |
+
writing); allow override.
|
| 579 |
+
- Capture `OLLAMA_CLOUD_API_KEY` via env-var reference (never
|
| 580 |
+
stored in config β `apiKeyEnv` indirection).
|
| 581 |
+
4. **No silent auto-fallback.** If GPU detection fails and the user
|
| 582 |
+
hasn't picked a path, setup exits with a clear next-step prompt.
|
| 583 |
+
No defaulting to cloud.
|
| 584 |
+
5. Writes the chosen entries into the project's opencoti config
|
| 585 |
+
(`opencoti.tiers.models[...]` and `chain[0].providers[0]`).
|
| 586 |
+
- **Ollama local registry detection.** Helper module reads the
|
| 587 |
+
manifest JSON, follows the `digest` field to the blobs/ entry,
|
| 588 |
+
returns `{path, sha256, sizeBytes}`. Pure read-only β never modifies
|
| 589 |
+
the Ollama store. Tests use a fixture `~/.ollama/` tree.
|
| 590 |
+
- **Reuse pattern in llamafile launcher.** When a registry entry
|
| 591 |
+
points at an Ollama blob (kind: `llamafile`, gguf path inside
|
| 592 |
+
`~/.ollama/`), the launcher passes the blob path directly to the
|
| 593 |
+
patched llamafile β no copy, no symlink, just read. Verified by a
|
| 594 |
+
smoke test.
|
| 595 |
+
- **Test coverage:** new test files in `packages/opencoti-tiers/test/`
|
| 596 |
+
(`model-registry.test.ts`, `ollama-detect.test.ts`) and
|
| 597 |
+
`packages/opencoti-llamafile/test/` (`ollama-blob-reuse.test.ts`).
|
| 598 |
+
GPU detection itself is shimmed in tests (the real probe lives
|
| 599 |
+
outside `@opencoti/tiers` to keep the package testable in CI).
|
| 600 |
+
|
| 601 |
+
Zero new upstream surgical hooks. M5.7 is entirely additive in
|
| 602 |
+
opencoti packages + a new CLI subcommand exposed via opencode's
|
| 603 |
+
existing command surface.
|
| 604 |
+
|
| 605 |
+
**M5.7 implementation notes (what actually shipped):**
|
| 606 |
+
|
| 607 |
+
- Schema: `ModelEntry` + `models: Record<string, ModelEntry>` on
|
| 608 |
+
`TierConfig`. Refinements enforce "llamafile requires exactly one
|
| 609 |
+
of `gguf`/`ollamaBlob`" and "openai-compat requires `baseURL`".
|
| 610 |
+
- `src/registry/ollama-detect.ts` β read-only manifest walker
|
| 611 |
+
(`registry.ollama.ai/<ns>/<model>/<tag>`), follows the GGUF layer
|
| 612 |
+
digest to `~/.ollama/models/blobs/sha256-β¦`. Tolerant of malformed
|
| 613 |
+
manifests.
|
| 614 |
+
- `src/registry/gpu-detect.ts` β async `detectGPU()` probes
|
| 615 |
+
`nvidia-smi --query-gpu=...` first, falls back to
|
| 616 |
+
`rocm-smi --showmeminfo vram --json`. 5 s default timeout.
|
| 617 |
+
`runImpl` test seam.
|
| 618 |
+
- `src/registry/quant-recommender.ts` β pure table-driven picker
|
| 619 |
+
encoding the Gemma 4 A4B-98e quant table from
|
| 620 |
+
`local_llamafile.md`. Reserves ~3 GB KV per 32 K context + 1.5 GB
|
| 621 |
+
safety; picks highest-quality (HE+) quant that fits.
|
| 622 |
+
- `src/registry/hf-download.ts` β minimal HF Hub HTTP client. Resume
|
| 623 |
+
via `${destPath}.partial` + `Range: bytes=N-`, optional
|
| 624 |
+
`$HF_TOKEN` auth, SHA256 verify (deletes the partial on mismatch),
|
| 625 |
+
per-chunk progress callback.
|
| 626 |
+
- `src/cli/setup-flow.ts` β pure-logic flow with injected deps
|
| 627 |
+
(`SetupDeps`). `runSetupFlow(deps)` returns `cancelled` or
|
| 628 |
+
`configured` with the `ConfigPatch` to write. Testable via
|
| 629 |
+
scripted-answers stub deps.
|
| 630 |
+
- `src/cli/setup.ts` β live wiring: @clack/prompts for input,
|
| 631 |
+
`jsonc-parser` `modify`/`applyEdits` for comment-preserving
|
| 632 |
+
config write to `~/.config/opencode/opencode.jsonc`. Exports
|
| 633 |
+
`tierCommand` (yargs parent) and `tierSetupCommand` (subcommand).
|
| 634 |
+
- Surgical hook in `packages/opencode/src/index.ts`: 1 import +
|
| 635 |
+
1 `.command(tierCommand)` call, both marker-tagged. Both
|
| 636 |
+
registered in `docs/protocols/UPSTREAM_SYNC.md`.
|
| 637 |
+
- `package.json`: adds `@clack/prompts`, `jsonc-parser`, `yargs`
|
| 638 |
+
as deps (same pinned versions opencode uses) and
|
| 639 |
+
`@types/yargs` as devDep. New `./cli` export.
|
| 640 |
+
- Test coverage: 35 net new tests across 5 new files
|
| 641 |
+
(`ollama-detect.test.ts` 6, `gpu-detect.test.ts` 6,
|
| 642 |
+
`quant-recommender.test.ts` 10, `hf-download.test.ts` 7,
|
| 643 |
+
`cli/setup-flow.test.ts` 8). Total `@opencoti/tiers`: 144 tests.
|
| 644 |
+
Workspace typecheck clean. Hook conformance: exactly 4 markers
|
| 645 |
+
(2 M2 + 2 M5.7).
|
| 646 |
+
|
| 647 |
+
### M6 β Fan-out tier (first-good) **[DONE 2026-05-21]**
|
| 648 |
+
|
| 649 |
+
Goal: when the plan picks a fanout tier (xhigh / xmax effort, or
|
| 650 |
+
explicit `tiermode` override), open every provider in that tier in
|
| 651 |
+
parallel and let the fastest useful response through. Losers are
|
| 652 |
+
aborted; losers that fail before being aborted are overlay-marked
|
| 653 |
+
the same way the M5 linear path marks them.
|
| 654 |
+
|
| 655 |
+
Behavior knobs the user locked this turn:
|
| 656 |
+
|
| 657 |
+
- **`winnerTrigger` (per tier)**: `"content"` (default) or `"finish"`.
|
| 658 |
+
`content` races on the first real-content event (same predicate
|
| 659 |
+
the M5 escalator uses β `text-delta`, `tool-call`, `tool-result`,
|
| 660 |
+
`file`, etc.). `finish` races on the first racer to complete a
|
| 661 |
+
clean turn.
|
| 662 |
+
- **Pre-trigger loser failures** β overlay-marked via
|
| 663 |
+
`_markEscalated(name, "fanout-prefail")`. Aborts-by-us are NOT
|
| 664 |
+
marked (the racer was cut off through no fault of its own β
|
| 665 |
+
tracked via a `abortedByUs` flag).
|
| 666 |
+
- **`maxConcurrency` (per tier, optional)**: caps how many providers
|
| 667 |
+
actually race. The rest are recorded in `skippedByConcurrencyCap`
|
| 668 |
+
on the executor result and are NOT overlay-marked β they just sit
|
| 669 |
+
out this turn.
|
| 670 |
+
|
| 671 |
+
The fanout `mode` enum was expanded to include `"synthesizer"` and
|
| 672 |
+
`"synthesizer-critic"` for forward-compatibility with the consultants-
|
| 673 |
+
council pattern (Tier 0 as synthesizer + separate critic model).
|
| 674 |
+
M6 ships `"first-good"` only; `"best-of-n"`, `"quorum"`,
|
| 675 |
+
`"synthesizer"`, and `"synthesizer-critic"` throw a typed
|
| 676 |
+
`FanoutModeNotImplemented` error that the runtime catches as a
|
| 677 |
+
forgiving fall-through.
|
| 678 |
+
|
| 679 |
+
Implementation surface (all additive in `packages/opencoti-tiers/`):
|
| 680 |
+
|
| 681 |
+
- `src/config.ts` β `FanoutTier` gets `winnerTrigger` + `maxConcurrency`;
|
| 682 |
+
`FanoutMode` enum expanded.
|
| 683 |
+
- `src/executor.ts` β `executeFanout()` opens every router-emitted
|
| 684 |
+
attempt for the first fanout step in parallel via `Promise.all`,
|
| 685 |
+
applying `maxConcurrency` and overlay-marking open-failures the
|
| 686 |
+
same way `execute()` does.
|
| 687 |
+
- `src/fanout.ts` (new) β `raceFanout()` runs N per-racer drain
|
| 688 |
+
goroutines, exposes `{proxy, outcome, settled}`. The proxy is a
|
| 689 |
+
`Proxy<StreamTextResult>` whose `fullStream` getter replays the
|
| 690 |
+
winner's buffered pre-trigger events then continues from the
|
| 691 |
+
underlying iterator. Per-racer buffer cap (default 4 MB) catches
|
| 692 |
+
pathological non-content streams.
|
| 693 |
+
- `src/runtime.ts` β `peekTopology()` decides linear vs fanout
|
| 694 |
+
branch from `plan()`. The fanout branch builds per-racer
|
| 695 |
+
streamText invocations with child `AbortController`s, calls
|
| 696 |
+
`raceFanout`, then wraps the proxy with `wrapWithEscalation` so
|
| 697 |
+
M5's escalation triggers apply on the winner's stream. The
|
| 698 |
+
`restartStream` callback handles the `all-failed` outcome the
|
| 699 |
+
same way M5's hard-fail-pre-stream path does.
|
| 700 |
+
- `src/escalator.ts` β `isRealContent` was exported so `fanout.ts`
|
| 701 |
+
shares the exact same predicate.
|
| 702 |
+
|
| 703 |
+
Test coverage: 20 net new tests across
|
| 704 |
+
`test/{config,executor,runtime,fanout}.test.ts`. Total
|
| 705 |
+
`@opencoti/tiers` count: **168 tests**.
|
| 706 |
+
|
| 707 |
+
Zero new surgical hooks. Hook count stays at 4 (2 M2 + 2 M5.7).
|
| 708 |
+
|
| 709 |
+
### M7 β TUI effort + tiermode pickers **[DONE 2026-05-21]**
|
| 710 |
+
|
| 711 |
+
Goal: in-TUI dials for the M5.5 `effort` (10 values) and `tiermode`
|
| 712 |
+
(named profiles) so users can change them mid-session without
|
| 713 |
+
re-editing `~/.config/opencode/opencode.jsonc`. Plus a runtime-side
|
| 714 |
+
override seam so a TUI pick actually takes effect on the very next
|
| 715 |
+
request, plus auto-wire of the new picker plugin into the M5.7
|
| 716 |
+
setup CLI.
|
| 717 |
+
|
| 718 |
+
Three additive surfaces:
|
| 719 |
+
|
| 720 |
+
- **`@opencoti/tiers/src/active-config.ts`** β module-level overlay
|
| 721 |
+
(`setActiveEffort` / `setActiveTiermode` / `getActiveOverride`)
|
| 722 |
+
matching the existing `stateOverlay` pattern. Both the TUI plugin
|
| 723 |
+
and the M2 hook run in the same Bun process, so module-level
|
| 724 |
+
singleton state is genuinely shared.
|
| 725 |
+
- **`runtime.ts` overlay + tiermode wiring** β `routeImpl` now
|
| 726 |
+
merges the overlay onto the parsed config, calls `resolveTiermode`
|
| 727 |
+
(which M5.5 left unwired), patches each tier's topology per the
|
| 728 |
+
resolver output, and forwards the effective effort to the
|
| 729 |
+
executor via `ExecutorOptions.effort`. Precedence:
|
| 730 |
+
**TUI overlay > tiermode profile > base `cfg.effort`**.
|
| 731 |
+
- **`@opencoti/tui-tiers`** β new sibling package. Ships an opencode
|
| 732 |
+
TUI plugin (`TuiPluginModule`) that registers two commands
|
| 733 |
+
(`opencoti.effort`, `opencoti.tiermode`), opens
|
| 734 |
+
`DialogSelect`s, and persists picks via `api.kv` under a
|
| 735 |
+
per-project key (`opencoti.<basename-hash12>.{effort|tiermode}`)
|
| 736 |
+
so each project remembers its own selection. On plugin start, the
|
| 737 |
+
persisted values replay into the runtime overlay β TUI restart
|
| 738 |
+
preserves the last pick.
|
| 739 |
+
|
| 740 |
+
Setup CLI extension (M5.7 β M7):
|
| 741 |
+
- `setup-flow.ts` got a `tuiPickerInstalled` sentinel on `SetupDeps`
|
| 742 |
+
and an `installTuiPicker` boolean on `ConfigPatch`. After the
|
| 743 |
+
GPU/no-GPU branch builds its patch, a `finalizePatch` helper asks
|
| 744 |
+
"Install the TUI picker plugin?" (skipped when the sentinel is
|
| 745 |
+
true β re-runs don't re-prompt).
|
| 746 |
+
- The live `setup.ts` reads the sentinel from the existing config
|
| 747 |
+
(best-effort `parseTree` + `findNodeAtLocation`) and pre-populates
|
| 748 |
+
`tuiPickerInstalled` on the live deps. When `installTuiPicker` is
|
| 749 |
+
true on the patch, the writer appends `"@opencoti/tui-tiers"` to
|
| 750 |
+
the user's `plugin: []` (deduped against the existing array) and
|
| 751 |
+
flips `opencoti.tiers.tuiPickerInstalled = true`.
|
| 752 |
+
|
| 753 |
+
Schema: `TierConfig` got one new optional sentinel field
|
| 754 |
+
`tuiPickerInstalled: z.boolean().default(false)`. Pure schema β
|
| 755 |
+
defining it in Zod makes a typo a clear error instead of silent.
|
| 756 |
+
|
| 757 |
+
Test coverage: **16 net new tests** across `@opencoti/tiers`
|
| 758 |
+
(active-config, runtime, executor, setup-flow) + **12 tests** in the
|
| 759 |
+
new `@opencoti/tui-tiers` package. Totals: tiers **184 tests**,
|
| 760 |
+
tui-tiers **12 tests**, workspace 18/18 typecheck clean.
|
| 761 |
+
|
| 762 |
+
Zero new surgical hooks. The TUI plugin loads via opencode's
|
| 763 |
+
existing `plugin: []` discovery path (the setup-flow auto-wire
|
| 764 |
+
writes the entry). Hook footprint stays at 4 markers.
|
| 765 |
+
|
| 766 |
+
**Behavior change worth noting**: `resolveTiermode` was unwired
|
| 767 |
+
since M5.5. Wiring it in M7 means users who set `tiermode` in their
|
| 768 |
+
config β silently ignored pre-M7 β now actually see the profile
|
| 769 |
+
apply. This is a fix, not a regression, but anyone debugging
|
| 770 |
+
tiermode behavior should know the activation date is 2026-05-21.
|
| 771 |
+
|
| 772 |
+
### M8 β Structured-quality validators v1
|
| 773 |
+
|
| 774 |
+
- Pluggable validators (code-parses, tests-pass-or-improve). Trigger
|
| 775 |
+
escalation when validator fails N times on the current tier.
|
| 776 |
+
|
| 777 |
+
## Open questions
|
| 778 |
+
|
| 779 |
+
- ~~Where exactly is the upstream call site?~~ Resolved at M2:
|
| 780 |
+
`packages/opencode/src/session/llm.ts`, just before the existing
|
| 781 |
+
experimental-native-llm seam.
|
| 782 |
+
- **Streaming under fan-out.** First-good is fine; quorum and
|
| 783 |
+
best-of-n are not naturally streaming. Defer their UX to M6+.
|
| 784 |
+
- **Cost accounting.** Should opencoti show per-tier token spend?
|
| 785 |
+
Probably yes, but it's not on the critical path. The M5 telemetry
|
| 786 |
+
interface already carries the data; F3's web dashboard is the
|
| 787 |
+
obvious place to surface it.
|
| 788 |
+
- **Capability negotiation between escalator and provider.** The
|
| 789 |
+
patched llamafile exposes capabilities the no-GPU path lacks (KV
|
| 790 |
+
reuse, big-context throughput). Today the executor doesn't ask
|
| 791 |
+
providers what they can do β it just opens them. M5.7+ may want to
|
| 792 |
+
add a `capabilities()` method to `TierProvider` so the escalator
|
| 793 |
+
can know whether KV-prefix-reuse is on the table for the next turn.
|
| 794 |
+
Not blocking; revisit when the second patched-llamafile feature
|
| 795 |
+
lands.
|
| 796 |
+
|
| 797 |
+
## Risks
|
| 798 |
+
|
| 799 |
+
- The opencode provider abstraction may not factor cleanly enough for
|
| 800 |
+
a single hook. If so, we widen the hook to a small adapter shim in
|
| 801 |
+
the upstream call site, and keep the adapter implementation in
|
| 802 |
+
`opencoti-tiers`. The hook stays small; the implementation stays
|
| 803 |
+
ours.
|
| 804 |
+
- Provider rate-limit signal parsing is fragile. We isolate it per
|
| 805 |
+
provider adapter so a broken adapter never poisons the router.
|
docs/features/tstar_auto_spill.md
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# T\*-aware auto rolling-KV spill (HAL #582 P1+, #621 follow-on)
|
| 2 |
+
|
| 3 |
+
Status: **T1βT5 VALIDATED on both hosts, 2026-07-11. T5 quality/tps gate PASS.** Opt-in under
|
| 4 |
+
`OPENCOTI_KV_AUTO_TIER=1` (same switch as the 0116 auto-tier). Builds on patch 0116
|
| 5 |
+
(`opencoti_auto_select_kv_tier`) and the rolling-KV window (0070/POSITION_WINDOW). **Host-only
|
| 6 |
+
β the FA microbench dispatches to the existing CUDA DSO, so no DSO rebuild.** Byte-identical
|
| 7 |
+
when the env is unset (opt-in gate returns before any measurement). Patch 0118 capture +
|
| 8 |
+
final commit are the only remaining step (held for explicit go) β see "T5 verdict" below.
|
| 9 |
+
|
| 10 |
+
## T5 verdict (2026-07-11, bs2 GPU0, Qwen2.5-14B-1M Q8_0, ctx 65536, DCA-off)
|
| 11 |
+
|
| 12 |
+
The physics reframes the gate: T\*(0.20) is only ~14β17 MiB at this config (small `t_resident`
|
| 13 |
+
at 65k, `c_engage ~1 ms` eats most of `0.20Β·t_resident`) while f16 spills ~2 GB, so the SHIP
|
| 14 |
+
question is not "does auto KEEP f16" (a razor-thin band) but **"is T\*'s quantize-vs-keep
|
| 15 |
+
verdict CORRECT?"**. Four uniform RULER-niah cells (`--vram-target 27000` forces the spill):
|
| 16 |
+
|
| 17 |
+
| cell | niah | decode tps |
|
| 18 |
+
|---|---|---|
|
| 19 |
+
| f16_resident (vram 0) | 100.0 | 50.2 |
|
| 20 |
+
| f16_spill (~2 GB, window) | 100.0 | **10.81** |
|
| 21 |
+
| q8_resident (fits) | 100.0 | 35.26 |
|
| 22 |
+
| auto (`OPENCOTI_KV_AUTO_TIER=1`) | 100.0 | 35.28 |
|
| 23 |
+
|
| 24 |
+
**PASS β all four checks:** (1) **auto β q8_0/q8_0**: boot WARN shows *"top tier f16 SPILLS by
|
| 25 |
+
1630 MiB β¦ T\* tolerable spill 14 MiB (bw_eff=10.3 host-RAM-bound, target_drop=0.20) β auto KV
|
| 26 |
+
tier = q8_0/q8_0 (fits)"* β the drop-model rejects f16 because 1630 β« 14 MiB. (2) **f16-spill tps
|
| 27 |
+
drop = 78.5%** (50.2β10.81) β« 20% target β keeping f16 would cost far more than `target_drop`,
|
| 28 |
+
so rejecting it was justified (host-RAM-bound 10.3 GB/s link craters even a 2 GB spill). (3) **q8
|
| 29 |
+
niah = f16 niah = 100.0** β quantizing lossless-enough; f16_spill niah 100.0 also confirms the
|
| 30 |
+
streaming window is itself lossless. (4) **q8 tps 35.26 β₯ f16_spill 10.81** β the fallback
|
| 31 |
+
strictly dominates (3.3Γ faster AND lossless). Consistency: **auto 35.28 β q8_resident 35.26**
|
| 32 |
+
(auto's end-to-end matches the tier it selected). Correctness via RULER niah, never greedy needle.
|
| 33 |
+
|
| 34 |
+
## Validation (2026-07-11, Qwen2.5-14B-Instruct-1M, both hosts)
|
| 35 |
+
|
| 36 |
+
Measured terms confirm T2 (bandwidth probes) + T3 (c_engage microbench) fire and T4 (drop model)
|
| 37 |
+
responds monotonically to `target_drop`:
|
| 38 |
+
|
| 39 |
+
| host | pcie | host_ram (1-thread memcpy) | **bw_eff = min** | binding | fa_ms/cell | c_engage |
|
| 40 |
+
|---|---|---|---|---|---|---|
|
| 41 |
+
| solidPC (3090, PCIe4, DDR4) | 6.7 | 11.5 | **6.7** | PCIe-bound | 6.2e-4 | 0.80 ms |
|
| 42 |
+
| bs2 (RTX 6000, PCIe5, DDR5) | 51.2 | 9.3 | **9.3** | **host-RAM-bound** | 2.6e-4 | 0.82 ms |
|
| 43 |
+
|
| 44 |
+
**req #3 demonstrated:** the binding bandwidth is host-dependent β PCIe on solidPC, host-RAM on
|
| 45 |
+
bs2 (fast PCIe5 makes DDR5 single-stream read the bottleneck). `min()` picks correctly on each;
|
| 46 |
+
without the host-RAM probe bw_eff would be 5Γ too optimistic on bs2. T\* drop-response (solidPC,
|
| 47 |
+
ctx 106 k, f16 spilling): tolerable spill **0 β 62 β 157 β 235 MiB** as drop 0.02 β 0.30 β 0.70
|
| 48 |
+
β 0.99 (jitter-immune, same-boot). `T*=0` at drop 0.02 (c_engage > 0.02Β·t_resident) confirms the
|
| 49 |
+
safe degradation to the T1 "fully resident" gate. Byte-identical when env unset: 0 auto-tier WARN
|
| 50 |
+
lines. **Caveat:** on the 3090's 6.7 GB/s link T\* is a few tensβhundreds of MiB, so a tier *flip*
|
| 51 |
+
via T\* is only reachable at long ctx on high-bandwidth hosts (bs2); on the 3090 the model
|
| 52 |
+
correctly almost always quantizes. `host_ram` is a single-thread `memcpy` proxy (conservative
|
| 53 |
+
under-estimate of DMA-engine host read) β bug-2151 fixed a dead-store-elimination defect that had
|
| 54 |
+
made it return garbage (~2e7 GB/s). Diagnostics are WARN-level (`budgetβ¦top tier`, `T* drop-model
|
| 55 |
+
β β¦tolerable spill`) mirroring the existing `auto KV tier` WARN, for operator auditability.
|
| 56 |
+
|
| 57 |
+
## Problem
|
| 58 |
+
|
| 59 |
+
0116 auto-tier picks the least-compressing scalar KV pair that keeps the cache **fully
|
| 60 |
+
resident**. But a *higher* KV tier (better quality) with a *small* host-tail spill can beat a
|
| 61 |
+
harder-compressed fully-resident tier β **when** the spill tail is within a "negligible
|
| 62 |
+
tps-drop" band. This spec adds that decision: prefer `f16-window + small spill` over
|
| 63 |
+
quantizing, but only when the spill is provably cheap on *this* host + model + context.
|
| 64 |
+
|
| 65 |
+
## The decision (fit-condition reformulation)
|
| 66 |
+
|
| 67 |
+
The 0116 ladder walk changes from "first tier that fully fits" to "first (least-compressing)
|
| 68 |
+
tier whose **spill** β€ T\*":
|
| 69 |
+
|
| 70 |
+
```
|
| 71 |
+
choose first tier i (leastβmost compression) s.t. kv_bytes(i) β€ budget + T*_bytes
|
| 72 |
+
```
|
| 73 |
+
|
| 74 |
+
`budget` is the existing resident budget (free β already-used β compute-reserve, capped by
|
| 75 |
+
`--vram-target`), identical to `opencoti_compute_resident_window_cells`. When even the floor
|
| 76 |
+
tier spills past T\*, pick the floor (rolling-KV spills the rest, as today). If f16's spill β€
|
| 77 |
+
T\*, f16 is chosen and the window spills it β **Policy C: prefer f16-window+spill over
|
| 78 |
+
quantizing** (user, 2026-07-11).
|
| 79 |
+
|
| 80 |
+
## T\* οΏ½οΏ½οΏ½ the negligible-drop tail band
|
| 81 |
+
|
| 82 |
+
Target drop = **20%** (user raised from 5% β at 5% it essentially never engaged because the
|
| 83 |
+
fixed streaming-engagement step already costs ~15%). T\* solved from a per-token drop model:
|
| 84 |
+
|
| 85 |
+
```
|
| 86 |
+
drop(tail) = ( c_engage + tail_bytes / bw_eff ) / t_resident
|
| 87 |
+
T*_bytes = ( 0.20 Β· t_resident β c_engage ) Β· bw_eff // β€ 0 β never spill β quantize
|
| 88 |
+
```
|
| 89 |
+
|
| 90 |
+
Terms and how each user requirement maps in:
|
| 91 |
+
|
| 92 |
+
- **`t_resident`** = `(W_active + KV_resident_bytes) / vram_bw` β per-token time with the cache
|
| 93 |
+
resident. `KV_resident` grows with context, so short-ctx β tiny `t_resident` β `c_engage`
|
| 94 |
+
dominates β `T*β€0` β quantize (never spill a short-ctx cache); long-ctx β spill viable.
|
| 95 |
+
**This is the ctx-value gate (req #2) β it falls out of the formula, no separate threshold.**
|
| 96 |
+
`W_active`/`KV_resident` are summed from the model's real per-layer KV-head structure
|
| 97 |
+
(`n_embd_k_gqa`/`n_embd_v_gqa`, global vs local heads) β **req #4 (attention structure)**.
|
| 98 |
+
- **`bw_eff`** = `min(pcie_bw_gbps, host_ram_bw_gbps)` β **req #3: RAMβGPU bandwidth matters,
|
| 99 |
+
not just PCIe.** On solidPC (PCIe4 ~6.5 + DDR4) PCIe binds; on bs2 (PCIe5 ~50 + DDR5) they
|
| 100 |
+
are comparable so `min` bites. `pcie_bw_gbps` exists (`common/pcie-profile.cpp`);
|
| 101 |
+
`host_ram_bw_gbps` is NEW (boot memcpy probe or dmidecode DDR type/speed).
|
| 102 |
+
- **`vram_bw`** = GPU memory bandwidth (**req E**), host-side from `nvidia-smi`/device props.
|
| 103 |
+
- **`c_engage`** = fixed streaming-path engagement cost (the β15% first-spill step, Β§5c β
|
| 104 |
+
~1.4 ms). **Boot micro-measured (user chose option B).** It is model- and DCA-dependent, so a
|
| 105 |
+
static constant is unreliable (Β§5c 256k shows β15% first-spill; the 12k clean p639 run showed
|
| 106 |
+
<1% β same host). See "c_engage probe" below.
|
| 107 |
+
- **DCA (req #5)**: DCA-on vs DCA-off have different knees (Β§5c: DCA-off linear
|
| 108 |
+
bandwidth-bound; DCA-on flat-then-sharp-cliff, post bug-2144). The probe runs in the actual
|
| 109 |
+
`cparams.dca_enabled` state, so `c_engage` and the post-knee ceiling are captured for the
|
| 110 |
+
live DCA setting automatically. A DCA-on validity ceiling clamps T\* to the pre-cliff band
|
| 111 |
+
(~800 MiB observed on bs2).
|
| 112 |
+
|
| 113 |
+
### Anchors (sanity)
|
| 114 |
+
- 3090 (Β§4/#588): `t β 23.6 ms + tail/6.5 GB/s` β T\*(20%) β a few tens of MiB β essentially
|
| 115 |
+
"quantize" on the slow link. β
|
| 116 |
+
- bs2 256k (Β§5c): β15% plateau holds to ~680 MiB then cliffs at ~1080 β T\*(20%) β up to the
|
| 117 |
+
~680 MiB plateau edge once `t_resident` is large. β
|
| 118 |
+
|
| 119 |
+
## c_engage boot probe (req B β boot micro-measure)
|
| 120 |
+
|
| 121 |
+
The tier is chosen in the kv-cache ctor *before* allocation, but timing `c_engage` needs a
|
| 122 |
+
decode-capable path. Two viable mechanisms (pick at implementation):
|
| 123 |
+
1. **Standalone synthetic streaming-FA microbench** β build a tiny ggml graph on the CUDA
|
| 124 |
+
backend: resident FA over a small K vs windowed FA (window + one-stride tail) over the same
|
| 125 |
+
shapes; time both; `c_engage = t_windowed β t_resident`. Self-contained, no model decode,
|
| 126 |
+
runs at ctor time. Preferred.
|
| 127 |
+
2. **Post-context two-phase** β build resident, run a few real decode steps resident vs
|
| 128 |
+
forced-tiny-tail, measure, then (only if the tier would change) rebuild. Most faithful but
|
| 129 |
+
double-allocates.
|
| 130 |
+
Cache the measured `c_engage` per `(device, dca_state, head_dim class)` in `.opencoti/` so
|
| 131 |
+
only the first boot pays the probe.
|
| 132 |
+
|
| 133 |
+
## Requirement #4 β honor an explicit `-ctk` as a block
|
| 134 |
+
|
| 135 |
+
**Simplification (2026-07-11): no new cparams fields / no cross-struct plumbing.** Auto-tier
|
| 136 |
+
runs in the ctor BEFORE it mutates `type_k`/`type_v`, and nothing else makes them non-f16 at
|
| 137 |
+
that point β so the *incoming* type already encodes explicitness. In
|
| 138 |
+
`opencoti_auto_select_kv_tier` (replacing the blanket `type_k != F16 || type_v != F16 β
|
| 139 |
+
return`):
|
| 140 |
+
- incoming `type_v != F16` β user set `-ctv` β **disable auto-tier entirely** (only rolling-KV
|
| 141 |
+
window stays auto). User.
|
| 142 |
+
- incoming `type_k != F16` (and `type_v == F16`) β user set `-ctk` β **hold K fixed**, walk
|
| 143 |
+
only the V axis (V-ladder `{f16, q8_0, q4_0}` under the pinned K).
|
| 144 |
+
- both F16 β full behavior above.
|
| 145 |
+
|
| 146 |
+
## Bandwidth acquisition β also no plumbing
|
| 147 |
+
|
| 148 |
+
- `pcie_bw_gbps` is ALREADY a `llama_kv_cache` ctor param (`cparams.pcie_bw_gbps`) β pass it to
|
| 149 |
+
the helper (one-arg change).
|
| 150 |
+
- `vram_bw_gbps` and `host_ram_bw_gbps` are measured **inside the helper**, host-side
|
| 151 |
+
(`nvidia-smi --query-gpu=β¦` bus/clock for VRAM bw; a one-shot `memcpy` bench for host DDR),
|
| 152 |
+
cached in `.opencoti/` β no new cparams fields, no arg.cpp / llama_context_params threading.
|
| 153 |
+
|
| 154 |
+
## File surface (all additive, opt-in, byte-identical when env unset)
|
| 155 |
+
|
| 156 |
+
**Contained to ONE file** (`src/llama-kv-cache.cpp`), plus one ctor-arg addition (pass the
|
| 157 |
+
existing `pcie_bw_gbps` to the helper). No `cparams.h` / `arg.cpp` /
|
| 158 |
+
`llama_context_params` changes β see the two simplifications above. `common/pcie-profile.*` may
|
| 159 |
+
optionally host the host-RAM memcpy probe for reuse, but it can also live in the helper.
|
| 160 |
+
|
| 161 |
+
- `src/llama-kv-cache.cpp` β `opencoti_auto_select_kv_tier`: explicit-axis handling (T1), a
|
| 162 |
+
host-side `vram_bw`/`host_ram_bw` probe (T2), the `c_engage` synthetic microbench (T3), the
|
| 163 |
+
T\* drop-model fit-condition + DCA branch (T4). One extra ctor arg: `pcie_bw_gbps`.
|
| 164 |
+
|
| 165 |
+
## Increment plan (all in `llama-kv-cache.cpp` unless noted)
|
| 166 |
+
|
| 167 |
+
1. **T1 β
** β explicit-axis handling in the helper (req #4): replace the blanket override guard
|
| 168 |
+
with the V-disables / K-holds / both-walks logic. No plumbing (incoming types encode
|
| 169 |
+
explicitness). Verified both hosts (bs2 + solidPC).
|
| 170 |
+
2. **T2 β
** β `host_ram_bw` host-side probe (req #3), static-cached (env
|
| 171 |
+
`OPENCOTI_KV_HOST_RAM_GBPS` override). `pcie_bw_gbps` passed from the ctor. Logs both +
|
| 172 |
+
`bw_eff = min(pcie, host_ram)`. **VRAM bw dropped β subsumed by the FA microbench** (which
|
| 173 |
+
reads KV from VRAM), so no separate device-memcpy probe. bug-2151: dead-store elision fixed.
|
| 174 |
+
3. **T3 β
** β `c_engage` boot probe (req B) β reused the existing FA compute microbench
|
| 175 |
+
(`opencoti_fa_compute_probe_ms`, #351) at two cell counts (8192 win, 256 floor) β
|
| 176 |
+
`fa_ms_cell` + `c_engage`. **HOST-ONLY** (dispatches to the existing DSO β no CUDA rebuild).
|
| 177 |
+
4. **T4 β
** β T\* drop-model fit-condition in the helper (20% default target, env
|
| 178 |
+
`OPENCOTI_KV_TSTAR_DROP`; cliff `OPENCOTI_KV_TSTAR_MAX_SPILL_MIB` default 800). Fast-path
|
| 179 |
+
guard keeps the common "f16 fits" boot byte-identical. `dca_enabled` plumbing deliberately
|
| 180 |
+
avoided β universal 800 MiB cliff instead (conservative DCA-on pre-cliff band).
|
| 181 |
+
5. **T5 (PENDING)** β RULER-niah quality gate: verify auto picks f16-window+spill at long-ctx
|
| 182 |
+
where it beats quant (niah + decode-tps β₯ the quant alternative). Needs a bs2 long-ctx cell
|
| 183 |
+
where T\* is genuinely large enough to KEEP f16 (on the 3090 T\* is too small to flip). Then
|
| 184 |
+
capture patch 0118 + bookkeeping. Build on both hosts already done.
|
| 185 |
+
|
| 186 |
+
## Standing gates
|
| 187 |
+
Correctness via RULER niah / logit-equiv, never greedy needle. Byte-identical no-op when
|
| 188 |
+
`OPENCOTI_KV_AUTO_TIER` unset. bs2 build (host-mostly; the synthetic probe may touch CUDA β if
|
| 189 |
+
so, DSO rebuild + dual-restamp + `nm -D`). Commit only when the user asks.
|