ManniX-ITA commited on
Commit
9cee049
Β·
verified Β·
1 Parent(s): d9f0f95

Upload folder using huggingface_hub

Browse files
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.