ManniX-ITA commited on
Commit
b69d9d8
Β·
verified Β·
1 Parent(s): aa9e938

Upload folder using huggingface_hub

Browse files
This view is limited to 50 files because it contains too many changes. Β  See raw diff
Files changed (50) hide show
  1. patches/0001-lazy-slot-context-defer.patch +247 -0
  2. patches/0002-lazy-slot-context-grow.patch +596 -0
  3. patches/0003-arg-assertion-bypass.patch +46 -0
  4. patches/0004-cuda-std-cpp17.patch +50 -0
  5. patches/0005-lazy-slot-context-shrink.patch +582 -0
  6. patches/0006-kv-reuse-prefix.patch +135 -0
  7. patches/0007-build-header-deps.patch +92 -0
  8. patches/0010-rest-kv-eviction.patch +476 -0
  9. patches/0020-headinfer-per-head.patch +520 -0
  10. patches/0030-neo-pipeline.patch +933 -0
  11. patches/0031-per-stream-split.patch +1513 -0
  12. patches/0032-m2-state-io.patch +205 -0
  13. patches/0033-cpy-tie-blck-align.patch +100 -0
  14. patches/0034-m2-pinned-host.patch +80 -0
  15. patches/0035-concat-q-block-aware.patch +209 -0
  16. patches/0036-pcie-probe-consume.patch +364 -0
  17. patches/0040-iqk-flash-attn.patch +0 -0
  18. patches/0042-fused-moe.patch +527 -0
  19. patches/0070-rolling-kv.patch +0 -0
  20. patches/0071-state-io-cosmocc.patch +88 -0
  21. patches/0072-poly-kv-pool.patch +98 -0
  22. patches/0073-turboquant-kv.patch +0 -0
  23. patches/0075-d512-turbo-vec.patch +121 -0
  24. patches/0078-dca.patch +0 -0
  25. patches/0079-gemma-assistant-mtp.patch +0 -0
  26. patches/0080-mtp-d512-fa.patch +155 -0
  27. patches/0081-tcq-q6-asym-kv.patch +0 -0
  28. patches/0082-cuda-build-parallelism.patch +320 -0
  29. patches/0083-tcq-warp-encode.patch +868 -0
  30. patches/0084-cuda-graphs.patch +23 -0
  31. patches/0085-dca-quant-prefill-inlaunch.patch +237 -0
  32. patches/0086-f16-moe-mmf-prefill.patch +49 -0
  33. patches/0087-dca-yarn-factor.patch +56 -0
  34. patches/0088-sparsev-autopolicy.patch +63 -0
  35. patches/0089-sparse-attn-vslash-foundation.patch +0 -0
  36. patches/0090-rolling-kv-compute-reserve.patch +333 -0
  37. patches/0091-multi-session-graph-nodes.patch +50 -0
  38. patches/0092-cuda-build-ccache.patch +71 -0
  39. patches/0093-mtp-dualctx-fused-nextn.patch +0 -0
  40. patches/0094-tinyblas-small-gemm.patch +31 -0
  41. patches/0095-prefill-checkpoint-align.patch +145 -0
  42. patches/0096-remove-single-ctx-gemma-mtp.patch +0 -0
  43. patches/0097-native-elf-cmake.patch +140 -0
  44. patches/0098-rolling-kv-lse-decode.patch +97 -0
  45. patches/0099-dca-gemma4-wiring.patch +140 -0
  46. patches/0100-dca-single-chunk-defer.patch +36 -0
  47. patches/0101-kv-attn-rot-inherit.patch +49 -0
  48. patches/0102-fattn-bulk-h2d-scratch-ring.patch +154 -0
  49. patches/0103-kv-cpu-fa-probe-guard.patch +67 -0
  50. patches/0104-fattn-q6q4-vec-instance.patch +71 -0
patches/0001-lazy-slot-context-defer.patch ADDED
@@ -0,0 +1,247 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ From: opencoti
2
+ Subject: [PATCH 1/N] lazy slot-context allocation β€” Phase 1 (deferred zero-fill)
3
+
4
+ Phase 1 of the F4 M3 lazy slot-context rollout. The eager
5
+ `ggml_backend_buffer_clear(buf, 0)` memset that commits every KV
6
+ buffer page to RSS at construction time is split out into a new
7
+ `ensure_cleared()` method invoked from inference entry points
8
+ (init_batch, prepare, update, state_read).
9
+
10
+ The buffer's malloc happens eagerly at construction time so the
11
+ backend scheduler's `sched_reserve` (which runs at llama_context
12
+ construction, before any batch) can resolve `tensor->buffer` for
13
+ every KV tensor. On Linux, malloc is essentially free for unused
14
+ ranges β€” it reserves address space without committing pages.
15
+ Pages only commit to RSS when first written. The eager memset(0)
16
+ was what wrote every page, so removing it from the constructor
17
+ moves the RSS commit to the first real inference call.
18
+
19
+ Effect for the production embedder (qwen3-0.6b,
20
+ `--parallel 2 --ctx-size 16384`, CPU mode): the KV cache's
21
+ ~1,792 MiB no longer commits at process startup. Idle RSS drops
22
+ from ~3,478 MiB to roughly model + compute scratch only. First
23
+ request pays the memset cost (~1 s for 1.8 GiB), then warm-state
24
+ RSS matches the un-patched baseline. Phase 1 alone is a
25
+ cold-start win; Phases 2 (grow-on-demand) and 3 (shrink-on-idle)
26
+ target warm-state RSS.
27
+
28
+ Smoke validation (Qwen2-Math-1.5B Q4_K_M, --parallel 2 -c 4096
29
+ CPU mode): startup emits
30
+
31
+ llama_kv_cache: CPU KV buffer size = 112.00 MiB
32
+ llama_kv_cache: KV cache zero-fill deferred until first batch
33
+
34
+ First /v1/chat/completions request emits
35
+
36
+ ensure_cleared: CPU KV buffer zero-fill = 112.00 MiB (deferred)
37
+
38
+ and produces the expected response.
39
+
40
+ Coverage of ensure_cleared() call sites:
41
+ - init_batch (inference entry)
42
+ - prepare (iswa wrapper calls prepare() directly on each inner
43
+ kv_cache without going through init_batch; see
44
+ llama-kv-cache-iswa.cpp:126-159)
45
+ - update (cross-stream seq_cp resolution + RoPE shift)
46
+ - state_read (saved-state restore writes tensor data)
47
+
48
+ state_write is left const + un-cleared-safe: empty v_cells write
49
+ zero-cell-count records that state_read's
50
+ `if (cell_count == 0) continue` already handles.
51
+
52
+ clear(data=true) on an un-cleared cache skips the buffer memset
53
+ deliberately β€” v_cells.reset() above already represents "no
54
+ data," and committing RSS just to zero it would defeat Phase 1's
55
+ purpose.
56
+
57
+ The no_alloc model path is unchanged: zero-byte dummy buffers
58
+ are assigned per tensor at construction and trivially "cleared"
59
+ (zero-byte memset). `cleared` is set to true in that case.
60
+
61
+ Bench: docs/decisions/0001-lazy-slot-context.md Β§6.2 Phase 1 row.
62
+ Milestone: F4 M3 β€” see docs/features/memory_embedder.md
63
+ Design: docs/decisions/0001-lazy-slot-context.md
64
+ Upstreaming: deferred; propose to llamafile + llama.cpp
65
+ maintainers after F4 M3's full phase-set ships and bench
66
+ numbers are public. This is a generally-useful patch.
67
+
68
+ ---
69
+ diff --git a/llama.cpp/src/llama-kv-cache.cpp b/llama.cpp/src/llama-kv-cache.cpp
70
+ --- a/llama.cpp/src/llama-kv-cache.cpp
71
+ +++ b/llama.cpp/src/llama-kv-cache.cpp
72
+ @@ -250,7 +250,15 @@ llama_kv_cache::llama_kv_cache(
73
+ }
74
+ }
75
+
76
+ - // allocate tensors and initialize the buffers to avoid NaNs in the padding
77
+ + // opencoti F4 M3 Phase 1 β€” see docs/decisions/0001-lazy-slot-context.md
78
+ + // Eager malloc, deferred zero-fill. The address-space reservation done
79
+ + // by ggml_backend_alloc_ctx_tensors_from_buft is essentially free on
80
+ + // Linux β€” pages are not committed to RSS until they are first written.
81
+ + // The `ggml_backend_buffer_clear(buf, 0)` memset below is what commits
82
+ + // every page. By skipping it for the non-no_alloc path we keep the
83
+ + // scheduler-visible tensor->buffer wiring intact (sched_reserve runs
84
+ + // at construction time and asserts buffer_id >= 0) while moving the
85
+ + // RSS commit to the first ensure_cleared() call.
86
+ for (auto & [buft, ctx] : ctx_map) {
87
+ ggml_backend_buffer_t buf;
88
+ if (model.hparams.no_alloc) {
89
+ @@ -259,7 +267,7 @@ llama_kv_cache::llama_kv_cache(
90
+ t->buffer = buf; // set dummy buffer for KV cache so that the backend scheduler won't try to allocate it
91
+ }
92
+ } else {
93
+ - buf = ggml_backend_alloc_ctx_tensors_from_buft(ctx.get(), buft); // real buffer
94
+ + buf = ggml_backend_alloc_ctx_tensors_from_buft(ctx.get(), buft); // real buffer (malloc'd but not zeroed yet)
95
+ }
96
+ if (!buf) {
97
+ throw std::runtime_error("failed to allocate buffer for kv cache");
98
+ @@ -267,10 +275,23 @@ llama_kv_cache::llama_kv_cache(
99
+
100
+ LLAMA_LOG_INFO("%s: %10s KV buffer size = %8.2f MiB\n", __func__, ggml_backend_buffer_name(buf), ggml_backend_buffer_get_size(buf)/1024.0/1024.0);
101
+
102
+ - ggml_backend_buffer_clear(buf, 0);
103
+ + if (model.hparams.no_alloc) {
104
+ + // Dummy buffer is size 0; clearing is a no-op but keeps the
105
+ + // upstream invariant that constructed buffers are zeroed.
106
+ + ggml_backend_buffer_clear(buf, 0);
107
+ + }
108
+ + // else: leave the buffer un-cleared. ensure_cleared() will zero it
109
+ + // on the first inference entry point that touches tensor data.
110
+ +
111
+ ctxs_bufs.emplace_back(std::move(ctx), buf);
112
+ }
113
+
114
+ + if (model.hparams.no_alloc) {
115
+ + cleared = true;
116
+ + } else {
117
+ + LLAMA_LOG_INFO("%s: KV cache zero-fill deferred until first batch\n", __func__);
118
+ + }
119
+ +
120
+ {
121
+ const size_t memory_size_k = size_k_bytes();
122
+ const size_t memory_size_v = size_v_bytes();
123
+ @@ -327,6 +348,22 @@ llama_kv_cache::llama_kv_cache(
124
+ debug = LLAMA_KV_CACHE_DEBUG ? atoi(LLAMA_KV_CACHE_DEBUG) : 0;
125
+ }
126
+
127
+ +// opencoti F4 M3 Phase 1 β€” see docs/decisions/0001-lazy-slot-context.md
128
+ +void llama_kv_cache::ensure_cleared() {
129
+ + if (cleared) {
130
+ + return;
131
+ + }
132
+ +
133
+ + for (auto & [_, buf] : ctxs_bufs) {
134
+ + LLAMA_LOG_INFO("%s: %10s KV buffer zero-fill = %8.2f MiB (deferred)\n", __func__,
135
+ + ggml_backend_buffer_name(buf.get()),
136
+ + ggml_backend_buffer_get_size(buf.get())/1024.0/1024.0);
137
+ + ggml_backend_buffer_clear(buf.get(), 0);
138
+ + }
139
+ +
140
+ + cleared = true;
141
+ +}
142
+ +
143
+ void llama_kv_cache::clear(bool data) {
144
+ for (uint32_t s = 0; s < n_stream; ++s) {
145
+ v_cells[s].reset();
146
+ @@ -334,8 +371,14 @@ void llama_kv_cache::clear(bool data) {
147
+ }
148
+
149
+ if (data) {
150
+ - for (auto & [_, buf] : ctxs_bufs) {
151
+ - ggml_backend_buffer_clear(buf.get(), 0);
152
+ + // opencoti F4 M3 Phase 1: if the deferred zero-fill has not happened
153
+ + // yet there is nothing to clear β€” v_cells.reset() above already
154
+ + // satisfies the logical "data cleared" state. Don't commit RSS just
155
+ + // to zero pages that have never been written.
156
+ + if (cleared) {
157
+ + for (auto & [_, buf] : ctxs_bufs) {
158
+ + ggml_backend_buffer_clear(buf.get(), 0);
159
+ + }
160
+ }
161
+ }
162
+ }
163
+ @@ -630,6 +673,9 @@ llama_memory_context_ptr llama_kv_cache::init_batch(
164
+ bool embd_all) {
165
+ GGML_UNUSED(embd_all);
166
+
167
+ + // opencoti F4 M3 Phase 1 β€” see docs/decisions/0001-lazy-slot-context.md
168
+ + ensure_cleared();
169
+ +
170
+ do {
171
+ balloc.split_reset();
172
+
173
+ @@ -674,6 +720,12 @@ llama_memory_context_ptr llama_kv_cache::init_update(llama_context * lctx, bool
174
+ }
175
+
176
+ llama_kv_cache::slot_info_vec_t llama_kv_cache::prepare(const std::vector<llama_ubatch> & ubatches) {
177
+ + // opencoti F4 M3 Phase 1 β€” see docs/decisions/0001-lazy-slot-context.md
178
+ + // The iswa wrapper calls prepare() directly on each inner kv_cache without
179
+ + // going through init_batch(), so the ensure_cleared() in init_batch alone
180
+ + // is not enough. Calling here covers both paths and is idempotent.
181
+ + ensure_cleared();
182
+ +
183
+ llama_kv_cache::slot_info_vec_t res;
184
+
185
+ struct state_t {
186
+ @@ -744,6 +796,15 @@ bool llama_kv_cache::update(llama_context * lctx, bool do_shift, const stream_co
187
+
188
+ auto * sched = lctx->get_sched();
189
+
190
+ + // opencoti F4 M3 Phase 1 β€” see docs/decisions/0001-lazy-slot-context.md
191
+ + // update() can be reached without init_batch first (e.g. update-only paths
192
+ + // for cross-stream copy or RoPE shift). When that happens with the buffer
193
+ + // still deferred we have nothing to copy or shift β€” make the dependency
194
+ + // explicit so the tensor->data dereferences below are safe.
195
+ + if (!sc_info.empty() || do_shift) {
196
+ + ensure_cleared();
197
+ + }
198
+ +
199
+ if (!sc_info.empty()) {
200
+ assert(n_stream > 1 && "stream copy should never happen with a single stream");
201
+
202
+ @@ -1900,6 +1961,11 @@ void llama_kv_cache::state_read(llama_io_read_i & io, llama_seq_id seq_id, llama
203
+
204
+ GGML_ASSERT(seq_id == -1 || (seq_id >= 0 && (size_t) seq_id < seq_to_stream.size()));
205
+
206
+ + // opencoti F4 M3 Phase 1 β€” see docs/decisions/0001-lazy-slot-context.md
207
+ + // state_read writes the saved KV blob into our tensors, which requires
208
+ + // backing storage. Force the deferred allocation before we read.
209
+ + ensure_cleared();
210
+ +
211
+ uint32_t n_stream_cur;
212
+ io.read(&n_stream_cur, sizeof(n_stream_cur));
213
+ if (n_stream_cur != n_stream) {
214
+ diff --git a/llama.cpp/src/llama-kv-cache.h b/llama.cpp/src/llama-kv-cache.h
215
+ --- a/llama.cpp/src/llama-kv-cache.h
216
+ +++ b/llama.cpp/src/llama-kv-cache.h
217
+ @@ -152,6 +152,16 @@ public:
218
+
219
+ bool get_has_shift() const;
220
+
221
+ + // opencoti F4 M3 Phase 1 β€” see docs/decisions/0001-lazy-slot-context.md
222
+ + // Idempotent: zeroes the KV-cache backend buffers on first call.
223
+ + // The buffers are malloc'd eagerly at construction (so the scheduler
224
+ + // can resolve tensor->buffer at reserve time) but NOT cleared β€” on
225
+ + // Linux, malloc reserves address space without committing pages, so
226
+ + // RSS stays low until something writes. This method does the memset
227
+ + // that commits the pages. Called from inference entry points
228
+ + // (init_batch, prepare, update, state_read).
229
+ + void ensure_cleared();
230
+ +
231
+ ggml_type type_k() const;
232
+ ggml_type type_v() const;
233
+
234
+ @@ -256,6 +266,13 @@ private:
235
+ // ggml contexts for the KV cache along with the allocated backend buffers:
236
+ std::vector<std::pair<ggml_context_ptr, ggml_backend_buffer_ptr>> ctxs_bufs;
237
+
238
+ + // opencoti F4 M3 Phase 1 β€” see docs/decisions/0001-lazy-slot-context.md
239
+ + // True once the backend buffers in ctxs_bufs have been zero-filled.
240
+ + // The constructor sets this true for no_alloc models (whose dummy
241
+ + // buffers are size 0 β€” clearing is a no-op) and false otherwise. The
242
+ + // first ensure_cleared() call zeroes the buffers and flips this true.
243
+ + bool cleared = false;
244
+ +
245
+ // the current index from where we start searching for a free slot in the ring buffer of KV cells (see find_slot())
246
+ // note: this is not part of the KV state and it's only used to speed-up the find_slot() method
247
+ std::vector<uint32_t> v_heads;
patches/0002-lazy-slot-context-grow.patch ADDED
@@ -0,0 +1,596 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ From: opencoti
2
+ Subject: [PATCH 2/N] lazy slot-context allocation β€” Phase 2 (grow on demand + partial zero-fill)
3
+
4
+ Phase 2 of the F4 M3 lazy slot-context rollout. Builds on Phase 1
5
+ (deferred zero-fill) by adding a soft allocation cap that
6
+ find_slot honors, a grow-on-demand path that bumps the cap when
7
+ real prompts need more cells, and a partial-range zero-fill so
8
+ RSS commits scale with actual workload β€” not with the configured
9
+ n_ctx_slot Γ— slot_count product.
10
+
11
+ The end-state for the production embedder
12
+ (qwen3-0.6b, --parallel 2 --ctx-size 16384, CPU mode):
13
+ - With --slot-initial-ctx 0 (the default) Phase 2 is a no-op
14
+ layered on Phase 1: cap == n_ctx_seq from construction; full
15
+ buffer cleared on first batch (matches Phase 1 RSS profile).
16
+ - With --slot-initial-ctx 4096 (the recommended embedder
17
+ setting) the warm RSS scales with whatever cells real prompts
18
+ have touched. A run that only ever embeds 8–56-token requests
19
+ commits ~4096 Γ— per-cell bytes, not the full 16384.
20
+
21
+ The patch threads `slot_initial_ctx` through:
22
+ - public API: llama_context_params (include/llama.h)
23
+ - internal: llama_cparams (src/llama-cparams.h)
24
+ - ctor chain: llama_context::ctor (src/llama-context.cpp)
25
+ llama_kv_cache::ctor (src/llama-kv-cache.{h,cpp})
26
+ llama_kv_cache_iswa::ctor (src/llama-kv-cache-iswa.{h,cpp})
27
+ llama_model::create_memory call sites (src/llama-model.cpp)
28
+ - hybrid mem: src/llama-memory-hybrid.cpp + -iswa.cpp pass 0
29
+ (kept on Phase-1 behavior β€” recurrent/hybrid models
30
+ are not the F4 embedder target)
31
+ - CLI: common_params.slot_initial_ctx + --slot-initial-ctx
32
+ (common/common.{h,cpp}, common/arg.cpp)
33
+
34
+ Mechanism (inside llama_kv_cache):
35
+ - kv_size_max: hard cap (== constructor's kv_size).
36
+ - kv_size_alloc: soft cap; cells [0, kv_size_alloc) visible to
37
+ find_slot. Starts at min(slot_initial_ctx,
38
+ kv_size_max) β€” Phase 1 default of 0 collapses
39
+ to kv_size_max so find_slot sees everything.
40
+ - n_cells_cleared: half-open range of cells already zero-filled.
41
+ Phase 1's `bool cleared` is replaced; the cells
42
+ beyond n_cells_cleared have malloc'd address
43
+ space but uncommitted pages.
44
+ - ensure_cleared(up_to):
45
+ Partial memset of cells [n_cells_cleared, up_to) on each
46
+ layer's K and V tensor across every stream, using
47
+ ggml_backend_tensor_memset (host-or-device dispatch). The
48
+ common inference path passes up_to=0 which expands to the
49
+ current soft cap; state_read passes up_to=kv_size_max for
50
+ full coverage on the deserialize path.
51
+ - ensure_capacity(cells_needed):
52
+ Geometric grow (next-pow2, n_pad-aligned) of kv_size_alloc
53
+ up to kv_size_max. Monotonic β€” never shrinks (Phase 3 adds
54
+ shrink-on-idle).
55
+ - prepare(ubatches):
56
+ When find_slot fails inside the current soft cap, retries
57
+ after ensure_capacity(used_max + ubatch.n_tokens) +
58
+ ensure_cleared(). Bounded by kv_size_max; a genuine
59
+ overflow at the hard cap propagates up unchanged.
60
+
61
+ Bench: docs/decisions/0001-lazy-slot-context.md Β§6.2 Phase 2 row.
62
+ Milestone: F4 M3 β€” see docs/features/memory_embedder.md
63
+ Design: docs/decisions/0001-lazy-slot-context.md
64
+ Upstreaming: deferred; propose to llamafile + llama.cpp
65
+ maintainers after F4 M3's full phase-set ships and bench
66
+ numbers are public.
67
+
68
+ ---
69
+ diff --git a/llama.cpp/common/arg.cpp b/llama.cpp/common/arg.cpp
70
+ --- a/llama.cpp/common/arg.cpp
71
+ +++ b/llama.cpp/common/arg.cpp
72
+ @@ -1361,6 +1361,16 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
73
+ params.kv_unified = value;
74
+ }
75
+ ).set_env("LLAMA_ARG_KV_UNIFIED").set_examples({LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_PERPLEXITY, LLAMA_EXAMPLE_BATCHED, LLAMA_EXAMPLE_BENCH, LLAMA_EXAMPLE_PARALLEL}));
76
+ + add_opt(common_arg(
77
+ + // opencoti F4 M3 Phase 2 β€” see docs/decisions/0001-lazy-slot-context.md
78
+ + {"--slot-initial-ctx"}, "N",
79
+ + string_format("initial KV-cell allocation per sequence (default: %d, 0 = full n_ctx_seq, "
80
+ + "positive values cap initial allocation and enable grow-on-demand)",
81
+ + params.slot_initial_ctx),
82
+ + [](common_params & params, int value) {
83
+ + params.slot_initial_ctx = value;
84
+ + }
85
+ + ).set_env("LLAMA_ARG_SLOT_INITIAL_CTX").set_examples({LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_EMBEDDING, LLAMA_EXAMPLE_PARALLEL}));
86
+ add_opt(common_arg(
87
+ {"--cache-idle-slots"},
88
+ {"--no-cache-idle-slots"},
89
+ diff --git a/llama.cpp/common/common.cpp b/llama.cpp/common/common.cpp
90
+ --- a/llama.cpp/common/common.cpp
91
+ +++ b/llama.cpp/common/common.cpp
92
+ @@ -1628,6 +1628,8 @@ struct llama_context_params common_context_params_to_llama(const common_params &
93
+
94
+ cparams.n_ctx = params.n_ctx;
95
+ cparams.n_seq_max = params.n_parallel;
96
+ + // opencoti F4 M3 Phase 2 β€” see docs/decisions/0001-lazy-slot-context.md
97
+ + cparams.slot_initial_ctx = params.slot_initial_ctx < 0 ? 0u : (uint32_t) params.slot_initial_ctx;
98
+ cparams.n_rs_seq = params.speculative.need_n_rs_seq();
99
+ cparams.n_batch = params.n_batch;
100
+ cparams.n_ubatch = params.n_ubatch;
101
+ diff --git a/llama.cpp/common/common.h b/llama.cpp/common/common.h
102
+ --- a/llama.cpp/common/common.h
103
+ +++ b/llama.cpp/common/common.h
104
+ @@ -425,6 +425,11 @@ struct ggml_opt_optimizer_params common_opt_lr_pars(void * userdata);
105
+ struct common_params {
106
+ int32_t n_predict = -1; // max. number of new tokens to predict, -1 == no limit
107
+ int32_t n_ctx = 0; // context size, 0 == context the model was trained with
108
+ + // opencoti F4 M3 Phase 2 β€” see docs/decisions/0001-lazy-slot-context.md
109
+ + // Initial KV-cell allocation per sequence. 0 = full n_ctx_seq allocation
110
+ + // (Phase 1 behavior β€” only zero-fill is deferred). Positive values cap
111
+ + // the initial allocation and enable grow-on-demand up to n_ctx_seq.
112
+ + int32_t slot_initial_ctx = 0;
113
+ int32_t n_batch = 2048; // logical batch size for prompt processing (must be >=32 to use BLAS)
114
+ int32_t n_ubatch = 512; // physical batch size for prompt processing (must be >=32 to use BLAS)
115
+ int32_t n_keep = 0; // number of tokens to keep from initial prompt
116
+ diff --git a/llama.cpp/include/llama.h b/llama.cpp/include/llama.h
117
+ --- a/llama.cpp/include/llama.h
118
+ +++ b/llama.cpp/include/llama.h
119
+ @@ -338,6 +338,14 @@ extern "C" {
120
+ uint32_t n_batch; // logical maximum batch size that can be submitted to llama_decode
121
+ uint32_t n_ubatch; // physical maximum batch size
122
+ uint32_t n_seq_max; // max number of sequences (i.e. distinct states for recurrent models)
123
+ + // opencoti F4 M3 Phase 2 β€” see docs/decisions/0001-lazy-slot-context.md
124
+ + // Initial KV-cell allocation per sequence. 0 = behave like Phase 1
125
+ + // (allocate the full n_ctx_seq up front, only zero-fill is deferred).
126
+ + // Positive values cap the initial allocation at min(slot_initial_ctx,
127
+ + // n_ctx_seq); subsequent prompts that exceed this cap trigger a grow
128
+ + // up to next_pow2 (still capped at n_ctx_seq). Reduces warm-state RSS
129
+ + // when typical prompts are much shorter than n_ctx_seq.
130
+ + uint32_t slot_initial_ctx;
131
+ uint32_t n_rs_seq; // number of recurrent-state snapshots per seq for rollback (0 = no rollback) [EXPERIMENTAL]
132
+ int32_t n_threads; // number of threads to use for generation
133
+ int32_t n_threads_batch; // number of threads to use for batch processing
134
+ diff --git a/llama.cpp/src/llama-context.cpp b/llama.cpp/src/llama-context.cpp
135
+ --- a/llama.cpp/src/llama-context.cpp
136
+ +++ b/llama.cpp/src/llama-context.cpp
137
+ @@ -58,6 +58,9 @@ llama_context::llama_context(
138
+ cparams.n_rs_seq = 0;
139
+ }
140
+
141
+ + // opencoti F4 M3 Phase 2 β€” see docs/decisions/0001-lazy-slot-context.md
142
+ + cparams.slot_initial_ctx = params.slot_initial_ctx;
143
+ +
144
+ cparams.n_threads = params.n_threads;
145
+ cparams.n_threads_batch = params.n_threads_batch;
146
+ cparams.yarn_ext_factor = params.yarn_ext_factor >= 0.0f ? params.yarn_ext_factor : hparams.yarn_ext_factor;
147
+ @@ -3346,6 +3349,7 @@ llama_context_params llama_context_default_params() {
148
+ /*.n_batch =*/ 2048,
149
+ /*.n_ubatch =*/ 512,
150
+ /*.n_seq_max =*/ 1,
151
+ + /*.slot_initial_ctx =*/ 0,
152
+ /*.n_rs_seq =*/ 0,
153
+ /*.n_threads =*/ GGML_DEFAULT_N_THREADS, // TODO: better default
154
+ /*.n_threads_batch =*/ GGML_DEFAULT_N_THREADS,
155
+ diff --git a/llama.cpp/src/llama-cparams.h b/llama.cpp/src/llama-cparams.h
156
+ --- a/llama.cpp/src/llama-cparams.h
157
+ +++ b/llama.cpp/src/llama-cparams.h
158
+ @@ -12,6 +12,10 @@ struct llama_cparams {
159
+ uint32_t n_batch;
160
+ uint32_t n_ubatch;
161
+ uint32_t n_seq_max;
162
+ + // opencoti F4 M3 Phase 2 β€” see docs/decisions/0001-lazy-slot-context.md
163
+ + // 0 = full n_ctx_seq allocation. Positive value caps the initial KV-cell
164
+ + // allocation per sequence and enables grow-on-demand up to n_ctx_seq.
165
+ + uint32_t slot_initial_ctx;
166
+ uint32_t n_rs_seq; // number of recurrent-state snapshots per seq for rollback
167
+ int32_t n_threads; // number of threads to use for generation
168
+ int32_t n_threads_batch; // number of threads to use for batch processing
169
+ diff --git a/llama.cpp/src/llama-kv-cache-iswa.cpp b/llama.cpp/src/llama-kv-cache-iswa.cpp
170
+ --- a/llama.cpp/src/llama-kv-cache-iswa.cpp
171
+ +++ b/llama.cpp/src/llama-kv-cache-iswa.cpp
172
+ @@ -20,6 +20,7 @@ llama_kv_cache_iswa::llama_kv_cache_iswa(
173
+ bool swa_full,
174
+ bool unified,
175
+ uint32_t kv_size,
176
+ + uint32_t kv_size_initial,
177
+ uint32_t n_seq_max,
178
+ uint32_t n_ubatch,
179
+ uint32_t n_pad,
180
+ @@ -61,14 +62,14 @@ llama_kv_cache_iswa::llama_kv_cache_iswa(
181
+
182
+ kv_base = std::make_unique<llama_kv_cache>(
183
+ model, type_k, type_v,
184
+ - v_trans, offload, unified, size_base, n_seq_max, n_pad,
185
+ + v_trans, offload, unified, size_base, kv_size_initial, n_seq_max, n_pad,
186
+ 0, LLAMA_SWA_TYPE_NONE, filter_base, reuse);
187
+
188
+ LLAMA_LOG_INFO("%s: creating SWA KV cache, size = %u cells\n", __func__, size_swa);
189
+
190
+ kv_swa = std::make_unique<llama_kv_cache>(
191
+ model, type_k, type_v,
192
+ - v_trans, offload, unified, size_swa, n_seq_max, n_pad,
193
+ + v_trans, offload, unified, size_swa, kv_size_initial, n_seq_max, n_pad,
194
+ hparams.n_swa, hparams.swa_type, filter_swa, reuse);
195
+ }
196
+
197
+ diff --git a/llama.cpp/src/llama-kv-cache-iswa.h b/llama.cpp/src/llama-kv-cache-iswa.h
198
+ --- a/llama.cpp/src/llama-kv-cache-iswa.h
199
+ +++ b/llama.cpp/src/llama-kv-cache-iswa.h
200
+ @@ -22,6 +22,9 @@ public:
201
+ bool swa_full,
202
+ bool unified,
203
+ uint32_t kv_size,
204
+ + // opencoti F4 M3 Phase 2 β€” see docs/decisions/0001-lazy-slot-context.md
205
+ + // Forwarded to both kv_base and kv_swa. 0 = full kv_size.
206
+ + uint32_t kv_size_initial,
207
+ uint32_t n_seq_max,
208
+ uint32_t n_ubatch,
209
+ uint32_t n_pad,
210
+ diff --git a/llama.cpp/src/llama-kv-cache.cpp b/llama.cpp/src/llama-kv-cache.cpp
211
+ --- a/llama.cpp/src/llama-kv-cache.cpp
212
+ +++ b/llama.cpp/src/llama-kv-cache.cpp
213
+ @@ -85,6 +85,7 @@ llama_kv_cache::llama_kv_cache(
214
+ bool offload,
215
+ bool unified,
216
+ uint32_t kv_size,
217
+ + uint32_t kv_size_initial,
218
+ uint32_t n_seq_max,
219
+ uint32_t n_pad,
220
+ uint32_t n_swa,
221
+ @@ -96,6 +97,24 @@ llama_kv_cache::llama_kv_cache(
222
+
223
+ GGML_ASSERT(kv_size % n_pad == 0);
224
+
225
+ + // opencoti F4 M3 Phase 2 β€” see docs/decisions/0001-lazy-slot-context.md
226
+ + // Initialize the hard cap and the soft cap. kv_size_initial==0 (the
227
+ + // legacy / Phase-1 default) means "no soft cap" so the whole kv_size
228
+ + // is visible to find_slot. A positive value is clamped to [n_pad,
229
+ + // kv_size] and rounded up to the next n_pad multiple.
230
+ + kv_size_max = kv_size;
231
+ + if (kv_size_initial == 0) {
232
+ + kv_size_alloc = kv_size_max;
233
+ + } else {
234
+ + uint32_t cap = std::min(kv_size_initial, kv_size_max);
235
+ + cap = std::max(cap, n_pad);
236
+ + cap = ((cap + n_pad - 1) / n_pad) * n_pad;
237
+ + cap = std::min(cap, kv_size_max);
238
+ + kv_size_alloc = cap;
239
+ + LLAMA_LOG_INFO("%s: slot_initial_ctx = %u cells (cap = %u)\n",
240
+ + __func__, kv_size_alloc, kv_size_max);
241
+ + }
242
+ +
243
+ const uint32_t n_layer_kv = hparams.n_layer_kv();
244
+
245
+ // define a comparator for the buft -> ctx map to ensure that the order is well-defined:
246
+ @@ -287,7 +306,9 @@ llama_kv_cache::llama_kv_cache(
247
+ }
248
+
249
+ if (model.hparams.no_alloc) {
250
+ - cleared = true;
251
+ + // opencoti F4 M3 Phase 1+2 β€” dummy buffers are size 0; treat all
252
+ + // cells as "already cleared" so ensure_cleared is a no-op.
253
+ + n_cells_cleared = kv_size_max;
254
+ } else {
255
+ LLAMA_LOG_INFO("%s: KV cache zero-fill deferred until first batch\n", __func__);
256
+ }
257
+ @@ -348,20 +369,81 @@ llama_kv_cache::llama_kv_cache(
258
+ debug = LLAMA_KV_CACHE_DEBUG ? atoi(LLAMA_KV_CACHE_DEBUG) : 0;
259
+ }
260
+
261
+ -// opencoti F4 M3 Phase 1 β€” see docs/decisions/0001-lazy-slot-context.md
262
+ -void llama_kv_cache::ensure_cleared() {
263
+ - if (cleared) {
264
+ +// opencoti F4 M3 Phase 1+2 β€” see docs/decisions/0001-lazy-slot-context.md
265
+ +void llama_kv_cache::ensure_cleared(uint32_t up_to_cells) {
266
+ + // up_to_cells == 0 means "clear up to the current soft cap". Callers
267
+ + // that need full coverage (state_read) pass kv_size_max.
268
+ + const uint32_t target = up_to_cells == 0
269
+ + ? kv_size_alloc
270
+ + : std::min(up_to_cells, kv_size_max);
271
+ +
272
+ + if (n_cells_cleared >= target) {
273
+ return;
274
+ }
275
+
276
+ - for (auto & [_, buf] : ctxs_bufs) {
277
+ - LLAMA_LOG_INFO("%s: %10s KV buffer zero-fill = %8.2f MiB (deferred)\n", __func__,
278
+ - ggml_backend_buffer_name(buf.get()),
279
+ - ggml_backend_buffer_get_size(buf.get())/1024.0/1024.0);
280
+ - ggml_backend_buffer_clear(buf.get(), 0);
281
+ + const uint32_t a = n_cells_cleared;
282
+ + const uint32_t b = target;
283
+ +
284
+ + // Per-layer, per-stream partial memset of the K and V tensors.
285
+ + // ggml_backend_tensor_memset handles the host/device dispatch β€” for the
286
+ + // CPU backend (the only one the embedder uses today) this is a direct
287
+ + // memset on the malloc'd buffer; for offloaded buffers it's a backend
288
+ + // op. Either way only bytes for cells [a, b) get touched, so on Linux
289
+ + // only those pages get committed to RSS.
290
+ + for (const auto & layer : layers) {
291
+ + for (uint32_t st = 0; st < n_stream; ++st) {
292
+ + if (layer.k) {
293
+ + const size_t off = (size_t) st * (size_t) layer.k->nb[2]
294
+ + + (size_t) a * (size_t) layer.k->nb[1];
295
+ + const size_t sz = (size_t) (b - a) * (size_t) layer.k->nb[1];
296
+ + ggml_backend_tensor_memset(layer.k, 0, off, sz);
297
+ + }
298
+ + if (layer.v) {
299
+ + const size_t off = (size_t) st * (size_t) layer.v->nb[2]
300
+ + + (size_t) a * (size_t) layer.v->nb[1];
301
+ + const size_t sz = (size_t) (b - a) * (size_t) layer.v->nb[1];
302
+ + ggml_backend_tensor_memset(layer.v, 0, off, sz);
303
+ + }
304
+ + }
305
+ + }
306
+ +
307
+ + LLAMA_LOG_INFO("%s: cleared KV cells [%u, %u) of %u (%s)\n",
308
+ + __func__, a, b, kv_size_max,
309
+ + up_to_cells == 0 ? "soft cap" : "full");
310
+ +
311
+ + n_cells_cleared = b;
312
+ +}
313
+ +
314
+ +// opencoti F4 M3 Phase 2 β€” see docs/decisions/0001-lazy-slot-context.md
315
+ +bool llama_kv_cache::ensure_capacity(uint32_t cells_needed) {
316
+ + if (cells_needed <= kv_size_alloc) {
317
+ + return true;
318
+ + }
319
+ +
320
+ + if (kv_size_alloc >= kv_size_max) {
321
+ + return false;
322
+ + }
323
+ +
324
+ + // Grow geometrically (next power-of-two) and align up to n_pad. The
325
+ + // pow2 ramp avoids re-growing every few tokens for long prompts; n_pad
326
+ + // alignment preserves the constructor's GGML_ASSERT(kv_size % n_pad == 0)
327
+ + // invariant for find_slot.
328
+ + uint32_t new_cap = std::max<uint32_t>(kv_size_alloc, n_pad);
329
+ + while (new_cap < cells_needed && new_cap < kv_size_max) {
330
+ + new_cap *= 2;
331
+ }
332
+ + new_cap = ((new_cap + n_pad - 1) / n_pad) * n_pad;
333
+ + new_cap = std::min(new_cap, kv_size_max);
334
+
335
+ - cleared = true;
336
+ + if (new_cap == kv_size_alloc) {
337
+ + return cells_needed <= kv_size_alloc;
338
+ + }
339
+ +
340
+ + LLAMA_LOG_INFO("%s: grow soft cap %u -> %u (need %u, max %u)\n",
341
+ + __func__, kv_size_alloc, new_cap, cells_needed, kv_size_max);
342
+ +
343
+ + kv_size_alloc = new_cap;
344
+ + return cells_needed <= kv_size_alloc;
345
+ }
346
+
347
+ void llama_kv_cache::clear(bool data) {
348
+ @@ -371,14 +453,16 @@ void llama_kv_cache::clear(bool data) {
349
+ }
350
+
351
+ if (data) {
352
+ - // opencoti F4 M3 Phase 1: if the deferred zero-fill has not happened
353
+ - // yet there is nothing to clear β€” v_cells.reset() above already
354
+ - // satisfies the logical "data cleared" state. Don't commit RSS just
355
+ - // to zero pages that have never been written.
356
+ - if (cleared) {
357
+ - for (auto & [_, buf] : ctxs_bufs) {
358
+ - ggml_backend_buffer_clear(buf.get(), 0);
359
+ - }
360
+ + // opencoti F4 M3 Phase 1+2: if no zero-fill has happened yet there
361
+ + // is nothing to clear β€” v_cells.reset() above already satisfies the
362
+ + // logical "data cleared" state. Don't commit RSS just to zero pages
363
+ + // that have never been written. When a partial clear is already on
364
+ + // the books, re-issue it against the current soft cap so any cells
365
+ + // touched between the first ensure_cleared and now are re-zeroed.
366
+ + if (n_cells_cleared > 0) {
367
+ + const uint32_t b = n_cells_cleared;
368
+ + n_cells_cleared = 0;
369
+ + ensure_cleared(b);
370
+ }
371
+ }
372
+ }
373
+ @@ -743,10 +827,28 @@ llama_kv_cache::slot_info_vec_t llama_kv_cache::prepare(const std::vector<llama_
374
+
375
+ for (const auto & ubatch : ubatches) {
376
+ // only find a suitable slot for the ubatch. don't modify the cells yet
377
+ - const auto sinfo_new = find_slot(ubatch, false);
378
+ + auto sinfo_new = find_slot(ubatch, false);
379
+ if (sinfo_new.empty()) {
380
+ - success = false;
381
+ - break;
382
+ + // opencoti F4 M3 Phase 2 β€” see docs/decisions/0001-lazy-slot-context.md
383
+ + // Phase 2 keeps the soft alloc cap (kv_size_alloc) below kv_size_max
384
+ + // until a real ubatch needs more cells than the cap exposes. When
385
+ + // find_slot fails, try to grow the cap to cover what we already
386
+ + // know is used plus the ubatch's token count, then retry. The grow
387
+ + // is bounded by kv_size_max; if we are already at the hard cap the
388
+ + // failure is genuine and we propagate it up.
389
+ + uint32_t used_max = 0;
390
+ + for (uint32_t st = 0; st < n_stream; ++st) {
391
+ + used_max = std::max(used_max, v_cells[st].used_max_p1());
392
+ + }
393
+ + const uint32_t want = used_max + ubatch.n_tokens;
394
+ + if (ensure_capacity(want)) {
395
+ + ensure_cleared();
396
+ + sinfo_new = find_slot(ubatch, false);
397
+ + }
398
+ + if (sinfo_new.empty()) {
399
+ + success = false;
400
+ + break;
401
+ + }
402
+ }
403
+
404
+ // remember the position that we found
405
+ @@ -981,14 +1083,20 @@ llama_kv_cache::slot_info llama_kv_cache::find_slot(const llama_ubatch & ubatch,
406
+
407
+ uint32_t head_cur = v_heads[seq_to_stream[seq_id]];
408
+
409
+ + // opencoti F4 M3 Phase 2 β€” see docs/decisions/0001-lazy-slot-context.md
410
+ + // The Phase-1 search swept the full hard cap (cells.size() == kv_size_max).
411
+ + // Phase 2 caps it at the current soft alloc; prepare() grows the cap and
412
+ + // retries when the soft cap is too small for the requested ubatch.
413
+ + const uint32_t cap = kv_size_alloc;
414
+ +
415
+ // if we have enough unused cells before the current head ->
416
+ // better to start searching from the beginning of the cache, hoping to fill it
417
+ if (head_cur > cells.get_used() + 2*n_tokens) {
418
+ head_cur = 0;
419
+ }
420
+
421
+ - if (n_tokens > cells.size()) {
422
+ - LLAMA_LOG_ERROR("%s: n_tokens = %d > size = %u\n", __func__, n_tokens, cells.size());
423
+ + if (n_tokens > cap) {
424
+ + // not necessarily fatal β€” prepare() may grow the cap and retry
425
+ return { };
426
+ }
427
+
428
+ @@ -999,8 +1107,8 @@ llama_kv_cache::slot_info llama_kv_cache::find_slot(const llama_ubatch & ubatch,
429
+ const uint32_t n_test = cont ? n_tokens : 1;
430
+
431
+ while (true) {
432
+ - if (head_cur + n_test > cells.size()) {
433
+ - n_tested += cells.size() - head_cur;
434
+ + if (head_cur + n_test > cap) {
435
+ + n_tested += cap - head_cur;
436
+ head_cur = 0;
437
+ continue;
438
+ }
439
+ @@ -1058,7 +1166,7 @@ llama_kv_cache::slot_info llama_kv_cache::find_slot(const llama_ubatch & ubatch,
440
+ res.idxs[s].clear();
441
+ }
442
+
443
+ - if (n_tested >= cells.size()) {
444
+ + if (n_tested >= cap) {
445
+ //LLAMA_LOG_ERROR("%s: failed to find a slot for %d tokens\n", __func__, n_tokens);
446
+ return { };
447
+ }
448
+ @@ -1961,10 +2069,13 @@ void llama_kv_cache::state_read(llama_io_read_i & io, llama_seq_id seq_id, llama
449
+
450
+ GGML_ASSERT(seq_id == -1 || (seq_id >= 0 && (size_t) seq_id < seq_to_stream.size()));
451
+
452
+ - // opencoti F4 M3 Phase 1 β€” see docs/decisions/0001-lazy-slot-context.md
453
+ - // state_read writes the saved KV blob into our tensors, which requires
454
+ - // backing storage. Force the deferred allocation before we read.
455
+ - ensure_cleared();
456
+ + // opencoti F4 M3 Phase 1+2 β€” see docs/decisions/0001-lazy-slot-context.md
457
+ + // state_read writes the saved KV blob into our tensors at arbitrary
458
+ + // offsets, so we cannot rely on the soft cap. Force a full-range
459
+ + // zero-fill before we read β€” this commits all KV pages but only on
460
+ + // the explicit deserialize path.
461
+ + ensure_capacity(kv_size_max);
462
+ + ensure_cleared(kv_size_max);
463
+
464
+ uint32_t n_stream_cur;
465
+ io.read(&n_stream_cur, sizeof(n_stream_cur));
466
+ diff --git a/llama.cpp/src/llama-kv-cache.h b/llama.cpp/src/llama-kv-cache.h
467
+ --- a/llama.cpp/src/llama-kv-cache.h
468
+ +++ b/llama.cpp/src/llama-kv-cache.h
469
+ @@ -101,6 +101,11 @@ public:
470
+ bool offload,
471
+ bool unified,
472
+ uint32_t kv_size,
473
+ + // opencoti F4 M3 Phase 2 β€” see docs/decisions/0001-lazy-slot-context.md
474
+ + // Initial allocation cap. 0 means the same as kv_size
475
+ + // (Phase 1 behavior). Otherwise clamped to [n_pad, kv_size]
476
+ + // and rounded up to the next n_pad multiple.
477
+ + uint32_t kv_size_initial,
478
+ uint32_t n_seq_max,
479
+ uint32_t n_pad,
480
+ uint32_t n_swa,
481
+ @@ -152,15 +157,28 @@ public:
482
+
483
+ bool get_has_shift() const;
484
+
485
+ - // opencoti F4 M3 Phase 1 β€” see docs/decisions/0001-lazy-slot-context.md
486
+ - // Idempotent: zeroes the KV-cache backend buffers on first call.
487
+ - // The buffers are malloc'd eagerly at construction (so the scheduler
488
+ - // can resolve tensor->buffer at reserve time) but NOT cleared β€” on
489
+ - // Linux, malloc reserves address space without committing pages, so
490
+ - // RSS stays low until something writes. This method does the memset
491
+ - // that commits the pages. Called from inference entry points
492
+ - // (init_batch, prepare, update, state_read).
493
+ - void ensure_cleared();
494
+ + // opencoti F4 M3 Phase 1+2 β€” see docs/decisions/0001-lazy-slot-context.md
495
+ + // Idempotent. Zeroes cells [n_cells_cleared, up_to_cells) of every KV
496
+ + // layer's K and V tensors across every stream. The buffers are malloc'd
497
+ + // eagerly at construction (so the scheduler can resolve tensor->buffer
498
+ + // at reserve time) but their pages are not committed to RSS until they
499
+ + // are first written. Partial memset (via ggml_backend_tensor_memset)
500
+ + // commits only the requested range β€” that is the Phase 2 RSS win.
501
+ + //
502
+ + // up_to_cells == 0 means "use the current soft cap kv_size_alloc"
503
+ + // (the normal inference path). Pass kv_size_max for paths that need
504
+ + // full coverage (state_read).
505
+ + void ensure_cleared(uint32_t up_to_cells = 0);
506
+ +
507
+ + // opencoti F4 M3 Phase 2 β€” see docs/decisions/0001-lazy-slot-context.md
508
+ + // Grow the soft allocation cap. Returns true iff the cap covers
509
+ + // cells_needed after the grow. Bound by kv_size_max. Idempotent at
510
+ + // the cap.
511
+ + bool ensure_capacity(uint32_t cells_needed);
512
+ +
513
+ + // opencoti F4 M3 Phase 2 β€” see docs/decisions/0001-lazy-slot-context.md
514
+ + // The current soft cap (cells visible to find_slot).
515
+ + uint32_t get_kv_size_alloc() const { return kv_size_alloc; }
516
+
517
+ ggml_type type_k() const;
518
+ ggml_type type_v() const;
519
+ @@ -266,12 +284,22 @@ private:
520
+ // ggml contexts for the KV cache along with the allocated backend buffers:
521
+ std::vector<std::pair<ggml_context_ptr, ggml_backend_buffer_ptr>> ctxs_bufs;
522
+
523
+ - // opencoti F4 M3 Phase 1 β€” see docs/decisions/0001-lazy-slot-context.md
524
+ - // True once the backend buffers in ctxs_bufs have been zero-filled.
525
+ - // The constructor sets this true for no_alloc models (whose dummy
526
+ - // buffers are size 0 β€” clearing is a no-op) and false otherwise. The
527
+ - // first ensure_cleared() call zeroes the buffers and flips this true.
528
+ - bool cleared = false;
529
+ + // opencoti F4 M3 Phase 1+2 β€” see docs/decisions/0001-lazy-slot-context.md
530
+ + // Cells in the half-open range [0, n_cells_cleared) are guaranteed to
531
+ + // be zeroed across every layer's K and V tensors and every stream.
532
+ + // ensure_cleared(up_to) grows this monotonically up to kv_size_alloc
533
+ + // (or kv_size_max for the explicit-full call). The constructor sets
534
+ + // this to kv_size_max for no_alloc models (whose dummy buffers are
535
+ + // size 0 β€” clearing is a no-op) and 0 otherwise.
536
+ + uint32_t n_cells_cleared = 0;
537
+ +
538
+ + // opencoti F4 M3 Phase 2 β€” see docs/decisions/0001-lazy-slot-context.md
539
+ + // kv_size_max: hard cap on cells (set from constructor's kv_size).
540
+ + // kv_size_alloc: current soft cap. find_slot only scans cells [0, kv_size_alloc).
541
+ + // Grows via ensure_capacity, never shrinks (Phase 2; Phase 3
542
+ + // adds shrink-on-idle).
543
+ + uint32_t kv_size_max = 0;
544
+ + uint32_t kv_size_alloc = 0;
545
+
546
+ // the current index from where we start searching for a free slot in the ring buffer of KV cells (see find_slot())
547
+ // note: this is not part of the KV state and it's only used to speed-up the find_slot() method
548
+ diff --git a/llama.cpp/src/llama-memory-hybrid-iswa.cpp b/llama.cpp/src/llama-memory-hybrid-iswa.cpp
549
+ --- a/llama.cpp/src/llama-memory-hybrid-iswa.cpp
550
+ +++ b/llama.cpp/src/llama-memory-hybrid-iswa.cpp
551
+ @@ -40,6 +40,10 @@ llama_memory_hybrid_iswa::llama_memory_hybrid_iswa(
552
+ swa_full,
553
+ unified,
554
+ kv_size,
555
+ + // opencoti F4 M3 Phase 2 β€” see docs/decisions/0001-lazy-slot-context.md
556
+ + // Hybrid-iswa does not yet thread slot_initial_ctx; pass 0 for
557
+ + // Phase 1 backward-compat (full eager allocation).
558
+ + 0,
559
+ n_seq_max,
560
+ n_ubatch,
561
+ n_pad,
562
+ diff --git a/llama.cpp/src/llama-memory-hybrid.cpp b/llama.cpp/src/llama-memory-hybrid.cpp
563
+ --- a/llama.cpp/src/llama-memory-hybrid.cpp
564
+ +++ b/llama.cpp/src/llama-memory-hybrid.cpp
565
+ @@ -39,6 +39,10 @@ llama_memory_hybrid::llama_memory_hybrid(
566
+ offload,
567
+ unified,
568
+ kv_size,
569
+ + // opencoti F4 M3 Phase 2 β€” see docs/decisions/0001-lazy-slot-context.md
570
+ + // Hybrid memory does not yet thread slot_initial_ctx; pass 0 for
571
+ + // Phase 1 backward-compat (full eager allocation).
572
+ + 0,
573
+ n_seq_max,
574
+ n_pad,
575
+ n_swa,
576
+ diff --git a/llama.cpp/src/llama-model.cpp b/llama.cpp/src/llama-model.cpp
577
+ --- a/llama.cpp/src/llama-model.cpp
578
+ +++ b/llama.cpp/src/llama-model.cpp
579
+ @@ -2073,6 +2073,8 @@ llama_memory_i * llama_model::create_memory(const llama_memory_params & params,
580
+ params.swa_full,
581
+ cparams.kv_unified,
582
+ cparams.n_ctx_seq,
583
+ + // opencoti F4 M3 Phase 2 β€” see docs/decisions/0001-lazy-slot-context.md
584
+ + cparams.slot_initial_ctx,
585
+ cparams.n_seq_max,
586
+ cparams.n_ubatch,
587
+ 1,
588
+ @@ -2089,6 +2091,8 @@ llama_memory_i * llama_model::create_memory(const llama_memory_params & params,
589
+ cparams.offload_kqv,
590
+ cparams.kv_unified,
591
+ cparams.n_ctx_seq,
592
+ + // opencoti F4 M3 Phase 2 β€” see docs/decisions/0001-lazy-slot-context.md
593
+ + cparams.slot_initial_ctx,
594
+ cparams.n_seq_max,
595
+ 1,
596
+ hparams.n_swa,
patches/0003-arg-assertion-bypass.patch ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ From: opencoti
2
+ Subject: [PATCH 3/N] bypass upstream `params.n_gpu_layers < 0` assertions
3
+
4
+ Upstream llama.cpp pin 5e9c63546 has TWO `GGML_ASSERT(n_gpu_layers < 0)`
5
+ calls in common/arg.cpp:
6
+
7
+ 1. At the `-ngl` option registration site (params.n_gpu_layers).
8
+ 2. At the `-ngld` (draft) option registration site
9
+ (params.speculative.n_gpu_layers).
10
+
11
+ Both fire on every `common_params_parser_init` call regardless of args,
12
+ making the patched binary unable to start in `--server`, `--cli`, or
13
+ `--chat` mode. Mozilla-Ocho v1.4.0 (pre-assertion) was unaffected.
14
+
15
+ Each assertion is a "display-format guard" for the next line, which
16
+ maps `n_gpu_layers == -1` to "auto" and anything else to "all" in the
17
+ help text. The fix is to either extend the format to handle non-
18
+ negative defaults, or to drop the assertion. We drop it β€” the
19
+ display "all" for a positive default is a minor cosmetic issue,
20
+ not a correctness problem.
21
+
22
+ This is an upstream-bug-bypass patch, not opencoti scope. To be
23
+ proposed upstream once we surface a minimal reproducer.
24
+
25
+ Bench: closes the F4 M3 bench path that requires booting the
26
+ patched binary in --server mode.
27
+ Milestone: F4 M3 β€” see docs/features/memory_embedder.md
28
+ Upstreaming: TBD β€” propose to llama.cpp once a minimal repro is
29
+ in hand. Likely already fixed in newer upstream commits.
30
+
31
+ ---
32
+ diff --git a/llama.cpp/common/arg.cpp b/llama.cpp/common/arg.cpp
33
+ --- a/llama.cpp/common/arg.cpp
34
+ +++ b/llama.cpp/common/arg.cpp
35
+ @@ -2362,7 +2362,10 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
36
+ }
37
+ }
38
+ ).set_env("LLAMA_ARG_N_CPU_MOE"));
39
+ - GGML_ASSERT(params.n_gpu_layers < 0); // string_format would need to be extended for a default >= 0
40
+ + // opencoti F4 M3 patch 0003 β€” see docs/decisions/0001-lazy-slot-context.md
41
+ + // Upstream assertion fires on every common_params_parser_init invocation
42
+ + // (display-format guard for the next line). Dropped β€” display says "all"
43
+ + // for non-negative defaults, which is cosmetic, not a correctness bug.
44
+ add_opt(common_arg(
45
+ {"-ngl", "--gpu-layers", "--n-gpu-layers"}, "N",
46
+ string_format("max. number of layers to store in VRAM, either an exact number, 'auto', or 'all' (default: %s)", params.n_gpu_layers == -1 ? "auto" : "all"),
patches/0004-cuda-std-cpp17.patch ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ From: opencoti
2
+ Subject: [PATCH 4/N] add -std=c++17 to ggml-cuda NVCC flags
3
+
4
+ Mozilla-Ocho's `llamafile/cuda.sh` builds the `ggml-cuda.so` DSO that
5
+ the llamafile runtime loads when the user passes `-ngl <N>`. The
6
+ ggml-cuda sources have moved to C++17 features (`std::is_same_v`, fold
7
+ expressions, structured bindings), but cuda.sh's COMMON_FLAGS do not
8
+ pass `-std=c++17` to nvcc.
9
+
10
+ For CUDA 11.x and older 12.x toolkits this worked because nvcc
11
+ defaulted to C++14 and the sources were C++14-clean. Newer CUDA
12
+ toolkits ship CCCL that requires C++17 explicitly via a #error
13
+ guard, so building against CUDA 12.6+ (and definitely CUDA 13.x)
14
+ fails with `error: namespace "std" has no member "is_same_v"` and
15
+ ~30 follow-on errors per .cu translation unit.
16
+
17
+ This patch adds `-std=c++17` to COMMON_FLAGS so cuda.sh builds
18
+ against any reasonable CUDA toolkit. The host compiler is already
19
+ on C++23 via cosmocc, so the host-flag side is fine; this is
20
+ purely the nvcc device-flag side.
21
+
22
+ Bench: closes the GPU validation path. Without this, the patched
23
+ llamafile cannot load ggml-cuda.so on solidPC (and on any host
24
+ where CUDA 12.6+ is the local toolkit), and the binary silently
25
+ falls back to CPU even with `-ngl all`.
26
+ Milestone: F4 M3 GPU validation (task #113) β€” see
27
+ docs/decisions/0001-lazy-slot-context.md and
28
+ docs/features/memory_embedder.md
29
+ Upstreaming: TBD β€” propose to Mozilla-Ocho once they decide on a
30
+ minimum-supported CUDA toolkit. Currently their build matrix is
31
+ unclear about whether CUDA 13.x is expected to work; this patch
32
+ is the minimal fix that does not regress older toolkits.
33
+
34
+ ---
35
+ diff --git a/llamafile/cuda.sh b/llamafile/cuda.sh
36
+ --- a/llamafile/cuda.sh
37
+ +++ b/llamafile/cuda.sh
38
+ @@ -232,7 +232,12 @@ if [ "$COMPRESS" = "1" ]; then
39
+ fi
40
+
41
+ # NVCC compiler flags
42
+ +# opencoti note: -std=c++17 added because the ggml-cuda .cu sources use
43
+ +# C++17 features (std::is_same_v, structured bindings, fold expressions).
44
+ +# Newer CUDA toolkits (12.x+) need this explicitly; older toolkits did C++14
45
+ +# by default. Both work with c++17.
46
+ COMMON_FLAGS="\
47
+ + -std=c++17 \
48
+ --use_fast_math \
49
+ --extended-lambda \
50
+ $EXTRA_INCLUDES \
patches/0005-lazy-slot-context-shrink.patch ADDED
@@ -0,0 +1,582 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ From: opencoti
2
+ Subject: [PATCH 5/N] lazy slot-context allocation β€” Phase 3 (shrink on idle)
3
+
4
+ Phase 2 added a soft cap on per-stream KV cells (kv_size_alloc <= kv_size_max)
5
+ and grow-on-demand via ensure_capacity. The soft cap never shrinks, so once a
6
+ single long prompt has caused it to grow, the embedder's RSS stays at the
7
+ grown size for the lifetime of the process. For an embedder running
8
+ `--parallel 2 --ctx-size 16384` with typical prompts well under 4K cells,
9
+ this means the headline Phase-2 win evaporates after the first long-context
10
+ request.
11
+
12
+ Phase 3 plugs that hole. When an idle threshold has elapsed since the last
13
+ apply_ubatch:
14
+
15
+ 1. Cells in the range [kv_size_shrink_target, kv_size_alloc) are evicted
16
+ across every stream via cells.rm(i). The server-context layer's
17
+ prompt cache keeps cells alive between embed requests for reuse;
18
+ decommitting their backing pages without first evicting would leave
19
+ the prompt cache pointing at zeroed memory and corrupt subsequent
20
+ re-uses. The eviction makes the shrink unconditional.
21
+
22
+ 2. The pages backing cells [target, current_alloc) of every layer's K
23
+ and V tensors are returned to the kernel via
24
+ madvise(addr, len, MADV_DONTNEED).
25
+
26
+ 3. n_cells_cleared is rolled back to the shrink target so the next
27
+ ensure_cleared() partial-memset re-zeros only what comes back.
28
+
29
+ 4. kv_size_alloc is reset to the shrink target so the next request can
30
+ grow again from the initial cap.
31
+
32
+ Why madvise(MADV_DONTNEED) and not posix_madvise(POSIX_MADV_DONTNEED):
33
+ glibc on Linux maps posix_madvise(POSIX_MADV_DONTNEED) to a no-op (see
34
+ glibc sysdeps/posix/posix_madvise.c β€” the standard explicitly forbids
35
+ "actively destroying" pages, which is what we need). Only the Linux-
36
+ specific madvise(MADV_DONTNEED) actually decommits anonymous pages and
37
+ returns RSS to the kernel. We declared MADV_DONTNEED with the canonical
38
+ Linux value (4) under #ifndef and extern-declared madvise() so cosmocc's
39
+ default feature set (which does not define _GNU_SOURCE and so does not
40
+ expose madvise() through sys/mman.h) builds cleanly.
41
+
42
+ The shrink check runs at the top of init_batch() and prepare() β€” both
43
+ entry points to actual KV work. It is gated by:
44
+ - slot_shrink_idle_ms_ > 0 (feature enabled)
45
+ - kv_size_alloc > kv_size_shrink_target (something to shrink)
46
+ - last_active_ms_ != 0 AND (now - last_active_ms_) >= idle threshold
47
+
48
+ Surface (CLI):
49
+ --slot-shrink-idle-ms N (default 0 = disabled; positive enables)
50
+ env LLAMA_ARG_SLOT_SHRINK_IDLE_MS
51
+
52
+ Surface (C API):
53
+ llama_context_params::slot_shrink_idle_ms (uint32_t, default 0)
54
+
55
+ Plumbed through llama_cparams -> llama_kv_cache ctor ->
56
+ (llama_kv_cache_iswa ctor for SWA models) -> llama-model.cpp call sites.
57
+ Backward-compat: llama-memory-hybrid{,-iswa}.cpp pass 0 (no shrink), the
58
+ same way they already pass 0 for kv_size_initial.
59
+
60
+ Bench (CPU, --gpu disable so KV lives in host pages): the embedder
61
+ configured `--parallel 2 --ctx-size 16384 --pooling last
62
+ --slot-initial-ctx 256 --slot-shrink-idle-ms 30000` starts at 719 MB
63
+ RSS, grows to 1720 MB after a long-prompt corpus pass (soft cap 256 ->
64
+ 2048), and after the 30 s idle drops to 1329 MB on the next short embed.
65
+ 391.7 MB returned to the kernel β€” measured by /proc/<pid>/status VmRSS.
66
+ Quality (cosine min vs M2 baseline) stays at 0.999797 across the 48-prompt
67
+ corpus.
68
+
69
+ Milestone: F4 M3 Phase 3 β€” see docs/decisions/0001-lazy-slot-context.md
70
+ Upstreaming: TBD β€” kv_cache idle-shrink is a generally-useful concept; the
71
+ cell-eviction + madvise(MADV_DONTNEED) approach is not opencoti-specific.
72
+ Proposal candidate after F4 closes.
73
+
74
+ ---
75
+ diff --git a/llama.cpp/common/arg.cpp b/llama.cpp/common/arg.cpp
76
+ --- a/llama.cpp/common/arg.cpp
77
+ +++ b/llama.cpp/common/arg.cpp
78
+ @@ -1371,6 +1371,17 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
79
+ params.slot_initial_ctx = value;
80
+ }
81
+ ).set_env("LLAMA_ARG_SLOT_INITIAL_CTX").set_examples({LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_EMBEDDING, LLAMA_EXAMPLE_PARALLEL}));
82
+ + add_opt(common_arg(
83
+ + // opencoti F4 M3 Phase 3 β€” see docs/decisions/0001-lazy-slot-context.md
84
+ + {"--slot-shrink-idle-ms"}, "N",
85
+ + string_format("idle threshold (ms) after which a grown KV soft cap is shrunk back to "
86
+ + "--slot-initial-ctx and the over-cap pages are decommitted via posix_madvise "
87
+ + "(default: %d, 0 disables; requires --slot-initial-ctx > 0)",
88
+ + params.slot_shrink_idle_ms),
89
+ + [](common_params & params, int value) {
90
+ + params.slot_shrink_idle_ms = value;
91
+ + }
92
+ + ).set_env("LLAMA_ARG_SLOT_SHRINK_IDLE_MS").set_examples({LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_EMBEDDING, LLAMA_EXAMPLE_PARALLEL}));
93
+ add_opt(common_arg(
94
+ {"--cache-idle-slots"},
95
+ {"--no-cache-idle-slots"},
96
+ diff --git a/llama.cpp/common/common.cpp b/llama.cpp/common/common.cpp
97
+ --- a/llama.cpp/common/common.cpp
98
+ +++ b/llama.cpp/common/common.cpp
99
+ @@ -1630,6 +1630,8 @@ struct llama_context_params common_context_params_to_llama(const common_params &
100
+ cparams.n_seq_max = params.n_parallel;
101
+ // opencoti F4 M3 Phase 2 β€” see docs/decisions/0001-lazy-slot-context.md
102
+ cparams.slot_initial_ctx = params.slot_initial_ctx < 0 ? 0u : (uint32_t) params.slot_initial_ctx;
103
+ + // opencoti F4 M3 Phase 3 β€” see docs/decisions/0001-lazy-slot-context.md
104
+ + cparams.slot_shrink_idle_ms = params.slot_shrink_idle_ms < 0 ? 0u : (uint32_t) params.slot_shrink_idle_ms;
105
+ cparams.n_rs_seq = params.speculative.need_n_rs_seq();
106
+ cparams.n_batch = params.n_batch;
107
+ cparams.n_ubatch = params.n_ubatch;
108
+ diff --git a/llama.cpp/common/common.h b/llama.cpp/common/common.h
109
+ --- a/llama.cpp/common/common.h
110
+ +++ b/llama.cpp/common/common.h
111
+ @@ -430,6 +430,11 @@ struct common_params {
112
+ // (Phase 1 behavior β€” only zero-fill is deferred). Positive values cap
113
+ // the initial allocation and enable grow-on-demand up to n_ctx_seq.
114
+ int32_t slot_initial_ctx = 0;
115
+ + // opencoti F4 M3 Phase 3 β€” see docs/decisions/0001-lazy-slot-context.md
116
+ + // Idle threshold (ms) after which a grown soft cap is shrunk back to
117
+ + // slot_initial_ctx and the over-cap pages are decommitted via
118
+ + // posix_madvise. 0 disables shrink-on-idle (Phase 2 behavior).
119
+ + int32_t slot_shrink_idle_ms = 0;
120
+ int32_t n_batch = 2048; // logical batch size for prompt processing (must be >=32 to use BLAS)
121
+ int32_t n_ubatch = 512; // physical batch size for prompt processing (must be >=32 to use BLAS)
122
+ int32_t n_keep = 0; // number of tokens to keep from initial prompt
123
+ diff --git a/llama.cpp/include/llama.h b/llama.cpp/include/llama.h
124
+ --- a/llama.cpp/include/llama.h
125
+ +++ b/llama.cpp/include/llama.h
126
+ @@ -346,6 +346,13 @@ extern "C" {
127
+ // up to next_pow2 (still capped at n_ctx_seq). Reduces warm-state RSS
128
+ // when typical prompts are much shorter than n_ctx_seq.
129
+ uint32_t slot_initial_ctx;
130
+ + // opencoti F4 M3 Phase 3 β€” see docs/decisions/0001-lazy-slot-context.md
131
+ + // Idle threshold after which a grown soft cap is shrunk back to
132
+ + // slot_initial_ctx and the pages beyond it are returned to the OS via
133
+ + // posix_madvise(POSIX_MADV_DONTNEED). 0 disables shrink-on-idle
134
+ + // (Phase 2 behavior β€” soft cap grows monotonically). Requires
135
+ + // slot_initial_ctx > 0 to have any visible effect.
136
+ + uint32_t slot_shrink_idle_ms;
137
+ uint32_t n_rs_seq; // number of recurrent-state snapshots per seq for rollback (0 = no rollback) [EXPERIMENTAL]
138
+ int32_t n_threads; // number of threads to use for generation
139
+ int32_t n_threads_batch; // number of threads to use for batch processing
140
+ diff --git a/llama.cpp/src/llama-context.cpp b/llama.cpp/src/llama-context.cpp
141
+ --- a/llama.cpp/src/llama-context.cpp
142
+ +++ b/llama.cpp/src/llama-context.cpp
143
+ @@ -60,6 +60,8 @@ llama_context::llama_context(
144
+
145
+ // opencoti F4 M3 Phase 2 β€” see docs/decisions/0001-lazy-slot-context.md
146
+ cparams.slot_initial_ctx = params.slot_initial_ctx;
147
+ + // opencoti F4 M3 Phase 3 β€” see docs/decisions/0001-lazy-slot-context.md
148
+ + cparams.slot_shrink_idle_ms = params.slot_shrink_idle_ms;
149
+
150
+ cparams.n_threads = params.n_threads;
151
+ cparams.n_threads_batch = params.n_threads_batch;
152
+ @@ -3350,6 +3352,7 @@ llama_context_params llama_context_default_params() {
153
+ /*.n_ubatch =*/ 512,
154
+ /*.n_seq_max =*/ 1,
155
+ /*.slot_initial_ctx =*/ 0,
156
+ + /*.slot_shrink_idle_ms =*/ 0,
157
+ /*.n_rs_seq =*/ 0,
158
+ /*.n_threads =*/ GGML_DEFAULT_N_THREADS, // TODO: better default
159
+ /*.n_threads_batch =*/ GGML_DEFAULT_N_THREADS,
160
+ diff --git a/llama.cpp/src/llama-cparams.h b/llama.cpp/src/llama-cparams.h
161
+ --- a/llama.cpp/src/llama-cparams.h
162
+ +++ b/llama.cpp/src/llama-cparams.h
163
+ @@ -16,6 +16,11 @@ struct llama_cparams {
164
+ // 0 = full n_ctx_seq allocation. Positive value caps the initial KV-cell
165
+ // allocation per sequence and enables grow-on-demand up to n_ctx_seq.
166
+ uint32_t slot_initial_ctx;
167
+ + // opencoti F4 M3 Phase 3 β€” see docs/decisions/0001-lazy-slot-context.md
168
+ + // 0 disables shrink-on-idle. Positive value is the idle threshold in
169
+ + // milliseconds: when a grown KV-cache is idle for at least this long, the
170
+ + // soft cap is reset to slot_initial_ctx and the pages beyond are decommitted.
171
+ + uint32_t slot_shrink_idle_ms;
172
+ uint32_t n_rs_seq; // number of recurrent-state snapshots per seq for rollback
173
+ int32_t n_threads; // number of threads to use for generation
174
+ int32_t n_threads_batch; // number of threads to use for batch processing
175
+ diff --git a/llama.cpp/src/llama-kv-cache-iswa.cpp b/llama.cpp/src/llama-kv-cache-iswa.cpp
176
+ --- a/llama.cpp/src/llama-kv-cache-iswa.cpp
177
+ +++ b/llama.cpp/src/llama-kv-cache-iswa.cpp
178
+ @@ -21,6 +21,7 @@ llama_kv_cache_iswa::llama_kv_cache_iswa(
179
+ bool unified,
180
+ uint32_t kv_size,
181
+ uint32_t kv_size_initial,
182
+ + uint32_t slot_shrink_idle_ms,
183
+ uint32_t n_seq_max,
184
+ uint32_t n_ubatch,
185
+ uint32_t n_pad,
186
+ @@ -62,15 +63,15 @@ llama_kv_cache_iswa::llama_kv_cache_iswa(
187
+
188
+ kv_base = std::make_unique<llama_kv_cache>(
189
+ model, type_k, type_v,
190
+ - v_trans, offload, unified, size_base, kv_size_initial, n_seq_max, n_pad,
191
+ - 0, LLAMA_SWA_TYPE_NONE, filter_base, reuse);
192
+ + v_trans, offload, unified, size_base, kv_size_initial, slot_shrink_idle_ms,
193
+ + n_seq_max, n_pad, 0, LLAMA_SWA_TYPE_NONE, filter_base, reuse);
194
+
195
+ LLAMA_LOG_INFO("%s: creating SWA KV cache, size = %u cells\n", __func__, size_swa);
196
+
197
+ kv_swa = std::make_unique<llama_kv_cache>(
198
+ model, type_k, type_v,
199
+ - v_trans, offload, unified, size_swa, kv_size_initial, n_seq_max, n_pad,
200
+ - hparams.n_swa, hparams.swa_type, filter_swa, reuse);
201
+ + v_trans, offload, unified, size_swa, kv_size_initial, slot_shrink_idle_ms,
202
+ + n_seq_max, n_pad, hparams.n_swa, hparams.swa_type, filter_swa, reuse);
203
+ }
204
+
205
+ void llama_kv_cache_iswa::clear(bool data) {
206
+ diff --git a/llama.cpp/src/llama-kv-cache-iswa.h b/llama.cpp/src/llama-kv-cache-iswa.h
207
+ --- a/llama.cpp/src/llama-kv-cache-iswa.h
208
+ +++ b/llama.cpp/src/llama-kv-cache-iswa.h
209
+ @@ -25,6 +25,9 @@ public:
210
+ // opencoti F4 M3 Phase 2 β€” see docs/decisions/0001-lazy-slot-context.md
211
+ // Forwarded to both kv_base and kv_swa. 0 = full kv_size.
212
+ uint32_t kv_size_initial,
213
+ + // opencoti F4 M3 Phase 3 β€” see docs/decisions/0001-lazy-slot-context.md
214
+ + // Forwarded to both kv_base and kv_swa. 0 = shrink disabled.
215
+ + uint32_t slot_shrink_idle_ms,
216
+ uint32_t n_seq_max,
217
+ uint32_t n_ubatch,
218
+ uint32_t n_pad,
219
+ diff --git a/llama.cpp/src/llama-kv-cache.cpp b/llama.cpp/src/llama-kv-cache.cpp
220
+ --- a/llama.cpp/src/llama-kv-cache.cpp
221
+ +++ b/llama.cpp/src/llama-kv-cache.cpp
222
+ @@ -7,12 +7,35 @@
223
+
224
+ #include <algorithm>
225
+ #include <cassert>
226
+ +#include <chrono>
227
+ #include <cmath>
228
+ #include <cstring>
229
+ #include <limits>
230
+ #include <map>
231
+ #include <stdexcept>
232
+
233
+ +// opencoti F4 M3 Phase 3 β€” see docs/decisions/0001-lazy-slot-context.md
234
+ +// On Linux, madvise(addr, len, MADV_DONTNEED) is the call that actually
235
+ +// decommits pages: the kernel zeroes the page table entries, releases the
236
+ +// physical pages, and the next access faults in fresh zero-filled pages.
237
+ +// glibc's posix_madvise(POSIX_MADV_DONTNEED) is documented as advisory and
238
+ +// is NOT guaranteed to reduce RSS β€” empirically it is a no-op on the
239
+ +// glibc/Debian-11 setup the embedder runs on. We must call madvise()
240
+ +// directly.
241
+ +//
242
+ +// cosmocc's default headers (without _GNU_SOURCE) do not declare madvise()
243
+ +// or MADV_DONTNEED, but the syscall exists on every Linux kernel the
244
+ +// embedder runs on. Hardcode the Linux ABI value (asm-generic/mman-common.h:
245
+ +// `#define MADV_DONTNEED 4`) and forward-declare madvise() to keep
246
+ +// _GNU_SOURCE out of the global include. <unistd.h> provides
247
+ +// sysconf(_SC_PAGESIZE) for page-boundary math.
248
+ +#include <sys/mman.h>
249
+ +#include <unistd.h>
250
+ +#ifndef MADV_DONTNEED
251
+ +# define MADV_DONTNEED 4
252
+ +#endif
253
+ +extern "C" int madvise(void *addr, size_t length, int advice);
254
+ +
255
+ static bool ggml_is_power_of_2(int n) {
256
+ return (n & (n - 1)) == 0;
257
+ }
258
+ @@ -86,6 +109,7 @@ llama_kv_cache::llama_kv_cache(
259
+ bool unified,
260
+ uint32_t kv_size,
261
+ uint32_t kv_size_initial,
262
+ + uint32_t slot_shrink_idle_ms,
263
+ uint32_t n_seq_max,
264
+ uint32_t n_pad,
265
+ uint32_t n_swa,
266
+ @@ -115,6 +139,18 @@ llama_kv_cache::llama_kv_cache(
267
+ __func__, kv_size_alloc, kv_size_max);
268
+ }
269
+
270
+ + // opencoti F4 M3 Phase 3 β€” see docs/decisions/0001-lazy-slot-context.md
271
+ + // The shrink target is the initial cap we just computed. last_active_ms_
272
+ + // starts at 0 (epoch) so a long pause before the first request still
273
+ + // shrinks (correct behavior β€” nothing to lose).
274
+ + kv_size_shrink_target = kv_size_alloc;
275
+ + slot_shrink_idle_ms_ = slot_shrink_idle_ms;
276
+ + last_active_ms_ = 0;
277
+ + if (slot_shrink_idle_ms_ > 0 && kv_size_initial > 0 && kv_size_initial < kv_size_max) {
278
+ + LLAMA_LOG_INFO("%s: slot_shrink_idle_ms = %u (target cap = %u)\n",
279
+ + __func__, slot_shrink_idle_ms_, kv_size_shrink_target);
280
+ + }
281
+ +
282
+ const uint32_t n_layer_kv = hparams.n_layer_kv();
283
+
284
+ // define a comparator for the buft -> ctx map to ensure that the order is well-defined:
285
+ @@ -446,6 +482,151 @@ bool llama_kv_cache::ensure_capacity(uint32_t cells_needed) {
286
+ return cells_needed <= kv_size_alloc;
287
+ }
288
+
289
+ +// opencoti F4 M3 Phase 3 β€” see docs/decisions/0001-lazy-slot-context.md
290
+ +// Decommit pages of every layer's K/V tensors in the byte-range that
291
+ +// corresponds to cells [from_cells, to_cells). Returns those pages to the OS
292
+ +// β€” RSS drops by the size of the range, and the next access to those pages
293
+ +// will fault them back in as fresh zero-filled pages.
294
+ +//
295
+ +// Only safe to call after kv_size_alloc has been reset to from_cells (or
296
+ +// lower) and n_cells_cleared has been reset accordingly, so the bookkeeping
297
+ +// stays in sync with what's actually backed by physical memory.
298
+ +//
299
+ +// The decommit is per-stream because the KV tensors are 3-D
300
+ +// [n_embd, kv_size, n_stream]; cells [a, b) within stream s occupy a
301
+ +// contiguous byte-range whose start address is (base + s*nb[2] + a*nb[1])
302
+ +// and length is (b-a)*nb[1]. posix_madvise is page-grained β€” bytes that
303
+ +// don't cover a full page are silently kept resident, which is fine for
304
+ +// the cells that straddle page boundaries.
305
+ +static void opencoti_decommit_layer_range(ggml_tensor * t, uint32_t n_stream,
306
+ + uint32_t from_cells, uint32_t to_cells) {
307
+ + if (!t || to_cells <= from_cells) {
308
+ + return;
309
+ + }
310
+ + ggml_backend_buffer_t buf = t->buffer;
311
+ + if (!buf) {
312
+ + return;
313
+ + }
314
+ + // Only the host (CPU) backend's buffer is malloc'd memory we can
315
+ + // decommit via madvise. For device buffers (CUDA/Metal/Vulkan) the K/V
316
+ + // cache lives in device memory; skip β€” there are no host-side pages
317
+ + // to return. This means Phase 3's RSS-shrink win applies to host-mode
318
+ + // (e.g. embedder with -ngl 0); GPU-mode binaries still benefit from
319
+ + // the soft-cap reset but RSS doesn't move.
320
+ + if (!ggml_backend_buffer_is_host(buf)) {
321
+ + return;
322
+ + }
323
+ + void * base = ggml_backend_buffer_get_base(buf);
324
+ + if (!base) {
325
+ + return;
326
+ + }
327
+ + const size_t stride_stream = (size_t) t->nb[2];
328
+ + const size_t stride_cell = (size_t) t->nb[1];
329
+ + const size_t off_in_buf = (size_t) ((char *) t->data - (char *) base);
330
+ + for (uint32_t s = 0; s < n_stream; ++s) {
331
+ + const size_t off = off_in_buf
332
+ + + (size_t) s * stride_stream
333
+ + + (size_t) from_cells * stride_cell;
334
+ + const size_t sz = (size_t) (to_cells - from_cells) * stride_cell;
335
+ + // posix_madvise wants the address β€” work in absolute terms.
336
+ + void * addr = (char *) base + off;
337
+ + // Round up to a page boundary on the low side, round down on the
338
+ + // high side, so we only decommit pages we fully own. The cell-aligned
339
+ + // address is unlikely to be page-aligned in general.
340
+ + const size_t page = (size_t) sysconf(_SC_PAGESIZE);
341
+ + if (page == 0 || page == (size_t) -1) {
342
+ + return;
343
+ + }
344
+ + uintptr_t lo = (uintptr_t) addr;
345
+ + uintptr_t hi = lo + sz;
346
+ + uintptr_t lo_aligned = (lo + page - 1) & ~(uintptr_t)(page - 1);
347
+ + uintptr_t hi_aligned = hi & ~(uintptr_t)(page - 1);
348
+ + if (hi_aligned <= lo_aligned) {
349
+ + continue;
350
+ + }
351
+ + // madvise(MADV_DONTNEED) returns 0 on success, -1 on failure (with
352
+ + // errno set). We treat any failure as advisory β€” the RSS win may
353
+ + // be skipped, but correctness is unaffected (the K/V buffer is
354
+ + // still mapped, just possibly still resident).
355
+ + (void) madvise((void *) lo_aligned, (size_t) (hi_aligned - lo_aligned),
356
+ + MADV_DONTNEED);
357
+ + }
358
+ +}
359
+ +
360
+ +void llama_kv_cache::shrink_if_idle() {
361
+ + if (slot_shrink_idle_ms_ == 0) {
362
+ + return; // shrink-on-idle disabled
363
+ + }
364
+ + if (kv_size_alloc <= kv_size_shrink_target) {
365
+ + return; // already at (or below) target β€” nothing to shrink
366
+ + }
367
+ + if (last_active_ms_ == 0) {
368
+ + return; // no apply_ubatch has ever run β€” leave the initial cap as-is
369
+ + }
370
+ +
371
+ + const uint64_t now = (uint64_t)
372
+ + std::chrono::duration_cast<std::chrono::milliseconds>(
373
+ + std::chrono::steady_clock::now().time_since_epoch()).count();
374
+ + if (now < last_active_ms_) {
375
+ + return; // clock went backwards; refuse to act
376
+ + }
377
+ + if ((now - last_active_ms_) < (uint64_t) slot_shrink_idle_ms_) {
378
+ + return; // not idle long enough
379
+ + }
380
+ +
381
+ + const uint32_t from_cells = kv_size_shrink_target;
382
+ + const uint32_t to_cells = kv_size_alloc;
383
+ +
384
+ + // Evict prompt-cache cell metadata for indices [from_cells, to_cells)
385
+ + // on every stream BEFORE decommitting the pages. The server's prompt
386
+ + // cache (server-context.cpp) keeps cells populated between requests so
387
+ + // a subsequent request with the same prefix can skip re-prefill. If we
388
+ + // decommitted pages without invalidating cell metadata, the server's
389
+ + // next prompt-cache lookup could "match" a prefix whose K/V data is
390
+ + // now decommitted (returns to zero pages on next access) = corruption.
391
+ + //
392
+ + // Semantics: shrink-on-idle is allowed to evict the over-cap portion
393
+ + // of the prompt cache because that is exactly what the user opted into
394
+ + // by setting --slot-shrink-idle-ms > 0. A cache miss on a subsequent
395
+ + // long prompt is the cost; the alternative (cap stays grown forever)
396
+ + // defeats Phase 3 entirely β€” slots that retain cells between requests
397
+ + // (the default for embed + completion) would never shrink.
398
+ + uint32_t evicted = 0;
399
+ + for (uint32_t st = 0; st < n_stream; ++st) {
400
+ + auto & cells = v_cells[st];
401
+ + const uint32_t hi = std::min<uint32_t>(cells.size(), to_cells);
402
+ + for (uint32_t i = from_cells; i < hi; ++i) {
403
+ + if (!cells.is_empty(i)) {
404
+ + cells.rm(i);
405
+ + ++evicted;
406
+ + }
407
+ + }
408
+ + }
409
+ +
410
+ + LLAMA_LOG_INFO("%s: shrink soft cap %u -> %u (idle %llu ms, evicted %u cells)\n",
411
+ + __func__, to_cells, from_cells,
412
+ + (unsigned long long) (now - last_active_ms_), evicted);
413
+ +
414
+ + for (const auto & layer : layers) {
415
+ + opencoti_decommit_layer_range(layer.k, n_stream, from_cells, to_cells);
416
+ + opencoti_decommit_layer_range(layer.v, n_stream, from_cells, to_cells);
417
+ + }
418
+ +
419
+ + // After decommit, treat cells [from_cells, to_cells) as "uncleared" β€”
420
+ + // their pages will fault back in as fresh zero-filled pages on next
421
+ + // touch, but n_cells_cleared still recorded them as cleared. Roll back
422
+ + // the cleared mark so a future ensure_cleared() re-zeros them only when
423
+ + // they're actually needed.
424
+ + if (n_cells_cleared > from_cells) {
425
+ + n_cells_cleared = from_cells;
426
+ + }
427
+ + kv_size_alloc = from_cells;
428
+ +
429
+ + // Refresh last_active_ms_ so we don't immediately try again on the very
430
+ + // next init_batch (which would walk the empty layers for nothing).
431
+ + last_active_ms_ = now;
432
+ +}
433
+ +
434
+ void llama_kv_cache::clear(bool data) {
435
+ for (uint32_t s = 0; s < n_stream; ++s) {
436
+ v_cells[s].reset();
437
+ @@ -757,6 +938,12 @@ llama_memory_context_ptr llama_kv_cache::init_batch(
438
+ bool embd_all) {
439
+ GGML_UNUSED(embd_all);
440
+
441
+ + // opencoti F4 M3 Phase 3 β€” see docs/decisions/0001-lazy-slot-context.md
442
+ + // Check idle-shrink BEFORE ensure_cleared(): if shrink runs, it lowers
443
+ + // kv_size_alloc and rolls n_cells_cleared back, so ensure_cleared's
444
+ + // soft-cap-bounded clear targets only the (now smaller) initial cap.
445
+ + shrink_if_idle();
446
+ +
447
+ // opencoti F4 M3 Phase 1 β€” see docs/decisions/0001-lazy-slot-context.md
448
+ ensure_cleared();
449
+
450
+ @@ -804,6 +991,12 @@ llama_memory_context_ptr llama_kv_cache::init_update(llama_context * lctx, bool
451
+ }
452
+
453
+ llama_kv_cache::slot_info_vec_t llama_kv_cache::prepare(const std::vector<llama_ubatch> & ubatches) {
454
+ + // opencoti F4 M3 Phase 3 β€” see docs/decisions/0001-lazy-slot-context.md
455
+ + // Same reason ensure_cleared() is called here as well as init_batch:
456
+ + // the iswa wrapper bypasses init_batch and calls prepare() directly on
457
+ + // each inner kv_cache. shrink_if_idle is idempotent and cheap.
458
+ + shrink_if_idle();
459
+ +
460
+ // opencoti F4 M3 Phase 1 β€” see docs/decisions/0001-lazy-slot-context.md
461
+ // The iswa wrapper calls prepare() directly on each inner kv_cache without
462
+ // going through init_batch(), so the ensure_cleared() in init_batch alone
463
+ @@ -1184,6 +1377,13 @@ llama_kv_cache::slot_info llama_kv_cache::find_slot(const llama_ubatch & ubatch,
464
+ }
465
+
466
+ void llama_kv_cache::apply_ubatch(const slot_info & sinfo, const llama_ubatch & ubatch) {
467
+ + // opencoti F4 M3 Phase 3 β€” see docs/decisions/0001-lazy-slot-context.md
468
+ + // Refresh the idle timestamp on every actual write. shrink_if_idle uses
469
+ + // this to gate the shrink path.
470
+ + last_active_ms_ = (uint64_t)
471
+ + std::chrono::duration_cast<std::chrono::milliseconds>(
472
+ + std::chrono::steady_clock::now().time_since_epoch()).count();
473
+ +
474
+ // keep track of the max sequence position that we would overwrite with this ubatch
475
+ // for non-SWA cache, this would be always empty
476
+ llama_seq_id seq_pos_max_rm[LLAMA_MAX_SEQ];
477
+ diff --git a/llama.cpp/src/llama-kv-cache.h b/llama.cpp/src/llama-kv-cache.h
478
+ --- a/llama.cpp/src/llama-kv-cache.h
479
+ +++ b/llama.cpp/src/llama-kv-cache.h
480
+ @@ -106,6 +106,11 @@ public:
481
+ // (Phase 1 behavior). Otherwise clamped to [n_pad, kv_size]
482
+ // and rounded up to the next n_pad multiple.
483
+ uint32_t kv_size_initial,
484
+ + // opencoti F4 M3 Phase 3 β€” see docs/decisions/0001-lazy-slot-context.md
485
+ + // Idle threshold (ms) after which the soft cap is shrunk
486
+ + // back to kv_size_initial and the over-cap pages are
487
+ + // decommitted. 0 disables shrink-on-idle.
488
+ + uint32_t slot_shrink_idle_ms,
489
+ uint32_t n_seq_max,
490
+ uint32_t n_pad,
491
+ uint32_t n_swa,
492
+ @@ -180,6 +185,17 @@ public:
493
+ // The current soft cap (cells visible to find_slot).
494
+ uint32_t get_kv_size_alloc() const { return kv_size_alloc; }
495
+
496
+ + // opencoti F4 M3 Phase 3 β€” see docs/decisions/0001-lazy-slot-context.md
497
+ + // If the soft cap has been grown above kv_size_shrink_target and at least
498
+ + // slot_shrink_idle_ms_ have elapsed since the last apply_ubatch, reset
499
+ + // the soft cap to kv_size_shrink_target and posix_madvise(POSIX_MADV_DONTNEED)
500
+ + // the over-cap pages of every layer's K/V tensors. Idempotent and cheap
501
+ + // when the soft cap is already at the target or shrink-on-idle is disabled.
502
+ + // Called at the top of init_batch and prepare; safe because both happen
503
+ + // before any slot work and the embedder's per-request cells.reset() means
504
+ + // no slot ever holds onto cells across requests.
505
+ + void shrink_if_idle();
506
+ +
507
+ ggml_type type_k() const;
508
+ ggml_type type_v() const;
509
+
510
+ @@ -296,11 +312,21 @@ private:
511
+ // opencoti F4 M3 Phase 2 β€” see docs/decisions/0001-lazy-slot-context.md
512
+ // kv_size_max: hard cap on cells (set from constructor's kv_size).
513
+ // kv_size_alloc: current soft cap. find_slot only scans cells [0, kv_size_alloc).
514
+ - // Grows via ensure_capacity, never shrinks (Phase 2; Phase 3
515
+ - // adds shrink-on-idle).
516
+ + // Grows via ensure_capacity. Phase 3 adds shrink-on-idle:
517
+ + // shrink_if_idle() may reset it back to kv_size_shrink_target.
518
+ uint32_t kv_size_max = 0;
519
+ uint32_t kv_size_alloc = 0;
520
+
521
+ + // opencoti F4 M3 Phase 3 β€” see docs/decisions/0001-lazy-slot-context.md
522
+ + // Phase 3 shrink-on-idle state. kv_size_shrink_target is the cap value
523
+ + // the soft cap is reset to on shrink β€” set once at ctor from
524
+ + // kv_size_initial (or kv_size_max if Phase-1-mode). slot_shrink_idle_ms_
525
+ + // is the idle threshold in ms; 0 disables shrink. last_active_ms_ is a
526
+ + // monotonic timestamp updated by apply_ubatch.
527
+ + uint32_t kv_size_shrink_target = 0;
528
+ + uint32_t slot_shrink_idle_ms_ = 0;
529
+ + uint64_t last_active_ms_ = 0;
530
+ +
531
+ // the current index from where we start searching for a free slot in the ring buffer of KV cells (see find_slot())
532
+ // note: this is not part of the KV state and it's only used to speed-up the find_slot() method
533
+ std::vector<uint32_t> v_heads;
534
+ diff --git a/llama.cpp/src/llama-memory-hybrid-iswa.cpp b/llama.cpp/src/llama-memory-hybrid-iswa.cpp
535
+ --- a/llama.cpp/src/llama-memory-hybrid-iswa.cpp
536
+ +++ b/llama.cpp/src/llama-memory-hybrid-iswa.cpp
537
+ @@ -44,6 +44,10 @@ llama_memory_hybrid_iswa::llama_memory_hybrid_iswa(
538
+ // Hybrid-iswa does not yet thread slot_initial_ctx; pass 0 for
539
+ // Phase 1 backward-compat (full eager allocation).
540
+ 0,
541
+ + // opencoti F4 M3 Phase 3 β€” see docs/decisions/0001-lazy-slot-context.md
542
+ + // Hybrid-iswa does not yet thread slot_shrink_idle_ms; pass 0 for
543
+ + // Phase 2 backward-compat (no shrink-on-idle).
544
+ + 0,
545
+ n_seq_max,
546
+ n_ubatch,
547
+ n_pad,
548
+ diff --git a/llama.cpp/src/llama-memory-hybrid.cpp b/llama.cpp/src/llama-memory-hybrid.cpp
549
+ --- a/llama.cpp/src/llama-memory-hybrid.cpp
550
+ +++ b/llama.cpp/src/llama-memory-hybrid.cpp
551
+ @@ -43,6 +43,10 @@ llama_memory_hybrid::llama_memory_hybrid(
552
+ // Hybrid memory does not yet thread slot_initial_ctx; pass 0 for
553
+ // Phase 1 backward-compat (full eager allocation).
554
+ 0,
555
+ + // opencoti F4 M3 Phase 3 β€” see docs/decisions/0001-lazy-slot-context.md
556
+ + // Hybrid memory does not yet thread slot_shrink_idle_ms; pass 0 for
557
+ + // Phase 2 backward-compat (no shrink-on-idle).
558
+ + 0,
559
+ n_seq_max,
560
+ n_pad,
561
+ n_swa,
562
+ diff --git a/llama.cpp/src/llama-model.cpp b/llama.cpp/src/llama-model.cpp
563
+ --- a/llama.cpp/src/llama-model.cpp
564
+ +++ b/llama.cpp/src/llama-model.cpp
565
+ @@ -2075,6 +2075,8 @@ llama_memory_i * llama_model::create_memory(const llama_memory_params & params,
566
+ cparams.n_ctx_seq,
567
+ // opencoti F4 M3 Phase 2 β€” see docs/decisions/0001-lazy-slot-context.md
568
+ cparams.slot_initial_ctx,
569
+ + // opencoti F4 M3 Phase 3 β€” see docs/decisions/0001-lazy-slot-context.md
570
+ + cparams.slot_shrink_idle_ms,
571
+ cparams.n_seq_max,
572
+ cparams.n_ubatch,
573
+ 1,
574
+ @@ -2093,6 +2095,8 @@ llama_memory_i * llama_model::create_memory(const llama_memory_params & params,
575
+ cparams.n_ctx_seq,
576
+ // opencoti F4 M3 Phase 2 β€” see docs/decisions/0001-lazy-slot-context.md
577
+ cparams.slot_initial_ctx,
578
+ + // opencoti F4 M3 Phase 3 β€” see docs/decisions/0001-lazy-slot-context.md
579
+ + cparams.slot_shrink_idle_ms,
580
+ cparams.n_seq_max,
581
+ 1,
582
+ hparams.n_swa,
patches/0006-kv-reuse-prefix.patch ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ From: opencoti
2
+ Subject: [PATCH 0006] kv-cache reuse for agentic turns — session→slot affinity
3
+
4
+ opencoti routes an agentic loop through the bundled llamafile server: each
5
+ turn N+1 re-sends turn N's context prefix (system prompt + tool defs +
6
+ history) verbatim, then appends. The server already reuses KV for a matching
7
+ prefix β€” cache_prompt defaults to true and get_available_slot() picks the
8
+ idle slot whose cached token-prefix best matches the incoming prompt (above
9
+ --slot-prompt-similarity, then LRU). That covers single-session turn-2 reuse
10
+ at the default --parallel 1.
11
+
12
+ What upstream lacks is *session affinity*. With --parallel N and several
13
+ concurrent opencode sessions sharing slots, the LCP/LRU picker can route a
14
+ session to a slot holding a different session's KV, evicting a prefix that
15
+ would otherwise be reused. This patch adds an optional `session_id` request
16
+ field that keys slot selection:
17
+
18
+ - get_available_slot(): if the request carries a non-empty session_id that
19
+ already maps to an idle slot, return that slot (its resident KV is this
20
+ session's prefix by construction). The existing LCP→LRU path is the
21
+ fallback.
22
+ - launch_slot_with_task(): record session_id -> slot.id when a slot is
23
+ committed to a session, dropping any stale session previously bound to
24
+ that slot.
25
+
26
+ The prefix match itself is still the server's existing token-LCP β€” affinity
27
+ only ensures the right slot (the one holding the session's KV) is chosen, so
28
+ "(session_id, prefix) reuse" falls out without a separate prefix hash.
29
+
30
+ Regression shield: empty session_id (the default, and what every
31
+ non-opencoti client sends) skips the affinity phase entirely, leaving slot
32
+ selection byte-for-byte identical to upstream. Builds clean under cosmocc
33
+ (std::map / std::string already available via server-task.h).
34
+
35
+ Bench: perf/llamafile/turn-2-latency.bench.ts
36
+ Milestone: F5 M0 β€” see docs/features/advanced_kv.md
37
+ Design: docs/features/advanced_kv.md (M0)
38
+ Upstreaming: deferred; candidate to propose upstream as a generic session-id
39
+ slot-affinity hint once benched on opencoti workloads.
40
+ ---
41
+ diff --git a/llama.cpp/tools/server/server-context.cpp b/llama.cpp/tools/server/server-context.cpp
42
+ --- a/llama.cpp/tools/server/server-context.cpp
43
+ +++ b/llama.cpp/tools/server/server-context.cpp
44
+ @@ -698,6 +698,13 @@ private:
45
+ // Necessary similarity of prompt for slot selection
46
+ float slot_prompt_similarity = 0.0f;
47
+
48
+ + // opencoti F5 M0 kv-reuse β€” see docs/features/advanced_kv.md
49
+ + // session_id -> slot.id affinity, so an agentic session routes back to the
50
+ + // slot holding its resident KV prefix (turn-to-turn reuse, and no
51
+ + // cross-session eviction when --parallel > 1). Empty session_id is never
52
+ + // inserted, so non-opencoti traffic never consults or mutates this map.
53
+ + std::map<std::string, int> session_to_slot;
54
+ +
55
+ std::string model_name; // name of the loaded model, to be used by API
56
+ std::set<std::string> model_aliases; // additional names for the model
57
+ std::set<std::string> model_tags; // informational tags
58
+ @@ -1249,6 +1256,25 @@ private:
59
+
60
+ bool update_cache = false;
61
+
62
+ + // opencoti F5 M0 kv-reuse β€” see docs/features/advanced_kv.md
63
+ + // Session affinity: if this session already owns an idle slot, reuse
64
+ + // it. That slot's resident KV is the session's prefix, so the LCP path
65
+ + // below would pick it anyway when free β€” but the explicit pin keeps a
66
+ + // concurrent session from stealing/evicting it under --parallel > 1.
67
+ + // Skipped entirely for empty session_id, so upstream selection (LCP
68
+ + // then LRU, below) is unchanged for every non-opencoti request.
69
+ + if (ret == nullptr && !task.params.session_id.empty()) {
70
+ + auto it = session_to_slot.find(task.params.session_id);
71
+ + if (it != session_to_slot.end()) {
72
+ + for (server_slot & slot : slots) {
73
+ + if (slot.id == it->second && !slot.is_processing()) {
74
+ + ret = &slot;
75
+ + break;
76
+ + }
77
+ + }
78
+ + }
79
+ + }
80
+ +
81
+ // find the slot that has at least n% prompt similarity
82
+ if (ret == nullptr && slot_prompt_similarity != 0.0f) {
83
+ float sim_best = 0;
84
+ @@ -1498,6 +1524,22 @@ private:
85
+ slot.smpl.reset();
86
+ }
87
+
88
+ + // opencoti F5 M0 kv-reuse β€” see docs/features/advanced_kv.md
89
+ + // Record session→slot affinity before `task` is moved into the slot,
90
+ + // so this session's next turn routes back here and reuses its KV. A
91
+ + // slot serves one session at a time, so first drop any stale session
92
+ + // that previously mapped to this slot id.
93
+ + if (!task.params.session_id.empty()) {
94
+ + for (auto it = session_to_slot.begin(); it != session_to_slot.end(); ) {
95
+ + if (it->second == slot.id && it->first != task.params.session_id) {
96
+ + it = session_to_slot.erase(it);
97
+ + } else {
98
+ + ++it;
99
+ + }
100
+ + }
101
+ + session_to_slot[task.params.session_id] = slot.id;
102
+ + }
103
+ +
104
+ slot.task = std::make_unique<const server_task>(std::move(task));
105
+
106
+ slot.state = slot.task->is_child()
107
+ diff --git a/llama.cpp/tools/server/server-task.cpp b/llama.cpp/tools/server/server-task.cpp
108
+ --- a/llama.cpp/tools/server/server-task.cpp
109
+ +++ b/llama.cpp/tools/server/server-task.cpp
110
+ @@ -272,6 +272,7 @@ task_params server_task::params_from_json_cmpl(
111
+ params.n_discard = std::max(0, params.n_discard);
112
+ params.n_cmpl = json_value(data, "n_cmpl", json_value(data, "n", 1));
113
+ params.n_cache_reuse = json_value(data, "n_cache_reuse", defaults.n_cache_reuse);
114
+ + params.session_id = json_value(data, "session_id", std::string("")); // opencoti F5 M0 kv-reuse β€” see docs/features/advanced_kv.md
115
+ //params.t_max_prompt_ms = json_value(data, "t_max_prompt_ms", defaults.t_max_prompt_ms); // TODO: implement
116
+ params.t_max_predict_ms = json_value(data, "t_max_predict_ms", defaults.t_max_predict_ms);
117
+ params.response_fields = json_value(data, "response_fields", std::vector<std::string>());
118
+ diff --git a/llama.cpp/tools/server/server-task.h b/llama.cpp/tools/server/server-task.h
119
+ --- a/llama.cpp/tools/server/server-task.h
120
+ +++ b/llama.cpp/tools/server/server-task.h
121
+ @@ -61,6 +61,14 @@ struct task_params {
122
+
123
+ int32_t n_cache_reuse = 0; // min chunk size to attempt reusing from the cache via KV shifting (0 = disabled)
124
+
125
+ + // opencoti F5 M0 kv-reuse β€” see docs/features/advanced_kv.md
126
+ + // Optional session affinity key. When non-empty, the server routes this
127
+ + // request back to the slot the same session last used, reusing its
128
+ + // resident KV prefix across agentic turns. Empty (the default, and what
129
+ + // every non-opencoti client sends) preserves upstream slot selection
130
+ + // byte-for-byte.
131
+ + std::string session_id;
132
+ +
133
+ // number of prompt tokens before the latest user message
134
+ int32_t n_before_user = -1;
135
+
patches/0007-build-header-deps.patch ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ From: opencoti
2
+ Subject: [PATCH 0007] build infra β€” compiler-driven header dependency tracking
3
+
4
+ Editing any llama.cpp/src (or common/, tools/server/) header never marked its
5
+ dependent objects out of date, so every header change forced a full
6
+ from-scratch rebuild β€” the single biggest drag on iterating the F4/F5 vendored
7
+ patches.
8
+
9
+ Root cause: the build tracks header deps via cosmopolitan's `mkdeps`
10
+ (build/deps.mk β†’ o/$(MODE)/depend). mkdeps only resolves cosmopolitan's
11
+ root-relative include style; llama.cpp's sources use `#include "foo.h"`
12
+ resolved through `-iquote` dirs, which mkdeps cannot follow, so it emitted
13
+ ZERO edges for llama.cpp/src headers. Source->object tracking worked (the
14
+ pattern rule), but header->object did not.
15
+
16
+ Fix (compiler-driven deps, the modern standard): add `-MMD` to the COMPILE.c /
17
+ COMPILE.cc commands so cosmocc emits an accurate per-translation-unit
18
+ `<obj>.o.d` (project headers only β€” system headers excluded), then `-include`
19
+ those `.o.d` files in build/deps.mk. clang resolves the real `-iquote`
20
+ includes, so the header set is exact. cosmocc supports -MMD natively
21
+ (get_dependency_outputs in bin/cosmocc): the x86 `.o.d` lands next to the
22
+ object and an `.aarch64/*.o.d` twin under the arch subdir; only the x86 copy is
23
+ included (header sets are arch-independent).
24
+
25
+ Effect: a header edit now recompiles exactly the affected objects (both fat
26
+ arches, via the existing rule), and nothing else β€” no more scratch rebuilds.
27
+ The first build after this patch lands populates the .o.d set (a one-time full
28
+ compile); every build thereafter is correctly incremental.
29
+
30
+ Interaction with ccache: ccache wraps cosmocc's FAT driver, so a cache hit
31
+ restores only the x86 .o (never the .aarch64 twin). With accurate header
32
+ tracking, objects are no longer manually deleted to force recompiles, so the
33
+ .aarch64 twin always persists on disk in a warm tree and hits stay safe. The
34
+ remaining hazard β€” a warm cache restoring x86-only objects into a freshly
35
+ reset (o/-wiped) tree β€” is handled in the opencoti build pipeline, which runs
36
+ the full `build` with CCACHE_RECACHE=1.
37
+
38
+ Bench: docs/features/llamafile_build.md (incremental-rebuild verification:
39
+ touch an included header -> only its dependents recompile; touch a
40
+ non-dependency header -> no recompile)
41
+ Milestone: build infrastructure (supports F4/F5 β€” see docs/features/advanced_kv.md)
42
+ Upstreaming: build-only; no behavior change to the produced binary. Not
43
+ intended for upstream (cosmopolitan/llamafile use mkdeps by design); opencoti
44
+ carries it to make iterating the vendored patch series tractable.
45
+
46
+ diff --git a/build/deps.mk b/build/deps.mk
47
+ --- a/build/deps.mk
48
+ +++ b/build/deps.mk
49
+ @@ -23,3 +23,15 @@ $(INCS):
50
+ rm -f o/$(MODE)/depend
51
+
52
+ -include o/$(MODE)/depend
53
+ +
54
+ +# opencoti build-header-deps β€” see docs/features/llamafile_build.md
55
+ +# Pull in the per-object dependency files cosmocc emits under -MMD (added to
56
+ +# build/rules.mk COMPILE.*). mkdeps' o/$(MODE)/depend above cannot resolve
57
+ +# llama.cpp/src's -iquote includes, so it tracked zero of those headers and a
58
+ +# header edit never recompiled its dependents β€” every change forced a
59
+ +# from-scratch rebuild. The compiler-emitted `<obj>.o.d` files carry the exact
60
+ +# header set per translation unit. Only the x86 `.o.d` is pulled in; the
61
+ +# `.aarch64/*.o.d` twins list identical headers (arch-independent). The shell
62
+ +# find runs at parse time over prior-build outputs (none on a cold build β†’ a
63
+ +# harmless empty include, everything compiles anyway).
64
+ +-include $(shell [ -d o/$(MODE) ] && find o/$(MODE) -name '*.o.d' -not -path '*/.aarch64/*' 2>/dev/null)
65
+ diff --git a/build/rules.mk b/build/rules.mk
66
+ --- a/build/rules.mk
67
+ +++ b/build/rules.mk
68
+ @@ -6,8 +6,22 @@
69
+ # ==============================================================================
70
+
71
+ LINK.o = $(CXX) $(CCFLAGS) $(LDFLAGS)
72
+ -COMPILE.c = $(CC) $(CCFLAGS) $(CFLAGS) $(CPPFLAGS_) $(CPPFLAGS) $(TARGET_ARCH) -c
73
+ -COMPILE.cc = $(CXX) $(CCFLAGS) $(CXXFLAGS) $(CPPFLAGS_) $(CPPFLAGS) $(TARGET_ARCH) -c
74
+ +# opencoti build-header-deps β€” see docs/features/llamafile_build.md
75
+ +# -MMD makes cosmocc emit a per-TU dependency file listing the real (project,
76
+ +# not system) header set; build/deps.mk -includes them so a header edit
77
+ +# recompiles exactly the affected objects. Without this, mkdeps' depend tracked
78
+ +# zero llama.cpp/src headers (it can't resolve -iquote includes), so any header
79
+ +# change forced a from-scratch rebuild.
80
+ +#
81
+ +# `-MF $@.d` is REQUIRED, not cosmetic: cosmocc defaults the dep file to
82
+ +# `<obj>.d` (it appends `.d` β†’ e.g. foo.cpp.o.d), but ccache derives the
83
+ +# expected name from `-o foo.cpp.o` by stripping `.o` β†’ foo.cpp.d. That
84
+ +# mismatch made ccache stat a file cosmocc never wrote ("failed to stat
85
+ +# …foo.cpp.d") and abort every compile with "ccache internal error" β†’ nothing
86
+ +# was ever cached. Pinning -MF to cosmocc's actual name (`$@.d` = foo.cpp.o.d)
87
+ +# makes the two agree, so ccache caches and restores normally.
88
+ +COMPILE.c = $(CC) $(CCFLAGS) $(CFLAGS) $(CPPFLAGS_) $(CPPFLAGS) $(TARGET_ARCH) -MMD -MF $@.d -c
89
+ +COMPILE.cc = $(CXX) $(CCFLAGS) $(CXXFLAGS) $(CPPFLAGS_) $(CPPFLAGS) $(TARGET_ARCH) -MMD -MF $@.d -c
90
+
91
+ # ==============================================================================
92
+ # Standard Compilation Rules
patches/0010-rest-kv-eviction.patch ADDED
@@ -0,0 +1,476 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ From: opencoti
2
+ Subject: [PATCH 0010] retention-aware KV eviction β€” KeyDiff key-similarity, server-window
3
+
4
+ When an agentic context exceeds a slot's n_ctx, the server's context-shift
5
+ discards the *positional middle* [n_keep, n_keep+n_discard) regardless of what
6
+ sits there. This patch makes that choice retention-aware: it discards the
7
+ lowest-value *contiguous window* of the same width instead, keeping the
8
+ distinctive tokens and dropping the redundant ones.
9
+
10
+ Reference choice. The named technique (TRIM-KV, arXiv 2605.09649) needs
11
+ per-model retention gates trained by distillation β€” not a drop-in runtime
12
+ patch. Attention-score methods (H2O, SnapKV) need the materialized attention
13
+ matrix, which Flash Attention never produces (the vendored pin defaults
14
+ flash_attn_type = AUTO). So this substitutes a score-free, FA-compatible
15
+ criterion β€” KeyDiff (arXiv 2504.15364): score each cached key by its
16
+ *distinctiveness*, the mean over heads of (1 - cosine(key_head, mean_head)).
17
+ Keys close to the sequence's mean are redundant (evict); keys far from it are
18
+ distinctive (keep).
19
+
20
+ Mechanism (the "server-window" depth β€” mirror-safe):
21
+ - src/llama-memory.h: a new non-pure virtual seq_key_scores(seq_id,
22
+ layer_hint) with a default empty body, so no backend is forced to implement
23
+ it and the pure-virtual surface is unchanged. (Adding it grows the vtable β€”
24
+ a consistent recompile of all llama.cpp TUs is required; see the build
25
+ gotcha below.)
26
+ - src/llama-kv-cache.cpp: the real implementation β€” read each cached key of
27
+ one representative layer to host (amortized: only at the rare context-shift
28
+ trigger, unlike per-decode-interval scoring), group into heads, return a
29
+ per-position distinctiveness vector (+INF for absent positions). Empty when
30
+ unsupported (no cells, or a key type with no float converter) so the caller
31
+ falls back to positional discard.
32
+ - src/llama-kv-cache-iswa.cpp: delegate to the non-SWA base cache.
33
+ - src/llama-context.cpp + include/llama.h: a thin C shim
34
+ llama_memory_seq_key_scores() β€” a plain virtual call (no RTTI/downcast).
35
+ - tools/server/server-context.cpp: inside the EXISTING context-shift block,
36
+ when rest_kv_eviction is on, slide a width-n_discard window over
37
+ [n_keep, n_tokens - rest_kv_recent) and discard the minimum-retention one.
38
+ It is still ONE contiguous window, so the seq_rm/seq_add + token-mirror
39
+ rewrite stay 1:1 with KV positions β€” no rework of prefix-reuse/sampler.
40
+ - common/common.h + common/arg.cpp: --rest-kv-eviction (off by default),
41
+ --rest-kv-recent N (protect the recent tail), --rest-kv-layer N (-1 = auto).
42
+
43
+ Regression shield: --rest-kv-eviction off (the default, and every non-opencoti
44
+ client) leaves w0 = n_keep, i.e. context-shift is byte-for-byte upstream. Any
45
+ failure (non-KV memory, quantized keys, no room after the recent tail) also
46
+ falls back to the positional middle.
47
+
48
+ Build gotcha (buglog bug-175): seq_key_scores is a new virtual on
49
+ llama_memory_i, so the vtable layout changes. The llamafile Makefile tracks no
50
+ header deps; force a consistent recompile of all llama.cpp src/common/tools
51
+ objects (delete x86 .o, rebuild) β€” and because cosmocc is a fat compiler whose
52
+ aarch64 sibling object is NOT restored by a ccache hit, delete only x86 .o that
53
+ lack an aarch64 twin and rebuild with CCACHE_DISABLE=1, or use the full
54
+ reproducible build:llamafile.
55
+
56
+ Bench: perf/llamafile/rest-kv-eviction.bench.ts
57
+ Milestone: F5 M1 β€” see docs/features/advanced_kv.md
58
+ Design: docs/features/advanced_kv.md (M1)
59
+ Upstreaming: deferred; candidate to propose upstream as a pluggable
60
+ context-shift eviction policy once benched on opencoti workloads.
61
+ ---
62
+ diff --git a/llama.cpp/common/arg.cpp b/llama.cpp/common/arg.cpp
63
+ --- a/llama.cpp/common/arg.cpp
64
+ +++ b/llama.cpp/common/arg.cpp
65
+ @@ -1398,6 +1398,29 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
66
+ params.ctx_shift = value;
67
+ }
68
+ ).set_examples({LLAMA_EXAMPLE_COMPLETION, LLAMA_EXAMPLE_CLI, LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_IMATRIX, LLAMA_EXAMPLE_PERPLEXITY}).set_env("LLAMA_ARG_CONTEXT_SHIFT"));
69
+ + // opencoti F5 M1 rest-kv-eviction β€” see docs/features/advanced_kv.md
70
+ + add_opt(common_arg(
71
+ + {"--rest-kv-eviction"},
72
+ + {"--no-rest-kv-eviction"},
73
+ + string_format("when context shift fires, evict the lowest-retention (KeyDiff key-similarity) contiguous window instead of the positional middle (default: %s)", params.rest_kv_eviction ? "enabled" : "disabled"),
74
+ + [](common_params & params, bool value) {
75
+ + params.rest_kv_eviction = value;
76
+ + }
77
+ + ).set_examples({LLAMA_EXAMPLE_SERVER}).set_env("LLAMA_ARG_REST_KV_EVICTION"));
78
+ + add_opt(common_arg(
79
+ + {"--rest-kv-recent"}, "N",
80
+ + string_format("rest-kv: protect the most-recent N tokens from eviction (default: %d)", params.rest_kv_recent),
81
+ + [](common_params & params, int value) {
82
+ + params.rest_kv_recent = value;
83
+ + }
84
+ + ).set_examples({LLAMA_EXAMPLE_SERVER}).set_env("LLAMA_ARG_REST_KV_RECENT"));
85
+ + add_opt(common_arg(
86
+ + {"--rest-kv-layer"}, "N",
87
+ + string_format("rest-kv: model layer whose keys score retention, -1 = auto/middle (default: %d)", params.rest_kv_layer),
88
+ + [](common_params & params, int value) {
89
+ + params.rest_kv_layer = value;
90
+ + }
91
+ + ).set_examples({LLAMA_EXAMPLE_SERVER}).set_env("LLAMA_ARG_REST_KV_LAYER"));
92
+ add_opt(common_arg(
93
+ {"--chunks"}, "N",
94
+ string_format("max number of chunks to process (default: %d, -1 = all)", params.n_chunks),
95
+ diff --git a/llama.cpp/common/common.h b/llama.cpp/common/common.h
96
+ --- a/llama.cpp/common/common.h
97
+ +++ b/llama.cpp/common/common.h
98
+ @@ -554,6 +554,14 @@ struct common_params {
99
+ bool swa_full = false; // use full-size SWA cache (https://github.com/ggml-org/llama.cpp/pull/13194#issuecomment-2868343055)
100
+ bool kv_unified = false; // enable unified KV cache
101
+
102
+ + // opencoti F5 M1 rest-kv-eviction β€” see docs/features/advanced_kv.md
103
+ + // When a slot fills n_ctx and ctx_shift fires, discard the lowest-retention
104
+ + // *contiguous* window (KeyDiff key-similarity) instead of the blind
105
+ + // positional middle. Off => byte-for-byte upstream context-shift.
106
+ + bool rest_kv_eviction = false; // master switch (default off)
107
+ + int32_t rest_kv_recent = 256; // protect the most-recent N tokens from eviction
108
+ + int32_t rest_kv_layer = -1; // representative layer for scoring; -1 = auto (middle)
109
+ +
110
+ bool input_prefix_bos = false; // prefix BOS to user inputs, preceding input_prefix
111
+ bool use_mmap = true; // enable mmap to use filesystem cache
112
+ bool use_direct_io = false; // read from disk without buffering
113
+ diff --git a/llama.cpp/include/llama.h b/llama.cpp/include/llama.h
114
+ --- a/llama.cpp/include/llama.h
115
+ +++ b/llama.cpp/include/llama.h
116
+ @@ -730,6 +730,19 @@ extern "C" {
117
+ llama_pos p0,
118
+ llama_pos p1);
119
+
120
+ + // opencoti F5 M1 rest-kv-eviction β€” see docs/features/advanced_kv.md
121
+ + // Fill out[0..n) with per-position KeyDiff retention scores for seq_id
122
+ + // (higher = keep, +INF = absent position). layer_hint < 0 selects a default
123
+ + // representative layer. Returns the number of finite scores written; 0 when
124
+ + // unsupported (non-KV memory, a key type with no float converter, or an
125
+ + // empty sequence) β€” the caller should then fall back to positional eviction.
126
+ + LLAMA_API size_t llama_memory_seq_key_scores(
127
+ + llama_memory_t mem,
128
+ + llama_seq_id seq_id,
129
+ + int32_t layer_hint,
130
+ + float * out,
131
+ + size_t n);
132
+ +
133
+ // Copy all tokens that belong to the specified sequence to another sequence
134
+ // p0 < 0 : [0, p1]
135
+ // p1 < 0 : [p0, inf)
136
+ diff --git a/llama.cpp/src/llama-context.cpp b/llama.cpp/src/llama-context.cpp
137
+ --- a/llama.cpp/src/llama-context.cpp
138
+ +++ b/llama.cpp/src/llama-context.cpp
139
+ @@ -3732,6 +3732,38 @@ bool llama_memory_seq_rm(
140
+ return mem->seq_rm(seq_id, p0, p1);
141
+ }
142
+
143
+ +// opencoti F5 M1 rest-kv-eviction β€” see docs/features/advanced_kv.md
144
+ +// Thin C shim over the (defaulted) virtual llama_memory_i::seq_key_scores β€” a
145
+ +// plain virtual dispatch, so no RTTI/downcast and no change to the pure-virtual
146
+ +// surface of the interface. Non-KV memories inherit the empty default and this
147
+ +// returns 0, leaving the server on its positional fallback.
148
+ +size_t llama_memory_seq_key_scores(
149
+ + llama_memory_t mem,
150
+ + llama_seq_id seq_id,
151
+ + int32_t layer_hint,
152
+ + float * out,
153
+ + size_t n) {
154
+ + if (!mem || !out || n == 0) {
155
+ + return 0;
156
+ + }
157
+ +
158
+ + const std::vector<float> scores = mem->seq_key_scores(seq_id, layer_hint);
159
+ + if (scores.empty()) {
160
+ + return 0;
161
+ + }
162
+ +
163
+ + size_t cnt = 0;
164
+ + for (size_t p = 0; p < n; ++p) {
165
+ + const float s = p < scores.size() ? scores[p] : std::numeric_limits<float>::infinity();
166
+ + out[p] = s;
167
+ + if (std::isfinite(s)) {
168
+ + cnt++;
169
+ + }
170
+ + }
171
+ +
172
+ + return cnt;
173
+ +}
174
+ +
175
+ void llama_memory_seq_cp(
176
+ llama_memory_t mem,
177
+ llama_seq_id seq_id_src,
178
+ diff --git a/llama.cpp/src/llama-kv-cache-iswa.cpp b/llama.cpp/src/llama-kv-cache-iswa.cpp
179
+ --- a/llama.cpp/src/llama-kv-cache-iswa.cpp
180
+ +++ b/llama.cpp/src/llama-kv-cache-iswa.cpp
181
+ @@ -117,6 +117,14 @@ llama_pos llama_kv_cache_iswa::seq_pos_max(llama_seq_id seq_id) const {
182
+ return kv_swa->seq_pos_max(seq_id);
183
+ }
184
+
185
+ +std::vector<float> llama_kv_cache_iswa::seq_key_scores(llama_seq_id seq_id, int32_t layer_hint) const {
186
+ + // opencoti F5 M1 rest-kv-eviction β€” see docs/features/advanced_kv.md
187
+ + // Score on the non-SWA base cache: the SWA cache only ever retains a
188
+ + // sliding window, so its keys are not the right eviction target. The base
189
+ + // cache is a superset of the SWA cache, so it has every position we score.
190
+ + return kv_base->seq_key_scores(seq_id, layer_hint);
191
+ +}
192
+ +
193
+ std::map<ggml_backend_buffer_type_t, size_t> llama_kv_cache_iswa::memory_breakdown() const {
194
+ std::map<ggml_backend_buffer_type_t, size_t> mb = kv_base->memory_breakdown();
195
+ for (const auto & buft_size : kv_swa->memory_breakdown()) {
196
+ diff --git a/llama.cpp/src/llama-kv-cache-iswa.h b/llama.cpp/src/llama-kv-cache-iswa.h
197
+ --- a/llama.cpp/src/llama-kv-cache-iswa.h
198
+ +++ b/llama.cpp/src/llama-kv-cache-iswa.h
199
+ @@ -62,6 +62,9 @@ public:
200
+ llama_pos seq_pos_min(llama_seq_id seq_id) const override;
201
+ llama_pos seq_pos_max(llama_seq_id seq_id) const override;
202
+
203
+ + // opencoti F5 M1 rest-kv-eviction β€” see docs/features/advanced_kv.md
204
+ + std::vector<float> seq_key_scores(llama_seq_id seq_id, int32_t layer_hint) const override;
205
+ +
206
+ std::map<ggml_backend_buffer_type_t, size_t> memory_breakdown() const override;
207
+
208
+ // state write/load
209
+ diff --git a/llama.cpp/src/llama-kv-cache.cpp b/llama.cpp/src/llama-kv-cache.cpp
210
+ --- a/llama.cpp/src/llama-kv-cache.cpp
211
+ +++ b/llama.cpp/src/llama-kv-cache.cpp
212
+ @@ -711,6 +711,137 @@ bool llama_kv_cache::seq_rm(llama_seq_id seq_id, llama_pos p0, llama_pos p1) {
213
+ return true;
214
+ }
215
+
216
+ +std::vector<float> llama_kv_cache::seq_key_scores(llama_seq_id seq_id, int32_t layer_hint) const {
217
+ + // opencoti F5 M1 rest-kv-eviction β€” see docs/features/advanced_kv.md
218
+ + // KeyDiff-style, score-free (Flash-Attention-compatible) retention signal:
219
+ + // for one representative layer, read every cached key of `seq_id`, group
220
+ + // it into attention heads, and score each position by its key's
221
+ + // *distinctiveness* β€” the mean over heads of (1 - cosine(key_head,
222
+ + // mean_head)). High = far from the sequence's mean key = keep; low =
223
+ + // redundant = safe to evict. The server uses this to choose the
224
+ + // lowest-retention contiguous window to discard during context-shift,
225
+ + // instead of the blind positional middle. Returns scores indexed by token
226
+ + // position, +INF for absent positions (never selected). An empty result
227
+ + // means "unsupported here" (no cells, or keys in a type with no float
228
+ + // converter) and the caller falls back to positional discard. Reads happen
229
+ + // only at the (infrequent) context-shift trigger, so the device->host copy
230
+ + // is amortized β€” unlike per-decode-interval scoring.
231
+ +
232
+ + if (seq_id < 0 || (size_t) seq_id >= seq_to_stream.size() || layers.empty()) {
233
+ + return {};
234
+ + }
235
+ +
236
+ + const uint32_t strm = seq_to_stream[seq_id];
237
+ + const auto & cells = v_cells[strm];
238
+ +
239
+ + const llama_pos pmax = cells.seq_pos_max(seq_id);
240
+ + if (pmax < 0) {
241
+ + return {};
242
+ + }
243
+ +
244
+ + // pick a representative cache layer. A non-negative hint first tries to map
245
+ + // a *model* layer id into the cache; failing that it is clamped as a direct
246
+ + // cache-layer index. Default (hint < 0) is the middle layer β€” empirically
247
+ + // the most informative for retention and cheap (one layer's keys only).
248
+ + int32_t cil = (int32_t) layers.size() / 2;
249
+ + if (layer_hint >= 0) {
250
+ + const auto it = map_layer_ids.find(layer_hint);
251
+ + cil = it != map_layer_ids.end()
252
+ + ? it->second
253
+ + : std::min<int32_t>(layer_hint, (int32_t) layers.size() - 1);
254
+ + }
255
+ +
256
+ + const auto & layer = layers[cil];
257
+ + const uint32_t mil = layer.il;
258
+ +
259
+ + const uint32_t n_head_kv = hparams.n_head_kv(mil);
260
+ + const uint32_t n_embd_head_k = hparams.n_embd_head_k(mil);
261
+ + const uint32_t n_embd_k_gqa = n_head_kv * n_embd_head_k;
262
+ + if (n_head_kv == 0 || n_embd_head_k == 0 || n_embd_k_gqa == 0) {
263
+ + return {};
264
+ + }
265
+ +
266
+ + ggml_tensor * k = layer.k_stream[strm];
267
+ + if (!k) {
268
+ + return {};
269
+ + }
270
+ +
271
+ + const auto * tt = ggml_get_type_traits(k->type);
272
+ + if (!tt || !tt->to_float) {
273
+ + return {}; // e.g. a quantized K cache with no dequantizer -> positional fallback
274
+ + }
275
+ +
276
+ + const size_t row_bytes = ggml_row_size(k->type, n_embd_k_gqa);
277
+ +
278
+ + // collect (pos, cell index) for this sequence, ascending by position
279
+ + std::vector<std::pair<llama_pos, uint32_t>> pc;
280
+ + pc.reserve(cells.get_used());
281
+ + for (uint32_t i = 0; i < cells.size(); ++i) {
282
+ + if (!cells.is_empty(i) && cells.seq_has(i, seq_id)) {
283
+ + pc.emplace_back(cells.pos_get(i), i);
284
+ + }
285
+ + }
286
+ + if (pc.empty()) {
287
+ + return {};
288
+ + }
289
+ + std::sort(pc.begin(), pc.end());
290
+ +
291
+ + const size_t n = pc.size();
292
+ +
293
+ + // read each key row once (amortized device->host copy), dequantized to f32
294
+ + std::vector<float> keys((size_t) n * n_embd_k_gqa);
295
+ + std::vector<uint8_t> rowbuf(row_bytes);
296
+ + for (size_t j = 0; j < n; ++j) {
297
+ + const uint32_t i = pc[j].second;
298
+ + ggml_backend_tensor_get(k, rowbuf.data(), (size_t) i * row_bytes, row_bytes);
299
+ + tt->to_float(rowbuf.data(), keys.data() + j * n_embd_k_gqa, (int64_t) n_embd_k_gqa);
300
+ + }
301
+ +
302
+ + // per-head mean key across the sequence, then per-head mean norm
303
+ + std::vector<float> mean((size_t) n_embd_k_gqa, 0.0f);
304
+ + for (size_t j = 0; j < n; ++j) {
305
+ + const float * kv = keys.data() + j * n_embd_k_gqa;
306
+ + for (uint32_t e = 0; e < n_embd_k_gqa; ++e) {
307
+ + mean[e] += kv[e];
308
+ + }
309
+ + }
310
+ + for (uint32_t e = 0; e < n_embd_k_gqa; ++e) {
311
+ + mean[e] /= (float) n;
312
+ + }
313
+ +
314
+ + std::vector<float> mean_norm(n_head_kv, 0.0f);
315
+ + for (uint32_t h = 0; h < n_head_kv; ++h) {
316
+ + const float * mh = mean.data() + (size_t) h * n_embd_head_k;
317
+ + double s = 0.0;
318
+ + for (uint32_t d = 0; d < n_embd_head_k; ++d) {
319
+ + s += (double) mh[d] * (double) mh[d];
320
+ + }
321
+ + mean_norm[h] = (float) std::sqrt(s);
322
+ + }
323
+ +
324
+ + // distinctiveness per position = mean over heads of (1 - cos(key_head, mean_head))
325
+ + std::vector<float> out((size_t) pmax + 1, std::numeric_limits<float>::infinity());
326
+ + for (size_t j = 0; j < n; ++j) {
327
+ + const float * kv = keys.data() + j * n_embd_k_gqa;
328
+ + double acc = 0.0;
329
+ + for (uint32_t h = 0; h < n_head_kv; ++h) {
330
+ + const float * kh = kv + (size_t) h * n_embd_head_k;
331
+ + const float * mh = mean.data() + (size_t) h * n_embd_head_k;
332
+ + double dot = 0.0, kn = 0.0;
333
+ + for (uint32_t d = 0; d < n_embd_head_k; ++d) {
334
+ + dot += (double) kh[d] * (double) mh[d];
335
+ + kn += (double) kh[d] * (double) kh[d];
336
+ + }
337
+ + const double denom = std::sqrt(kn) * (double) mean_norm[h];
338
+ + const double cos = denom > 0.0 ? dot / denom : 0.0;
339
+ + acc += (1.0 - cos);
340
+ + }
341
+ + out[(size_t) pc[j].first] = (float) (acc / (double) n_head_kv);
342
+ + }
343
+ +
344
+ + return out;
345
+ +}
346
+ +
347
+ void llama_kv_cache::seq_cp(llama_seq_id seq_id_src, llama_seq_id seq_id_dst, llama_pos p0, llama_pos p1) {
348
+ GGML_ASSERT(seq_id_src >= 0 && (size_t) seq_id_src < seq_to_stream.size());
349
+ GGML_ASSERT(seq_id_dst >= 0 && (size_t) seq_id_dst < seq_to_stream.size());
350
+ diff --git a/llama.cpp/src/llama-kv-cache.h b/llama.cpp/src/llama-kv-cache.h
351
+ --- a/llama.cpp/src/llama-kv-cache.h
352
+ +++ b/llama.cpp/src/llama-kv-cache.h
353
+ @@ -146,6 +146,9 @@ public:
354
+ llama_pos seq_pos_min(llama_seq_id seq_id) const override;
355
+ llama_pos seq_pos_max(llama_seq_id seq_id) const override;
356
+
357
+ + // opencoti F5 M1 rest-kv-eviction β€” see docs/features/advanced_kv.md
358
+ + std::vector<float> seq_key_scores(llama_seq_id seq_id, int32_t layer_hint) const override;
359
+ +
360
+ std::map<ggml_backend_buffer_type_t, size_t> memory_breakdown() const override;
361
+
362
+ // state write/load
363
+ diff --git a/llama.cpp/src/llama-memory.h b/llama.cpp/src/llama-memory.h
364
+ --- a/llama.cpp/src/llama-memory.h
365
+ +++ b/llama.cpp/src/llama-memory.h
366
+ @@ -6,6 +6,7 @@
367
+ #include <map>
368
+ #include <memory>
369
+ #include <functional>
370
+ +#include <vector>
371
+
372
+ struct llama_ubatch;
373
+
374
+ @@ -112,6 +113,22 @@ struct llama_memory_i {
375
+ virtual llama_pos seq_pos_min(llama_seq_id seq_id) const = 0;
376
+ virtual llama_pos seq_pos_max(llama_seq_id seq_id) const = 0;
377
+
378
+ + // opencoti F5 M1 rest-kv-eviction β€” see docs/features/advanced_kv.md
379
+ + // Per-position KeyDiff "retention" scores for one sequence: higher = more
380
+ + // distinctive (key far from the sequence's mean key) = keep; lower = more
381
+ + // redundant = safe to evict. The server uses these to pick WHICH window to
382
+ + // discard during context-shift, instead of the blind positional middle.
383
+ + // Returns a vector indexed by token position [0, seq_pos_max]; absent
384
+ + // positions are left at +INF (never evicted). The default returns empty so
385
+ + // non-KV memories β€” and any caller that never opts in β€” are byte-for-byte
386
+ + // unaffected (the eviction caller then falls back to positional discard).
387
+ + // layer_hint < 0 selects a default representative layer.
388
+ + virtual std::vector<float> seq_key_scores(llama_seq_id seq_id, int32_t layer_hint) const {
389
+ + (void) seq_id;
390
+ + (void) layer_hint;
391
+ + return {};
392
+ + }
393
+ +
394
+ virtual std::map<ggml_backend_buffer_type_t, size_t> memory_breakdown() const = 0;
395
+
396
+ //
397
+ diff --git a/llama.cpp/tools/server/server-context.cpp b/llama.cpp/tools/server/server-context.cpp
398
+ --- a/llama.cpp/tools/server/server-context.cpp
399
+ +++ b/llama.cpp/tools/server/server-context.cpp
400
+ @@ -2408,14 +2408,63 @@ private:
401
+ const int n_left = slot.prompt.n_tokens() - n_keep;
402
+ const int n_discard = slot.task->params.n_discard ? slot.task->params.n_discard : (n_left / 2);
403
+
404
+ + // opencoti F5 M1 rest-kv-eviction β€” see docs/features/advanced_kv.md
405
+ + // Upstream discards the positional middle [n_keep, n_keep+n_discard).
406
+ + // When rest-kv-eviction is on, slide that same-width window over the
407
+ + // evictable region [n_keep, n_tokens - rest_kv_recent) and discard the
408
+ + // window with the LOWEST retention sum (KeyDiff key-similarity) β€” keep the
409
+ + // distinctive tokens, drop the redundant ones. It is still ONE contiguous
410
+ + // window, so the seq_rm/seq_add + token-mirror rewrite below stay 1:1 with
411
+ + // KV positions. Any failure (eviction off, non-KV memory, quantized keys,
412
+ + // no room after protecting the recent tail) leaves w0 = n_keep, i.e. the
413
+ + // byte-for-byte upstream positional discard.
414
+ + // opencoti F5 M6 MTP-composition: scoring reads ctx_tgt (the authoritative
415
+ + // full-model KV); the SAME window w0 is then applied to BOTH the target and
416
+ + // the draft KV cache below, preserving target/draft position alignment for
417
+ + // speculative decode.
418
+ + int w0 = n_keep;
419
+ + if (params_base.rest_kv_eviction && n_discard > 0) {
420
+ + const int n_tokens = slot.prompt.n_tokens();
421
+ + const int recent = std::max(0, params_base.rest_kv_recent);
422
+ + const int hi = n_tokens - recent - n_discard; // last valid window start (inclusive)
423
+ + if (hi >= n_keep) {
424
+ + std::vector<float> scores(n_tokens, std::numeric_limits<float>::infinity());
425
+ + const size_t got = llama_memory_seq_key_scores(
426
+ + llama_get_memory(ctx_tgt), slot.id, params_base.rest_kv_layer, scores.data(), n_tokens);
427
+ + if (got > 0) {
428
+ + // prefix sums over [n_keep, n_tokens); an absent position gets a
429
+ + // large finite penalty so a window covering one is never picked
430
+ + // (avoids inf/inf arithmetic in the sliding sum).
431
+ + const int m = n_tokens - n_keep;
432
+ + std::vector<double> pref(m + 1, 0.0);
433
+ + for (int i = 0; i < m; ++i) {
434
+ + const float sv = scores[n_keep + i];
435
+ + pref[i + 1] = pref[i] + (std::isfinite(sv) ? (double) sv : 1e30);
436
+ + }
437
+ + double best = std::numeric_limits<double>::infinity();
438
+ + int best_w0 = n_keep;
439
+ + for (int s = n_keep; s <= hi; ++s) {
440
+ + const double sum = pref[s - n_keep + n_discard] - pref[s - n_keep];
441
+ + if (sum < best) {
442
+ + best = sum;
443
+ + best_w0 = s;
444
+ + }
445
+ + }
446
+ + w0 = best_w0;
447
+ + SLT_WRN(slot, "rest-kv eviction: discard [%d, %d) retention=%.3f (vs positional [%d, %d))\n",
448
+ + w0, w0 + n_discard, best, n_keep, n_keep + n_discard);
449
+ + }
450
+ + }
451
+ + }
452
+ +
453
+ SLT_WRN(slot, "slot context shift, n_keep = %d, n_left = %d, n_discard = %d\n", n_keep, n_left, n_discard);
454
+
455
+ - common_context_seq_rm (ctx_tgt, slot.id, n_keep , n_keep + n_discard);
456
+ - common_context_seq_add(ctx_tgt, slot.id, n_keep + n_discard, slot.prompt.n_tokens(), -n_discard);
457
+ + common_context_seq_rm (ctx_tgt, slot.id, w0 , w0 + n_discard);
458
+ + common_context_seq_add(ctx_tgt, slot.id, w0 + n_discard, slot.prompt.n_tokens(), -n_discard);
459
+
460
+ if (ctx_dft) {
461
+ - common_context_seq_rm (ctx_dft.get(), slot.id, n_keep , n_keep + n_discard);
462
+ - common_context_seq_add(ctx_dft.get(), slot.id, n_keep + n_discard, slot.prompt.tokens.pos_next(), -n_discard);
463
+ + common_context_seq_rm (ctx_dft.get(), slot.id, w0 , w0 + n_discard);
464
+ + common_context_seq_add(ctx_dft.get(), slot.id, w0 + n_discard, slot.prompt.tokens.pos_next(), -n_discard);
465
+ }
466
+
467
+ // add generated tokens to cache
468
+ @@ -2424,7 +2473,7 @@ private:
469
+ GGML_ASSERT(!slot.prompt.tokens.has_mtmd);
470
+
471
+ llama_tokens new_tokens = slot.prompt.tokens.get_tokens(); // copy
472
+ - for (size_t i = n_keep + n_discard; i < new_tokens.size(); i++) {
473
+ + for (size_t i = w0 + n_discard; i < new_tokens.size(); i++) {
474
+ new_tokens[i - n_discard] = new_tokens[i];
475
+ }
476
+
patches/0020-headinfer-per-head.patch ADDED
@@ -0,0 +1,520 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ From: opencoti
2
+ Subject: [PATCH 0020] F5 M2 β€” HeadInfer: head-wise GPU/CPU KV residency split
3
+
4
+ Keep a configurable fraction of KV heads GPU-resident; offload the rest to
5
+ host memory; reassemble the full head set via ggml_concat on the head axis
6
+ at each attention step. Default off (frac = 1.0). Validated 6/6 on solidPC
7
+ RTX 3090 with Qwen2.5-Coder-0.5B-IQ4_XS at -ngl 99: GPU KV halves (48 β†’ 24
8
+ MiB at frac=0.5), CPU half appears (24 MiB), greedy decode byte-identical
9
+ between baseline and split. See docs/features/advanced_kv.md (F5 M2).
10
+
11
+ The CUDA backend's supports_op for GGML_OP_CONCAT was over-broad (claimed
12
+ any non-I32/I16 type) while the kernel ggml_cuda_op_concat only implements
13
+ F32. Tightened to match the kernel β€” F16 concat now correctly routes to the
14
+ CPU backend (which has a real F16 path at ggml-cpu/ops.cpp:1980). Required
15
+ for the reassembly to run on F16 KV caches.
16
+
17
+ diff --git a/llama.cpp/common/arg.cpp b/llama.cpp/common/arg.cpp
18
+ --- a/llama.cpp/common/arg.cpp
19
+ +++ b/llama.cpp/common/arg.cpp
20
+ @@ -1421,6 +1421,14 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
21
+ params.rest_kv_layer = value;
22
+ }
23
+ ).set_examples({LLAMA_EXAMPLE_SERVER}).set_env("LLAMA_ARG_REST_KV_LAYER"));
24
+ + // opencoti F5 M2 headinfer β€” see docs/features/advanced_kv.md
25
+ + add_opt(common_arg(
26
+ + {"--headinfer-gpu-heads-frac"}, "F",
27
+ + string_format("headinfer: fraction of KV heads kept GPU-resident; the rest are offloaded to host memory and streamed back per step (default: %.2f, 1.0 = off). Only effective for GPU-offloaded layers with flash-attention.", (double) params.headinfer_gpu_heads_frac),
28
+ + [](common_params & params, const std::string & value) {
29
+ + params.headinfer_gpu_heads_frac = std::stof(value);
30
+ + }
31
+ + ).set_examples({LLAMA_EXAMPLE_SERVER}).set_env("LLAMA_ARG_HEADINFER_GPU_HEADS_FRAC"));
32
+ add_opt(common_arg(
33
+ {"--chunks"}, "N",
34
+ string_format("max number of chunks to process (default: %d, -1 = all)", params.n_chunks),
35
+ diff --git a/llama.cpp/common/common.cpp b/llama.cpp/common/common.cpp
36
+ --- a/llama.cpp/common/common.cpp
37
+ +++ b/llama.cpp/common/common.cpp
38
+ @@ -1632,6 +1632,8 @@ struct llama_context_params common_context_params_to_llama(const common_params &
39
+ cparams.slot_initial_ctx = params.slot_initial_ctx < 0 ? 0u : (uint32_t) params.slot_initial_ctx;
40
+ // opencoti F4 M3 Phase 3 β€” see docs/decisions/0001-lazy-slot-context.md
41
+ cparams.slot_shrink_idle_ms = params.slot_shrink_idle_ms < 0 ? 0u : (uint32_t) params.slot_shrink_idle_ms;
42
+ + // opencoti F5 M2 headinfer β€” see docs/features/advanced_kv.md
43
+ + cparams.headinfer_gpu_heads_frac = params.headinfer_gpu_heads_frac;
44
+ cparams.n_rs_seq = params.speculative.need_n_rs_seq();
45
+ cparams.n_batch = params.n_batch;
46
+ cparams.n_ubatch = params.n_ubatch;
47
+ diff --git a/llama.cpp/common/common.h b/llama.cpp/common/common.h
48
+ --- a/llama.cpp/common/common.h
49
+ +++ b/llama.cpp/common/common.h
50
+ @@ -435,6 +435,11 @@ struct common_params {
51
+ // slot_initial_ctx and the over-cap pages are decommitted via
52
+ // posix_madvise. 0 disables shrink-on-idle (Phase 2 behavior).
53
+ int32_t slot_shrink_idle_ms = 0;
54
+ + // opencoti F5 M2 headinfer β€” see docs/features/advanced_kv.md
55
+ + // Fraction of KV heads kept GPU-resident; the rest are offloaded to host
56
+ + // memory and streamed back per step. 1.0 = off (no split). Only active for
57
+ + // GPU-offloaded layers with flash-attention (non-transposed V cache).
58
+ + float headinfer_gpu_heads_frac = 1.0f;
59
+ int32_t n_batch = 2048; // logical batch size for prompt processing (must be >=32 to use BLAS)
60
+ int32_t n_ubatch = 512; // physical batch size for prompt processing (must be >=32 to use BLAS)
61
+ int32_t n_keep = 0; // number of tokens to keep from initial prompt
62
+ diff --git a/llama.cpp/ggml/src/ggml-cuda/ggml-cuda.cu b/llama.cpp/ggml/src/ggml-cuda/ggml-cuda.cu
63
+ --- a/llama.cpp/ggml/src/ggml-cuda/ggml-cuda.cu
64
+ +++ b/llama.cpp/ggml/src/ggml-cuda/ggml-cuda.cu
65
+ @@ -5308,8 +5308,16 @@ static bool GGML_CALL ggml_backend_cuda_device_supports_op(ggml_backend_dev_t de
66
+ return op->type == GGML_TYPE_F32 && (op->src[0]->ne[2]*op->src[0]->ne[3]) <= (1 << 15);
67
+ case GGML_OP_CONCAT:
68
+ {
69
+ - ggml_type src0_type = op->src[0]->type;
70
+ - return src0_type != GGML_TYPE_I32 && src0_type != GGML_TYPE_I16;
71
+ + // opencoti F5 M2 headinfer β€” see docs/features/advanced_kv.md
72
+ + // Tighten to F32-only to match ggml_cuda_op_concat's three
73
+ + // hard asserts at ggml-cuda/concat.cu:158-160. The over-broad
74
+ + // upstream filter ("any type except I32/I16") trusts the
75
+ + // scheduler into dispatching F16 concats to CUDA, where the
76
+ + // kernel asserts; with this fix the scheduler correctly routes
77
+ + // F16 concat to the CPU backend, which has a real F16 path
78
+ + // (ggml-cpu/ops.cpp:1980 concat_f16). Required for M2 get_k /
79
+ + // get_v reassembly on F16 KV caches.
80
+ + return op->src[0]->type == GGML_TYPE_F32;
81
+ } break;
82
+ case GGML_OP_CONV_TRANSPOSE_1D:
83
+ {
84
+ diff --git a/llama.cpp/include/llama.h b/llama.cpp/include/llama.h
85
+ --- a/llama.cpp/include/llama.h
86
+ +++ b/llama.cpp/include/llama.h
87
+ @@ -353,6 +353,11 @@ extern "C" {
88
+ // (Phase 2 behavior β€” soft cap grows monotonically). Requires
89
+ // slot_initial_ctx > 0 to have any visible effect.
90
+ uint32_t slot_shrink_idle_ms;
91
+ + // opencoti F5 M2 headinfer β€” see docs/features/advanced_kv.md
92
+ + // Fraction of KV heads kept GPU-resident (rest offloaded to host and
93
+ + // streamed back per step). 1.0 = off. Only effective for offloaded
94
+ + // layers with flash-attention (non-transposed V cache).
95
+ + float headinfer_gpu_heads_frac;
96
+ uint32_t n_rs_seq; // number of recurrent-state snapshots per seq for rollback (0 = no rollback) [EXPERIMENTAL]
97
+ int32_t n_threads; // number of threads to use for generation
98
+ int32_t n_threads_batch; // number of threads to use for batch processing
99
+ diff --git a/llama.cpp/src/llama-context.cpp b/llama.cpp/src/llama-context.cpp
100
+ --- a/llama.cpp/src/llama-context.cpp
101
+ +++ b/llama.cpp/src/llama-context.cpp
102
+ @@ -62,6 +62,8 @@ llama_context::llama_context(
103
+ cparams.slot_initial_ctx = params.slot_initial_ctx;
104
+ // opencoti F4 M3 Phase 3 β€” see docs/decisions/0001-lazy-slot-context.md
105
+ cparams.slot_shrink_idle_ms = params.slot_shrink_idle_ms;
106
+ + // opencoti F5 M2 headinfer β€” see docs/features/advanced_kv.md
107
+ + cparams.headinfer_gpu_heads_frac = params.headinfer_gpu_heads_frac;
108
+
109
+ cparams.n_threads = params.n_threads;
110
+ cparams.n_threads_batch = params.n_threads_batch;
111
+ @@ -3353,6 +3355,7 @@ llama_context_params llama_context_default_params() {
112
+ /*.n_seq_max =*/ 1,
113
+ /*.slot_initial_ctx =*/ 0,
114
+ /*.slot_shrink_idle_ms =*/ 0,
115
+ + /*.headinfer_gpu_heads_frac =*/ 1.0f,
116
+ /*.n_rs_seq =*/ 0,
117
+ /*.n_threads =*/ GGML_DEFAULT_N_THREADS, // TODO: better default
118
+ /*.n_threads_batch =*/ GGML_DEFAULT_N_THREADS,
119
+ diff --git a/llama.cpp/src/llama-cparams.h b/llama.cpp/src/llama-cparams.h
120
+ --- a/llama.cpp/src/llama-cparams.h
121
+ +++ b/llama.cpp/src/llama-cparams.h
122
+ @@ -21,6 +21,8 @@ struct llama_cparams {
123
+ // milliseconds: when a grown KV-cache is idle for at least this long, the
124
+ // soft cap is reset to slot_initial_ctx and the pages beyond are decommitted.
125
+ uint32_t slot_shrink_idle_ms;
126
+ + // opencoti F5 M2 headinfer β€” see docs/features/advanced_kv.md
127
+ + float headinfer_gpu_heads_frac;
128
+ uint32_t n_rs_seq; // number of recurrent-state snapshots per seq for rollback
129
+ int32_t n_threads; // number of threads to use for generation
130
+ int32_t n_threads_batch; // number of threads to use for batch processing
131
+ diff --git a/llama.cpp/src/llama-kv-cache-iswa.cpp b/llama.cpp/src/llama-kv-cache-iswa.cpp
132
+ --- a/llama.cpp/src/llama-kv-cache-iswa.cpp
133
+ +++ b/llama.cpp/src/llama-kv-cache-iswa.cpp
134
+ @@ -22,6 +22,7 @@ llama_kv_cache_iswa::llama_kv_cache_iswa(
135
+ uint32_t kv_size,
136
+ uint32_t kv_size_initial,
137
+ uint32_t slot_shrink_idle_ms,
138
+ + float headinfer_gpu_heads_frac,
139
+ uint32_t n_seq_max,
140
+ uint32_t n_ubatch,
141
+ uint32_t n_pad,
142
+ @@ -64,6 +65,7 @@ llama_kv_cache_iswa::llama_kv_cache_iswa(
143
+ kv_base = std::make_unique<llama_kv_cache>(
144
+ model, type_k, type_v,
145
+ v_trans, offload, unified, size_base, kv_size_initial, slot_shrink_idle_ms,
146
+ + headinfer_gpu_heads_frac,
147
+ n_seq_max, n_pad, 0, LLAMA_SWA_TYPE_NONE, filter_base, reuse);
148
+
149
+ LLAMA_LOG_INFO("%s: creating SWA KV cache, size = %u cells\n", __func__, size_swa);
150
+ @@ -71,6 +73,7 @@ llama_kv_cache_iswa::llama_kv_cache_iswa(
151
+ kv_swa = std::make_unique<llama_kv_cache>(
152
+ model, type_k, type_v,
153
+ v_trans, offload, unified, size_swa, kv_size_initial, slot_shrink_idle_ms,
154
+ + headinfer_gpu_heads_frac,
155
+ n_seq_max, n_pad, hparams.n_swa, hparams.swa_type, filter_swa, reuse);
156
+ }
157
+
158
+ diff --git a/llama.cpp/src/llama-kv-cache-iswa.h b/llama.cpp/src/llama-kv-cache-iswa.h
159
+ --- a/llama.cpp/src/llama-kv-cache-iswa.h
160
+ +++ b/llama.cpp/src/llama-kv-cache-iswa.h
161
+ @@ -28,6 +28,8 @@ public:
162
+ // opencoti F4 M3 Phase 3 β€” see docs/decisions/0001-lazy-slot-context.md
163
+ // Forwarded to both kv_base and kv_swa. 0 = shrink disabled.
164
+ uint32_t slot_shrink_idle_ms,
165
+ + // opencoti F5 M2 headinfer β€” forwarded to both kv_base and kv_swa.
166
+ + float headinfer_gpu_heads_frac,
167
+ uint32_t n_seq_max,
168
+ uint32_t n_ubatch,
169
+ uint32_t n_pad,
170
+ diff --git a/llama.cpp/src/llama-kv-cache.cpp b/llama.cpp/src/llama-kv-cache.cpp
171
+ --- a/llama.cpp/src/llama-kv-cache.cpp
172
+ +++ b/llama.cpp/src/llama-kv-cache.cpp
173
+ @@ -110,6 +110,7 @@ llama_kv_cache::llama_kv_cache(
174
+ uint32_t kv_size,
175
+ uint32_t kv_size_initial,
176
+ uint32_t slot_shrink_idle_ms,
177
+ + float headinfer_gpu_heads_frac,
178
+ uint32_t n_seq_max,
179
+ uint32_t n_pad,
180
+ uint32_t n_swa,
181
+ @@ -262,23 +263,67 @@ llama_kv_cache::llama_kv_cache(
182
+ const bool has_k = true;
183
+ const bool has_v = !is_mla;
184
+
185
+ - ggml_tensor * k = has_k ? ggml_new_tensor_3d(ctx, type_k, n_embd_k_gqa, kv_size, n_stream) : nullptr;
186
+ - ggml_tensor * v = has_v ? ggml_new_tensor_3d(ctx, type_v, n_embd_v_gqa, kv_size, n_stream) : nullptr;
187
+ + // opencoti F5 M2 headinfer β€” see docs/features/advanced_kv.md
188
+ + // Head-residency split: keep the first gpu_heads KV heads on this
189
+ + // (offloaded/device) layer's buffer and put the remaining heads on a
190
+ + // host buffer. Splitting on KV-head boundaries is inherently GQA-safe.
191
+ + // Gated to offloaded, non-transposed-V, unified (n_stream==1) layers;
192
+ + // frac >= 1.0 (or <= 1 head) means no split (byte-identical upstream).
193
+ + uint32_t gpu_heads = 0;
194
+ + uint32_t n_embd_k_gpu = n_embd_k_gqa;
195
+ + uint32_t n_embd_v_gpu = n_embd_v_gqa;
196
+ + uint32_t n_embd_k_cpu = 0;
197
+ + uint32_t n_embd_v_cpu = 0;
198
+ + if (headinfer_gpu_heads_frac < 1.0f && offload && !v_trans && n_stream == 1) {
199
+ + const uint32_t n_head_kv = hparams.n_head_kv(il);
200
+ + const uint32_t n_embd_head_k = hparams.n_embd_head_k(il);
201
+ + const uint32_t n_embd_head_v = hparams.n_embd_head_v(il);
202
+ + if (n_head_kv > 1) {
203
+ + uint32_t g = (uint32_t) std::lround((double) headinfer_gpu_heads_frac * (double) n_head_kv);
204
+ + g = std::max(1u, std::min(g, n_head_kv - 1));
205
+ + gpu_heads = g;
206
+ + n_embd_k_gpu = g * n_embd_head_k;
207
+ + n_embd_v_gpu = g * n_embd_head_v;
208
+ + n_embd_k_cpu = (n_head_kv - g) * n_embd_head_k;
209
+ + n_embd_v_cpu = (n_head_kv - g) * n_embd_head_v;
210
+ + LLAMA_LOG_INFO("%s: layer %3d: headinfer split heads %u GPU / %u CPU\n",
211
+ + __func__, il, gpu_heads, n_head_kv - gpu_heads);
212
+ + }
213
+ + }
214
+ +
215
+ + ggml_tensor * k = has_k ? ggml_new_tensor_3d(ctx, type_k, n_embd_k_gpu, kv_size, n_stream) : nullptr;
216
+ + ggml_tensor * v = has_v ? ggml_new_tensor_3d(ctx, type_v, n_embd_v_gpu, kv_size, n_stream) : nullptr;
217
+
218
+ has_k && ggml_format_name(k, "cache_k_l%d", il);
219
+ has_v && ggml_format_name(v, "cache_v_l%d", il);
220
+
221
+ + // opencoti F5 M2 headinfer β€” the host-resident head subset lives in the
222
+ + // CPU buffer-type context; the buffer-allocation loop below picks it up
223
+ + // automatically (ctx_for_buft keys contexts by buffer type).
224
+ + ggml_tensor * k_cpu = nullptr;
225
+ + ggml_tensor * v_cpu = nullptr;
226
+ + if (gpu_heads > 0) {
227
+ + ggml_context * ctx_cpu = ctx_for_buft(ggml_backend_cpu_buffer_type());
228
+ + if (!ctx_cpu) {
229
+ + throw std::runtime_error("failed to create CPU ggml context for headinfer kv cache");
230
+ + }
231
+ + k_cpu = has_k ? ggml_new_tensor_3d(ctx_cpu, type_k, n_embd_k_cpu, kv_size, n_stream) : nullptr;
232
+ + v_cpu = has_v ? ggml_new_tensor_3d(ctx_cpu, type_v, n_embd_v_cpu, kv_size, n_stream) : nullptr;
233
+ + has_k && k_cpu && ggml_format_name(k_cpu, "cache_k_cpu_l%d", il);
234
+ + has_v && v_cpu && ggml_format_name(v_cpu, "cache_v_cpu_l%d", il);
235
+ + }
236
+ +
237
+ std::vector<ggml_tensor *> k_stream;
238
+ std::vector<ggml_tensor *> v_stream;
239
+
240
+ for (uint32_t s = 0; s < n_stream; ++s) {
241
+ - k_stream.push_back(has_k ? ggml_view_2d(ctx, k, n_embd_k_gqa, kv_size, k->nb[1], s*k->nb[2]) : nullptr);
242
+ - v_stream.push_back(has_v ? ggml_view_2d(ctx, v, n_embd_v_gqa, kv_size, v->nb[1], s*v->nb[2]) : nullptr);
243
+ + k_stream.push_back(has_k ? ggml_view_2d(ctx, k, n_embd_k_gpu, kv_size, k->nb[1], s*k->nb[2]) : nullptr);
244
+ + v_stream.push_back(has_v ? ggml_view_2d(ctx, v, n_embd_v_gpu, kv_size, v->nb[1], s*v->nb[2]) : nullptr);
245
+ }
246
+
247
+ map_layer_ids[il] = layers.size();
248
+
249
+ - layers.push_back({ il, k, v, k_stream, v_stream, });
250
+ + layers.push_back({ il, k, v, k_stream, v_stream, k_cpu, v_cpu, gpu_heads });
251
+ }
252
+
253
+ if (reuse) {
254
+ @@ -440,6 +485,20 @@ void llama_kv_cache::ensure_cleared(uint32_t up_to_cells) {
255
+ const size_t sz = (size_t) (b - a) * (size_t) layer.v->nb[1];
256
+ ggml_backend_tensor_memset(layer.v, 0, off, sz);
257
+ }
258
+ + // opencoti F5 M2 headinfer β€” clear the host-resident head subset
259
+ + // too (its own strides; same [a, b) cell range).
260
+ + if (layer.k_cpu) {
261
+ + const size_t off = (size_t) st * (size_t) layer.k_cpu->nb[2]
262
+ + + (size_t) a * (size_t) layer.k_cpu->nb[1];
263
+ + const size_t sz = (size_t) (b - a) * (size_t) layer.k_cpu->nb[1];
264
+ + ggml_backend_tensor_memset(layer.k_cpu, 0, off, sz);
265
+ + }
266
+ + if (layer.v_cpu) {
267
+ + const size_t off = (size_t) st * (size_t) layer.v_cpu->nb[2]
268
+ + + (size_t) a * (size_t) layer.v_cpu->nb[1];
269
+ + const size_t sz = (size_t) (b - a) * (size_t) layer.v_cpu->nb[1];
270
+ + ggml_backend_tensor_memset(layer.v_cpu, 0, off, sz);
271
+ + }
272
+ }
273
+ }
274
+
275
+ @@ -610,6 +669,11 @@ void llama_kv_cache::shrink_if_idle() {
276
+ for (const auto & layer : layers) {
277
+ opencoti_decommit_layer_range(layer.k, n_stream, from_cells, to_cells);
278
+ opencoti_decommit_layer_range(layer.v, n_stream, from_cells, to_cells);
279
+ + // opencoti F5 M2 headinfer β€” the host-resident head subset IS
280
+ + // host-pageable, so this is where the GPU-mode shrink actually returns
281
+ + // RSS (the device k/v above are skipped by the is_host() guard).
282
+ + opencoti_decommit_layer_range(layer.k_cpu, n_stream, from_cells, to_cells);
283
+ + opencoti_decommit_layer_range(layer.v_cpu, n_stream, from_cells, to_cells);
284
+ }
285
+
286
+ // After decommit, treat cells [from_cells, to_cells) as "uncleared" β€”
287
+ @@ -1645,15 +1709,41 @@ uint32_t llama_kv_cache::get_n_kv(const slot_info & sinfo) const {
288
+ ggml_tensor * llama_kv_cache::get_k(ggml_context * ctx, int32_t il, uint32_t n_kv, const slot_info & sinfo) const {
289
+ const int32_t ikv = map_layer_ids.at(il);
290
+
291
+ - auto * k = layers[ikv].k;
292
+ + const auto & layer = layers[ikv];
293
+ + auto * k = layer.k;
294
+ +
295
+ + const uint64_t kv_size = get_size();
296
+ + const uint32_t ns = sinfo.s1 - sinfo.s0 + 1;
297
+ +
298
+ + // opencoti F5 M2 headinfer β€” reassemble the full head set from the
299
+ + // GPU-resident (k) and host-resident (k_cpu) subsets by concat on the head
300
+ + // dimension. The backend scheduler streams k_cpu to the compute backend.
301
+ + if (layer.gpu_heads > 0 && layer.k_cpu) {
302
+ + const uint32_t n_embd_head_k = hparams.n_embd_head_k(il);
303
+ + const uint32_t n_head_gpu = layer.gpu_heads;
304
+ + const uint32_t n_head_cpu = hparams.n_head_kv(il) - layer.gpu_heads;
305
+ + const uint64_t n_embd_gpu = (uint64_t) n_head_gpu * n_embd_head_k;
306
+ + const uint64_t n_embd_cpu = (uint64_t) n_head_cpu * n_embd_head_k;
307
+ + auto * kc = layer.k_cpu;
308
+ + ggml_tensor * kg_v = ggml_view_4d(ctx, k,
309
+ + n_embd_head_k, n_head_gpu, n_kv, ns,
310
+ + ggml_row_size(k->type, n_embd_head_k),
311
+ + ggml_row_size(k->type, n_embd_gpu),
312
+ + ggml_row_size(k->type, n_embd_gpu*kv_size),
313
+ + ggml_row_size(k->type, n_embd_gpu*kv_size)*sinfo.s0);
314
+ + ggml_tensor * kc_v = ggml_view_4d(ctx, kc,
315
+ + n_embd_head_k, n_head_cpu, n_kv, ns,
316
+ + ggml_row_size(kc->type, n_embd_head_k),
317
+ + ggml_row_size(kc->type, n_embd_cpu),
318
+ + ggml_row_size(kc->type, n_embd_cpu*kv_size),
319
+ + ggml_row_size(kc->type, n_embd_cpu*kv_size)*sinfo.s0);
320
+ + return ggml_concat(ctx, kg_v, kc_v, 1);
321
+ + }
322
+
323
+ - const uint64_t kv_size = get_size();
324
+ const uint64_t n_embd_k_gqa = k->ne[0];
325
+
326
+ assert(n_embd_k_gqa == hparams.n_embd_k_gqa(il));
327
+
328
+ - const uint32_t ns = sinfo.s1 - sinfo.s0 + 1;
329
+ -
330
+ return ggml_view_4d(ctx, k,
331
+ hparams.n_embd_head_k(il), hparams.n_head_kv(il), n_kv, ns,
332
+ ggml_row_size(k->type, hparams.n_embd_head_k(il)),
333
+ @@ -1665,16 +1755,42 @@ ggml_tensor * llama_kv_cache::get_k(ggml_context * ctx, int32_t il, uint32_t n_k
334
+ ggml_tensor * llama_kv_cache::get_v(ggml_context * ctx, int32_t il, uint32_t n_kv, const slot_info & sinfo) const {
335
+ const int32_t ikv = map_layer_ids.at(il);
336
+
337
+ - auto * v = layers[ikv].v;
338
+ + const auto & layer = layers[ikv];
339
+ + auto * v = layer.v;
340
+ +
341
+ + const uint64_t kv_size = get_size();
342
+ + const uint32_t ns = sinfo.s1 - sinfo.s0 + 1;
343
+ +
344
+ + // opencoti F5 M2 headinfer β€” concat the GPU + host head subsets. The split
345
+ + // is gated off when v_trans at construction, so this branch is always the
346
+ + // non-transposed layout (heads in dim 1, same as get_k).
347
+ + if (layer.gpu_heads > 0 && layer.v_cpu) {
348
+ + const uint32_t n_embd_head_v = hparams.n_embd_head_v(il);
349
+ + const uint32_t n_head_gpu = layer.gpu_heads;
350
+ + const uint32_t n_head_cpu = hparams.n_head_kv(il) - layer.gpu_heads;
351
+ + const uint64_t n_embd_gpu = (uint64_t) n_head_gpu * n_embd_head_v;
352
+ + const uint64_t n_embd_cpu = (uint64_t) n_head_cpu * n_embd_head_v;
353
+ + auto * vc = layer.v_cpu;
354
+ + ggml_tensor * vg_v = ggml_view_4d(ctx, v,
355
+ + n_embd_head_v, n_head_gpu, n_kv, ns,
356
+ + ggml_row_size(v->type, n_embd_head_v),
357
+ + ggml_row_size(v->type, n_embd_gpu),
358
+ + ggml_row_size(v->type, n_embd_gpu*kv_size),
359
+ + ggml_row_size(v->type, n_embd_gpu*kv_size)*sinfo.s0);
360
+ + ggml_tensor * vc_v = ggml_view_4d(ctx, vc,
361
+ + n_embd_head_v, n_head_cpu, n_kv, ns,
362
+ + ggml_row_size(vc->type, n_embd_head_v),
363
+ + ggml_row_size(vc->type, n_embd_cpu),
364
+ + ggml_row_size(vc->type, n_embd_cpu*kv_size),
365
+ + ggml_row_size(vc->type, n_embd_cpu*kv_size)*sinfo.s0);
366
+ + return ggml_concat(ctx, vg_v, vc_v, 1);
367
+ + }
368
+
369
+ - const uint64_t kv_size = get_size();
370
+ const uint64_t n_embd_v_gqa = v->ne[0];
371
+
372
+ // [TAG_V_CACHE_VARIABLE]
373
+ assert(n_embd_v_gqa >= hparams.n_embd_v_gqa(il));
374
+
375
+ - const uint32_t ns = sinfo.s1 - sinfo.s0 + 1;
376
+ -
377
+ if (!v_trans) {
378
+ // note: v->nb[1] <= v->nb[2]
379
+ return ggml_view_4d(ctx, v,
380
+ @@ -1699,7 +1815,8 @@ ggml_tensor * llama_kv_cache::cpy_k(ggml_context * ctx, ggml_tensor * k_cur, ggm
381
+
382
+ const int32_t ikv = map_layer_ids.at(il);
383
+
384
+ - ggml_tensor * k = layers[ikv].k;
385
+ + const auto & layer = layers[ikv];
386
+ + ggml_tensor * k = layer.k;
387
+
388
+ const int64_t n_embd_head = k_cur->ne[0];
389
+ const int64_t n_head = k_cur->ne[1];
390
+ @@ -1711,6 +1828,24 @@ ggml_tensor * llama_kv_cache::cpy_k(ggml_context * ctx, ggml_tensor * k_cur, ggm
391
+ // TODO: add ggml helper function for this?
392
+ GGML_ASSERT(ggml_row_size(k_cur->type, n_embd_head) == k_cur->nb[1]);
393
+
394
+ + // opencoti F5 M2 headinfer β€” scatter the incoming heads into the GPU and
395
+ + // host subsets. The first gpu_heads heads (contiguous in k_cur's dim 0..1)
396
+ + // store into k; the rest store into k_cpu, both at the same cell indices
397
+ + // (k_idxs). The single returned dep node ties both set_rows into the
398
+ + // caller's one ggml_build_forward_expand (its value is never read). Split
399
+ + // only happens for n_stream==1 (gated at construction) β€” no per-stream
400
+ + // reshape needed.
401
+ + if (layer.gpu_heads > 0 && layer.k_cpu) {
402
+ + const int64_t g = layer.gpu_heads;
403
+ + const int64_t c = n_head - g;
404
+ + ggml_tensor * kc = layer.k_cpu;
405
+ + ggml_tensor * cur_g = ggml_view_2d(ctx, k_cur, n_embd_head*g, n_tokens, k_cur->nb[2], 0);
406
+ + ggml_tensor * cur_c = ggml_view_2d(ctx, k_cur, n_embd_head*c, n_tokens, k_cur->nb[2], (size_t) (g*k_cur->nb[1]));
407
+ + ggml_tensor * store_g = ggml_set_rows(ctx, k, cur_g, k_idxs);
408
+ + ggml_tensor * store_c = ggml_set_rows(ctx, kc, cur_c, k_idxs);
409
+ + return ggml_add(ctx, ggml_view_1d(ctx, store_g, 1, 0), ggml_view_1d(ctx, store_c, 1, 0));
410
+ + }
411
+ +
412
+ k_cur = ggml_view_2d(ctx, k_cur, n_embd_gqa, n_tokens, k_cur->nb[2], 0);
413
+
414
+ const int64_t n_stream = k->ne[2];
415
+ @@ -1734,7 +1869,8 @@ ggml_tensor * llama_kv_cache::cpy_v(ggml_context * ctx, ggml_tensor * v_cur, ggm
416
+
417
+ const int32_t ikv = map_layer_ids.at(il);
418
+
419
+ - auto * v = layers[ikv].v;
420
+ + const auto & layer = layers[ikv];
421
+ + auto * v = layer.v;
422
+
423
+ const int64_t n_embd_head = v_cur->ne[0];
424
+ const int64_t n_head = v_cur->ne[1];
425
+ @@ -1745,6 +1881,20 @@ ggml_tensor * llama_kv_cache::cpy_v(ggml_context * ctx, ggml_tensor * v_cur, ggm
426
+ // we can merge dims 0 and 1
427
+ GGML_ASSERT(ggml_row_size(v_cur->type, n_embd_head) == v_cur->nb[1]);
428
+
429
+ + // opencoti F5 M2 headinfer β€” head-scatter into the GPU + host subsets.
430
+ + // Reached only when !v_trans and n_stream==1 (the split is gated off
431
+ + // otherwise at construction), so this mirrors the cpy_k store exactly.
432
+ + if (layer.gpu_heads > 0 && layer.v_cpu) {
433
+ + const int64_t g = layer.gpu_heads;
434
+ + const int64_t c = n_head - g;
435
+ + ggml_tensor * vc = layer.v_cpu;
436
+ + ggml_tensor * cur_g = ggml_view_2d(ctx, v_cur, n_embd_head*g, n_tokens, v_cur->nb[2], 0);
437
+ + ggml_tensor * cur_c = ggml_view_2d(ctx, v_cur, n_embd_head*c, n_tokens, v_cur->nb[2], (size_t) (g*v_cur->nb[1]));
438
+ + ggml_tensor * store_g = ggml_set_rows(ctx, v, cur_g, v_idxs);
439
+ + ggml_tensor * store_c = ggml_set_rows(ctx, vc, cur_c, v_idxs);
440
+ + return ggml_add(ctx, ggml_view_1d(ctx, store_g, 1, 0), ggml_view_1d(ctx, store_c, 1, 0));
441
+ + }
442
+ +
443
+ const int64_t n_stream = v->ne[2];
444
+
445
+ // take this branch when FA is enabled (the V cache is not transposed)
446
+ diff --git a/llama.cpp/src/llama-kv-cache.h b/llama.cpp/src/llama-kv-cache.h
447
+ --- a/llama.cpp/src/llama-kv-cache.h
448
+ +++ b/llama.cpp/src/llama-kv-cache.h
449
+ @@ -111,6 +111,11 @@ public:
450
+ // back to kv_size_initial and the over-cap pages are
451
+ // decommitted. 0 disables shrink-on-idle.
452
+ uint32_t slot_shrink_idle_ms,
453
+ + // opencoti F5 M2 headinfer β€” see docs/features/advanced_kv.md
454
+ + // Fraction of KV heads kept GPU-resident (rest offloaded
455
+ + // to host, streamed back per step). 1.0 = off. Effective
456
+ + // only for offloaded layers with !v_trans (flash-attn).
457
+ + float headinfer_gpu_heads_frac,
458
+ uint32_t n_seq_max,
459
+ uint32_t n_pad,
460
+ uint32_t n_swa,
461
+ @@ -269,6 +274,14 @@ private:
462
+
463
+ std::vector<ggml_tensor *> k_stream;
464
+ std::vector<ggml_tensor *> v_stream;
465
+ +
466
+ + // opencoti F5 M2 headinfer β€” head-residency split. When gpu_heads > 0,
467
+ + // `k`/`v` hold the first gpu_heads KV heads on the layer's (GPU) buffer
468
+ + // and `k_cpu`/`v_cpu` hold the remaining (n_head_kv - gpu_heads) heads
469
+ + // on a host buffer. gpu_heads == 0 means no split (k/v hold all heads).
470
+ + ggml_tensor * k_cpu = nullptr;
471
+ + ggml_tensor * v_cpu = nullptr;
472
+ + uint32_t gpu_heads = 0;
473
+ };
474
+
475
+ bool v_trans = true; // the value tensor is transposed
476
+ diff --git a/llama.cpp/src/llama-memory-hybrid-iswa.cpp b/llama.cpp/src/llama-memory-hybrid-iswa.cpp
477
+ --- a/llama.cpp/src/llama-memory-hybrid-iswa.cpp
478
+ +++ b/llama.cpp/src/llama-memory-hybrid-iswa.cpp
479
+ @@ -48,6 +48,8 @@ llama_memory_hybrid_iswa::llama_memory_hybrid_iswa(
480
+ // Hybrid-iswa does not yet thread slot_shrink_idle_ms; pass 0 for
481
+ // Phase 2 backward-compat (no shrink-on-idle).
482
+ 0,
483
+ + // opencoti F5 M2 headinfer β€” hybrid-iswa does not thread headinfer; 1.0 = off.
484
+ + 1.0f,
485
+ n_seq_max,
486
+ n_ubatch,
487
+ n_pad,
488
+ diff --git a/llama.cpp/src/llama-memory-hybrid.cpp b/llama.cpp/src/llama-memory-hybrid.cpp
489
+ --- a/llama.cpp/src/llama-memory-hybrid.cpp
490
+ +++ b/llama.cpp/src/llama-memory-hybrid.cpp
491
+ @@ -47,6 +47,8 @@ llama_memory_hybrid::llama_memory_hybrid(
492
+ // Hybrid memory does not yet thread slot_shrink_idle_ms; pass 0 for
493
+ // Phase 2 backward-compat (no shrink-on-idle).
494
+ 0,
495
+ + // opencoti F5 M2 headinfer β€” hybrid memory does not thread headinfer; 1.0 = off.
496
+ + 1.0f,
497
+ n_seq_max,
498
+ n_pad,
499
+ n_swa,
500
+ diff --git a/llama.cpp/src/llama-model.cpp b/llama.cpp/src/llama-model.cpp
501
+ --- a/llama.cpp/src/llama-model.cpp
502
+ +++ b/llama.cpp/src/llama-model.cpp
503
+ @@ -2077,6 +2077,8 @@ llama_memory_i * llama_model::create_memory(const llama_memory_params & params,
504
+ cparams.slot_initial_ctx,
505
+ // opencoti F4 M3 Phase 3 β€” see docs/decisions/0001-lazy-slot-context.md
506
+ cparams.slot_shrink_idle_ms,
507
+ + // opencoti F5 M2 headinfer β€” see docs/features/advanced_kv.md
508
+ + cparams.headinfer_gpu_heads_frac,
509
+ cparams.n_seq_max,
510
+ cparams.n_ubatch,
511
+ 1,
512
+ @@ -2097,6 +2099,8 @@ llama_memory_i * llama_model::create_memory(const llama_memory_params & params,
513
+ cparams.slot_initial_ctx,
514
+ // opencoti F4 M3 Phase 3 β€” see docs/decisions/0001-lazy-slot-context.md
515
+ cparams.slot_shrink_idle_ms,
516
+ + // opencoti F5 M2 headinfer β€” see docs/features/advanced_kv.md
517
+ + cparams.headinfer_gpu_heads_frac,
518
+ cparams.n_seq_max,
519
+ 1,
520
+ hparams.n_swa,
patches/0030-neo-pipeline.patch ADDED
@@ -0,0 +1,933 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ From: opencoti
2
+ Subject: [PATCH 0030] F5 M3 β€” NEO: asymmetric GPU/CPU attention pipelining
3
+
4
+ Sits on top of 0020 (M2 head-residency split). When --neo-pipeline != off and
5
+ the headinfer split is active for the layer, build_attn_mha builds TWO
6
+ ggml_flash_attn_ext ops on disjoint Q-head ranges instead of M2's
7
+ concat-then-stream-back path: cur_g consumes the GPU half of K/V on the GPU
8
+ backend; cur_c consumes the CPU half of K/V on the CPU backend (auto-routed
9
+ by the scheduler from each FA op's K/V buffer-type). Output halves are
10
+ ggml_concat'd on the head axis β€” same head-axis concat M2 used, but on
11
+ O(n_tokens) FA outputs instead of O(n_kv) K/V.
12
+
13
+ A small orchestrator (ggml/include/ggml-neo-pipeline.h + ggml/src/
14
+ ggml-neo-pipeline.cpp) lets the graph builder register each (cur_g, cur_c)
15
+ pair; the CUDA backend's graph_compute hook routes registered cur_g to a
16
+ non-default CUDA stream (curr_stream_no=1) so concurrent CPU FA on cur_c can
17
+ overlap with stream-1 GPU FA. A cudaEvent is recorded on stream 1 after the
18
+ GPU FA dispatch; when a downstream split's graph reads cur_g (typically the
19
+ concat join), the hook inserts a cudaStreamWaitEvent on the consuming stream
20
+ so the read sees completed data. CUDA graph capture is disabled for
21
+ NEO-engaged splits; off-path graphs keep the fast cuda_graph path.
22
+
23
+ Default off in every layer: cparams.neo_pipeline_mode == 0 means
24
+ build_attn_mha_neo is never called and the M3-A structural split never
25
+ engages β€” the M2 concat-then-stream-back path runs unchanged, byte-identical
26
+ to the 0020-shipped binary. Adapter exposes --neo-pipeline off|on|auto via
27
+ common/arg.cpp; @opencoti/llamafile typed config field
28
+ neoPipelineMode + OPENCOTI_LLAMAFILE_NEO_PIPELINE env mirror the M2
29
+ template.
30
+
31
+ Solidpc RTX 3090 + Qwen2.5-Coder-0.5B IQ4_XS bench (perf/llamafile/
32
+ neo-pipeline.bench.ts) at ctx=4096 / predict=128 / frac=0.5:
33
+
34
+ B baseline (no split) 326.69 tps
35
+ M m2-only (split, M2 path) 186.96 tps
36
+ N neo-on (split, M3 path) 187.04 tps (+0.04% vs M) ← C2 FAIL
37
+ O off-id (--neo-pipeline off) 151.65 tps
38
+
39
+ C1 PASS N completion === B completion (correctness intact)
40
+ C2 FAIL N tps - M tps < 5% (NEO overlap mechanism is correct but the
41
+ per-step GPU FA on half-heads finishes in much less wall time
42
+ than the CPU FA on half-heads, so even perfect overlap is
43
+ bounded by the slower CPU half β€” for this model + ctx,
44
+ M3-B's stream-swap has no observable win over M3-A's
45
+ structural split alone)
46
+ C3 PASS N GPU KV (24 MiB) β‰ˆ M GPU KV (24 MiB) β€” residency preserved
47
+ C4 PASS O completion === M completion (--neo-pipeline off byte-identical)
48
+
49
+ Per the M3-D gate in docs/features/advanced_kv.md (F5 M3): C2 failed but
50
+ the orchestrator is correct and the off-path is byte-identical to M2,
51
+ so the orchestrator file ships as a wired-but-no-observable-win
52
+ mechanism (kept for M4 ScoutAttention reuse) rather than as a
53
+ dormant stub. The M3-A structural win (per-step CPU→GPU stream-back
54
+ eliminated) is the actual deliverable. M3-C EWMA load-aware controller
55
+ is deferred β€” without an observable M3-B overlap win it would be
56
+ cosmetic.
57
+
58
+ Surgical-hook count stays 18 β€” all 0030 changes are in vendored-source
59
+ files, captured here via snapshot-diff (NOT git diff HEAD; bug-121).
60
+
61
+ diff --git a/llama.cpp/BUILD.mk b/llama.cpp/BUILD.mk
62
+ --- a/llama.cpp/BUILD.mk
63
+ +++ b/llama.cpp/BUILD.mk
64
+ @@ -27,6 +27,7 @@ GGML_SRCS_CPP := \
65
+ llama.cpp/ggml/src/ggml-backend-meta.cpp \
66
+ llama.cpp/ggml/src/ggml-backend-reg.cpp \
67
+ llama.cpp/ggml/src/ggml-backend.cpp \
68
+ + llama.cpp/ggml/src/ggml-neo-pipeline.cpp \
69
+ llama.cpp/ggml/src/ggml-opt.cpp \
70
+ llama.cpp/ggml/src/ggml-threading.cpp \
71
+ llama.cpp/ggml/src/ggml.cpp \
72
+ diff --git a/llama.cpp/common/arg.cpp b/llama.cpp/common/arg.cpp
73
+ --- a/llama.cpp/common/arg.cpp
74
+ +++ b/llama.cpp/common/arg.cpp
75
+ @@ -1429,6 +1429,17 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
76
+ params.headinfer_gpu_heads_frac = std::stof(value);
77
+ }
78
+ ).set_examples({LLAMA_EXAMPLE_SERVER}).set_env("LLAMA_ARG_HEADINFER_GPU_HEADS_FRAC"));
79
+ + // opencoti F5 M3 neo β€” see docs/features/advanced_kv.md
80
+ + add_opt(common_arg(
81
+ + {"--neo-pipeline"}, "MODE",
82
+ + "neo (asymmetric GPU/CPU attention pipelining): off (default), on (engage when headinfer split is active for the layer), auto (reserved, same as on). Requires --headinfer-gpu-heads-frac < 1.0 to do anything.",
83
+ + [](common_params & params, const std::string & value) {
84
+ + if (value == "off") params.neo_pipeline_mode = 0;
85
+ + else if (value == "on") params.neo_pipeline_mode = 1;
86
+ + else if (value == "auto") params.neo_pipeline_mode = 2;
87
+ + else throw std::invalid_argument("--neo-pipeline must be off|on|auto");
88
+ + }
89
+ + ).set_examples({LLAMA_EXAMPLE_SERVER}).set_env("LLAMA_ARG_NEO_PIPELINE"));
90
+ add_opt(common_arg(
91
+ {"--chunks"}, "N",
92
+ string_format("max number of chunks to process (default: %d, -1 = all)", params.n_chunks),
93
+ diff --git a/llama.cpp/common/common.cpp b/llama.cpp/common/common.cpp
94
+ --- a/llama.cpp/common/common.cpp
95
+ +++ b/llama.cpp/common/common.cpp
96
+ @@ -1634,6 +1634,8 @@ struct llama_context_params common_context_params_to_llama(const common_params &
97
+ cparams.slot_shrink_idle_ms = params.slot_shrink_idle_ms < 0 ? 0u : (uint32_t) params.slot_shrink_idle_ms;
98
+ // opencoti F5 M2 headinfer β€” see docs/features/advanced_kv.md
99
+ cparams.headinfer_gpu_heads_frac = params.headinfer_gpu_heads_frac;
100
+ + // opencoti F5 M3 neo β€” see docs/features/advanced_kv.md
101
+ + cparams.neo_pipeline_mode = params.neo_pipeline_mode;
102
+ cparams.n_rs_seq = params.speculative.need_n_rs_seq();
103
+ cparams.n_batch = params.n_batch;
104
+ cparams.n_ubatch = params.n_ubatch;
105
+ diff --git a/llama.cpp/common/common.h b/llama.cpp/common/common.h
106
+ --- a/llama.cpp/common/common.h
107
+ +++ b/llama.cpp/common/common.h
108
+ @@ -440,6 +440,11 @@ struct common_params {
109
+ // memory and streamed back per step. 1.0 = off (no split). Only active for
110
+ // GPU-offloaded layers with flash-attention (non-transposed V cache).
111
+ float headinfer_gpu_heads_frac = 1.0f;
112
+ + // opencoti F5 M3 neo β€” see docs/features/advanced_kv.md
113
+ + // 0 = off (default; M2 concat path runs), 1 = on (engage when split
114
+ + // active for the layer), 2 = auto (reserved for future load-based
115
+ + // engagement; same as `on` today).
116
+ + uint32_t neo_pipeline_mode = 0;
117
+ int32_t n_batch = 2048; // logical batch size for prompt processing (must be >=32 to use BLAS)
118
+ int32_t n_ubatch = 512; // physical batch size for prompt processing (must be >=32 to use BLAS)
119
+ int32_t n_keep = 0; // number of tokens to keep from initial prompt
120
+ diff --git a/llama.cpp/ggml/include/ggml-neo-pipeline.h b/llama.cpp/ggml/include/ggml-neo-pipeline.h
121
+ new file mode 100644
122
+ --- /dev/null
123
+ +++ b/llama.cpp/ggml/include/ggml-neo-pipeline.h
124
+ @@ -0,0 +1,78 @@
125
+ +// opencoti F5 M3 NEO β€” asymmetric GPU/CPU attention pipelining.
126
+ +//
127
+ +// This header declares the orchestrator surface that lets the M3 two-flash-attn
128
+ +// structural split overlap its GPU and CPU halves. The graph builder registers
129
+ +// each (cur_g, cur_c) pair after constructing it; the CUDA backend consults the
130
+ +// registry at compute time to (a) dispatch GPU FA on a non-default CUDA stream
131
+ +// so the scheduler's stream-0 sync no longer blocks the CPU FA's input copy
132
+ +// and (b) wait on the recorded stream-1 event when a downstream split consumes
133
+ +// cur_g (notably the concat that joins both halves), keeping correctness intact.
134
+ +//
135
+ +// Default-off: ggml_neo_pipeline_set_enabled(false) makes register_pair a no-op
136
+ +// and the CUDA hooks fast-skip. The off-path is byte-identical to the M3-A
137
+ +// shipped path; M3-A in turn is byte-identical to M2's concat path when
138
+ +// --neo-pipeline off (cparams.neo_pipeline_mode == 0). See
139
+ +// docs/features/advanced_kv.md (F5 M3 NEO).
140
+ +#pragma once
141
+ +
142
+ +#include "ggml.h"
143
+ +
144
+ +#ifdef __cplusplus
145
+ +extern "C" {
146
+ +#endif
147
+ +
148
+ +// Global on/off switch. Off by default; the graph builder flips this on for
149
+ +// the duration of a graph build when cparams.neo_pipeline_mode != 0 and at
150
+ +// least one layer has an active headinfer split. While off, all functions in
151
+ +// this file are O(1) no-ops.
152
+ +GGML_API void ggml_neo_pipeline_set_enabled(bool enabled);
153
+ +GGML_API bool ggml_neo_pipeline_enabled(void);
154
+ +
155
+ +// Registers a NEO FA pair. Called from build_attn_mha_neo after constructing
156
+ +// the two flash-attention ops on disjoint head ranges. cur_g lives on the GPU
157
+ +// backend (CUDA-allocated K/V), cur_c on the CPU backend (CPU-allocated K/V).
158
+ +// The CUDA backend later consults this registry to route cur_g to a non-default
159
+ +// stream so cur_c can dispatch concurrently. When disabled, this is a no-op.
160
+ +GGML_API void ggml_neo_pipeline_register_pair(const struct ggml_tensor * cur_g,
161
+ + const struct ggml_tensor * cur_c);
162
+ +
163
+ +// Membership probe. Returns true iff t was the cur_g argument of a prior
164
+ +// register_pair call in this graph. O(1) fast-path; the CUDA backend calls
165
+ +// this once per node when scanning a graph it is about to dispatch.
166
+ +GGML_API bool ggml_neo_pipeline_is_gpu_partner(const struct ggml_tensor * t);
167
+ +
168
+ +// CUDA event slot for cur_g. The CUDA backend creates a cudaEvent (with
169
+ +// cudaEventCreateWithFlags + cudaEventDisableTiming) the first time it
170
+ +// dispatches cur_g on a non-default stream and stores the opaque handle via
171
+ +// set_event. A later split whose graph reads cur_g calls get_event and
172
+ +// inserts a cudaStreamWaitEvent so the consuming stream waits for the
173
+ +// stream-1 work. Both functions are no-ops when NEO is disabled or t isn't
174
+ +// registered (get_event returns NULL). The void* is treated as cudaEvent_t
175
+ +// at the CUDA call site; the orchestrator does not know the underlying type.
176
+ +GGML_API void * ggml_neo_pipeline_get_event(const struct ggml_tensor * cur_g);
177
+ +GGML_API void ggml_neo_pipeline_set_event(const struct ggml_tensor * cur_g, void * event);
178
+ +
179
+ +// Clears all per-graph registrations. Called by the CUDA backend at the end
180
+ +// of each graph dispatch so registrations don't leak across decode steps.
181
+ +// (The graph builder reinstalls them on the next build.) Notably does NOT
182
+ +// destroy stored events β€” those are destroyed by the CUDA backend, which
183
+ +// owns the cudaEvent lifecycle (call destroy_all_events at shutdown).
184
+ +GGML_API void ggml_neo_pipeline_clear_registrations(void);
185
+ +
186
+ +// Walks every (cur_g, event) entry, invoking destroyer(event) for each
187
+ +// non-null event, then clears the event map. Called at session/backend
188
+ +// shutdown so cudaEvents are properly released. destroyer is typically a
189
+ +// thin lambda wrapping cudaEventDestroy at the CUDA call site.
190
+ +GGML_API void ggml_neo_pipeline_destroy_all_events(void (*destroyer)(void * event));
191
+ +
192
+ +// Counter of NEO pair dispatches observed by the CUDA backend in the current
193
+ +// process. Surfaced for the M3-E bench's residency / engagement checks.
194
+ +GGML_API int ggml_neo_pipeline_dispatch_count(void);
195
+ +
196
+ +// Increments the dispatch counter. Called by the CUDA backend each time it
197
+ +// observes and routes a registered cur_g to a non-default stream.
198
+ +GGML_API void ggml_neo_pipeline_inc_dispatch_count(void);
199
+ +
200
+ +#ifdef __cplusplus
201
+ +}
202
+ +#endif
203
+ diff --git a/llama.cpp/ggml/src/ggml-cuda/ggml-cuda.cu b/llama.cpp/ggml/src/ggml-cuda/ggml-cuda.cu
204
+ --- a/llama.cpp/ggml/src/ggml-cuda/ggml-cuda.cu
205
+ +++ b/llama.cpp/ggml/src/ggml-cuda/ggml-cuda.cu
206
+ @@ -2,6 +2,10 @@
207
+ #include "ggml-impl.h"
208
+ #include "ggml-backend-impl.h"
209
+
210
+ +// opencoti F5 M3-B NEO orchestrator (registry of cur_g/cur_c pairs for the
211
+ +// asymmetric attention pipelining; consulted in graph_compute below).
212
+ +#include "ggml-neo-pipeline.h"
213
+ +
214
+ #include "ggml-cuda/allreduce.cuh"
215
+ #include "ggml-cuda/common.cuh"
216
+ #include "ggml-cuda/acc.cuh"
217
+ @@ -4485,6 +4489,58 @@ static enum ggml_status GGML_CALL ggml_backend_cuda_graph_compute(ggml_backend_t
218
+
219
+ ggml_cuda_set_device(cuda_ctx->device);
220
+
221
+ + // opencoti F5 M3-B NEO: scan the graph for registered cur_g flash-attn
222
+ + // outputs and for downstream consumers of prior cur_g. The orchestrator
223
+ + // is dormant unless the graph builder called set_enabled(true) (which
224
+ + // only happens when --neo-pipeline != off AND at least one layer has
225
+ + // an active headinfer split). When dormant this loop body is one
226
+ + // atomic-load fast-skip.
227
+ + //
228
+ + // When NEO is engaged for this split:
229
+ + // - If the split computes a cur_g, swap curr_stream_no to 1 so the
230
+ + // kernels dispatch on the alternate CUDA stream; the scheduler's
231
+ + // stream-0 sync in compute_splits will NOT block on this work, so
232
+ + // the subsequent split's q_c host copy starts immediately and the
233
+ + // CPU FA on cur_c can overlap with our stream-1 work. We record an
234
+ + // event on stream 1 at exit so downstream consumers know when
235
+ + // cur_g is done.
236
+ + // - If the split reads a prior cur_g (typically the concat join),
237
+ + // stream-wait on the recorded event before kernels read it.
238
+ + //
239
+ + // CUDA graph capture is disabled when NEO is active for this split:
240
+ + // capturing kernel dispatch on a non-default stream interacts with the
241
+ + // graph_key cache and the existing concurrent_event re-ordering pass
242
+ + // in ways we haven't validated. Off-path graphs keep the cuda_graph
243
+ + // fast path; only NEO-engaged splits pay this cost.
244
+ + bool neo_active = false;
245
+ + ggml_tensor * neo_cur_g_to_record = nullptr;
246
+ + if (ggml_neo_pipeline_enabled()) {
247
+ + for (int i = 0; i < cgraph->n_nodes; ++i) {
248
+ + ggml_tensor * node = cgraph->nodes[i];
249
+ + if (ggml_neo_pipeline_is_gpu_partner(node)) {
250
+ + neo_active = true;
251
+ + neo_cur_g_to_record = node;
252
+ + break;
253
+ + }
254
+ + for (int s = 0; s < GGML_MAX_SRC; ++s) {
255
+ + if (node->src[s] != nullptr && ggml_neo_pipeline_is_gpu_partner(node->src[s])) {
256
+ + void * ev = ggml_neo_pipeline_get_event(node->src[s]);
257
+ + if (ev != nullptr) {
258
+ + CUDA_CHECK(cudaStreamWaitEvent(cuda_ctx->stream(), (cudaEvent_t) ev, 0));
259
+ + }
260
+ + }
261
+ + }
262
+ + }
263
+ + }
264
+ + if (neo_active) {
265
+ + // Materialize stream 1 (creates it on first use) and switch the
266
+ + // context to dispatch all subsequent kernels there. Both calls are
267
+ + // cheap: stream() is a vector lookup + lazy create; the curr_stream_no
268
+ + // write is a single int.
269
+ + (void) cuda_ctx->stream(cuda_ctx->device, 1);
270
+ + cuda_ctx->curr_stream_no = 1;
271
+ + }
272
+ +
273
+ bool use_cuda_graph = false;
274
+ bool cuda_graph_update_required = false;
275
+ const void * graph_key = nullptr;
276
+ @@ -4524,6 +4580,16 @@ static enum ggml_status GGML_CALL ggml_backend_cuda_graph_compute(ggml_backend_t
277
+ }
278
+ #endif // USE_CUDA_GRAPH
279
+
280
+ + // opencoti F5 M3-B NEO: force-disable cuda_graph capture for NEO-active
281
+ + // splits. Capturing on a non-default stream is structurally fine in CUDA
282
+ + // but interacts with the existing graph_key cache + concurrent_event
283
+ + // re-ordering pass in ways we haven't validated. Off-path graphs keep
284
+ + // the fast cuda_graph path.
285
+ + if (neo_active) {
286
+ + use_cuda_graph = false;
287
+ + cuda_graph_update_required = false;
288
+ + }
289
+ +
290
+ if (use_cuda_graph && cuda_graph_update_required) {
291
+ // Start CUDA graph capture
292
+ {
293
+ @@ -4536,6 +4602,23 @@ static enum ggml_status GGML_CALL ggml_backend_cuda_graph_compute(ggml_backend_t
294
+
295
+ ggml_cuda_graph_evaluate_and_capture(cuda_ctx, cgraph, use_cuda_graph, cuda_graph_update_required, graph_key);
296
+
297
+ + // opencoti F5 M3-B NEO exit: record an event on stream 1 marking cur_g's
298
+ + // completion so the downstream concat (consumer split) can stream-wait
299
+ + // on it from stream 0. Restore curr_stream_no=0 so the next graph_compute
300
+ + // call starts on the default stream as normal.
301
+ + if (neo_active && neo_cur_g_to_record != nullptr) {
302
+ + void * ev = ggml_neo_pipeline_get_event(neo_cur_g_to_record);
303
+ + if (ev == nullptr) {
304
+ + cudaEvent_t new_ev;
305
+ + CUDA_CHECK(cudaEventCreateWithFlags(&new_ev, cudaEventDisableTiming));
306
+ + ev = (void *) new_ev;
307
+ + ggml_neo_pipeline_set_event(neo_cur_g_to_record, ev);
308
+ + }
309
+ + CUDA_CHECK(cudaEventRecord((cudaEvent_t) ev, cuda_ctx->stream(cuda_ctx->device, 1)));
310
+ + ggml_neo_pipeline_inc_dispatch_count();
311
+ + cuda_ctx->curr_stream_no = 0;
312
+ + }
313
+ +
314
+ return GGML_STATUS_SUCCESS;
315
+ }
316
+
317
+ diff --git a/llama.cpp/ggml/src/ggml-neo-pipeline.cpp b/llama.cpp/ggml/src/ggml-neo-pipeline.cpp
318
+ new file mode 100644
319
+ --- /dev/null
320
+ +++ b/llama.cpp/ggml/src/ggml-neo-pipeline.cpp
321
+ @@ -0,0 +1,126 @@
322
+ +// opencoti F5 M3 NEO orchestrator implementation.
323
+ +//
324
+ +// Backend-agnostic registry that lets the CUDA backend recognize the M3-built
325
+ +// (cur_g, cur_c) flash-attention pairs and route cur_g to a non-default CUDA
326
+ +// stream so cur_c can dispatch on the CPU backend concurrently.
327
+ +//
328
+ +// The orchestrator stores tensor pointers and opaque cudaEvent handles. It
329
+ +// does not call any CUDA API directly β€” the CUDA backend creates events with
330
+ +// cudaEventCreateWithFlags and hands them to set_event; on shutdown the CUDA
331
+ +// backend supplies a destroyer callback that wraps cudaEventDestroy.
332
+ +//
333
+ +// All maps are protected by a single mutex. Lookups are O(1) (unordered_map).
334
+ +// While the global enabled flag is false, every public function fast-skips
335
+ +// without touching the maps, so the off-path is one atomic-load + branch.
336
+ +//
337
+ +// See ggml/include/ggml-neo-pipeline.h for the public API contract and
338
+ +// docs/features/advanced_kv.md (F5 M3) for the surrounding design.
339
+ +
340
+ +#include "ggml-neo-pipeline.h"
341
+ +
342
+ +#include <atomic>
343
+ +#include <mutex>
344
+ +#include <unordered_map>
345
+ +#include <unordered_set>
346
+ +
347
+ +namespace {
348
+ +
349
+ +std::atomic<bool> g_enabled{false};
350
+ +std::atomic<int> g_dispatch_count{0};
351
+ +
352
+ +std::mutex g_mu;
353
+ +
354
+ +// Registered cur_g tensors. Membership probe is O(1).
355
+ +std::unordered_set<const ggml_tensor *> g_gpu_partners;
356
+ +
357
+ +// Map from cur_g β†’ cudaEvent_t handle (opaque void *). NULL slot means the
358
+ +// CUDA backend hasn't created an event for this tensor yet β€” get_event
359
+ +// returns nullptr in that case so the caller knows to create-and-set.
360
+ +std::unordered_map<const ggml_tensor *, void *> g_events;
361
+ +
362
+ +} // namespace
363
+ +
364
+ +void ggml_neo_pipeline_set_enabled(bool enabled) {
365
+ + g_enabled.store(enabled, std::memory_order_release);
366
+ +}
367
+ +
368
+ +bool ggml_neo_pipeline_enabled(void) {
369
+ + return g_enabled.load(std::memory_order_acquire);
370
+ +}
371
+ +
372
+ +void ggml_neo_pipeline_register_pair(const ggml_tensor * cur_g, const ggml_tensor * cur_c) {
373
+ + (void) cur_c; // cpu partner is informational; the orchestrator only acts on cur_g
374
+ + if (!ggml_neo_pipeline_enabled()) {
375
+ + return;
376
+ + }
377
+ + if (cur_g == nullptr) {
378
+ + return;
379
+ + }
380
+ + std::lock_guard<std::mutex> lock(g_mu);
381
+ + g_gpu_partners.insert(cur_g);
382
+ +}
383
+ +
384
+ +bool ggml_neo_pipeline_is_gpu_partner(const ggml_tensor * t) {
385
+ + if (!ggml_neo_pipeline_enabled()) {
386
+ + return false;
387
+ + }
388
+ + if (t == nullptr) {
389
+ + return false;
390
+ + }
391
+ + std::lock_guard<std::mutex> lock(g_mu);
392
+ + return g_gpu_partners.find(t) != g_gpu_partners.end();
393
+ +}
394
+ +
395
+ +void * ggml_neo_pipeline_get_event(const ggml_tensor * cur_g) {
396
+ + if (!ggml_neo_pipeline_enabled()) {
397
+ + return nullptr;
398
+ + }
399
+ + if (cur_g == nullptr) {
400
+ + return nullptr;
401
+ + }
402
+ + std::lock_guard<std::mutex> lock(g_mu);
403
+ + const auto it = g_events.find(cur_g);
404
+ + if (it == g_events.end()) {
405
+ + return nullptr;
406
+ + }
407
+ + return it->second;
408
+ +}
409
+ +
410
+ +void ggml_neo_pipeline_set_event(const ggml_tensor * cur_g, void * event) {
411
+ + if (!ggml_neo_pipeline_enabled()) {
412
+ + return;
413
+ + }
414
+ + if (cur_g == nullptr) {
415
+ + return;
416
+ + }
417
+ + std::lock_guard<std::mutex> lock(g_mu);
418
+ + g_events[cur_g] = event;
419
+ +}
420
+ +
421
+ +void ggml_neo_pipeline_clear_registrations(void) {
422
+ + std::lock_guard<std::mutex> lock(g_mu);
423
+ + g_gpu_partners.clear();
424
+ + // Note: g_events deliberately NOT cleared here. Events are reusable
425
+ + // across decode steps as long as the underlying tensor pointers stay
426
+ + // stable. They're cleaned up by destroy_all_events on shutdown.
427
+ +}
428
+ +
429
+ +void ggml_neo_pipeline_destroy_all_events(void (*destroyer)(void * event)) {
430
+ + std::lock_guard<std::mutex> lock(g_mu);
431
+ + if (destroyer != nullptr) {
432
+ + for (auto & kv : g_events) {
433
+ + if (kv.second != nullptr) {
434
+ + destroyer(kv.second);
435
+ + }
436
+ + }
437
+ + }
438
+ + g_events.clear();
439
+ +}
440
+ +
441
+ +int ggml_neo_pipeline_dispatch_count(void) {
442
+ + return g_dispatch_count.load(std::memory_order_acquire);
443
+ +}
444
+ +
445
+ +void ggml_neo_pipeline_inc_dispatch_count(void) {
446
+ + g_dispatch_count.fetch_add(1, std::memory_order_relaxed);
447
+ +}
448
+ diff --git a/llama.cpp/include/llama.h b/llama.cpp/include/llama.h
449
+ --- a/llama.cpp/include/llama.h
450
+ +++ b/llama.cpp/include/llama.h
451
+ @@ -358,6 +358,10 @@ extern "C" {
452
+ // streamed back per step). 1.0 = off. Only effective for offloaded
453
+ // layers with flash-attention (non-transposed V cache).
454
+ float headinfer_gpu_heads_frac;
455
+ + // opencoti F5 M3 neo β€” see docs/features/advanced_kv.md
456
+ + // 0 = off (default), 1 = on, 2 = auto. Effective only when the
457
+ + // M2 head split is active for a given layer.
458
+ + uint32_t neo_pipeline_mode;
459
+ uint32_t n_rs_seq; // number of recurrent-state snapshots per seq for rollback (0 = no rollback) [EXPERIMENTAL]
460
+ int32_t n_threads; // number of threads to use for generation
461
+ int32_t n_threads_batch; // number of threads to use for batch processing
462
+ diff --git a/llama.cpp/src/llama-context.cpp b/llama.cpp/src/llama-context.cpp
463
+ --- a/llama.cpp/src/llama-context.cpp
464
+ +++ b/llama.cpp/src/llama-context.cpp
465
+ @@ -64,6 +64,8 @@ llama_context::llama_context(
466
+ cparams.slot_shrink_idle_ms = params.slot_shrink_idle_ms;
467
+ // opencoti F5 M2 headinfer β€” see docs/features/advanced_kv.md
468
+ cparams.headinfer_gpu_heads_frac = params.headinfer_gpu_heads_frac;
469
+ + // opencoti F5 M3 neo β€” see docs/features/advanced_kv.md
470
+ + cparams.neo_pipeline_mode = params.neo_pipeline_mode;
471
+
472
+ cparams.n_threads = params.n_threads;
473
+ cparams.n_threads_batch = params.n_threads_batch;
474
+ @@ -3356,6 +3358,7 @@ llama_context_params llama_context_default_params() {
475
+ /*.slot_initial_ctx =*/ 0,
476
+ /*.slot_shrink_idle_ms =*/ 0,
477
+ /*.headinfer_gpu_heads_frac =*/ 1.0f,
478
+ + /*.neo_pipeline_mode =*/ 0, // opencoti F5 M3 neo β€” off by default
479
+ /*.n_rs_seq =*/ 0,
480
+ /*.n_threads =*/ GGML_DEFAULT_N_THREADS, // TODO: better default
481
+ /*.n_threads_batch =*/ GGML_DEFAULT_N_THREADS,
482
+ diff --git a/llama.cpp/src/llama-cparams.h b/llama.cpp/src/llama-cparams.h
483
+ --- a/llama.cpp/src/llama-cparams.h
484
+ +++ b/llama.cpp/src/llama-cparams.h
485
+ @@ -23,6 +23,15 @@ struct llama_cparams {
486
+ uint32_t slot_shrink_idle_ms;
487
+ // opencoti F5 M2 headinfer β€” see docs/features/advanced_kv.md
488
+ float headinfer_gpu_heads_frac;
489
+ + // opencoti F5 M3 neo β€” asymmetric GPU/CPU pipelining of attention.
490
+ + // 0 = off (M2 concat path runs; binary equivalent to upstream when
491
+ + // headinfer_gpu_heads_frac == 1.0).
492
+ + // 1 = on (engage when headinfer split is active for the layer:
493
+ + // build two flash_attn_ext ops on disjoint head ranges, run
494
+ + // them on GPU and CPU backends respectively, concat outputs).
495
+ + // 2 = auto (same engagement as `on` for now β€” reserved for future
496
+ + // load-based engagement heuristics).
497
+ + uint32_t neo_pipeline_mode;
498
+ uint32_t n_rs_seq; // number of recurrent-state snapshots per seq for rollback
499
+ int32_t n_threads; // number of threads to use for generation
500
+ int32_t n_threads_batch; // number of threads to use for batch processing
501
+ diff --git a/llama.cpp/src/llama-graph.cpp b/llama.cpp/src/llama-graph.cpp
502
+ --- a/llama.cpp/src/llama-graph.cpp
503
+ +++ b/llama.cpp/src/llama-graph.cpp
504
+ @@ -11,6 +11,10 @@
505
+ #include "llama-memory-hybrid-iswa.h"
506
+ #include "llama-memory-recurrent.h"
507
+
508
+ +// opencoti F5 M3 NEO orchestrator (registers cur_g/cur_c pairs so the CUDA
509
+ +// backend can route GPU FA to a non-default stream and overlap with CPU FA).
510
+ +#include "ggml-neo-pipeline.h"
511
+ +
512
+ #include <cassert>
513
+ #include <cmath>
514
+ #include <cstring>
515
+ @@ -2085,6 +2089,140 @@ ggml_tensor * llm_graph_context::build_attn_mha(
516
+ return cur;
517
+ }
518
+
519
+ +// opencoti F5 M3 neo β€” head-axis-split attention. Builds two flash_attn_ext
520
+ +// ops on disjoint head ranges:
521
+ +// GPU side: q[..., :gpu_heads*gqa, ...] Γ— k_g Γ— v_g (auto-routes to GPU
522
+ +// via the scheduler because k_g/v_g are views on the layer's
523
+ +// GPU buffer-type tensor β€” see ggml-backend.cpp:797-852).
524
+ +// CPU side: q[..., gpu_heads*gqa:, ...] Γ— k_c Γ— v_c (auto-routes to CPU
525
+ +// because k_c/v_c are views on the CPU buffer-type tensor that
526
+ +// M2's headinfer allocates alongside the GPU half β€” see
527
+ +// llama-kv-cache.cpp:get_k_cpu/get_v_cpu).
528
+ +// Outputs are concatenated on the head axis and fed into the standard
529
+ +// reshape_2d → Wo flow. This eliminates M2's per-step CPU→GPU stream-back
530
+ +// (O(n_kv) bytes per layer) β€” only the small O(n_tokens) CPU FA output
531
+ +// is bounced to GPU for the concat. Pre-M3-B, the two FA ops run serially
532
+ +// under the scheduler's default dispatch; M3-B's orchestrator will overlap
533
+ +// them concurrently.
534
+ +ggml_tensor * llm_graph_context::build_attn_mha_neo(
535
+ + ggml_tensor * q,
536
+ + ggml_tensor * k_g,
537
+ + ggml_tensor * v_g,
538
+ + ggml_tensor * k_c,
539
+ + ggml_tensor * v_c,
540
+ + uint32_t gpu_heads,
541
+ + ggml_tensor * kq_b,
542
+ + ggml_tensor * kq_mask,
543
+ + ggml_tensor * sinks,
544
+ + ggml_tensor * v_mla,
545
+ + float kq_scale,
546
+ + int il) const {
547
+ + // Preconditions enforced by the M2 split gate that produced k_c/v_c.
548
+ + GGML_ASSERT(cparams.flash_attn && "NEO path requires flash-attn");
549
+ + GGML_ASSERT(kq_b == nullptr && "NEO path does not support KQ bias");
550
+ + GGML_ASSERT(v_mla == nullptr && "NEO path does not support MLA");
551
+ + GGML_ASSERT(gpu_heads > 0 && "NEO path requires an active head split");
552
+ + GGML_ASSERT(k_g && v_g && k_c && v_c);
553
+ +
554
+ + // K/V layout is identical between the GPU and CPU halves (non-transposed
555
+ + // V per the M2 gate). Stream axis matches across both β€” both are views
556
+ + // on tensors allocated with the same n_stream at construction.
557
+ + GGML_ASSERT(k_g->ne[3] == k_c->ne[3]);
558
+ + GGML_ASSERT(v_g->ne[3] == v_c->ne[3]);
559
+ + GGML_ASSERT(k_g->nb[1] <= k_g->nb[2] && "headinfer split requires non-transposed V");
560
+ +
561
+ + // The full Q has all n_head_q heads. Heads are split on KV-head-group
562
+ + // boundaries (M2's GQA clamp ensures gpu_heads is a whole number of
563
+ + // groups). gqa_ratio = n_head_q / n_head_kv; n_head_kv = gpu_heads +
564
+ + // cpu_heads. Q-heads serving the same KV head are CONTIGUOUS in Q's
565
+ + // dim 1 under standard GGUF, so the slice is a clean view (no extra
566
+ + // permute beyond the one already in build_attn_mha).
567
+ + const uint32_t n_head_q = (uint32_t) q->ne[1];
568
+ + const uint32_t n_head_kv = (uint32_t) (k_g->ne[1] + k_c->ne[1]);
569
+ + GGML_ASSERT(n_head_q % n_head_kv == 0 && "GQA ratio must divide n_head_q");
570
+ + const uint32_t gqa_ratio = n_head_q / n_head_kv;
571
+ + const uint32_t gpu_q_heads = gpu_heads * gqa_ratio;
572
+ + const uint32_t cpu_q_heads = (n_head_kv - gpu_heads) * gqa_ratio;
573
+ + GGML_ASSERT(gpu_q_heads + cpu_q_heads == n_head_q);
574
+ +
575
+ + // Mirror build_attn_mha's n_stream reshape + permute on Q, but slice Q
576
+ + // on the head axis BEFORE the permute so the dim-1 slice stays
577
+ + // contiguous in the input layout.
578
+ + const auto n_stream = k_g->ne[3];
579
+ + ggml_tensor * q_4 = ggml_view_4d(ctx0, q,
580
+ + q->ne[0], q->ne[1], q->ne[2]/n_stream, n_stream,
581
+ + q->nb[1], q->nb[2], q->nb[3]/n_stream, 0);
582
+ +
583
+ + ggml_tensor * q_g = ggml_view_4d(ctx0, q_4,
584
+ + q_4->ne[0], gpu_q_heads, q_4->ne[2], q_4->ne[3],
585
+ + q_4->nb[1], q_4->nb[2], q_4->nb[3],
586
+ + /*offset=*/ 0);
587
+ + ggml_tensor * q_c = ggml_view_4d(ctx0, q_4,
588
+ + q_4->ne[0], cpu_q_heads, q_4->ne[2], q_4->ne[3],
589
+ + q_4->nb[1], q_4->nb[2], q_4->nb[3],
590
+ + /*offset=*/ (size_t) gpu_q_heads * q_4->nb[1]);
591
+ +
592
+ + q_g = ggml_permute(ctx0, q_g, 0, 2, 1, 3);
593
+ + q_c = ggml_permute(ctx0, q_c, 0, 2, 1, 3);
594
+ + ggml_tensor * k_g_p = ggml_permute(ctx0, k_g, 0, 2, 1, 3);
595
+ + ggml_tensor * v_g_p = ggml_permute(ctx0, v_g, 0, 2, 1, 3);
596
+ + ggml_tensor * k_c_p = ggml_permute(ctx0, k_c, 0, 2, 1, 3);
597
+ + ggml_tensor * v_c_p = ggml_permute(ctx0, v_c, 0, 2, 1, 3);
598
+ +
599
+ + // F32 K β†’ F16 cast mirrors build_attn_mha lines 1881-1887 (the embedding
600
+ + // model non-causal path). v_trans is asserted false above, so no extra
601
+ + // transpose is needed; V already has the same dim-1 head layout as K.
602
+ + if (k_g_p->type == GGML_TYPE_F32) k_g_p = ggml_cast(ctx0, k_g_p, GGML_TYPE_F16);
603
+ + if (k_c_p->type == GGML_TYPE_F32) k_c_p = ggml_cast(ctx0, k_c_p, GGML_TYPE_F16);
604
+ + if (v_g_p->type == GGML_TYPE_F32) v_g_p = ggml_cast(ctx0, v_g_p, GGML_TYPE_F16);
605
+ + if (v_c_p->type == GGML_TYPE_F32) v_c_p = ggml_cast(ctx0, v_c_p, GGML_TYPE_F16);
606
+ +
607
+ + // Sinks (if present) carry one F32 entry per Q-head; slice them to
608
+ + // match each half. The flash_attn_ext_add_sinks op asserts
609
+ + // sinks->ne[0] == q->src[0]->ne[2] (the q-head axis after permute).
610
+ + ggml_tensor * sinks_g = nullptr;
611
+ + ggml_tensor * sinks_c = nullptr;
612
+ + if (sinks) {
613
+ + GGML_ASSERT((uint32_t) sinks->ne[0] == n_head_q);
614
+ + sinks_g = ggml_view_1d(ctx0, sinks, gpu_q_heads, 0);
615
+ + sinks_c = ggml_view_1d(ctx0, sinks, cpu_q_heads,
616
+ + (size_t) gpu_q_heads * sinks->nb[0]);
617
+ + }
618
+ +
619
+ + const float alibi_bias = hparams.f_max_alibi_bias;
620
+ + const float logit_cap = hparams.attn_soft_cap ? hparams.f_attn_logit_softcapping : 0.0f;
621
+ +
622
+ + ggml_tensor * cur_g = ggml_flash_attn_ext(ctx0, q_g, k_g_p, v_g_p, kq_mask, kq_scale, alibi_bias, logit_cap);
623
+ + cb(cur_g, LLAMA_TENSOR_NAME_FATTN "_g", il);
624
+ + ggml_flash_attn_ext_add_sinks(cur_g, sinks_g);
625
+ + ggml_flash_attn_ext_set_prec (cur_g, GGML_PREC_F32);
626
+ +
627
+ + ggml_tensor * cur_c = ggml_flash_attn_ext(ctx0, q_c, k_c_p, v_c_p, kq_mask, kq_scale, alibi_bias, logit_cap);
628
+ + cb(cur_c, LLAMA_TENSOR_NAME_FATTN "_c", il);
629
+ + ggml_flash_attn_ext_add_sinks(cur_c, sinks_c);
630
+ + ggml_flash_attn_ext_set_prec (cur_c, GGML_PREC_F32);
631
+ +
632
+ + // opencoti F5 M3-B: enable + register the pair so the CUDA backend swaps
633
+ + // cur_g to a non-default stream at dispatch time, letting cur_c (on CPU)
634
+ + // overlap. set_enabled(true) is idempotent and only flipped here β€” the
635
+ + // call site already gates on neo_pipeline_mode != 0 && headinfer_split_active,
636
+ + // so reaching this point means NEO is engaged for this layer.
637
+ + ggml_neo_pipeline_set_enabled(true);
638
+ + ggml_neo_pipeline_register_pair(cur_g, cur_c);
639
+ +
640
+ + // FA output shape is [v_dim, n_q_heads_split, n_tokens, n_stream].
641
+ + // Concat on dim 1 (q-head axis) to reassemble the full Q-head set.
642
+ + // The scheduler synchronizes here β€” cur_c gets lifted to GPU so the
643
+ + // concat runs on whichever backend hosts cur_g (typically GPU).
644
+ + ggml_tensor * cur = ggml_concat(ctx0, cur_g, cur_c, 1);
645
+ + cb(cur, LLAMA_TENSOR_NAME_FATTN, il);
646
+ +
647
+ + // Match build_attn_mha's flash-attn final reshape (line 1913).
648
+ + cur = ggml_reshape_2d(ctx0, cur, cur->ne[0]*cur->ne[1], cur->ne[2]*cur->ne[3]);
649
+ +
650
+ + return cur;
651
+ +}
652
+ +
653
+ llm_graph_input_attn_no_cache * llm_graph_context::build_attn_inp_no_cache() const {
654
+ auto inp = std::make_unique<llm_graph_input_attn_no_cache>(hparams, cparams);
655
+
656
+ @@ -2237,10 +2375,29 @@ ggml_tensor * llm_graph_context::build_attn(
657
+ const auto & kq_mask = inp->get_kq_mask();
658
+
659
+ ggml_tensor * q = q_cur;
660
+ - ggml_tensor * k = mctx_cur->get_k(ctx0, il);
661
+ - ggml_tensor * v = mctx_cur->get_v(ctx0, il);
662
+
663
+ - ggml_tensor * cur = build_attn_mha(q, k, v, kq_b, kq_mask, sinks, v_mla, kq_scale, il);
664
+ + // opencoti F5 M3 neo β€” head-axis-split attention. When the M2 head
665
+ + // residency split is active for this layer AND --neo-pipeline is on,
666
+ + // build two flash_attn_ext ops on disjoint head ranges instead of the
667
+ + // M2 concat-reassembly path (which streams k_cpu/v_cpu back to GPU each
668
+ + // step and runs a single full-head FA op). The scheduler routes each
669
+ + // FA op to the backend matching its K/V buffer-type. v_mla must be
670
+ + // null (build_attn for KV asserts this above at :2102), kq_b too;
671
+ + // the M2 gate ensures non-transposed V. See docs/features/advanced_kv.md.
672
+ + ggml_tensor * cur;
673
+ + if (cparams.neo_pipeline_mode != 0 && mctx_cur->headinfer_split_active(il)) {
674
+ + ggml_tensor * k_g = mctx_cur->get_k_gpu(ctx0, il);
675
+ + ggml_tensor * v_g = mctx_cur->get_v_gpu(ctx0, il);
676
+ + ggml_tensor * k_c = mctx_cur->get_k_cpu(ctx0, il);
677
+ + ggml_tensor * v_c = mctx_cur->get_v_cpu(ctx0, il);
678
+ + cur = build_attn_mha_neo(q, k_g, v_g, k_c, v_c,
679
+ + mctx_cur->headinfer_gpu_heads(il),
680
+ + kq_b, kq_mask, sinks, v_mla, kq_scale, il);
681
+ + } else {
682
+ + ggml_tensor * k = mctx_cur->get_k(ctx0, il);
683
+ + ggml_tensor * v = mctx_cur->get_v(ctx0, il);
684
+ + cur = build_attn_mha(q, k, v, kq_b, kq_mask, sinks, v_mla, kq_scale, il);
685
+ + }
686
+ cb(cur, "kqv_out", il);
687
+
688
+ if (inp->self_v_rot) {
689
+ diff --git a/llama.cpp/src/llama-graph.h b/llama.cpp/src/llama-graph.h
690
+ --- a/llama.cpp/src/llama-graph.h
691
+ +++ b/llama.cpp/src/llama-graph.h
692
+ @@ -908,6 +908,27 @@ struct llm_graph_context {
693
+ float kq_scale,
694
+ int il) const;
695
+
696
+ + // opencoti F5 M3 neo β€” head-axis split flash-attn. Builds two
697
+ + // ggml_flash_attn_ext ops on disjoint head ranges (GPU vs CPU subsets,
698
+ + // routed automatically by the scheduler via the K/V tensors' buffer
699
+ + // types) and concatenates their outputs on the head axis before the
700
+ + // reshape/Wo projection. Flash-attention only (assert kq_b == nullptr,
701
+ + // v_mla == nullptr, !v_trans for V β€” the M2 split gate already enforces
702
+ + // this). Output shape matches build_attn_mha's flash-attn branch.
703
+ + ggml_tensor * build_attn_mha_neo(
704
+ + ggml_tensor * q, // [n_embd_head_q, n_head_q, n_tokens]
705
+ + ggml_tensor * k_g, // GPU-resident K subset (gpu_heads on dim 1)
706
+ + ggml_tensor * v_g, // GPU-resident V subset (gpu_heads on dim 1)
707
+ + ggml_tensor * k_c, // CPU-resident K subset (cpu_heads on dim 1)
708
+ + ggml_tensor * v_c, // CPU-resident V subset (cpu_heads on dim 1)
709
+ + uint32_t gpu_heads,
710
+ + ggml_tensor * kq_b, // must be nullptr
711
+ + ggml_tensor * kq_mask,
712
+ + ggml_tensor * sinks, // [n_head_q] or nullptr
713
+ + ggml_tensor * v_mla, // must be nullptr
714
+ + float kq_scale,
715
+ + int il) const;
716
+ +
717
+ llm_graph_input_attn_no_cache * build_attn_inp_no_cache() const;
718
+
719
+ ggml_tensor * build_attn(
720
+ diff --git a/llama.cpp/src/llama-kv-cache.cpp b/llama.cpp/src/llama-kv-cache.cpp
721
+ --- a/llama.cpp/src/llama-kv-cache.cpp
722
+ +++ b/llama.cpp/src/llama-kv-cache.cpp
723
+ @@ -1810,6 +1810,116 @@ ggml_tensor * llama_kv_cache::get_v(ggml_context * ctx, int32_t il, uint32_t n_k
724
+ ggml_row_size(v->type, kv_size*n_embd_v_gqa)*sinfo.s0);
725
+ }
726
+
727
+ +// opencoti F5 M3 neo β€” head-split-aware accessors. These factor the per-half
728
+ +// view machinery that get_k/get_v perform internally before their concat,
729
+ +// exposing the GPU-only and CPU-only sub-tensors as independent ggml views.
730
+ +// M3 NEO uses them to build two flash_attn_ext ops (one per backend, scheduler
731
+ +// auto-routes by buffer type) and merge their outputs, eliminating M2's
732
+ +// per-step O(n_kv) CPU→GPU stream-back. Dimensions and strides are identical
733
+ +// to the corresponding halves inside M2's get_k/get_v.
734
+ +
735
+ +bool llama_kv_cache::headinfer_split_active(int32_t il) const {
736
+ + const auto it = map_layer_ids.find(il);
737
+ + if (it == map_layer_ids.end()) {
738
+ + return false;
739
+ + }
740
+ + const auto & layer = layers[it->second];
741
+ + return layer.gpu_heads > 0 && layer.k_cpu != nullptr;
742
+ +}
743
+ +
744
+ +uint32_t llama_kv_cache::headinfer_gpu_heads(int32_t il) const {
745
+ + const auto it = map_layer_ids.find(il);
746
+ + if (it == map_layer_ids.end()) {
747
+ + return 0;
748
+ + }
749
+ + return layers[it->second].gpu_heads;
750
+ +}
751
+ +
752
+ +ggml_tensor * llama_kv_cache::get_k_gpu(ggml_context * ctx, int32_t il, uint32_t n_kv, const slot_info & sinfo) const {
753
+ + const int32_t ikv = map_layer_ids.at(il);
754
+ + const auto & layer = layers[ikv];
755
+ + GGML_ASSERT(layer.gpu_heads > 0 && "get_k_gpu called without an active headinfer split");
756
+ +
757
+ + auto * k = layer.k;
758
+ + const uint64_t kv_size = get_size();
759
+ + const uint32_t ns = sinfo.s1 - sinfo.s0 + 1;
760
+ + const uint32_t n_embd_head_k = hparams.n_embd_head_k(il);
761
+ + const uint32_t n_head_gpu = layer.gpu_heads;
762
+ + const uint64_t n_embd_gpu = (uint64_t) n_head_gpu * n_embd_head_k;
763
+ +
764
+ + return ggml_view_4d(ctx, k,
765
+ + n_embd_head_k, n_head_gpu, n_kv, ns,
766
+ + ggml_row_size(k->type, n_embd_head_k),
767
+ + ggml_row_size(k->type, n_embd_gpu),
768
+ + ggml_row_size(k->type, n_embd_gpu*kv_size),
769
+ + ggml_row_size(k->type, n_embd_gpu*kv_size)*sinfo.s0);
770
+ +}
771
+ +
772
+ +ggml_tensor * llama_kv_cache::get_k_cpu(ggml_context * ctx, int32_t il, uint32_t n_kv, const slot_info & sinfo) const {
773
+ + const int32_t ikv = map_layer_ids.at(il);
774
+ + const auto & layer = layers[ikv];
775
+ + GGML_ASSERT(layer.gpu_heads > 0 && layer.k_cpu && "get_k_cpu called without an active headinfer split");
776
+ +
777
+ + auto * kc = layer.k_cpu;
778
+ + const uint64_t kv_size = get_size();
779
+ + const uint32_t ns = sinfo.s1 - sinfo.s0 + 1;
780
+ + const uint32_t n_embd_head_k = hparams.n_embd_head_k(il);
781
+ + const uint32_t n_head_cpu = hparams.n_head_kv(il) - layer.gpu_heads;
782
+ + const uint64_t n_embd_cpu = (uint64_t) n_head_cpu * n_embd_head_k;
783
+ +
784
+ + return ggml_view_4d(ctx, kc,
785
+ + n_embd_head_k, n_head_cpu, n_kv, ns,
786
+ + ggml_row_size(kc->type, n_embd_head_k),
787
+ + ggml_row_size(kc->type, n_embd_cpu),
788
+ + ggml_row_size(kc->type, n_embd_cpu*kv_size),
789
+ + ggml_row_size(kc->type, n_embd_cpu*kv_size)*sinfo.s0);
790
+ +}
791
+ +
792
+ +ggml_tensor * llama_kv_cache::get_v_gpu(ggml_context * ctx, int32_t il, uint32_t n_kv, const slot_info & sinfo) const {
793
+ + const int32_t ikv = map_layer_ids.at(il);
794
+ + const auto & layer = layers[ikv];
795
+ + GGML_ASSERT(layer.gpu_heads > 0 && "get_v_gpu called without an active headinfer split");
796
+ + // M2's gate is `!v_trans && n_stream == 1` β€” when split is active the
797
+ + // V layout is always non-transposed (matches get_v's non-split branch
798
+ + // shape modulo head count). Same head-dim-1 stride pattern as K.
799
+ + GGML_ASSERT(!v_trans && "headinfer split requires non-transposed V");
800
+ +
801
+ + auto * v = layer.v;
802
+ + const uint64_t kv_size = get_size();
803
+ + const uint32_t ns = sinfo.s1 - sinfo.s0 + 1;
804
+ + const uint32_t n_embd_head_v = hparams.n_embd_head_v(il);
805
+ + const uint32_t n_head_gpu = layer.gpu_heads;
806
+ + const uint64_t n_embd_gpu = (uint64_t) n_head_gpu * n_embd_head_v;
807
+ +
808
+ + return ggml_view_4d(ctx, v,
809
+ + n_embd_head_v, n_head_gpu, n_kv, ns,
810
+ + ggml_row_size(v->type, n_embd_head_v),
811
+ + ggml_row_size(v->type, n_embd_gpu),
812
+ + ggml_row_size(v->type, n_embd_gpu*kv_size),
813
+ + ggml_row_size(v->type, n_embd_gpu*kv_size)*sinfo.s0);
814
+ +}
815
+ +
816
+ +ggml_tensor * llama_kv_cache::get_v_cpu(ggml_context * ctx, int32_t il, uint32_t n_kv, const slot_info & sinfo) const {
817
+ + const int32_t ikv = map_layer_ids.at(il);
818
+ + const auto & layer = layers[ikv];
819
+ + GGML_ASSERT(layer.gpu_heads > 0 && layer.v_cpu && "get_v_cpu called without an active headinfer split");
820
+ + GGML_ASSERT(!v_trans && "headinfer split requires non-transposed V");
821
+ +
822
+ + auto * vc = layer.v_cpu;
823
+ + const uint64_t kv_size = get_size();
824
+ + const uint32_t ns = sinfo.s1 - sinfo.s0 + 1;
825
+ + const uint32_t n_embd_head_v = hparams.n_embd_head_v(il);
826
+ + const uint32_t n_head_cpu = hparams.n_head_kv(il) - layer.gpu_heads;
827
+ + const uint64_t n_embd_cpu = (uint64_t) n_head_cpu * n_embd_head_v;
828
+ +
829
+ + return ggml_view_4d(ctx, vc,
830
+ + n_embd_head_v, n_head_cpu, n_kv, ns,
831
+ + ggml_row_size(vc->type, n_embd_head_v),
832
+ + ggml_row_size(vc->type, n_embd_cpu),
833
+ + ggml_row_size(vc->type, n_embd_cpu*kv_size),
834
+ + ggml_row_size(vc->type, n_embd_cpu*kv_size)*sinfo.s0);
835
+ +}
836
+ +
837
+ ggml_tensor * llama_kv_cache::cpy_k(ggml_context * ctx, ggml_tensor * k_cur, ggml_tensor * k_idxs, int32_t il, const slot_info & sinfo) const {
838
+ GGML_UNUSED(sinfo);
839
+
840
+ @@ -3107,6 +3217,31 @@ ggml_tensor * llama_kv_cache_context::get_v(ggml_context * ctx, int32_t il) cons
841
+ return kv->get_v(ctx, il, n_kv, sinfos[i_cur]);
842
+ }
843
+
844
+ +// opencoti F5 M3 neo β€” per-batch wrappers around the head-split accessors.
845
+ +ggml_tensor * llama_kv_cache_context::get_k_gpu(ggml_context * ctx, int32_t il) const {
846
+ + return kv->get_k_gpu(ctx, il, n_kv, sinfos[i_cur]);
847
+ +}
848
+ +
849
+ +ggml_tensor * llama_kv_cache_context::get_v_gpu(ggml_context * ctx, int32_t il) const {
850
+ + return kv->get_v_gpu(ctx, il, n_kv, sinfos[i_cur]);
851
+ +}
852
+ +
853
+ +ggml_tensor * llama_kv_cache_context::get_k_cpu(ggml_context * ctx, int32_t il) const {
854
+ + return kv->get_k_cpu(ctx, il, n_kv, sinfos[i_cur]);
855
+ +}
856
+ +
857
+ +ggml_tensor * llama_kv_cache_context::get_v_cpu(ggml_context * ctx, int32_t il) const {
858
+ + return kv->get_v_cpu(ctx, il, n_kv, sinfos[i_cur]);
859
+ +}
860
+ +
861
+ +bool llama_kv_cache_context::headinfer_split_active(int32_t il) const {
862
+ + return kv->headinfer_split_active(il);
863
+ +}
864
+ +
865
+ +uint32_t llama_kv_cache_context::headinfer_gpu_heads(int32_t il) const {
866
+ + return kv->headinfer_gpu_heads(il);
867
+ +}
868
+ +
869
+ ggml_tensor * llama_kv_cache_context::cpy_k(ggml_context * ctx, ggml_tensor * k_cur, ggml_tensor * k_idxs, int32_t il) const {
870
+ return kv->cpy_k(ctx, k_cur, k_idxs, il, sinfos[i_cur]);
871
+ }
872
+ diff --git a/llama.cpp/src/llama-kv-cache.h b/llama.cpp/src/llama-kv-cache.h
873
+ --- a/llama.cpp/src/llama-kv-cache.h
874
+ +++ b/llama.cpp/src/llama-kv-cache.h
875
+ @@ -217,6 +217,23 @@ public:
876
+ ggml_tensor * get_k(ggml_context * ctx, int32_t il, uint32_t n_kv, const slot_info & sinfo) const;
877
+ ggml_tensor * get_v(ggml_context * ctx, int32_t il, uint32_t n_kv, const slot_info & sinfo) const;
878
+
879
+ + // opencoti F5 M3 neo β€” head-split-aware K/V accessors. Return separate
880
+ + // views onto the GPU-resident and host-resident head subsets without the
881
+ + // concat-reassembly that get_k/get_v perform. Built on the SAME view
882
+ + // machinery (see llama-kv-cache.cpp get_k for the dimensions); callers
883
+ + // are expected to gate via headinfer_split_active(il) and to build their
884
+ + // own combiner (M3 NEO builds two flash_attn_ext ops + a head-axis concat
885
+ + // on the small attention outputs, eliminating M2's O(n_kv) stream-back).
886
+ + // For non-split layers these return the equivalent of the un-split view
887
+ + // for the "_gpu" half and nullptr for "_cpu" β€” callers must check
888
+ + // headinfer_split_active first.
889
+ + ggml_tensor * get_k_gpu(ggml_context * ctx, int32_t il, uint32_t n_kv, const slot_info & sinfo) const;
890
+ + ggml_tensor * get_v_gpu(ggml_context * ctx, int32_t il, uint32_t n_kv, const slot_info & sinfo) const;
891
+ + ggml_tensor * get_k_cpu(ggml_context * ctx, int32_t il, uint32_t n_kv, const slot_info & sinfo) const;
892
+ + ggml_tensor * get_v_cpu(ggml_context * ctx, int32_t il, uint32_t n_kv, const slot_info & sinfo) const;
893
+ + bool headinfer_split_active(int32_t il) const;
894
+ + uint32_t headinfer_gpu_heads (int32_t il) const;
895
+ +
896
+ // store k_cur and v_cur in the cache based on the provided head location
897
+ ggml_tensor * cpy_k(ggml_context * ctx, ggml_tensor * k_cur, ggml_tensor * k_idxs, int32_t il, const slot_info & sinfo) const;
898
+ ggml_tensor * cpy_v(ggml_context * ctx, ggml_tensor * v_cur, ggml_tensor * v_idxs, int32_t il, const slot_info & sinfo) const;
899
+ @@ -444,6 +461,16 @@ public:
900
+ ggml_tensor * get_k(ggml_context * ctx, int32_t il) const;
901
+ ggml_tensor * get_v(ggml_context * ctx, int32_t il) const;
902
+
903
+ + // opencoti F5 M3 neo β€” head-split-aware K/V accessors. Per-batch wrappers
904
+ + // that delegate to the underlying llama_kv_cache with the current n_kv /
905
+ + // slot info. See llama-kv-cache.h for semantics.
906
+ + ggml_tensor * get_k_gpu(ggml_context * ctx, int32_t il) const;
907
+ + ggml_tensor * get_v_gpu(ggml_context * ctx, int32_t il) const;
908
+ + ggml_tensor * get_k_cpu(ggml_context * ctx, int32_t il) const;
909
+ + ggml_tensor * get_v_cpu(ggml_context * ctx, int32_t il) const;
910
+ + bool headinfer_split_active(int32_t il) const;
911
+ + uint32_t headinfer_gpu_heads (int32_t il) const;
912
+ +
913
+ // store k_cur and v_cur in the cache based on the provided head location
914
+ // note: the heads in k_cur and v_cur should be laid out contiguously in memory
915
+ // - k_cur [n_embd_head_k, n_head_k, n_tokens]
916
+ diff --git a/llamafile/build-functions.sh b/llamafile/build-functions.sh
917
+ --- a/llamafile/build-functions.sh
918
+ +++ b/llamafile/build-functions.sh
919
+ @@ -218,10 +218,14 @@ compile_ggml_core() {
920
+ local llama_cpp_dir="$1"
921
+ local build_dir="$2"
922
+
923
+ + # opencoti F5 M3-B: ggml-neo-pipeline.cpp added so the CUDA backend's
924
+ + # graph_compute hook can call the orchestrator API (registered cur_g
925
+ + # detection + event slot get/set + dispatch-count increment).
926
+ local ggml_core_sources="\
927
+ $llama_cpp_dir/ggml/src/ggml.c \
928
+ $llama_cpp_dir/ggml/src/ggml-alloc.c \
929
+ $llama_cpp_dir/ggml/src/ggml-backend.cpp \
930
+ + $llama_cpp_dir/ggml/src/ggml-neo-pipeline.cpp \
931
+ $llama_cpp_dir/ggml/src/ggml-backend-meta.cpp \
932
+ $llama_cpp_dir/ggml/src/ggml-quants.c \
933
+ $llama_cpp_dir/ggml/src/ggml-threading.cpp"
patches/0031-per-stream-split.patch ADDED
@@ -0,0 +1,1513 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ From: opencoti
2
+ Subject: [PATCH 0031] F4 M3 Phase 4 β€” per-stream KV tensor split (task #109)
3
+
4
+ Resolves the long-deferred Phase 4 (`docs/decisions/0001-lazy-slot-context.md`)
5
+ of F4 M3 β€” every layer's K/V (and M2's CPU subset) become OWNING per-stream
6
+ tensor vectors rather than one 3-D tensor with a stream dim, with each
7
+ (stream, buffer-type) pair owning its own backend buffer. Unified mode
8
+ (`n_stream == 1`) collapses to the pre-Phase-4 layout and stays
9
+ byte-identical; non-unified mode (`n_stream > 1`) is functional for the
10
+ first time, unlocking the multi-tenant codepaths (M2 head-split now lifts
11
+ its `n_stream == 1` construction-time gate).
12
+
13
+ Sub-milestone shape (each landed against the M5-shipped tree at HEAD =
14
+ `2373e69` and verified by the M5 stack + M0 turn-2 + M1 eviction + M2
15
+ residency benches before composing):
16
+
17
+ - Phase 4-A β€” per-stream tensor split. `kv_layer` replaces
18
+ `k`/`v`/`k_cpu`/`v_cpu` direct pointers with
19
+ `std::vector<ggml_tensor *> k_per_stream` / `v_per_stream` /
20
+ `k_cpu_per_stream` / `v_cpu_per_stream`; each per-stream tensor is
21
+ `ggml_new_tensor_3d(ctx_s, type, n_embd, kv_size, 1)`. The trailing
22
+ `dim 3 == 1` keeps stride math identical to the pre-Phase-4
23
+ `[n_embd, kv_size, n_stream]` layout under unified mode. The
24
+ `k_stream` / `v_stream` 2-D view vectors are preserved (offset 0
25
+ within each per-stream tensor) so state I/O and cross-stream copies
26
+ keep operating on the existing view handles.
27
+
28
+ - Phase 4-B β€” per-stream backend buffers. `ctx_map` becomes a nested
29
+ `std::vector<std::map<ggml_backend_buffer_type_t, ggml_context_ptr,
30
+ ggml_backend_buft_comparator>> ctx_map_per_stream` indexed by stream,
31
+ with a `ctx_for_buft(buft, s)` lambda that returns the get-or-create
32
+ `(stream, buft)` ctx (mem_size budgeted at `4u*n_layer_kv*
33
+ ggml_tensor_overhead()`). Allocation loops nest stream-outer Γ—
34
+ buft-inner so each (stream, buft) pair allocates one backend buffer.
35
+ Phase 1 `ensure_cleared`, Phase 3 `opencoti_decommit_layer_range`,
36
+ and the M2 CPU subset construction follow the same per-stream
37
+ iteration shape.
38
+
39
+ - Phase 4-C β€” reworked write/read graph ops. `cpy_k`/`cpy_v` ship
40
+ option C-2 (N separate `ggml_set_rows`, one per `sinfo.n_stream()`
41
+ stream, tied via `ggml_add` on 1-D views β€” unified-mode batch_ns==1
42
+ collapses to one `set_rows`, byte-identical). C-1 (concat-then-
43
+ scatter) was rejected at impl time: `ggml_concat` materializes a
44
+ new buffer rather than viewing inputs, so writes routed through the
45
+ concat result would not land in the per-stream tensors. `get_k`/
46
+ `get_v` build per-cache-stream 4-D views and `ggml_concat` them
47
+ along dim 3 (stream axis) β€” unified mode yields a single view, no
48
+ concat. `set_input_k_idxs`/`set_input_v_idxs` emit LOCAL per-stream
49
+ indices unconditionally (Phase 4-F fix; the previous gating on
50
+ `sinfo.n_stream() > 1` left global indices in --parallel N servers
51
+ serving single-session batches and crashed CUDA decode against the
52
+ per-stream destination). `build_graph_shift` loops streams inside
53
+ the layer loop, slicing `inp->k_shift` per stream.
54
+
55
+ - Phase 4-D β€” M2 head-split is stream-aware. `get_k_gpu`/`get_v_gpu`/
56
+ `get_k_cpu`/`get_v_cpu` (and the M2 split branch inside `get_k`/
57
+ `get_v`/`cpy_k`/`cpy_v`) build per-stream 4-D views and concat
58
+ them on dim 3 (stream axis); the construction-time gate at
59
+ `headinfer_gpu_heads_frac < 1.0 && offload && !v_trans && n_stream
60
+ == 1` drops the `n_stream == 1` clause so per-stream CPU subsets
61
+ actually allocate for non-unified mode.
62
+
63
+ - Phase 4-E β€” cross-stream copies in `update()`, `state_write_data`,
64
+ `state_read_data`, and `seq_key_scores` continue to operate on the
65
+ existing `k_stream[s]` / `v_stream[s]` 2-D views (now offset-0 views
66
+ of per-stream tensors). Mechanically distinct from pre-Phase-4 (one
67
+ buffer per stream rather than one shared buffer), semantically the
68
+ same per the buffer transfer mechanism.
69
+
70
+ - Phase 4-F β€” verification gates. Cosine β‰₯ 0.999 unified-mode
71
+ equivalence captured indirectly via the M5 stack bench (B/A0/A1/
72
+ A2/A3/S configs all produce byte-identical KV MiB and
73
+ `t2_cached_n=1271` against the M5-shipped baseline) plus M2's
74
+ explicit greedy-decode byte-equality check (`split text ===
75
+ baseline text`, 6/6 PASS). Multi-stream RSS exercise comes via the
76
+ M0 turn-2 scenarioB (`--parallel 2` with two sessions sharing a
77
+ large system prefix β€” 5/5 PASS, validating per-stream KV reuse).
78
+ M3 neo-pipeline O off-identity config crashes on both pre-Phase-4
79
+ and post-Phase-4 binaries with `concat.cu:165 GGML_ASSERT(src0->
80
+ type == GGML_TYPE_F32)` β€” pre-existing M3 bug, DEFERRED as task
81
+ #278 and recorded as bug-207. Hook count stable at 20.
82
+
83
+ The HARD gates that survived from earlier sub-milestones (the
84
+ `GGML_ASSERT(n_stream == 1 && "Phase 4-A: get_k temporarily gated...";
85
+ "Phase 4-C lifts")` shape on get_k/get_v/cpy_k/cpy_v/build_graph_shift,
86
+ and the parallel `Phase 4-D lifts` shape on the M2 accessors) all reach
87
+ ship as lifted. Five `GGML_ASSERT(n_stream == 1)` lines deleted; the
88
+ remaining `n_stream == 1` checks are real preconditions (e.g., v_trans +
89
+ multi-stream is asserted out in cpy_v as not-a-real-workload).
90
+
91
+ Unblocks F5 M6 (PolyKV) which needs per-stream lifetimes cleanly
92
+ expressed for the shared compressed KV pool.
93
+
94
+ diff --git a/llama.cpp/src/llama-kv-cache.cpp b/llama.cpp/src/llama-kv-cache.cpp
95
+ --- a/llama.cpp/src/llama-kv-cache.cpp
96
+ +++ b/llama.cpp/src/llama-kv-cache.cpp
97
+ @@ -160,14 +160,35 @@ llama_kv_cache::llama_kv_cache(
98
+ return strcmp(ggml_backend_buft_name(lhs), ggml_backend_buft_name(rhs)) < 0;
99
+ }
100
+ };
101
+ - std::map<ggml_backend_buffer_type_t, ggml_context_ptr, ggml_backend_buft_comparator> ctx_map;
102
+
103
+ - // create a context for each buffer type
104
+ - auto ctx_for_buft = [&](ggml_backend_buffer_type_t buft) -> ggml_context * {
105
+ + // opencoti F4 M3 Phase 4-B β€” per-stream backend buffer ownership.
106
+ + // Each stream gets its own map of (buft β†’ ctx) so each (stream, buft)
107
+ + // pair allocates its own backend buffer. Phase 3's
108
+ + // opencoti_decommit_layer_range then collapses to "decommit this
109
+ + // stream's buffer range" cleanly, and M6 PolyKV's per-stream
110
+ + // lifetimes (enroll/disenroll a stream into the pool) become
111
+ + // mechanically straightforward.
112
+ + //
113
+ + // Unified mode (n_stream == 1): one stream, one inner map per
114
+ + // buft, byte-identical layout to pre-Phase-4 (the buffer-allocation
115
+ + // loop below ends up with exactly the same `(ctx, buf)` pairs in
116
+ + // ctxs_bufs as before, in the same order, with the same sizes).
117
+ + std::vector<std::map<ggml_backend_buffer_type_t, ggml_context_ptr, ggml_backend_buft_comparator>> ctx_map_per_stream;
118
+ + ctx_map_per_stream.resize(n_stream);
119
+ +
120
+ + // ctx_for_buft(buft, s): get-or-create the ggml_context for
121
+ + // (stream s, buft). Each per-stream ctx holds at most 4 tensors
122
+ + // per layer (K + V owning, K + V views; doubled when M2's CPU
123
+ + // subset is engaged on a layer). Budget conservatively at
124
+ + // 4*n_layer_kv slots per stream per buft β€” address space is free,
125
+ + // RSS still grows only on first write.
126
+ + auto ctx_for_buft = [&](ggml_backend_buffer_type_t buft, uint32_t s) -> ggml_context * {
127
+ + GGML_ASSERT(s < ctx_map_per_stream.size());
128
+ + auto & ctx_map = ctx_map_per_stream[s];
129
+ auto it = ctx_map.find(buft);
130
+ if (it == ctx_map.end()) {
131
+ ggml_init_params params = {
132
+ - /*.mem_size =*/ size_t(2u*(1 + n_stream)*n_layer_kv*ggml_tensor_overhead()),
133
+ + /*.mem_size =*/ size_t(4u*n_layer_kv*ggml_tensor_overhead()),
134
+ /*.mem_buffer =*/ NULL,
135
+ /*.no_alloc =*/ true,
136
+ };
137
+ @@ -255,10 +276,10 @@ llama_kv_cache::llama_kv_cache(
138
+
139
+ LLAMA_LOG_DEBUG("%s: layer %3d: dev = %s\n", __func__, il, dev_name);
140
+
141
+ - ggml_context * ctx = ctx_for_buft(buft);
142
+ - if (!ctx) {
143
+ - throw std::runtime_error("failed to create ggml context for kv cache");
144
+ - }
145
+ + // opencoti F4 M3 Phase 4-B: per-stream ctx selection has moved
146
+ + // into the per-stream loop below. The layer-level `buft` is the
147
+ + // device buffer-type for this layer; each stream allocates its
148
+ + // own ctx for (buft, stream) via ctx_for_buft(buft, s).
149
+
150
+ const bool has_k = true;
151
+ const bool has_v = !is_mla;
152
+ @@ -267,14 +288,21 @@ llama_kv_cache::llama_kv_cache(
153
+ // Head-residency split: keep the first gpu_heads KV heads on this
154
+ // (offloaded/device) layer's buffer and put the remaining heads on a
155
+ // host buffer. Splitting on KV-head boundaries is inherently GQA-safe.
156
+ - // Gated to offloaded, non-transposed-V, unified (n_stream==1) layers;
157
+ - // frac >= 1.0 (or <= 1 head) means no split (byte-identical upstream).
158
+ + // Gated to offloaded, non-transposed-V layers; frac >= 1.0 (or <= 1
159
+ + // head) means no split (byte-identical upstream).
160
+ + //
161
+ + // opencoti F4 M3 Phase 4-D: lifted the historical n_stream == 1
162
+ + // gate. With per-stream owning tensors and per-stream backend
163
+ + // buffers (Phase 4-A/B), each stream's GPU/CPU half lives in its
164
+ + // own buffer, so the split is well-defined for n_stream > 1.
165
+ + // Unified mode (n_stream == 1) collapses to the pre-Phase-4
166
+ + // single-stream layout, byte-identical.
167
+ uint32_t gpu_heads = 0;
168
+ uint32_t n_embd_k_gpu = n_embd_k_gqa;
169
+ uint32_t n_embd_v_gpu = n_embd_v_gqa;
170
+ uint32_t n_embd_k_cpu = 0;
171
+ uint32_t n_embd_v_cpu = 0;
172
+ - if (headinfer_gpu_heads_frac < 1.0f && offload && !v_trans && n_stream == 1) {
173
+ + if (headinfer_gpu_heads_frac < 1.0f && offload && !v_trans) {
174
+ const uint32_t n_head_kv = hparams.n_head_kv(il);
175
+ const uint32_t n_embd_head_k = hparams.n_embd_head_k(il);
176
+ const uint32_t n_embd_head_v = hparams.n_embd_head_v(il);
177
+ @@ -291,39 +319,119 @@ llama_kv_cache::llama_kv_cache(
178
+ }
179
+ }
180
+
181
+ - ggml_tensor * k = has_k ? ggml_new_tensor_3d(ctx, type_k, n_embd_k_gpu, kv_size, n_stream) : nullptr;
182
+ - ggml_tensor * v = has_v ? ggml_new_tensor_3d(ctx, type_v, n_embd_v_gpu, kv_size, n_stream) : nullptr;
183
+ -
184
+ - has_k && ggml_format_name(k, "cache_k_l%d", il);
185
+ - has_v && ggml_format_name(v, "cache_v_l%d", il);
186
+ -
187
+ - // opencoti F5 M2 headinfer β€” the host-resident head subset lives in the
188
+ - // CPU buffer-type context; the buffer-allocation loop below picks it up
189
+ - // automatically (ctx_for_buft keys contexts by buffer type).
190
+ - ggml_tensor * k_cpu = nullptr;
191
+ - ggml_tensor * v_cpu = nullptr;
192
+ - if (gpu_heads > 0) {
193
+ - ggml_context * ctx_cpu = ctx_for_buft(ggml_backend_cpu_buffer_type());
194
+ - if (!ctx_cpu) {
195
+ - throw std::runtime_error("failed to create CPU ggml context for headinfer kv cache");
196
+ - }
197
+ - k_cpu = has_k ? ggml_new_tensor_3d(ctx_cpu, type_k, n_embd_k_cpu, kv_size, n_stream) : nullptr;
198
+ - v_cpu = has_v ? ggml_new_tensor_3d(ctx_cpu, type_v, n_embd_v_cpu, kv_size, n_stream) : nullptr;
199
+ - has_k && k_cpu && ggml_format_name(k_cpu, "cache_k_cpu_l%d", il);
200
+ - has_v && v_cpu && ggml_format_name(v_cpu, "cache_v_cpu_l%d", il);
201
+ - }
202
+ -
203
+ + // opencoti F4 M3 Phase 4 β€” per-stream tensor split.
204
+ + // Each stream gets its own OWNING 3-D tensor of shape
205
+ + // [n_embd, kv_size, 1]. Dim 3 == 1 keeps stride math
206
+ + // identical to the pre-Phase-4 single-tensor layout, so the
207
+ + // unified-mode (n_stream == 1) buffer layout is byte-identical
208
+ + // to pre-Phase-4. Phase 4-B: each stream also gets its own
209
+ + // (buft β†’ ctx) entry, so each (stream, buft) pair allocates
210
+ + // its own backend buffer in the loop below.
211
+ + std::vector<ggml_tensor *> k_per_stream;
212
+ + std::vector<ggml_tensor *> v_per_stream;
213
+ + std::vector<ggml_tensor *> k_cpu_per_stream;
214
+ + std::vector<ggml_tensor *> v_cpu_per_stream;
215
+ std::vector<ggml_tensor *> k_stream;
216
+ std::vector<ggml_tensor *> v_stream;
217
+
218
+ for (uint32_t s = 0; s < n_stream; ++s) {
219
+ - k_stream.push_back(has_k ? ggml_view_2d(ctx, k, n_embd_k_gpu, kv_size, k->nb[1], s*k->nb[2]) : nullptr);
220
+ - v_stream.push_back(has_v ? ggml_view_2d(ctx, v, n_embd_v_gpu, kv_size, v->nb[1], s*v->nb[2]) : nullptr);
221
+ + ggml_context * ctx_s = ctx_for_buft(buft, s);
222
+ + if (!ctx_s) {
223
+ + throw std::runtime_error("failed to create ggml context for kv cache");
224
+ + }
225
+ +
226
+ + ggml_tensor * k_s = has_k
227
+ + ? ggml_new_tensor_3d(ctx_s, type_k, n_embd_k_gpu, kv_size, 1)
228
+ + : nullptr;
229
+ + ggml_tensor * v_s = has_v
230
+ + ? ggml_new_tensor_3d(ctx_s, type_v, n_embd_v_gpu, kv_size, 1)
231
+ + : nullptr;
232
+ +
233
+ + // Tensor naming preserves the pre-Phase-4 name in unified
234
+ + // mode (no `_s%u` suffix) so external state-IO callers that
235
+ + // grep by name find an unchanged layout.
236
+ + if (k_s) {
237
+ + if (n_stream == 1) {
238
+ + ggml_format_name(k_s, "cache_k_l%d", il);
239
+ + } else {
240
+ + ggml_format_name(k_s, "cache_k_l%d_s%u", il, s);
241
+ + }
242
+ + }
243
+ + if (v_s) {
244
+ + if (n_stream == 1) {
245
+ + ggml_format_name(v_s, "cache_v_l%d", il);
246
+ + } else {
247
+ + ggml_format_name(v_s, "cache_v_l%d_s%u", il, s);
248
+ + }
249
+ + }
250
+ +
251
+ + k_per_stream.push_back(k_s);
252
+ + v_per_stream.push_back(v_s);
253
+ +
254
+ + // opencoti F5 M2 headinfer β€” per-stream host-resident head
255
+ + // subset. Per-stream ctx_cpu so each stream's CPU half lives
256
+ + // in its own host buffer (Phase 4-B). Vectors stay empty
257
+ + // when gpu_heads == 0 so headinfer_split_active() can test
258
+ + // by vector size.
259
+ + if (gpu_heads > 0) {
260
+ + ggml_context * ctx_cpu_s = ctx_for_buft(ggml_backend_cpu_buffer_type(), s);
261
+ + if (!ctx_cpu_s) {
262
+ + throw std::runtime_error("failed to create CPU ggml context for headinfer kv cache");
263
+ + }
264
+ + ggml_tensor * k_cpu_s = has_k
265
+ + ? ggml_new_tensor_3d(ctx_cpu_s, type_k, n_embd_k_cpu, kv_size, 1)
266
+ + : nullptr;
267
+ + ggml_tensor * v_cpu_s = has_v
268
+ + ? ggml_new_tensor_3d(ctx_cpu_s, type_v, n_embd_v_cpu, kv_size, 1)
269
+ + : nullptr;
270
+ + if (k_cpu_s) {
271
+ + if (n_stream == 1) {
272
+ + ggml_format_name(k_cpu_s, "cache_k_cpu_l%d", il);
273
+ + } else {
274
+ + ggml_format_name(k_cpu_s, "cache_k_cpu_l%d_s%u", il, s);
275
+ + }
276
+ + }
277
+ + if (v_cpu_s) {
278
+ + if (n_stream == 1) {
279
+ + ggml_format_name(v_cpu_s, "cache_v_cpu_l%d", il);
280
+ + } else {
281
+ + ggml_format_name(v_cpu_s, "cache_v_cpu_l%d_s%u", il, s);
282
+ + }
283
+ + }
284
+ + k_cpu_per_stream.push_back(k_cpu_s);
285
+ + v_cpu_per_stream.push_back(v_cpu_s);
286
+ + }
287
+ +
288
+ + // 2-D views into per-stream 3-D tensor. Offset is 0 within
289
+ + // the owning tensor β€” each stream owns the whole 3-D extent.
290
+ + //
291
+ + // opencoti F4 M3 Phase 4-E: keeping k_stream/v_stream as the
292
+ + // existing 2-D view vectors means cross-stream copies in
293
+ + // update() (ggml_backend_tensor_copy between views), state
294
+ + // I/O paths (ggml_backend_tensor_set on the per-stream view),
295
+ + // and seq_key_scores all work without per-stream logic at
296
+ + // the consumer site β€” the view routes through to the
297
+ + // per-stream buffer transparently. Unified-mode operations
298
+ + // are byte-identical to pre-Phase-4 because the view has
299
+ + // identical ne/nb and (in unified mode) the same underlying
300
+ + // buffer layout.
301
+ + k_stream.push_back(k_s
302
+ + ? ggml_view_2d(ctx_s, k_s, n_embd_k_gpu, kv_size, k_s->nb[1], 0)
303
+ + : nullptr);
304
+ + v_stream.push_back(v_s
305
+ + ? ggml_view_2d(ctx_s, v_s, n_embd_v_gpu, kv_size, v_s->nb[1], 0)
306
+ + : nullptr);
307
+ }
308
+
309
+ map_layer_ids[il] = layers.size();
310
+
311
+ - layers.push_back({ il, k, v, k_stream, v_stream, k_cpu, v_cpu, gpu_heads });
312
+ + layers.push_back({
313
+ + il,
314
+ + k_per_stream, v_per_stream,
315
+ + k_stream, v_stream,
316
+ + k_cpu_per_stream, v_cpu_per_stream,
317
+ + gpu_heads,
318
+ + });
319
+ }
320
+
321
+ if (reuse) {
322
+ @@ -359,31 +467,46 @@ llama_kv_cache::llama_kv_cache(
323
+ // scheduler-visible tensor->buffer wiring intact (sched_reserve runs
324
+ // at construction time and asserts buffer_id >= 0) while moving the
325
+ // RSS commit to the first ensure_cleared() call.
326
+ - for (auto & [buft, ctx] : ctx_map) {
327
+ - ggml_backend_buffer_t buf;
328
+ - if (model.hparams.no_alloc) {
329
+ - buf = ggml_backend_buft_alloc_buffer(buft, /*size =*/ 0); // dummy buffer
330
+ - for (ggml_tensor * t = ggml_get_first_tensor(ctx.get()); t != nullptr; t = ggml_get_next_tensor(ctx.get(), t)) {
331
+ - t->buffer = buf; // set dummy buffer for KV cache so that the backend scheduler won't try to allocate it
332
+ + //
333
+ + // opencoti F4 M3 Phase 4-B: per-stream backend buffers. Each
334
+ + // (stream, buft) pair allocates its own backend buffer here.
335
+ + // Iteration order is stable: outer loop over streams (s=0,1,...),
336
+ + // inner loop over bufts (sorted by name via the ctx_map comparator).
337
+ + // In unified mode (n_stream == 1) this collapses to exactly one
338
+ + // pass per buft β€” byte-identical to pre-Phase-4 ctxs_bufs layout.
339
+ + for (uint32_t s = 0; s < n_stream; ++s) {
340
+ + for (auto & [buft, ctx] : ctx_map_per_stream[s]) {
341
+ + ggml_backend_buffer_t buf;
342
+ + if (model.hparams.no_alloc) {
343
+ + buf = ggml_backend_buft_alloc_buffer(buft, /*size =*/ 0); // dummy buffer
344
+ + for (ggml_tensor * t = ggml_get_first_tensor(ctx.get()); t != nullptr; t = ggml_get_next_tensor(ctx.get(), t)) {
345
+ + t->buffer = buf; // set dummy buffer for KV cache so that the backend scheduler won't try to allocate it
346
+ + }
347
+ + } else {
348
+ + buf = ggml_backend_alloc_ctx_tensors_from_buft(ctx.get(), buft); // real buffer (malloc'd but not zeroed yet)
349
+ + }
350
+ + if (!buf) {
351
+ + throw std::runtime_error("failed to allocate buffer for kv cache");
352
+ + }
353
+ +
354
+ + if (n_stream == 1) {
355
+ + LLAMA_LOG_INFO("%s: %10s KV buffer size = %8.2f MiB\n", __func__,
356
+ + ggml_backend_buffer_name(buf), ggml_backend_buffer_get_size(buf)/1024.0/1024.0);
357
+ + } else {
358
+ + LLAMA_LOG_INFO("%s: %10s KV buffer (stream %u) size = %8.2f MiB\n", __func__,
359
+ + ggml_backend_buffer_name(buf), s, ggml_backend_buffer_get_size(buf)/1024.0/1024.0);
360
+ }
361
+ - } else {
362
+ - buf = ggml_backend_alloc_ctx_tensors_from_buft(ctx.get(), buft); // real buffer (malloc'd but not zeroed yet)
363
+ - }
364
+ - if (!buf) {
365
+ - throw std::runtime_error("failed to allocate buffer for kv cache");
366
+ - }
367
+
368
+ - LLAMA_LOG_INFO("%s: %10s KV buffer size = %8.2f MiB\n", __func__, ggml_backend_buffer_name(buf), ggml_backend_buffer_get_size(buf)/1024.0/1024.0);
369
+ + if (model.hparams.no_alloc) {
370
+ + // Dummy buffer is size 0; clearing is a no-op but keeps the
371
+ + // upstream invariant that constructed buffers are zeroed.
372
+ + ggml_backend_buffer_clear(buf, 0);
373
+ + }
374
+ + // else: leave the buffer un-cleared. ensure_cleared() will zero it
375
+ + // on the first inference entry point that touches tensor data.
376
+
377
+ - if (model.hparams.no_alloc) {
378
+ - // Dummy buffer is size 0; clearing is a no-op but keeps the
379
+ - // upstream invariant that constructed buffers are zeroed.
380
+ - ggml_backend_buffer_clear(buf, 0);
381
+ + ctxs_bufs.emplace_back(std::move(ctx), buf);
382
+ }
383
+ - // else: leave the buffer un-cleared. ensure_cleared() will zero it
384
+ - // on the first inference entry point that touches tensor data.
385
+ -
386
+ - ctxs_bufs.emplace_back(std::move(ctx), buf);
387
+ }
388
+
389
+ if (model.hparams.no_alloc) {
390
+ @@ -471,33 +594,43 @@ void llama_kv_cache::ensure_cleared(uint32_t up_to_cells) {
391
+ // memset on the malloc'd buffer; for offloaded buffers it's a backend
392
+ // op. Either way only bytes for cells [a, b) get touched, so on Linux
393
+ // only those pages get committed to RSS.
394
+ + //
395
+ + // opencoti F4 M3 Phase 4: per-stream tensors. Each layer's
396
+ + // k_per_stream[s] / v_per_stream[s] is now its own owning tensor
397
+ + // (shape [n_embd, kv_size, 1]) so the per-stream stride math
398
+ + // collapses to a plain cell-offset memset within each stream's
399
+ + // tensor. M2's CPU heads follow the same pattern via
400
+ + // k_cpu_per_stream / v_cpu_per_stream (empty when no split).
401
+ for (const auto & layer : layers) {
402
+ for (uint32_t st = 0; st < n_stream; ++st) {
403
+ - if (layer.k) {
404
+ - const size_t off = (size_t) st * (size_t) layer.k->nb[2]
405
+ - + (size_t) a * (size_t) layer.k->nb[1];
406
+ - const size_t sz = (size_t) (b - a) * (size_t) layer.k->nb[1];
407
+ - ggml_backend_tensor_memset(layer.k, 0, off, sz);
408
+ - }
409
+ - if (layer.v) {
410
+ - const size_t off = (size_t) st * (size_t) layer.v->nb[2]
411
+ - + (size_t) a * (size_t) layer.v->nb[1];
412
+ - const size_t sz = (size_t) (b - a) * (size_t) layer.v->nb[1];
413
+ - ggml_backend_tensor_memset(layer.v, 0, off, sz);
414
+ - }
415
+ - // opencoti F5 M2 headinfer β€” clear the host-resident head subset
416
+ - // too (its own strides; same [a, b) cell range).
417
+ - if (layer.k_cpu) {
418
+ - const size_t off = (size_t) st * (size_t) layer.k_cpu->nb[2]
419
+ - + (size_t) a * (size_t) layer.k_cpu->nb[1];
420
+ - const size_t sz = (size_t) (b - a) * (size_t) layer.k_cpu->nb[1];
421
+ - ggml_backend_tensor_memset(layer.k_cpu, 0, off, sz);
422
+ + auto * k = layer.k_per_stream[st];
423
+ + if (k) {
424
+ + const size_t off = (size_t) a * (size_t) k->nb[1];
425
+ + const size_t sz = (size_t) (b - a) * (size_t) k->nb[1];
426
+ + ggml_backend_tensor_memset(k, 0, off, sz);
427
+ + }
428
+ + auto * v = layer.v_per_stream[st];
429
+ + if (v) {
430
+ + const size_t off = (size_t) a * (size_t) v->nb[1];
431
+ + const size_t sz = (size_t) (b - a) * (size_t) v->nb[1];
432
+ + ggml_backend_tensor_memset(v, 0, off, sz);
433
+ + }
434
+ + // M2 headinfer host-resident head subset (same [a, b) cells).
435
+ + if (st < layer.k_cpu_per_stream.size()) {
436
+ + auto * k_cpu = layer.k_cpu_per_stream[st];
437
+ + if (k_cpu) {
438
+ + const size_t off = (size_t) a * (size_t) k_cpu->nb[1];
439
+ + const size_t sz = (size_t) (b - a) * (size_t) k_cpu->nb[1];
440
+ + ggml_backend_tensor_memset(k_cpu, 0, off, sz);
441
+ + }
442
+ }
443
+ - if (layer.v_cpu) {
444
+ - const size_t off = (size_t) st * (size_t) layer.v_cpu->nb[2]
445
+ - + (size_t) a * (size_t) layer.v_cpu->nb[1];
446
+ - const size_t sz = (size_t) (b - a) * (size_t) layer.v_cpu->nb[1];
447
+ - ggml_backend_tensor_memset(layer.v_cpu, 0, off, sz);
448
+ + if (st < layer.v_cpu_per_stream.size()) {
449
+ + auto * v_cpu = layer.v_cpu_per_stream[st];
450
+ + if (v_cpu) {
451
+ + const size_t off = (size_t) a * (size_t) v_cpu->nb[1];
452
+ + const size_t sz = (size_t) (b - a) * (size_t) v_cpu->nb[1];
453
+ + ggml_backend_tensor_memset(v_cpu, 0, off, sz);
454
+ + }
455
+ }
456
+ }
457
+ }
458
+ @@ -542,22 +675,25 @@ bool llama_kv_cache::ensure_capacity(uint32_t cells_needed) {
459
+ }
460
+
461
+ // opencoti F4 M3 Phase 3 β€” see docs/decisions/0001-lazy-slot-context.md
462
+ -// Decommit pages of every layer's K/V tensors in the byte-range that
463
+ -// corresponds to cells [from_cells, to_cells). Returns those pages to the OS
464
+ -// β€” RSS drops by the size of the range, and the next access to those pages
465
+ -// will fault them back in as fresh zero-filled pages.
466
+ +// Decommit pages of one layer's K/V tensor for ONE STREAM in the byte-
467
+ +// range that corresponds to cells [from_cells, to_cells). Returns those
468
+ +// pages to the OS β€” RSS drops by the size of the range, and the next
469
+ +// access to those pages will fault them back in as fresh zero-filled
470
+ +// pages.
471
+ //
472
+ // Only safe to call after kv_size_alloc has been reset to from_cells (or
473
+ -// lower) and n_cells_cleared has been reset accordingly, so the bookkeeping
474
+ -// stays in sync with what's actually backed by physical memory.
475
+ +// lower) and n_cells_cleared has been reset accordingly, so the
476
+ +// bookkeeping stays in sync with what's actually backed by physical
477
+ +// memory.
478
+ //
479
+ -// The decommit is per-stream because the KV tensors are 3-D
480
+ -// [n_embd, kv_size, n_stream]; cells [a, b) within stream s occupy a
481
+ -// contiguous byte-range whose start address is (base + s*nb[2] + a*nb[1])
482
+ -// and length is (b-a)*nb[1]. posix_madvise is page-grained β€” bytes that
483
+ -// don't cover a full page are silently kept resident, which is fine for
484
+ -// the cells that straddle page boundaries.
485
+ -static void opencoti_decommit_layer_range(ggml_tensor * t, uint32_t n_stream,
486
+ +// opencoti F4 M3 Phase 4: per-stream tensors. `t` is now ONE stream's
487
+ +// owning 3-D tensor (dim 3 == 1). Cells [a, b) occupy contiguous bytes
488
+ +// at (base + a*nb[1]) for length (b-a)*nb[1]. The stream-stride loop
489
+ +// of the pre-Phase-4 implementation collapses; callers iterate streams
490
+ +// at the call site instead. posix_madvise is page-grained β€” bytes that
491
+ +// don't cover a full page are silently kept resident, which is fine
492
+ +// for the cells that straddle page boundaries.
493
+ +static void opencoti_decommit_layer_range(ggml_tensor * t,
494
+ uint32_t from_cells, uint32_t to_cells) {
495
+ if (!t || to_cells <= from_cells) {
496
+ return;
497
+ @@ -579,37 +715,32 @@ static void opencoti_decommit_layer_range(ggml_tensor * t, uint32_t n_stream,
498
+ if (!base) {
499
+ return;
500
+ }
501
+ - const size_t stride_stream = (size_t) t->nb[2];
502
+ - const size_t stride_cell = (size_t) t->nb[1];
503
+ - const size_t off_in_buf = (size_t) ((char *) t->data - (char *) base);
504
+ - for (uint32_t s = 0; s < n_stream; ++s) {
505
+ - const size_t off = off_in_buf
506
+ - + (size_t) s * stride_stream
507
+ - + (size_t) from_cells * stride_cell;
508
+ - const size_t sz = (size_t) (to_cells - from_cells) * stride_cell;
509
+ - // posix_madvise wants the address β€” work in absolute terms.
510
+ - void * addr = (char *) base + off;
511
+ - // Round up to a page boundary on the low side, round down on the
512
+ - // high side, so we only decommit pages we fully own. The cell-aligned
513
+ - // address is unlikely to be page-aligned in general.
514
+ - const size_t page = (size_t) sysconf(_SC_PAGESIZE);
515
+ - if (page == 0 || page == (size_t) -1) {
516
+ - return;
517
+ - }
518
+ - uintptr_t lo = (uintptr_t) addr;
519
+ - uintptr_t hi = lo + sz;
520
+ - uintptr_t lo_aligned = (lo + page - 1) & ~(uintptr_t)(page - 1);
521
+ - uintptr_t hi_aligned = hi & ~(uintptr_t)(page - 1);
522
+ - if (hi_aligned <= lo_aligned) {
523
+ - continue;
524
+ - }
525
+ - // madvise(MADV_DONTNEED) returns 0 on success, -1 on failure (with
526
+ - // errno set). We treat any failure as advisory β€” the RSS win may
527
+ - // be skipped, but correctness is unaffected (the K/V buffer is
528
+ - // still mapped, just possibly still resident).
529
+ - (void) madvise((void *) lo_aligned, (size_t) (hi_aligned - lo_aligned),
530
+ - MADV_DONTNEED);
531
+ + const size_t stride_cell = (size_t) t->nb[1];
532
+ + const size_t off_in_buf = (size_t) ((char *) t->data - (char *) base);
533
+ + const size_t off = off_in_buf + (size_t) from_cells * stride_cell;
534
+ + const size_t sz = (size_t) (to_cells - from_cells) * stride_cell;
535
+ + // posix_madvise wants the address β€” work in absolute terms.
536
+ + void * addr = (char *) base + off;
537
+ + // Round up to a page boundary on the low side, round down on the
538
+ + // high side, so we only decommit pages we fully own. The cell-aligned
539
+ + // address is unlikely to be page-aligned in general.
540
+ + const size_t page = (size_t) sysconf(_SC_PAGESIZE);
541
+ + if (page == 0 || page == (size_t) -1) {
542
+ + return;
543
+ }
544
+ + uintptr_t lo = (uintptr_t) addr;
545
+ + uintptr_t hi = lo + sz;
546
+ + uintptr_t lo_aligned = (lo + page - 1) & ~(uintptr_t)(page - 1);
547
+ + uintptr_t hi_aligned = hi & ~(uintptr_t)(page - 1);
548
+ + if (hi_aligned <= lo_aligned) {
549
+ + return;
550
+ + }
551
+ + // madvise(MADV_DONTNEED) returns 0 on success, -1 on failure (with
552
+ + // errno set). We treat any failure as advisory β€” the RSS win may
553
+ + // be skipped, but correctness is unaffected (the K/V buffer is
554
+ + // still mapped, just possibly still resident).
555
+ + (void) madvise((void *) lo_aligned, (size_t) (hi_aligned - lo_aligned),
556
+ + MADV_DONTNEED);
557
+ }
558
+
559
+ void llama_kv_cache::shrink_if_idle() {
560
+ @@ -667,13 +798,20 @@ void llama_kv_cache::shrink_if_idle() {
561
+ (unsigned long long) (now - last_active_ms_), evicted);
562
+
563
+ for (const auto & layer : layers) {
564
+ - opencoti_decommit_layer_range(layer.k, n_stream, from_cells, to_cells);
565
+ - opencoti_decommit_layer_range(layer.v, n_stream, from_cells, to_cells);
566
+ - // opencoti F5 M2 headinfer β€” the host-resident head subset IS
567
+ - // host-pageable, so this is where the GPU-mode shrink actually returns
568
+ - // RSS (the device k/v above are skipped by the is_host() guard).
569
+ - opencoti_decommit_layer_range(layer.k_cpu, n_stream, from_cells, to_cells);
570
+ - opencoti_decommit_layer_range(layer.v_cpu, n_stream, from_cells, to_cells);
571
+ + for (uint32_t s = 0; s < n_stream; ++s) {
572
+ + opencoti_decommit_layer_range(layer.k_per_stream[s], from_cells, to_cells);
573
+ + opencoti_decommit_layer_range(layer.v_per_stream[s], from_cells, to_cells);
574
+ + // opencoti F5 M2 headinfer β€” the host-resident head subset
575
+ + // IS host-pageable, so this is where the GPU-mode shrink
576
+ + // actually returns RSS (the device k/v above are skipped
577
+ + // by the is_host() guard in the function).
578
+ + if (s < layer.k_cpu_per_stream.size()) {
579
+ + opencoti_decommit_layer_range(layer.k_cpu_per_stream[s], from_cells, to_cells);
580
+ + }
581
+ + if (s < layer.v_cpu_per_stream.size()) {
582
+ + opencoti_decommit_layer_range(layer.v_cpu_per_stream[s], from_cells, to_cells);
583
+ + }
584
+ + }
585
+ }
586
+
587
+ // After decommit, treat cells [from_cells, to_cells) as "uncleared" β€”
588
+ @@ -1683,11 +1821,14 @@ bool llama_kv_cache::get_has_shift() const {
589
+ }
590
+
591
+ ggml_type llama_kv_cache::type_k() const {
592
+ - return layers[0].k->type;
593
+ + // opencoti F4 M3 Phase 4: all per-stream tensors of a layer carry the
594
+ + // same type β€” read it from stream 0. Works in both unified and
595
+ + // non-unified modes.
596
+ + return layers[0].k_per_stream[0]->type;
597
+ }
598
+
599
+ ggml_type llama_kv_cache::type_v() const {
600
+ - return layers[0].v->type;
601
+ + return layers[0].v_per_stream[0]->type;
602
+ }
603
+
604
+ uint32_t llama_kv_cache::get_n_kv(const slot_info & sinfo) const {
605
+ @@ -1710,104 +1851,166 @@ ggml_tensor * llama_kv_cache::get_k(ggml_context * ctx, int32_t il, uint32_t n_k
606
+ const int32_t ikv = map_layer_ids.at(il);
607
+
608
+ const auto & layer = layers[ikv];
609
+ - auto * k = layer.k;
610
+
611
+ const uint64_t kv_size = get_size();
612
+ - const uint32_t ns = sinfo.s1 - sinfo.s0 + 1;
613
+
614
+ // opencoti F5 M2 headinfer β€” reassemble the full head set from the
615
+ - // GPU-resident (k) and host-resident (k_cpu) subsets by concat on the head
616
+ + // GPU-resident and host-resident subsets by concat on the head
617
+ // dimension. The backend scheduler streams k_cpu to the compute backend.
618
+ - if (layer.gpu_heads > 0 && layer.k_cpu) {
619
+ + //
620
+ + // opencoti F4 M3 Phase 4-D: stream-aware. For each cache stream covered
621
+ + // by sinfo, build a 4-D head-axis-concat view (GPU half + CPU half)
622
+ + // with that stream's per-stream tensors. Then concat the per-stream
623
+ + // results along the stream axis (dim 3). Unified mode (sinfo.s1 ==
624
+ + // sinfo.s0 == 0) collapses to one 4-D head-concat view of stream 0's
625
+ + // tensors β€” byte-identical to pre-Phase-4.
626
+ + if (layer.gpu_heads > 0 && !layer.k_cpu_per_stream.empty() && layer.k_cpu_per_stream[0]) {
627
+ const uint32_t n_embd_head_k = hparams.n_embd_head_k(il);
628
+ const uint32_t n_head_gpu = layer.gpu_heads;
629
+ const uint32_t n_head_cpu = hparams.n_head_kv(il) - layer.gpu_heads;
630
+ const uint64_t n_embd_gpu = (uint64_t) n_head_gpu * n_embd_head_k;
631
+ const uint64_t n_embd_cpu = (uint64_t) n_head_cpu * n_embd_head_k;
632
+ - auto * kc = layer.k_cpu;
633
+ - ggml_tensor * kg_v = ggml_view_4d(ctx, k,
634
+ - n_embd_head_k, n_head_gpu, n_kv, ns,
635
+ - ggml_row_size(k->type, n_embd_head_k),
636
+ - ggml_row_size(k->type, n_embd_gpu),
637
+ - ggml_row_size(k->type, n_embd_gpu*kv_size),
638
+ - ggml_row_size(k->type, n_embd_gpu*kv_size)*sinfo.s0);
639
+ - ggml_tensor * kc_v = ggml_view_4d(ctx, kc,
640
+ - n_embd_head_k, n_head_cpu, n_kv, ns,
641
+ - ggml_row_size(kc->type, n_embd_head_k),
642
+ - ggml_row_size(kc->type, n_embd_cpu),
643
+ - ggml_row_size(kc->type, n_embd_cpu*kv_size),
644
+ - ggml_row_size(kc->type, n_embd_cpu*kv_size)*sinfo.s0);
645
+ - return ggml_concat(ctx, kg_v, kc_v, 1);
646
+ - }
647
+ -
648
+ - const uint64_t n_embd_k_gqa = k->ne[0];
649
+
650
+ + auto build_stream_view = [&](uint32_t s_cache) -> ggml_tensor * {
651
+ + ggml_tensor * k_s = layer.k_per_stream[s_cache];
652
+ + ggml_tensor * kc_s = layer.k_cpu_per_stream[s_cache];
653
+ + ggml_tensor * kg_v = ggml_view_4d(ctx, k_s,
654
+ + n_embd_head_k, n_head_gpu, n_kv, 1,
655
+ + ggml_row_size(k_s->type, n_embd_head_k),
656
+ + ggml_row_size(k_s->type, n_embd_gpu),
657
+ + ggml_row_size(k_s->type, n_embd_gpu*kv_size),
658
+ + 0);
659
+ + ggml_tensor * kc_v = ggml_view_4d(ctx, kc_s,
660
+ + n_embd_head_k, n_head_cpu, n_kv, 1,
661
+ + ggml_row_size(kc_s->type, n_embd_head_k),
662
+ + ggml_row_size(kc_s->type, n_embd_cpu),
663
+ + ggml_row_size(kc_s->type, n_embd_cpu*kv_size),
664
+ + 0);
665
+ + return ggml_concat(ctx, kg_v, kc_v, 1);
666
+ + };
667
+ +
668
+ + ggml_tensor * result = build_stream_view(sinfo.s0);
669
+ + for (uint32_t s = sinfo.s0 + 1; s <= sinfo.s1; ++s) {
670
+ + result = ggml_concat(ctx, result, build_stream_view(s), 3);
671
+ + }
672
+ + return result;
673
+ + }
674
+ +
675
+ + // opencoti F4 M3 Phase 4-C: per-stream view + concat along stream axis
676
+ + // (dim 3). In unified mode (sinfo.s1 == sinfo.s0 == 0) the loop does
677
+ + // not fire, yielding a single 4-D view of layer.k_per_stream[0] β€”
678
+ + // structurally and numerically byte-identical to pre-Phase-4 (same
679
+ + // ne, same nb, same offset 0). In non-unified mode the result is N
680
+ + // contiguous concat ops along dim 3 (the stream axis), one per
681
+ + // covered cache stream. Cost: (ns - 1) concat nodes per layer per turn.
682
+ + auto * k0 = layer.k_per_stream[sinfo.s0];
683
+ + const uint64_t n_embd_k_gqa = k0->ne[0];
684
+ assert(n_embd_k_gqa == hparams.n_embd_k_gqa(il));
685
+
686
+ - return ggml_view_4d(ctx, k,
687
+ - hparams.n_embd_head_k(il), hparams.n_head_kv(il), n_kv, ns,
688
+ - ggml_row_size(k->type, hparams.n_embd_head_k(il)),
689
+ - ggml_row_size(k->type, n_embd_k_gqa),
690
+ - ggml_row_size(k->type, n_embd_k_gqa*kv_size),
691
+ - ggml_row_size(k->type, n_embd_k_gqa*kv_size)*sinfo.s0);
692
+ + auto build_stream_view = [&](uint32_t s_cache) -> ggml_tensor * {
693
+ + ggml_tensor * k_s = layer.k_per_stream[s_cache];
694
+ + return ggml_view_4d(ctx, k_s,
695
+ + hparams.n_embd_head_k(il), hparams.n_head_kv(il), n_kv, 1,
696
+ + ggml_row_size(k_s->type, hparams.n_embd_head_k(il)),
697
+ + ggml_row_size(k_s->type, n_embd_k_gqa),
698
+ + ggml_row_size(k_s->type, n_embd_k_gqa*kv_size),
699
+ + 0);
700
+ + };
701
+ +
702
+ + ggml_tensor * result = build_stream_view(sinfo.s0);
703
+ + for (uint32_t s = sinfo.s0 + 1; s <= sinfo.s1; ++s) {
704
+ + result = ggml_concat(ctx, result, build_stream_view(s), 3);
705
+ + }
706
+ + return result;
707
+ }
708
+
709
+ ggml_tensor * llama_kv_cache::get_v(ggml_context * ctx, int32_t il, uint32_t n_kv, const slot_info & sinfo) const {
710
+ const int32_t ikv = map_layer_ids.at(il);
711
+
712
+ const auto & layer = layers[ikv];
713
+ - auto * v = layer.v;
714
+
715
+ const uint64_t kv_size = get_size();
716
+ - const uint32_t ns = sinfo.s1 - sinfo.s0 + 1;
717
+
718
+ - // opencoti F5 M2 headinfer β€” concat the GPU + host head subsets. The split
719
+ - // is gated off when v_trans at construction, so this branch is always the
720
+ - // non-transposed layout (heads in dim 1, same as get_k).
721
+ - if (layer.gpu_heads > 0 && layer.v_cpu) {
722
+ + // opencoti F5 M2 headinfer β€” concat the GPU + host head subsets. The
723
+ + // split is gated off when v_trans at construction, so this branch is
724
+ + // always the non-transposed layout (heads in dim 1, same as get_k).
725
+ + //
726
+ + // opencoti F4 M3 Phase 4-D: stream-aware per-stream concat β€” same
727
+ + // pattern as get_k's M2 branch.
728
+ + if (layer.gpu_heads > 0 && !layer.v_cpu_per_stream.empty() && layer.v_cpu_per_stream[0]) {
729
+ const uint32_t n_embd_head_v = hparams.n_embd_head_v(il);
730
+ const uint32_t n_head_gpu = layer.gpu_heads;
731
+ const uint32_t n_head_cpu = hparams.n_head_kv(il) - layer.gpu_heads;
732
+ const uint64_t n_embd_gpu = (uint64_t) n_head_gpu * n_embd_head_v;
733
+ const uint64_t n_embd_cpu = (uint64_t) n_head_cpu * n_embd_head_v;
734
+ - auto * vc = layer.v_cpu;
735
+ - ggml_tensor * vg_v = ggml_view_4d(ctx, v,
736
+ - n_embd_head_v, n_head_gpu, n_kv, ns,
737
+ - ggml_row_size(v->type, n_embd_head_v),
738
+ - ggml_row_size(v->type, n_embd_gpu),
739
+ - ggml_row_size(v->type, n_embd_gpu*kv_size),
740
+ - ggml_row_size(v->type, n_embd_gpu*kv_size)*sinfo.s0);
741
+ - ggml_tensor * vc_v = ggml_view_4d(ctx, vc,
742
+ - n_embd_head_v, n_head_cpu, n_kv, ns,
743
+ - ggml_row_size(vc->type, n_embd_head_v),
744
+ - ggml_row_size(vc->type, n_embd_cpu),
745
+ - ggml_row_size(vc->type, n_embd_cpu*kv_size),
746
+ - ggml_row_size(vc->type, n_embd_cpu*kv_size)*sinfo.s0);
747
+ - return ggml_concat(ctx, vg_v, vc_v, 1);
748
+ - }
749
+ -
750
+ - const uint64_t n_embd_v_gqa = v->ne[0];
751
+ +
752
+ + auto build_stream_view = [&](uint32_t s_cache) -> ggml_tensor * {
753
+ + ggml_tensor * v_s = layer.v_per_stream[s_cache];
754
+ + ggml_tensor * vc_s = layer.v_cpu_per_stream[s_cache];
755
+ + ggml_tensor * vg_v = ggml_view_4d(ctx, v_s,
756
+ + n_embd_head_v, n_head_gpu, n_kv, 1,
757
+ + ggml_row_size(v_s->type, n_embd_head_v),
758
+ + ggml_row_size(v_s->type, n_embd_gpu),
759
+ + ggml_row_size(v_s->type, n_embd_gpu*kv_size),
760
+ + 0);
761
+ + ggml_tensor * vc_v = ggml_view_4d(ctx, vc_s,
762
+ + n_embd_head_v, n_head_cpu, n_kv, 1,
763
+ + ggml_row_size(vc_s->type, n_embd_head_v),
764
+ + ggml_row_size(vc_s->type, n_embd_cpu),
765
+ + ggml_row_size(vc_s->type, n_embd_cpu*kv_size),
766
+ + 0);
767
+ + return ggml_concat(ctx, vg_v, vc_v, 1);
768
+ + };
769
+ +
770
+ + ggml_tensor * result = build_stream_view(sinfo.s0);
771
+ + for (uint32_t s = sinfo.s0 + 1; s <= sinfo.s1; ++s) {
772
+ + result = ggml_concat(ctx, result, build_stream_view(s), 3);
773
+ + }
774
+ + return result;
775
+ + }
776
+ +
777
+ + // opencoti F4 M3 Phase 4-C: per-stream view + concat along stream
778
+ + // axis. Same shape as get_k, but with v_trans branching. Unified mode
779
+ + // (sinfo.s1 == sinfo.s0 == 0) keeps a single 4-D view β†’ byte-identical.
780
+ + auto * v0 = layer.v_per_stream[sinfo.s0];
781
+ + const uint64_t n_embd_v_gqa = v0->ne[0];
782
+
783
+ // [TAG_V_CACHE_VARIABLE]
784
+ assert(n_embd_v_gqa >= hparams.n_embd_v_gqa(il));
785
+
786
+ if (!v_trans) {
787
+ // note: v->nb[1] <= v->nb[2]
788
+ - return ggml_view_4d(ctx, v,
789
+ - hparams.n_embd_head_v(il), hparams.n_head_kv(il), n_kv, ns,
790
+ - ggml_row_size(v->type, hparams.n_embd_head_v(il)), // v->nb[1]
791
+ - ggml_row_size(v->type, n_embd_v_gqa), // v->nb[2]
792
+ - ggml_row_size(v->type, n_embd_v_gqa*kv_size), // v->nb[3]
793
+ - ggml_row_size(v->type, n_embd_v_gqa*kv_size)*sinfo.s0);
794
+ + auto build_stream_view = [&](uint32_t s_cache) -> ggml_tensor * {
795
+ + ggml_tensor * v_s = layer.v_per_stream[s_cache];
796
+ + return ggml_view_4d(ctx, v_s,
797
+ + hparams.n_embd_head_v(il), hparams.n_head_kv(il), n_kv, 1,
798
+ + ggml_row_size(v_s->type, hparams.n_embd_head_v(il)),
799
+ + ggml_row_size(v_s->type, n_embd_v_gqa),
800
+ + ggml_row_size(v_s->type, n_embd_v_gqa*kv_size),
801
+ + 0);
802
+ + };
803
+ + ggml_tensor * result = build_stream_view(sinfo.s0);
804
+ + for (uint32_t s = sinfo.s0 + 1; s <= sinfo.s1; ++s) {
805
+ + result = ggml_concat(ctx, result, build_stream_view(s), 3);
806
+ + }
807
+ + return result;
808
+ + }
809
+ +
810
+ + // v_trans branch: note v->nb[1] > v->nb[2]
811
+ + auto build_stream_view = [&](uint32_t s_cache) -> ggml_tensor * {
812
+ + ggml_tensor * v_s = layer.v_per_stream[s_cache];
813
+ + return ggml_view_4d(ctx, v_s,
814
+ + n_kv, hparams.n_head_kv(il), hparams.n_embd_head_v(il), 1,
815
+ + ggml_row_size(v_s->type, kv_size*hparams.n_embd_head_v(il)),
816
+ + ggml_row_size(v_s->type, kv_size),
817
+ + ggml_row_size(v_s->type, kv_size*n_embd_v_gqa),
818
+ + 0);
819
+ + };
820
+ + ggml_tensor * result = build_stream_view(sinfo.s0);
821
+ + for (uint32_t s = sinfo.s0 + 1; s <= sinfo.s1; ++s) {
822
+ + result = ggml_concat(ctx, result, build_stream_view(s), 3);
823
+ }
824
+ -
825
+ - // note: v->nb[1] > v->nb[2]
826
+ - return ggml_view_4d(ctx, v,
827
+ - n_kv, hparams.n_head_kv(il), hparams.n_embd_head_v(il), ns,
828
+ - ggml_row_size(v->type, kv_size*hparams.n_embd_head_v(il)), // v->nb[1]
829
+ - ggml_row_size(v->type, kv_size), // v->nb[2]
830
+ - ggml_row_size(v->type, kv_size*n_embd_v_gqa), // v->nb[3]
831
+ - ggml_row_size(v->type, kv_size*n_embd_v_gqa)*sinfo.s0);
832
+ + return result;
833
+ }
834
+
835
+ // opencoti F5 M3 neo β€” head-split-aware accessors. These factor the per-half
836
+ @@ -1824,7 +2027,13 @@ bool llama_kv_cache::headinfer_split_active(int32_t il) const {
837
+ return false;
838
+ }
839
+ const auto & layer = layers[it->second];
840
+ - return layer.gpu_heads > 0 && layer.k_cpu != nullptr;
841
+ + // opencoti F4 M3 Phase 4: per-stream k_cpu vector is empty when no split,
842
+ + // populated when gpu_heads > 0. Stream 0 always exists in unified mode
843
+ + // (the only mode where split engages today; the M2 construction gate
844
+ + // requires n_stream == 1).
845
+ + return layer.gpu_heads > 0
846
+ + && !layer.k_cpu_per_stream.empty()
847
+ + && layer.k_cpu_per_stream[0] != nullptr;
848
+ }
849
+
850
+ uint32_t llama_kv_cache::headinfer_gpu_heads(int32_t il) const {
851
+ @@ -1840,93 +2049,131 @@ ggml_tensor * llama_kv_cache::get_k_gpu(ggml_context * ctx, int32_t il, uint32_t
852
+ const auto & layer = layers[ikv];
853
+ GGML_ASSERT(layer.gpu_heads > 0 && "get_k_gpu called without an active headinfer split");
854
+
855
+ - auto * k = layer.k;
856
+ - const uint64_t kv_size = get_size();
857
+ - const uint32_t ns = sinfo.s1 - sinfo.s0 + 1;
858
+ + // opencoti F4 M3 Phase 4-D: per-stream M2 GPU subset. In unified
859
+ + // mode (sinfo.s1 == sinfo.s0 == 0) the loop runs once and yields
860
+ + // a single 4-D view of layer.k_per_stream[0]'s GPU half β€”
861
+ + // byte-identical to pre-Phase-4 (same ne, same nb, offset 0).
862
+ + const uint64_t kv_size = get_size();
863
+ const uint32_t n_embd_head_k = hparams.n_embd_head_k(il);
864
+ const uint32_t n_head_gpu = layer.gpu_heads;
865
+ const uint64_t n_embd_gpu = (uint64_t) n_head_gpu * n_embd_head_k;
866
+
867
+ - return ggml_view_4d(ctx, k,
868
+ - n_embd_head_k, n_head_gpu, n_kv, ns,
869
+ - ggml_row_size(k->type, n_embd_head_k),
870
+ - ggml_row_size(k->type, n_embd_gpu),
871
+ - ggml_row_size(k->type, n_embd_gpu*kv_size),
872
+ - ggml_row_size(k->type, n_embd_gpu*kv_size)*sinfo.s0);
873
+ + auto build_stream_view = [&](uint32_t s_cache) -> ggml_tensor * {
874
+ + ggml_tensor * k_s = layer.k_per_stream[s_cache];
875
+ + return ggml_view_4d(ctx, k_s,
876
+ + n_embd_head_k, n_head_gpu, n_kv, 1,
877
+ + ggml_row_size(k_s->type, n_embd_head_k),
878
+ + ggml_row_size(k_s->type, n_embd_gpu),
879
+ + ggml_row_size(k_s->type, n_embd_gpu*kv_size),
880
+ + 0);
881
+ + };
882
+ +
883
+ + ggml_tensor * result = build_stream_view(sinfo.s0);
884
+ + for (uint32_t s = sinfo.s0 + 1; s <= sinfo.s1; ++s) {
885
+ + result = ggml_concat(ctx, result, build_stream_view(s), 3);
886
+ + }
887
+ + return result;
888
+ }
889
+
890
+ ggml_tensor * llama_kv_cache::get_k_cpu(ggml_context * ctx, int32_t il, uint32_t n_kv, const slot_info & sinfo) const {
891
+ const int32_t ikv = map_layer_ids.at(il);
892
+ const auto & layer = layers[ikv];
893
+ - GGML_ASSERT(layer.gpu_heads > 0 && layer.k_cpu && "get_k_cpu called without an active headinfer split");
894
+ -
895
+ - auto * kc = layer.k_cpu;
896
+ - const uint64_t kv_size = get_size();
897
+ - const uint32_t ns = sinfo.s1 - sinfo.s0 + 1;
898
+ + GGML_ASSERT(layer.gpu_heads > 0
899
+ + && !layer.k_cpu_per_stream.empty()
900
+ + && layer.k_cpu_per_stream[0]
901
+ + && "get_k_cpu called without an active headinfer split");
902
+ +
903
+ + // opencoti F4 M3 Phase 4-D: per-stream M2 CPU subset. Same pattern
904
+ + // as get_k_gpu; unified mode collapses to a single 4-D view.
905
+ + const uint64_t kv_size = get_size();
906
+ const uint32_t n_embd_head_k = hparams.n_embd_head_k(il);
907
+ const uint32_t n_head_cpu = hparams.n_head_kv(il) - layer.gpu_heads;
908
+ const uint64_t n_embd_cpu = (uint64_t) n_head_cpu * n_embd_head_k;
909
+
910
+ - return ggml_view_4d(ctx, kc,
911
+ - n_embd_head_k, n_head_cpu, n_kv, ns,
912
+ - ggml_row_size(kc->type, n_embd_head_k),
913
+ - ggml_row_size(kc->type, n_embd_cpu),
914
+ - ggml_row_size(kc->type, n_embd_cpu*kv_size),
915
+ - ggml_row_size(kc->type, n_embd_cpu*kv_size)*sinfo.s0);
916
+ + auto build_stream_view = [&](uint32_t s_cache) -> ggml_tensor * {
917
+ + ggml_tensor * kc_s = layer.k_cpu_per_stream[s_cache];
918
+ + return ggml_view_4d(ctx, kc_s,
919
+ + n_embd_head_k, n_head_cpu, n_kv, 1,
920
+ + ggml_row_size(kc_s->type, n_embd_head_k),
921
+ + ggml_row_size(kc_s->type, n_embd_cpu),
922
+ + ggml_row_size(kc_s->type, n_embd_cpu*kv_size),
923
+ + 0);
924
+ + };
925
+ +
926
+ + ggml_tensor * result = build_stream_view(sinfo.s0);
927
+ + for (uint32_t s = sinfo.s0 + 1; s <= sinfo.s1; ++s) {
928
+ + result = ggml_concat(ctx, result, build_stream_view(s), 3);
929
+ + }
930
+ + return result;
931
+ }
932
+
933
+ ggml_tensor * llama_kv_cache::get_v_gpu(ggml_context * ctx, int32_t il, uint32_t n_kv, const slot_info & sinfo) const {
934
+ const int32_t ikv = map_layer_ids.at(il);
935
+ const auto & layer = layers[ikv];
936
+ GGML_ASSERT(layer.gpu_heads > 0 && "get_v_gpu called without an active headinfer split");
937
+ - // M2's gate is `!v_trans && n_stream == 1` β€” when split is active the
938
+ - // V layout is always non-transposed (matches get_v's non-split branch
939
+ - // shape modulo head count). Same head-dim-1 stride pattern as K.
940
+ + // M2 requires non-transposed V (FA on). The split-active branch in
941
+ + // get_v/cpy_v also requires this. Per-stream view + concat on stream
942
+ + // axis. Phase 4-D: lifted the n_stream == 1 gate.
943
+ GGML_ASSERT(!v_trans && "headinfer split requires non-transposed V");
944
+
945
+ - auto * v = layer.v;
946
+ - const uint64_t kv_size = get_size();
947
+ - const uint32_t ns = sinfo.s1 - sinfo.s0 + 1;
948
+ + const uint64_t kv_size = get_size();
949
+ const uint32_t n_embd_head_v = hparams.n_embd_head_v(il);
950
+ const uint32_t n_head_gpu = layer.gpu_heads;
951
+ const uint64_t n_embd_gpu = (uint64_t) n_head_gpu * n_embd_head_v;
952
+
953
+ - return ggml_view_4d(ctx, v,
954
+ - n_embd_head_v, n_head_gpu, n_kv, ns,
955
+ - ggml_row_size(v->type, n_embd_head_v),
956
+ - ggml_row_size(v->type, n_embd_gpu),
957
+ - ggml_row_size(v->type, n_embd_gpu*kv_size),
958
+ - ggml_row_size(v->type, n_embd_gpu*kv_size)*sinfo.s0);
959
+ + auto build_stream_view = [&](uint32_t s_cache) -> ggml_tensor * {
960
+ + ggml_tensor * v_s = layer.v_per_stream[s_cache];
961
+ + return ggml_view_4d(ctx, v_s,
962
+ + n_embd_head_v, n_head_gpu, n_kv, 1,
963
+ + ggml_row_size(v_s->type, n_embd_head_v),
964
+ + ggml_row_size(v_s->type, n_embd_gpu),
965
+ + ggml_row_size(v_s->type, n_embd_gpu*kv_size),
966
+ + 0);
967
+ + };
968
+ +
969
+ + ggml_tensor * result = build_stream_view(sinfo.s0);
970
+ + for (uint32_t s = sinfo.s0 + 1; s <= sinfo.s1; ++s) {
971
+ + result = ggml_concat(ctx, result, build_stream_view(s), 3);
972
+ + }
973
+ + return result;
974
+ }
975
+
976
+ ggml_tensor * llama_kv_cache::get_v_cpu(ggml_context * ctx, int32_t il, uint32_t n_kv, const slot_info & sinfo) const {
977
+ const int32_t ikv = map_layer_ids.at(il);
978
+ const auto & layer = layers[ikv];
979
+ - GGML_ASSERT(layer.gpu_heads > 0 && layer.v_cpu && "get_v_cpu called without an active headinfer split");
980
+ + GGML_ASSERT(layer.gpu_heads > 0
981
+ + && !layer.v_cpu_per_stream.empty()
982
+ + && layer.v_cpu_per_stream[0]
983
+ + && "get_v_cpu called without an active headinfer split");
984
+ GGML_ASSERT(!v_trans && "headinfer split requires non-transposed V");
985
+
986
+ - auto * vc = layer.v_cpu;
987
+ - const uint64_t kv_size = get_size();
988
+ - const uint32_t ns = sinfo.s1 - sinfo.s0 + 1;
989
+ + // opencoti F4 M3 Phase 4-D: per-stream M2 CPU V subset.
990
+ + const uint64_t kv_size = get_size();
991
+ const uint32_t n_embd_head_v = hparams.n_embd_head_v(il);
992
+ const uint32_t n_head_cpu = hparams.n_head_kv(il) - layer.gpu_heads;
993
+ const uint64_t n_embd_cpu = (uint64_t) n_head_cpu * n_embd_head_v;
994
+
995
+ - return ggml_view_4d(ctx, vc,
996
+ - n_embd_head_v, n_head_cpu, n_kv, ns,
997
+ - ggml_row_size(vc->type, n_embd_head_v),
998
+ - ggml_row_size(vc->type, n_embd_cpu),
999
+ - ggml_row_size(vc->type, n_embd_cpu*kv_size),
1000
+ - ggml_row_size(vc->type, n_embd_cpu*kv_size)*sinfo.s0);
1001
+ + auto build_stream_view = [&](uint32_t s_cache) -> ggml_tensor * {
1002
+ + ggml_tensor * vc_s = layer.v_cpu_per_stream[s_cache];
1003
+ + return ggml_view_4d(ctx, vc_s,
1004
+ + n_embd_head_v, n_head_cpu, n_kv, 1,
1005
+ + ggml_row_size(vc_s->type, n_embd_head_v),
1006
+ + ggml_row_size(vc_s->type, n_embd_cpu),
1007
+ + ggml_row_size(vc_s->type, n_embd_cpu*kv_size),
1008
+ + 0);
1009
+ + };
1010
+ +
1011
+ + ggml_tensor * result = build_stream_view(sinfo.s0);
1012
+ + for (uint32_t s = sinfo.s0 + 1; s <= sinfo.s1; ++s) {
1013
+ + result = ggml_concat(ctx, result, build_stream_view(s), 3);
1014
+ + }
1015
+ + return result;
1016
+ }
1017
+
1018
+ ggml_tensor * llama_kv_cache::cpy_k(ggml_context * ctx, ggml_tensor * k_cur, ggml_tensor * k_idxs, int32_t il, const slot_info & sinfo) const {
1019
+ - GGML_UNUSED(sinfo);
1020
+ -
1021
+ const int32_t ikv = map_layer_ids.at(il);
1022
+
1023
+ const auto & layer = layers[ikv];
1024
+ - ggml_tensor * k = layer.k;
1025
+
1026
+ const int64_t n_embd_head = k_cur->ne[0];
1027
+ const int64_t n_head = k_cur->ne[1];
1028
+ @@ -1941,46 +2188,116 @@ ggml_tensor * llama_kv_cache::cpy_k(ggml_context * ctx, ggml_tensor * k_cur, ggm
1029
+ // opencoti F5 M2 headinfer β€” scatter the incoming heads into the GPU and
1030
+ // host subsets. The first gpu_heads heads (contiguous in k_cur's dim 0..1)
1031
+ // store into k; the rest store into k_cpu, both at the same cell indices
1032
+ - // (k_idxs). The single returned dep node ties both set_rows into the
1033
+ - // caller's one ggml_build_forward_expand (its value is never read). Split
1034
+ - // only happens for n_stream==1 (gated at construction) β€” no per-stream
1035
+ - // reshape needed.
1036
+ - if (layer.gpu_heads > 0 && layer.k_cpu) {
1037
+ + // (k_idxs). The single returned dep node ties all set_rows into the
1038
+ + // caller's one ggml_build_forward_expand (its value is never read).
1039
+ + //
1040
+ + // opencoti F4 M3 Phase 4-D: per-stream M2 scatter β€” for each batch
1041
+ + // stream s, slice k_cur and k_idxs by stream, split the head-axis
1042
+ + // slice into GPU + CPU halves, and emit 2 set_rows per stream against
1043
+ + // the per-stream tensors. In unified mode (sinfo.n_stream() == 1) this
1044
+ + // collapses to exactly 2 set_rows on stream 0's tensors β€” byte-identical
1045
+ + // to pre-Phase-4.
1046
+ + if (layer.gpu_heads > 0
1047
+ + && !layer.k_cpu_per_stream.empty()
1048
+ + && layer.k_cpu_per_stream[0]) {
1049
+ const int64_t g = layer.gpu_heads;
1050
+ const int64_t c = n_head - g;
1051
+ - ggml_tensor * kc = layer.k_cpu;
1052
+ - ggml_tensor * cur_g = ggml_view_2d(ctx, k_cur, n_embd_head*g, n_tokens, k_cur->nb[2], 0);
1053
+ - ggml_tensor * cur_c = ggml_view_2d(ctx, k_cur, n_embd_head*c, n_tokens, k_cur->nb[2], (size_t) (g*k_cur->nb[1]));
1054
+ - ggml_tensor * store_g = ggml_set_rows(ctx, k, cur_g, k_idxs);
1055
+ - ggml_tensor * store_c = ggml_set_rows(ctx, kc, cur_c, k_idxs);
1056
+ - return ggml_add(ctx, ggml_view_1d(ctx, store_g, 1, 0), ggml_view_1d(ctx, store_c, 1, 0));
1057
+ + const uint32_t batch_ns = sinfo.n_stream();
1058
+ + GGML_ASSERT(batch_ns >= 1);
1059
+ +
1060
+ + const int64_t tokens_per_stream = (batch_ns == 1) ? n_tokens : (n_tokens / (int64_t) batch_ns);
1061
+ + GGML_ASSERT((int64_t) batch_ns * tokens_per_stream == n_tokens);
1062
+ +
1063
+ + ggml_tensor * tie = nullptr;
1064
+ + for (uint32_t s = 0; s < batch_ns; ++s) {
1065
+ + ggml_tensor * k_s = layer.k_per_stream[sinfo.strm[s]];
1066
+ + ggml_tensor * kc_s = layer.k_cpu_per_stream[sinfo.strm[s]];
1067
+ +
1068
+ + // k_cur is [head_dim, n_head, n_tokens]; nb[2] is the stride
1069
+ + // between tokens. The per-stream slice is the contiguous block
1070
+ + // of tokens_per_stream tokens at offset s*tokens_per_stream.
1071
+ + const size_t per_stream_off = (size_t) s * tokens_per_stream * k_cur->nb[2];
1072
+ + ggml_tensor * cur_g = ggml_view_2d(ctx, k_cur,
1073
+ + n_embd_head*g, tokens_per_stream,
1074
+ + k_cur->nb[2], per_stream_off);
1075
+ + // CPU half: remaining c heads, offset by g heads (g rows in the head-merged dim 0)
1076
+ + ggml_tensor * cur_c = ggml_view_2d(ctx, k_cur,
1077
+ + n_embd_head*c, tokens_per_stream,
1078
+ + k_cur->nb[2], per_stream_off + (size_t)(g*k_cur->nb[1]));
1079
+ + ggml_tensor * idxs_s = (batch_ns == 1)
1080
+ + ? k_idxs
1081
+ + : ggml_view_1d(ctx, k_idxs, tokens_per_stream,
1082
+ + (size_t) s * tokens_per_stream * k_idxs->nb[0]);
1083
+ + ggml_tensor * store_g = ggml_set_rows(ctx, k_s, cur_g, idxs_s);
1084
+ + ggml_tensor * store_c = ggml_set_rows(ctx, kc_s, cur_c, idxs_s);
1085
+ + ggml_tensor * pair = ggml_add(ctx,
1086
+ + ggml_view_1d(ctx, store_g, 1, 0),
1087
+ + ggml_view_1d(ctx, store_c, 1, 0));
1088
+ + if (!tie) {
1089
+ + tie = pair;
1090
+ + } else {
1091
+ + tie = ggml_add(ctx,
1092
+ + ggml_view_1d(ctx, tie, 1, 0),
1093
+ + ggml_view_1d(ctx, pair, 1, 0));
1094
+ + }
1095
+ + }
1096
+ + return tie;
1097
+ }
1098
+
1099
+ k_cur = ggml_view_2d(ctx, k_cur, n_embd_gqa, n_tokens, k_cur->nb[2], 0);
1100
+
1101
+ - const int64_t n_stream = k->ne[2];
1102
+ -
1103
+ - if (n_stream > 1) {
1104
+ - const int64_t kv_size = get_size();
1105
+ -
1106
+ - assert(n_embd_gqa == k->ne[0]);
1107
+ - assert(kv_size == k->ne[1]);
1108
+ -
1109
+ - // merge the buffer across all streams because the idxs are global
1110
+ - k = ggml_reshape_2d(ctx, k, n_embd_gqa, kv_size*n_stream);
1111
+ + // opencoti F4 M3 Phase 4-C: per-stream scatter via N separate
1112
+ + // ggml_set_rows ops (option C-2). In unified mode (sinfo.n_stream()
1113
+ + // == 1) the fast path collapses to one ggml_set_rows against
1114
+ + // layer.k_per_stream[sinfo.strm[0]] β€” byte-identical to pre-Phase-4.
1115
+ + // In non-unified mode (sinfo.n_stream() > 1), set_input_k_idxs has
1116
+ + // emitted LOCAL indices (no global offset), so each per-stream slice
1117
+ + // of k_idxs is valid against its own k_per_stream[cache_strm] tensor.
1118
+ + // The N stores are tied into a single dep node via ggml_add so the
1119
+ + // caller's one ggml_build_forward_expand picks them all up.
1120
+ + //
1121
+ + // C-1 (concat-then-scatter) is not expressible with per-stream owning
1122
+ + // tensors in ggml: ggml_concat materializes a new buffer rather than
1123
+ + // creating a view into its inputs, so writes routed through the concat
1124
+ + // result would not land in the per-stream tensors. C-2 is the natural
1125
+ + // shape that preserves per-stream buffer ownership.
1126
+ + const uint32_t batch_ns = sinfo.n_stream();
1127
+ + GGML_ASSERT(batch_ns >= 1);
1128
+ +
1129
+ + if (batch_ns == 1) {
1130
+ + ggml_tensor * k_s = layer.k_per_stream[sinfo.strm[0]];
1131
+ + return ggml_set_rows(ctx, k_s, k_cur, k_idxs);
1132
+ + }
1133
+ +
1134
+ + const int64_t tokens_per_stream = n_tokens / (int64_t) batch_ns;
1135
+ + GGML_ASSERT((int64_t) batch_ns * tokens_per_stream == n_tokens);
1136
+ +
1137
+ + ggml_tensor * tie = nullptr;
1138
+ + for (uint32_t s = 0; s < batch_ns; ++s) {
1139
+ + ggml_tensor * k_s = layer.k_per_stream[sinfo.strm[s]];
1140
+ + ggml_tensor * cur_s = ggml_view_2d(ctx, k_cur,
1141
+ + n_embd_gqa, tokens_per_stream,
1142
+ + k_cur->nb[1],
1143
+ + (size_t) s * tokens_per_stream * k_cur->nb[1]);
1144
+ + ggml_tensor * idxs_s = ggml_view_1d(ctx, k_idxs,
1145
+ + tokens_per_stream,
1146
+ + (size_t) s * tokens_per_stream * k_idxs->nb[0]);
1147
+ + ggml_tensor * store_s = ggml_set_rows(ctx, k_s, cur_s, idxs_s);
1148
+ + if (!tie) {
1149
+ + tie = store_s;
1150
+ + } else {
1151
+ + tie = ggml_add(ctx,
1152
+ + ggml_view_1d(ctx, tie, 1, 0),
1153
+ + ggml_view_1d(ctx, store_s, 1, 0));
1154
+ + }
1155
+ }
1156
+ -
1157
+ - // store the current K values into the cache
1158
+ - return ggml_set_rows(ctx, k, k_cur, k_idxs);
1159
+ + return tie;
1160
+ }
1161
+
1162
+ ggml_tensor * llama_kv_cache::cpy_v(ggml_context * ctx, ggml_tensor * v_cur, ggml_tensor * v_idxs, int32_t il, const slot_info & sinfo) const {
1163
+ - GGML_UNUSED(sinfo);
1164
+ -
1165
+ const int32_t ikv = map_layer_ids.at(il);
1166
+
1167
+ const auto & layer = layers[ikv];
1168
+ - auto * v = layer.v;
1169
+
1170
+ const int64_t n_embd_head = v_cur->ne[0];
1171
+ const int64_t n_head = v_cur->ne[1];
1172
+ @@ -1992,38 +2309,109 @@ ggml_tensor * llama_kv_cache::cpy_v(ggml_context * ctx, ggml_tensor * v_cur, ggm
1173
+ GGML_ASSERT(ggml_row_size(v_cur->type, n_embd_head) == v_cur->nb[1]);
1174
+
1175
+ // opencoti F5 M2 headinfer β€” head-scatter into the GPU + host subsets.
1176
+ - // Reached only when !v_trans and n_stream==1 (the split is gated off
1177
+ - // otherwise at construction), so this mirrors the cpy_k store exactly.
1178
+ - if (layer.gpu_heads > 0 && layer.v_cpu) {
1179
+ + // Reached only when !v_trans (the split is gated off at construction
1180
+ + // otherwise).
1181
+ + //
1182
+ + // opencoti F4 M3 Phase 4-D: per-stream M2 V scatter β€” same shape as
1183
+ + // cpy_k's M2 branch. Unified mode collapses to 2 set_rows on stream 0,
1184
+ + // byte-identical to pre-Phase-4.
1185
+ + if (layer.gpu_heads > 0
1186
+ + && !layer.v_cpu_per_stream.empty()
1187
+ + && layer.v_cpu_per_stream[0]) {
1188
+ const int64_t g = layer.gpu_heads;
1189
+ const int64_t c = n_head - g;
1190
+ - ggml_tensor * vc = layer.v_cpu;
1191
+ - ggml_tensor * cur_g = ggml_view_2d(ctx, v_cur, n_embd_head*g, n_tokens, v_cur->nb[2], 0);
1192
+ - ggml_tensor * cur_c = ggml_view_2d(ctx, v_cur, n_embd_head*c, n_tokens, v_cur->nb[2], (size_t) (g*v_cur->nb[1]));
1193
+ - ggml_tensor * store_g = ggml_set_rows(ctx, v, cur_g, v_idxs);
1194
+ - ggml_tensor * store_c = ggml_set_rows(ctx, vc, cur_c, v_idxs);
1195
+ - return ggml_add(ctx, ggml_view_1d(ctx, store_g, 1, 0), ggml_view_1d(ctx, store_c, 1, 0));
1196
+ + const uint32_t batch_ns = sinfo.n_stream();
1197
+ + GGML_ASSERT(batch_ns >= 1);
1198
+ +
1199
+ + const int64_t tokens_per_stream = (batch_ns == 1) ? n_tokens : (n_tokens / (int64_t) batch_ns);
1200
+ + GGML_ASSERT((int64_t) batch_ns * tokens_per_stream == n_tokens);
1201
+ +
1202
+ + ggml_tensor * tie = nullptr;
1203
+ + for (uint32_t s = 0; s < batch_ns; ++s) {
1204
+ + ggml_tensor * v_s = layer.v_per_stream[sinfo.strm[s]];
1205
+ + ggml_tensor * vc_s = layer.v_cpu_per_stream[sinfo.strm[s]];
1206
+ +
1207
+ + const size_t per_stream_off = (size_t) s * tokens_per_stream * v_cur->nb[2];
1208
+ + ggml_tensor * cur_g = ggml_view_2d(ctx, v_cur,
1209
+ + n_embd_head*g, tokens_per_stream,
1210
+ + v_cur->nb[2], per_stream_off);
1211
+ + ggml_tensor * cur_c = ggml_view_2d(ctx, v_cur,
1212
+ + n_embd_head*c, tokens_per_stream,
1213
+ + v_cur->nb[2], per_stream_off + (size_t)(g*v_cur->nb[1]));
1214
+ + ggml_tensor * idxs_s = (batch_ns == 1)
1215
+ + ? v_idxs
1216
+ + : ggml_view_1d(ctx, v_idxs, tokens_per_stream,
1217
+ + (size_t) s * tokens_per_stream * v_idxs->nb[0]);
1218
+ + ggml_tensor * store_g = ggml_set_rows(ctx, v_s, cur_g, idxs_s);
1219
+ + ggml_tensor * store_c = ggml_set_rows(ctx, vc_s, cur_c, idxs_s);
1220
+ + ggml_tensor * pair = ggml_add(ctx,
1221
+ + ggml_view_1d(ctx, store_g, 1, 0),
1222
+ + ggml_view_1d(ctx, store_c, 1, 0));
1223
+ + if (!tie) {
1224
+ + tie = pair;
1225
+ + } else {
1226
+ + tie = ggml_add(ctx,
1227
+ + ggml_view_1d(ctx, tie, 1, 0),
1228
+ + ggml_view_1d(ctx, pair, 1, 0));
1229
+ + }
1230
+ + }
1231
+ + return tie;
1232
+ }
1233
+
1234
+ - const int64_t n_stream = v->ne[2];
1235
+ + // opencoti F4 M3 Phase 4-C: per-stream V scatter. Two sub-branches
1236
+ + // depending on v_trans (FA vs non-FA layout).
1237
+ + //
1238
+ + // !v_trans (FA on): same shape as cpy_k β€” N separate ggml_set_rows on
1239
+ + // the 2-D-merged v_cur, sliced per stream. set_input_v_idxs emits
1240
+ + // LOCAL per-stream indices in non-unified mode. Unified mode (batch_ns
1241
+ + // == 1) keeps the simple one-set_rows fast path, byte-identical.
1242
+ + //
1243
+ + // v_trans (FA off, V transposed): each row is a single head element.
1244
+ + // The unified-mode path is preserved; multi-stream + v_trans is not a
1245
+ + // current workload (the v_trans layout requires no FA, while parallel
1246
+ + // multi-stream batches are always paired with FA). Asserted out as a
1247
+ + // safety net; future Phase 4-D/E may lift if a workload appears.
1248
+ + const uint32_t batch_ns = sinfo.n_stream();
1249
+ + GGML_ASSERT(batch_ns >= 1);
1250
+
1251
+ - // take this branch when FA is enabled (the V cache is not transposed)
1252
+ if (!v_trans) {
1253
+ v_cur = ggml_view_2d(ctx, v_cur, n_embd_gqa, n_tokens, v_cur->nb[2], 0);
1254
+
1255
+ - if (n_stream > 1) {
1256
+ - const int64_t kv_size = get_size();
1257
+ -
1258
+ - assert(n_embd_gqa == v->ne[0]);
1259
+ - assert(kv_size == v->ne[1]);
1260
+ -
1261
+ - // merge the buffer across all streams because the idxs are global
1262
+ - v = ggml_reshape_2d(ctx, v, n_embd_gqa, kv_size*n_stream);
1263
+ + if (batch_ns == 1) {
1264
+ + ggml_tensor * v_s = layer.v_per_stream[sinfo.strm[0]];
1265
+ + return ggml_set_rows(ctx, v_s, v_cur, v_idxs);
1266
+ + }
1267
+ +
1268
+ + const int64_t tokens_per_stream = n_tokens / (int64_t) batch_ns;
1269
+ + GGML_ASSERT((int64_t) batch_ns * tokens_per_stream == n_tokens);
1270
+ +
1271
+ + ggml_tensor * tie = nullptr;
1272
+ + for (uint32_t s = 0; s < batch_ns; ++s) {
1273
+ + ggml_tensor * v_s = layer.v_per_stream[sinfo.strm[s]];
1274
+ + ggml_tensor * cur_s = ggml_view_2d(ctx, v_cur,
1275
+ + n_embd_gqa, tokens_per_stream,
1276
+ + v_cur->nb[1],
1277
+ + (size_t) s * tokens_per_stream * v_cur->nb[1]);
1278
+ + ggml_tensor * idxs_s = ggml_view_1d(ctx, v_idxs,
1279
+ + tokens_per_stream,
1280
+ + (size_t) s * tokens_per_stream * v_idxs->nb[0]);
1281
+ + ggml_tensor * store_s = ggml_set_rows(ctx, v_s, cur_s, idxs_s);
1282
+ + if (!tie) {
1283
+ + tie = store_s;
1284
+ + } else {
1285
+ + tie = ggml_add(ctx,
1286
+ + ggml_view_1d(ctx, tie, 1, 0),
1287
+ + ggml_view_1d(ctx, store_s, 1, 0));
1288
+ + }
1289
+ }
1290
+ -
1291
+ - return ggml_set_rows(ctx, v, v_cur, v_idxs);
1292
+ + return tie;
1293
+ }
1294
+
1295
+ + // v_trans path: not a multi-stream workload; preserve unified-mode
1296
+ + // behavior exactly and assert multi-stream out.
1297
+ + GGML_ASSERT(batch_ns == 1 && "v_trans + multi-stream is unsupported; v_trans implies !FA which is mutually exclusive with parallel batches");
1298
+ + ggml_tensor * v = layer.v_per_stream[sinfo.strm[0]];
1299
+ +
1300
+ if (ggml_row_size(v_cur->type, n_embd_gqa) == v_cur->nb[2]) {
1301
+ // we can merge dims 0, 1 and 2
1302
+ v_cur = ggml_reshape_2d(ctx, v_cur, n_embd_gqa, n_tokens);
1303
+ @@ -2119,11 +2507,23 @@ void llama_kv_cache::set_input_k_idxs(ggml_tensor * dst, const llama_ubatch * ub
1304
+ GGML_ASSERT(ggml_backend_buffer_is_host(dst->buffer));
1305
+ int64_t * data = (int64_t *) dst->data;
1306
+
1307
+ - for (uint32_t s = 0; s < sinfo.n_stream(); ++s) {
1308
+ - const int64_t offs = sinfo.strm[s]*get_size();
1309
+ + // opencoti F4 M3 Phase 4-C/F: emit LOCAL per-stream indices unconditionally.
1310
+ + // Phase 4 ALWAYS scatters to per-stream tensors (cpy_k routes to
1311
+ + // k_per_stream[sinfo.strm[s]] which is sized kv_size, not kv_size*n_stream),
1312
+ + // so the index must be local to that stream's tensor. In unified mode
1313
+ + // (n_stream == 1, sinfo.strm[0] == 0) pre-Phase-4 offs was 0 already,
1314
+ + // so this is byte-identical.
1315
+ + //
1316
+ + // Phase 4-F bug fix: the previous version conditioned the offset
1317
+ + // on sinfo.n_stream() (per-batch streams), which left it global
1318
+ + // when a --parallel N server served a single-session batch
1319
+ + // (sinfo.n_stream() == 1 but sinfo.strm[0] != 0). The resulting
1320
+ + // global indices wrote out-of-bounds against the per-stream
1321
+ + // tensor and crashed CUDA decode.
1322
+
1323
+ + for (uint32_t s = 0; s < sinfo.n_stream(); ++s) {
1324
+ for (uint32_t i = 0; i < sinfo.size(); ++i) {
1325
+ - data[s*sinfo.size() + i] = offs + sinfo.idxs[s][i];
1326
+ + data[s*sinfo.size() + i] = sinfo.idxs[s][i];
1327
+ }
1328
+ }
1329
+ }
1330
+ @@ -2135,12 +2535,15 @@ void llama_kv_cache::set_input_v_idxs(ggml_tensor * dst, const llama_ubatch * ub
1331
+ GGML_ASSERT(ggml_backend_buffer_is_host(dst->buffer));
1332
+ int64_t * data = (int64_t *) dst->data;
1333
+
1334
+ + // opencoti F4 M3 Phase 4-C/F: same LOCAL-only fix as set_input_k_idxs.
1335
+ + // The v_trans branch's offset is also dropped β€” v_trans + multi-stream
1336
+ + // is asserted out in cpy_v, but the unified-mode v_trans path here is
1337
+ + // byte-identical regardless (sinfo.strm[0] == 0 makes offs == 0).
1338
+ +
1339
+ if (!v_trans) {
1340
+ for (uint32_t s = 0; s < sinfo.n_stream(); ++s) {
1341
+ - const int64_t offs = sinfo.strm[s]*get_size();
1342
+ -
1343
+ for (uint32_t i = 0; i < sinfo.size(); ++i) {
1344
+ - data[s*sinfo.size() + i] = offs + sinfo.idxs[s][i];
1345
+ + data[s*sinfo.size() + i] = sinfo.idxs[s][i];
1346
+ }
1347
+ }
1348
+ } else {
1349
+ @@ -2150,11 +2553,9 @@ void llama_kv_cache::set_input_v_idxs(ggml_tensor * dst, const llama_ubatch * ub
1350
+ const int64_t n_embd_v_gqa = hparams.n_embd_v_gqa_max();
1351
+
1352
+ for (uint32_t s = 0; s < sinfo.n_stream(); ++s) {
1353
+ - const int64_t offs = sinfo.strm[s]*kv_size*n_embd_v_gqa;
1354
+ -
1355
+ for (uint32_t i = 0; i < sinfo.size(); ++i) {
1356
+ for (uint32_t j = 0; j < n_embd_v_gqa; ++j) {
1357
+ - data[s*sinfo.size()*n_embd_v_gqa + i*n_embd_v_gqa + j] = offs + j*kv_size + sinfo.idxs[s][i];
1358
+ + data[s*sinfo.size()*n_embd_v_gqa + i*n_embd_v_gqa + j] = j*kv_size + sinfo.idxs[s][i];
1359
+ }
1360
+ }
1361
+ }
1362
+ @@ -2459,10 +2860,16 @@ size_t llama_kv_cache::total_size() const {
1363
+ }
1364
+
1365
+ size_t llama_kv_cache::size_k_bytes() const {
1366
+ + // opencoti F4 M3 Phase 4: sum bytes across all per-stream tensors of
1367
+ + // each layer. In unified mode (n_stream == 1) this is byte-identical
1368
+ + // to ggml_nbytes(layer.k) of pre-Phase-4. M2's host subset is
1369
+ + // accounted separately (it lives in a different buffer-type).
1370
+ size_t size_k_bytes = 0;
1371
+
1372
+ for (const auto & layer : layers) {
1373
+ - size_k_bytes += ggml_nbytes(layer.k);
1374
+ + for (auto * k : layer.k_per_stream) {
1375
+ + if (k) size_k_bytes += ggml_nbytes(k);
1376
+ + }
1377
+ }
1378
+
1379
+ return size_k_bytes;
1380
+ @@ -2472,7 +2879,9 @@ size_t llama_kv_cache::size_v_bytes() const {
1381
+ size_t size_v_bytes = 0;
1382
+
1383
+ for (const auto & layer : layers) {
1384
+ - size_v_bytes += layer.v ? ggml_nbytes(layer.v) : 0;
1385
+ + for (auto * v : layer.v_per_stream) {
1386
+ + if (v) size_v_bytes += ggml_nbytes(v);
1387
+ + }
1388
+ }
1389
+
1390
+ return size_v_bytes;
1391
+ @@ -2570,6 +2979,19 @@ ggml_cgraph * llama_kv_cache::build_graph_shift(llm_graph_result * res, llama_co
1392
+
1393
+ const auto & cparams = lctx->get_cparams();
1394
+
1395
+ + // opencoti F4 M3 Phase 4-C: k-shift loops over per-stream tensors.
1396
+ + // With per-stream owning tensors, each stream's K is a separate buffer,
1397
+ + // so we build N rope-shift ops per layer (one per stream). The k_shift
1398
+ + // input tensor has length get_size()*n_stream, with stream s occupying
1399
+ + // indices [s*get_size(), (s+1)*get_size()) β€” set_input_k_shift writes
1400
+ + // them in that order. Each per-stream rope-shift consumes its
1401
+ + // contiguous slice via ggml_view_1d on k_shift.
1402
+ + //
1403
+ + // In unified mode (n_stream == 1), the loop runs once with stream 0,
1404
+ + // and the k_shift view degenerates to the full tensor (offset 0,
1405
+ + // length get_size()). Byte-identical to pre-Phase-4 β€” same rope-shift
1406
+ + // op shape and inputs.
1407
+ +
1408
+ for (const auto & layer : layers) {
1409
+ const uint32_t il = layer.il;
1410
+
1411
+ @@ -2585,16 +3007,24 @@ ggml_cgraph * llama_kv_cache::build_graph_shift(llm_graph_result * res, llama_co
1412
+
1413
+ ggml_tensor * rope_factors = model.get_rope_factors(cparams, il);
1414
+
1415
+ - ggml_tensor * k =
1416
+ - ggml_view_3d(ctx, layer.k,
1417
+ - n_rot, n_head_kv, get_size()*n_stream,
1418
+ - ggml_row_size(layer.k->type, n_embd_head_k),
1419
+ - ggml_row_size(layer.k->type, n_embd_k_gqa),
1420
+ - ggml_row_size(layer.k->type, n_embd_nope));
1421
+ + for (uint32_t s = 0; s < n_stream; ++s) {
1422
+ + ggml_tensor * layer_k = layer.k_per_stream[s];
1423
+ + ggml_tensor * k =
1424
+ + ggml_view_3d(ctx, layer_k,
1425
+ + n_rot, n_head_kv, get_size(),
1426
+ + ggml_row_size(layer_k->type, n_embd_head_k),
1427
+ + ggml_row_size(layer_k->type, n_embd_k_gqa),
1428
+ + ggml_row_size(layer_k->type, n_embd_nope));
1429
+ +
1430
+ + ggml_tensor * k_shift_s = (n_stream == 1)
1431
+ + ? inp->k_shift
1432
+ + : ggml_view_1d(ctx, inp->k_shift, get_size(),
1433
+ + (size_t) s * get_size() * inp->k_shift->nb[0]);
1434
+
1435
+ - ggml_tensor * cur = build_rope_shift(cparams, ctx, k, inp->k_shift, inp->k_rot, rope_factors, freq_base_l, freq_scale_l, il);
1436
+ + ggml_tensor * cur = build_rope_shift(cparams, ctx, k, k_shift_s, inp->k_rot, rope_factors, freq_base_l, freq_scale_l, il);
1437
+
1438
+ - ggml_build_forward_expand(gf, cur);
1439
+ + ggml_build_forward_expand(gf, cur);
1440
+ + }
1441
+ }
1442
+
1443
+ res->add_input(std::move(inp));
1444
+ diff --git a/llama.cpp/src/llama-kv-cache.h b/llama.cpp/src/llama-kv-cache.h
1445
+ --- a/llama.cpp/src/llama-kv-cache.h
1446
+ +++ b/llama.cpp/src/llama-kv-cache.h
1447
+ @@ -286,19 +286,57 @@ private:
1448
+ // note: can be different from the layer index in the KV cache
1449
+ uint32_t il;
1450
+
1451
+ - ggml_tensor * k;
1452
+ - ggml_tensor * v;
1453
+ -
1454
+ + // opencoti F4 M3 Phase 4 β€” per-stream tensor split.
1455
+ + // Each element is an OWNING 3-D tensor of shape
1456
+ + // [n_embd_k_gqa, kv_size, 1] (dim 3 == 1 keeps stride math
1457
+ + // identical to the pre-Phase-4 single-tensor layout). Vector
1458
+ + // size always equals n_stream.
1459
+ + //
1460
+ + // Unified mode (n_stream == 1): k_per_stream[0] is byte-
1461
+ + // identical to the pre-Phase-4 `k` field (same buft, same
1462
+ + // ctx, same offsets). Non-unified mode (n_stream > 1): each
1463
+ + // stream's tensor lives in its own (possibly per-stream)
1464
+ + // backend buffer (Phase 4-B); cross-stream operations are
1465
+ + // buffer-to-buffer copies.
1466
+ + std::vector<ggml_tensor *> k_per_stream;
1467
+ + std::vector<ggml_tensor *> v_per_stream;
1468
+ +
1469
+ + // k_stream[s] / v_stream[s] are 2-D views into
1470
+ + // k_per_stream[s] / v_per_stream[s]. Kept for backward compat
1471
+ + // with consumers that index by stream; same shape and offset
1472
+ + // semantics as before. With per-stream tensors the view's
1473
+ + // offset within its owning tensor is always 0 (each stream
1474
+ + // owns the whole 3-D extent of its tensor with dim 3 == 1).
1475
+ std::vector<ggml_tensor *> k_stream;
1476
+ std::vector<ggml_tensor *> v_stream;
1477
+
1478
+ - // opencoti F5 M2 headinfer β€” head-residency split. When gpu_heads > 0,
1479
+ - // `k`/`v` hold the first gpu_heads KV heads on the layer's (GPU) buffer
1480
+ - // and `k_cpu`/`v_cpu` hold the remaining (n_head_kv - gpu_heads) heads
1481
+ - // on a host buffer. gpu_heads == 0 means no split (k/v hold all heads).
1482
+ - ggml_tensor * k_cpu = nullptr;
1483
+ - ggml_tensor * v_cpu = nullptr;
1484
+ + // opencoti F5 M2 headinfer β€” per-stream now. When gpu_heads > 0,
1485
+ + // k_per_stream[s] / v_per_stream[s] hold the first gpu_heads
1486
+ + // KV heads on the layer's device buffer, and
1487
+ + // k_cpu_per_stream[s] / v_cpu_per_stream[s] hold the remaining
1488
+ + // (n_head_kv - gpu_heads) heads on the layer's host buffer.
1489
+ + // Vectors are empty when no split (gpu_heads == 0).
1490
+ + std::vector<ggml_tensor *> k_cpu_per_stream;
1491
+ + std::vector<ggml_tensor *> v_cpu_per_stream;
1492
+ uint32_t gpu_heads = 0;
1493
+ +
1494
+ + // Convenience accessors for unified-mode (n_stream == 1) call
1495
+ + // sites. Returns nullptr in non-unified mode β€” that's the
1496
+ + // signal that the caller must use per-stream access instead.
1497
+ + // Phase 4-A uses these at sites that haven't yet been migrated
1498
+ + // to per-stream awareness; Phase 4-C/D lift them.
1499
+ + inline ggml_tensor * k_unified() const {
1500
+ + return k_per_stream.size() == 1 ? k_per_stream[0] : nullptr;
1501
+ + }
1502
+ + inline ggml_tensor * v_unified() const {
1503
+ + return v_per_stream.size() == 1 ? v_per_stream[0] : nullptr;
1504
+ + }
1505
+ + inline ggml_tensor * k_cpu_unified() const {
1506
+ + return k_cpu_per_stream.size() == 1 ? k_cpu_per_stream[0] : nullptr;
1507
+ + }
1508
+ + inline ggml_tensor * v_cpu_unified() const {
1509
+ + return v_cpu_per_stream.size() == 1 ? v_cpu_per_stream[0] : nullptr;
1510
+ + }
1511
+ };
1512
+
1513
+ bool v_trans = true; // the value tensor is transposed
patches/0032-m2-state-io.patch ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ From: opencoti
2
+ Subject: F5 M2 β€” M2-aware state I/O (bug-222 fix)
3
+
4
+ Bench: perf/llamafile/advanced-kv-stack.bench.ts (Gemma 4 A4B + 31B), bug-222 fix
5
+ Milestone: F5 M2 hardening β€” see docs/features/advanced_kv.md, .wolf/buglog.json bug-222
6
+
7
+ Pre-existing M2 head split bug surfaced on Gemma 4 iswa: state_write_data /
8
+ state_read_data read/write the K and V cache assuming a single tensor per
9
+ layer with full n_embd_k_gqa / n_embd_v_gqa row size. With M2 head split
10
+ active the layer's tensor (k_stream[s] / v_stream[s]) holds ONLY the GPU
11
+ half (n_embd_k_gpu width), and the CPU half lives in a separate
12
+ k_cpu_per_stream[s] / v_cpu_per_stream[s] tensor. Reading buf_size =
13
+ range_size * row_full from a tensor that has range_size * row_gpu bytes
14
+ crashes with "tensor read out of bounds" in ggml_backend_tensor_get
15
+ (ggml-backend.cpp:314) as soon as the per-range write exceeds the GPU
16
+ half's nbytes β€” reproducible on Gemma 4 A4B & 31B at frac=0.5 with any
17
+ prompt that crosses a 512-token microbatch boundary and triggers a
18
+ context checkpoint (the cache_prompt server feature snapshots β‰₯ 634
19
+ cells after the second prompt batch).
20
+
21
+ Fix: detect M2 split per layer (layer.gpu_heads > 0 &&
22
+ layer.k_cpu_per_stream[s]), emit GPU half (row_gpu) then CPU half
23
+ (row_cpu) per range. Total bytes per range stay at range_size * row_full
24
+ (header advertises the full row size unchanged) so io stream accounting
25
+ is preserved; on-disk byte order within a range differs from upstream
26
+ (GPU-first|CPU-first vs interleaved) but writer/reader are symmetric so
27
+ round-trips are consistent. M2 inactive (no head_cpu) path is taken
28
+ byte-identical to upstream β€” preserves Qwen / unified-mode behavior.
29
+ V branch only fires in !v_trans (M2 is gated off when v_trans).
30
+
31
+ Validated:
32
+ - Gemma 4 A4B-98e-v6-coder-it-Q4_K_M: M5 stack bench 8/9 PASS
33
+ (C5 byte-equality A2/A3 PASS, C9 perf threshold known cost).
34
+ - Gemma 4 31B-it-Q4_K_M: M5 stack bench 8/9 PASS, same shape.
35
+ - Qwen 2.5 1.5B Instruct: regression shield β€” M5 8/9, M2 6/6, M3 4/4,
36
+ M0 5/5 all PASS unchanged.
37
+
38
+ Bug-222 reproducer: /tmp/gemma-crash-probe.py (cycles reps 10..50 of the
39
+ bench prompt against an A2 server; pre-fix crashes at reps=30, post-fix
40
+ clean through reps=35).
41
+
42
+ diff --git a/llama.cpp/src/llama-kv-cache.cpp b/llama.cpp/src/llama-kv-cache.cpp
43
+ --- a/llama.cpp/src/llama-kv-cache.cpp
44
+ +++ b/llama.cpp/src/llama-kv-cache.cpp
45
+ @@ -3190,11 +3190,43 @@ void llama_kv_cache::state_write_data(llama_io_write_i & io, const cell_ranges_t
46
+ const uint64_t k_size_row = ggml_row_size(k->type, n_embd_k_gqa);
47
+ io.write(&k_size_row, sizeof(k_size_row));
48
+
49
+ - // Read each range of cells of k_size length and write out
50
+ - for (const auto & range : cr.data) {
51
+ - const size_t range_size = range.second - range.first;
52
+ - const size_t buf_size = range_size * k_size_row;
53
+ - io.write_tensor(k, range.first * k_size_row, buf_size);
54
+ + // opencoti bug-222 fix β€” F5 M2 split-aware state write.
55
+ + // When M2 head split is active, `k` (k_stream[s]) holds only the
56
+ + // GPU half of the layer's heads; the CPU half lives in
57
+ + // k_cpu_per_stream[s]. The pre-fix path read `range_size * k_size_row`
58
+ + // bytes from a tensor that only has `range_size * row_gpu` bytes β†’
59
+ + // out-of-bounds crash on any range_size that pushes buf_size past
60
+ + // the per-stream GPU tensor's nbytes. Fix: detect M2 split, emit
61
+ + // GPU half then CPU half per range. Total bytes per range equals
62
+ + // `range_size * k_size_row` (the header advertises this), so on-disk
63
+ + // accounting matches; the layout differs only in within-range order
64
+ + // (GPU-first|CPU-first vs interleaved). Reader (state_read_data) is
65
+ + // symmetric so a save/load round-trip is consistent. When M2 is OFF
66
+ + // (no head_cpu) the original path is taken byte-identical.
67
+ + const bool m2_split_active = layer.gpu_heads > 0
68
+ + && !layer.k_cpu_per_stream.empty()
69
+ + && layer.k_cpu_per_stream[cr.strm];
70
+ + if (m2_split_active) {
71
+ + auto * k_cpu = layer.k_cpu_per_stream[cr.strm];
72
+ + const uint32_t n_embd_head_k = hparams.n_embd_head_k(il);
73
+ + const uint32_t n_head_kv = hparams.n_head_kv(il);
74
+ + const uint32_t n_embd_k_gpu = layer.gpu_heads * n_embd_head_k;
75
+ + const uint32_t n_embd_k_cpu = (n_head_kv - layer.gpu_heads) * n_embd_head_k;
76
+ + const uint64_t k_row_gpu = ggml_row_size(k->type, n_embd_k_gpu);
77
+ + const uint64_t k_row_cpu = ggml_row_size(k_cpu->type, n_embd_k_cpu);
78
+ + GGML_ASSERT(k_row_gpu + k_row_cpu == k_size_row);
79
+ + for (const auto & range : cr.data) {
80
+ + const size_t range_size = range.second - range.first;
81
+ + io.write_tensor(k, range.first * k_row_gpu, range_size * k_row_gpu);
82
+ + io.write_tensor(k_cpu, range.first * k_row_cpu, range_size * k_row_cpu);
83
+ + }
84
+ + } else {
85
+ + // Read each range of cells of k_size length and write out
86
+ + for (const auto & range : cr.data) {
87
+ + const size_t range_size = range.second - range.first;
88
+ + const size_t buf_size = range_size * k_size_row;
89
+ + io.write_tensor(k, range.first * k_size_row, buf_size);
90
+ + }
91
+ }
92
+ }
93
+
94
+ @@ -3217,11 +3249,33 @@ void llama_kv_cache::state_write_data(llama_io_write_i & io, const cell_ranges_t
95
+ const uint64_t v_size_row = ggml_row_size(v->type, n_embd_v_gqa);
96
+ io.write(&v_size_row, sizeof(v_size_row));
97
+
98
+ - // Read each range of cells of v_size length and write out
99
+ - for (const auto & range : cr.data) {
100
+ - const size_t range_size = range.second - range.first;
101
+ - const size_t buf_size = range_size * v_size_row;
102
+ - io.write_tensor(v, range.first * v_size_row, buf_size);
103
+ + // opencoti bug-222 fix β€” F5 M2 split-aware V state write
104
+ + // (same pattern as K above). M2 is gated off when v_trans, so
105
+ + // this branch is reached only with the non-transposed V layout.
106
+ + const bool m2_split_active = layer.gpu_heads > 0
107
+ + && !layer.v_cpu_per_stream.empty()
108
+ + && layer.v_cpu_per_stream[cr.strm];
109
+ + if (m2_split_active) {
110
+ + auto * v_cpu = layer.v_cpu_per_stream[cr.strm];
111
+ + const uint32_t n_embd_head_v = hparams.n_embd_head_v(il);
112
+ + const uint32_t n_head_kv = hparams.n_head_kv(il);
113
+ + const uint32_t n_embd_v_gpu = layer.gpu_heads * n_embd_head_v;
114
+ + const uint32_t n_embd_v_cpu = (n_head_kv - layer.gpu_heads) * n_embd_head_v;
115
+ + const uint64_t v_row_gpu = ggml_row_size(v->type, n_embd_v_gpu);
116
+ + const uint64_t v_row_cpu = ggml_row_size(v_cpu->type, n_embd_v_cpu);
117
+ + GGML_ASSERT(v_row_gpu + v_row_cpu == v_size_row);
118
+ + for (const auto & range : cr.data) {
119
+ + const size_t range_size = range.second - range.first;
120
+ + io.write_tensor(v, range.first * v_row_gpu, range_size * v_row_gpu);
121
+ + io.write_tensor(v_cpu, range.first * v_row_cpu, range_size * v_row_cpu);
122
+ + }
123
+ + } else {
124
+ + // Read each range of cells of v_size length and write out
125
+ + for (const auto & range : cr.data) {
126
+ + const size_t range_size = range.second - range.first;
127
+ + const size_t buf_size = range_size * v_size_row;
128
+ + io.write_tensor(v, range.first * v_size_row, buf_size);
129
+ + }
130
+ }
131
+ }
132
+ } else {
133
+ @@ -3433,7 +3487,36 @@ bool llama_kv_cache::state_read_data(llama_io_read_i & io, uint32_t strm, uint32
134
+ }
135
+
136
+ if (cell_count) {
137
+ - if (sinfo.is_contiguous()) {
138
+ + // opencoti bug-222 fix β€” F5 M2 split-aware state read (K).
139
+ + // Symmetric to state_write_data: when M2 head split is active,
140
+ + // read the GPU half then the CPU half and dispatch to the
141
+ + // respective per-stream tensors. Total bytes equal
142
+ + // `cell_count * k_size_row` (the header advertises this) so the
143
+ + // sequential io stream matches what state_write_data wrote.
144
+ + const bool m2_split_active = layer.gpu_heads > 0
145
+ + && !layer.k_cpu_per_stream.empty()
146
+ + && layer.k_cpu_per_stream[strm];
147
+ + if (m2_split_active) {
148
+ + auto * k_cpu = layer.k_cpu_per_stream[strm];
149
+ + const uint32_t n_embd_head_k = hparams.n_embd_head_k(il);
150
+ + const uint32_t n_head_kv = hparams.n_head_kv(il);
151
+ + const uint32_t n_embd_k_gpu = layer.gpu_heads * n_embd_head_k;
152
+ + const uint32_t n_embd_k_cpu = (n_head_kv - layer.gpu_heads) * n_embd_head_k;
153
+ + const uint64_t k_row_gpu = ggml_row_size(k->type, n_embd_k_gpu);
154
+ + const uint64_t k_row_cpu = ggml_row_size(k_cpu->type, n_embd_k_cpu);
155
+ + GGML_ASSERT(k_row_gpu + k_row_cpu == k_size_row);
156
+ + if (sinfo.is_contiguous()) {
157
+ + io.read_tensor(k, sinfo.head() * k_row_gpu, cell_count * k_row_gpu);
158
+ + io.read_tensor(k_cpu, sinfo.head() * k_row_cpu, cell_count * k_row_cpu);
159
+ + } else {
160
+ + for (uint32_t i = 0; i < cell_count; ++i) {
161
+ + io.read_tensor(k, sinfo.idxs[0][i] * k_row_gpu, k_row_gpu);
162
+ + }
163
+ + for (uint32_t i = 0; i < cell_count; ++i) {
164
+ + io.read_tensor(k_cpu, sinfo.idxs[0][i] * k_row_cpu, k_row_cpu);
165
+ + }
166
+ + }
167
+ + } else if (sinfo.is_contiguous()) {
168
+ // Fast path: contiguous cells, single memcpy
169
+ io.read_tensor(k, sinfo.head() * k_size_row, cell_count * k_size_row);
170
+ } else {
171
+ @@ -3476,7 +3559,33 @@ bool llama_kv_cache::state_read_data(llama_io_read_i & io, uint32_t strm, uint32
172
+ }
173
+
174
+ if (cell_count) {
175
+ - if (sinfo.is_contiguous()) {
176
+ + // opencoti bug-222 fix β€” F5 M2 split-aware state read (V),
177
+ + // same pattern as K. M2 is gated off when v_trans, so this
178
+ + // branch is only reached with the non-transposed V layout.
179
+ + const bool m2_split_active = layer.gpu_heads > 0
180
+ + && !layer.v_cpu_per_stream.empty()
181
+ + && layer.v_cpu_per_stream[strm];
182
+ + if (m2_split_active) {
183
+ + auto * v_cpu = layer.v_cpu_per_stream[strm];
184
+ + const uint32_t n_embd_head_v = hparams.n_embd_head_v(il);
185
+ + const uint32_t n_head_kv = hparams.n_head_kv(il);
186
+ + const uint32_t n_embd_v_gpu = layer.gpu_heads * n_embd_head_v;
187
+ + const uint32_t n_embd_v_cpu = (n_head_kv - layer.gpu_heads) * n_embd_head_v;
188
+ + const uint64_t v_row_gpu = ggml_row_size(v->type, n_embd_v_gpu);
189
+ + const uint64_t v_row_cpu = ggml_row_size(v_cpu->type, n_embd_v_cpu);
190
+ + GGML_ASSERT(v_row_gpu + v_row_cpu == v_size_row);
191
+ + if (sinfo.is_contiguous()) {
192
+ + io.read_tensor(v, sinfo.head() * v_row_gpu, cell_count * v_row_gpu);
193
+ + io.read_tensor(v_cpu, sinfo.head() * v_row_cpu, cell_count * v_row_cpu);
194
+ + } else {
195
+ + for (uint32_t i = 0; i < cell_count; ++i) {
196
+ + io.read_tensor(v, sinfo.idxs[0][i] * v_row_gpu, v_row_gpu);
197
+ + }
198
+ + for (uint32_t i = 0; i < cell_count; ++i) {
199
+ + io.read_tensor(v_cpu, sinfo.idxs[0][i] * v_row_cpu, v_row_cpu);
200
+ + }
201
+ + }
202
+ + } else if (sinfo.is_contiguous()) {
203
+ // Fast path: contiguous cells, single memcpy
204
+ io.read_tensor(v, sinfo.head() * v_size_row, cell_count * v_size_row);
205
+ } else {
patches/0033-cpy-tie-blck-align.patch ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ opencoti F5 bug-225 β€” block-align cpy_k/cpy_v dependency-tie views over quantized KV
2
+
3
+ At 256k + q8_0 KV with M2 head split (--headinfer-gpu-heads-frac < 1.0) on Gemma 4
4
+ iswa, server boot crashed during common_init_result's graph_reserve with:
5
+ ggml.c:1301: assert(ne % ggml_blck_size(type) == 0) failed
6
+
7
+ Root cause: the M2 and Phase-4 branches of cpy_k / cpy_v built a graph-topology
8
+ dependency tie via `ggml_view_1d(ctx, store, 1, 0)` over the K/V cache scatter
9
+ result. The tie value is never read by computation β€” it only forces multiple
10
+ per-stream / per-head writes to be reachable from one ggml_build_forward_expand
11
+ call. At f16 (block size 1), ne=1 passes ggml_row_size silently. At q8_0
12
+ (block size 32), ne=1 fails the assert at graph_reserve before any inference
13
+ runs.
14
+
15
+ Fix at all 4 callsites (cpy_k M2 branch + Phase-4 multi-stream branch; cpy_v
16
+ M2 branch + Phase-4 !v_trans multi-stream branch β€” 12 view sites total):
17
+ - Replace `ne = 1` with `ggml_blck_size(target->type)` so the view passes
18
+ the quantized-alignment check.
19
+ - Wrap each view in `ggml_cast(..., GGML_TYPE_F32)` so the subsequent
20
+ `ggml_add` survives CUDA bin_bcast's F32/F16-only check (binbcast.cu:376).
21
+
22
+ At f16 KV both wraps are no-ops (blck_size==1, cast on f16-view is byte-identical
23
+ for ne=1 tie semantics never read). At q8_0 the wraps unblock 256k boot.
24
+
25
+ Verification:
26
+ - Boot at 4096 / 32768 / 262144 + q8_0 + M2 frac=0.5 on Gemma 4 A4B Q4_K_M:
27
+ all succeed (no graph_reserve crash).
28
+ - RULER vt@4096 + vt@32768 + niah_single_1@{4096,32768} ours-c1 scored 100/100
29
+ against the post-fix binary on 2026-05-29 β€” confirms no semantic regression
30
+ at moderate ctx.
31
+
32
+ Related: bug-222 (n_kv/view stride on iswa tail microbatch); bug-224 (concat-F32
33
+ on iswa); bug-226 (256k semantic collapse β€” different root cause, separate fix).
34
+
35
+ Patch order: applies after 0031-per-stream-split (Phase 4) and 0032-m2-state-io.
36
+ Cosmocc-only edit; CUDA DSO unchanged.
37
+
38
+ diff --git a/llama.cpp/src/llama-kv-cache.cpp b/llama.cpp/src/llama-kv-cache.cpp
39
+ --- a/llama.cpp/src/llama-kv-cache.cpp
40
+ +++ b/llama.cpp/src/llama-kv-cache.cpp
41
+ @@ -2231,14 +2231,14 @@ ggml_tensor * llama_kv_cache::cpy_k(ggml_context * ctx, ggml_tensor * k_cur, ggm
42
+ ggml_tensor * store_g = ggml_set_rows(ctx, k_s, cur_g, idxs_s);
43
+ ggml_tensor * store_c = ggml_set_rows(ctx, kc_s, cur_c, idxs_s);
44
+ ggml_tensor * pair = ggml_add(ctx,
45
+ - ggml_view_1d(ctx, store_g, 1, 0),
46
+ - ggml_view_1d(ctx, store_c, 1, 0));
47
+ + ggml_cast(ctx, ggml_view_1d(ctx, store_g, ggml_blck_size(store_g->type), 0), GGML_TYPE_F32),
48
+ + ggml_cast(ctx, ggml_view_1d(ctx, store_c, ggml_blck_size(store_c->type), 0), GGML_TYPE_F32));
49
+ if (!tie) {
50
+ tie = pair;
51
+ } else {
52
+ tie = ggml_add(ctx,
53
+ - ggml_view_1d(ctx, tie, 1, 0),
54
+ - ggml_view_1d(ctx, pair, 1, 0));
55
+ + ggml_cast(ctx, ggml_view_1d(ctx, tie, ggml_blck_size(tie->type), 0), GGML_TYPE_F32),
56
+ + ggml_cast(ctx, ggml_view_1d(ctx, pair, ggml_blck_size(pair->type), 0), GGML_TYPE_F32));
57
+ }
58
+ }
59
+ return tie;
60
+ @@ -2287,8 +2287,8 @@ ggml_tensor * llama_kv_cache::cpy_k(ggml_context * ctx, ggml_tensor * k_cur, ggm
61
+ tie = store_s;
62
+ } else {
63
+ tie = ggml_add(ctx,
64
+ - ggml_view_1d(ctx, tie, 1, 0),
65
+ - ggml_view_1d(ctx, store_s, 1, 0));
66
+ + ggml_cast(ctx, ggml_view_1d(ctx, tie, ggml_blck_size(tie->type), 0), GGML_TYPE_F32),
67
+ + ggml_cast(ctx, ggml_view_1d(ctx, store_s, ggml_blck_size(store_s->type), 0), GGML_TYPE_F32));
68
+ }
69
+ }
70
+ return tie;
71
+ @@ -2345,14 +2345,14 @@ ggml_tensor * llama_kv_cache::cpy_v(ggml_context * ctx, ggml_tensor * v_cur, ggm
72
+ ggml_tensor * store_g = ggml_set_rows(ctx, v_s, cur_g, idxs_s);
73
+ ggml_tensor * store_c = ggml_set_rows(ctx, vc_s, cur_c, idxs_s);
74
+ ggml_tensor * pair = ggml_add(ctx,
75
+ - ggml_view_1d(ctx, store_g, 1, 0),
76
+ - ggml_view_1d(ctx, store_c, 1, 0));
77
+ + ggml_cast(ctx, ggml_view_1d(ctx, store_g, ggml_blck_size(store_g->type), 0), GGML_TYPE_F32),
78
+ + ggml_cast(ctx, ggml_view_1d(ctx, store_c, ggml_blck_size(store_c->type), 0), GGML_TYPE_F32));
79
+ if (!tie) {
80
+ tie = pair;
81
+ } else {
82
+ tie = ggml_add(ctx,
83
+ - ggml_view_1d(ctx, tie, 1, 0),
84
+ - ggml_view_1d(ctx, pair, 1, 0));
85
+ + ggml_cast(ctx, ggml_view_1d(ctx, tie, ggml_blck_size(tie->type), 0), GGML_TYPE_F32),
86
+ + ggml_cast(ctx, ggml_view_1d(ctx, pair, ggml_blck_size(pair->type), 0), GGML_TYPE_F32));
87
+ }
88
+ }
89
+ return tie;
90
+ @@ -2400,8 +2400,8 @@ ggml_tensor * llama_kv_cache::cpy_v(ggml_context * ctx, ggml_tensor * v_cur, ggm
91
+ tie = store_s;
92
+ } else {
93
+ tie = ggml_add(ctx,
94
+ - ggml_view_1d(ctx, tie, 1, 0),
95
+ - ggml_view_1d(ctx, store_s, 1, 0));
96
+ + ggml_cast(ctx, ggml_view_1d(ctx, tie, ggml_blck_size(tie->type), 0), GGML_TYPE_F32),
97
+ + ggml_cast(ctx, ggml_view_1d(ctx, store_s, ggml_blck_size(store_s->type), 0), GGML_TYPE_F32));
98
+ }
99
+ }
100
+ return tie;
patches/0034-m2-pinned-host.patch ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ opencoti F5 M7-A (0034) β€” pinned host buffer for M2 CPU-half KV
2
+ opencoti note (2026-05-31, bug-250): hunk regenerated via snapshot-diff against the post-0033 bootstrap so it strict git-applies β€” the original removed two trailing-whitespace lines absent from the current tree. Content unchanged (pinned cpu_buft resolution); verified against the live r2a tree.
3
+
4
+
5
+ Switches the M2 head-split CPU-half allocation from
6
+ ggml_backend_cpu_buffer_type() (pageable) to ggml_backend_dev_host_buffer_type(dev)
7
+ (CUDA pinned host memory) when the layer is offloaded to a CUDA device. Falls
8
+ back to pageable CPU buft when no CUDA dev / no dev_host_buffer_type.
9
+
10
+ Why: pageable host memory caps PCIe transfers at ~50% of theoretical
11
+ bandwidth because every DMA has to first pin the page or stage through a
12
+ driver-side bounce buffer. Pinned host memory unlocks full PCIe bandwidth
13
+ on every M2 CPU→GPU stream-back AND is the foundation Rolling KV (F5 M7)
14
+ builds on β€” without pinned residency, the M7 streaming pipeline can't hit
15
+ its compute/copy-overlap budget at PCIe 3.0 x8.
16
+
17
+ Mechanics:
18
+ - Layer scope (~line 270): resolve cpu_buft alongside the existing
19
+ layer GPU buft. Read dev_host_buffer_type(dev); fall back to plain
20
+ cpu_buffer_type if unsupported.
21
+ - Per-stream scope (~line 391): consume the layer-scope cpu_buft
22
+ instead of calling ggml_backend_cpu_buffer_type() inline.
23
+
24
+ f16 and CPU-only paths byte-identical to pre-0034 (dev_host_buffer_type
25
+ returns null β†’ fallback fires β†’ identical alloc). q8_0 + M2 + CUDA paths
26
+ get pinned residency. Foundation for #296 (Rolling KV / M7).
27
+
28
+ Verification:
29
+ - Cosmocc build clean (~80s incremental).
30
+ - Smoke at 32k + f16 + M2 frac=0.5 + M0/M1/M3 flags engaged on Gemma 4
31
+ A4B Q4_K_M: server boots clean, /v1/completions returns coherent
32
+ tokens (no crash, no DSO mismatch).
33
+ - Does NOT fix bug-226 (256k+q8_0 semantic collapse β€” confirmed
34
+ deterministic via 1-sample canary 2026-05-29 21:51, scored 0/1 with
35
+ same wrong output 'LJXLD' as the original --no-kv-unified failure;
36
+ pinning is orthogonal to the corruption mechanism).
37
+ - Held for bug-226 root cause before shipping as default β€” pinning a
38
+ broken stack doesn't fix it but doesn't make it worse either.
39
+
40
+ Patch order: applies after 0033-cpy-tie-blck-align. Cosmocc-only edit;
41
+ CUDA DSO unchanged (host-side buffer-type resolution, no kernel changes).
42
+
43
+ diff --git a/llama.cpp/src/llama-kv-cache.cpp b/llama.cpp/src/llama-kv-cache.cpp
44
+ --- a/llama.cpp/src/llama-kv-cache.cpp
45
+ +++ b/llama.cpp/src/llama-kv-cache.cpp
46
+ @@ -267,11 +267,20 @@ llama_kv_cache::llama_kv_cache(
47
+
48
+ ggml_backend_buffer_type_t buft = ggml_backend_cpu_buffer_type();
49
+
50
+ + // opencoti F5 M7-A (0034) β€” resolved at layer scope alongside the
51
+ + // per-layer GPU buft so the M2 CPU-half allocation below (line ~390)
52
+ + // can reach pinned host memory when CUDA is offloaded. Falls back to
53
+ + // pageable CPU buft when no CUDA dev / no dev_host_buffer_type. Pinned
54
+ + // host buffers DMA at full PCIe bandwidth; pageable hits ~50%.
55
+ + ggml_backend_buffer_type_t cpu_buft = ggml_backend_cpu_buffer_type();
56
+ if (offload) {
57
+ auto * dev = model.dev_layer(il);
58
+ buft = ggml_backend_dev_buffer_type(dev);
59
+
60
+ dev_name = ggml_backend_dev_name(dev);
61
+ +
62
+ + ggml_backend_buffer_type_t pinned = ggml_backend_dev_host_buffer_type(dev);
63
+ + if (pinned) cpu_buft = pinned;
64
+ }
65
+
66
+ LLAMA_LOG_DEBUG("%s: layer %3d: dev = %s\n", __func__, il, dev_name);
67
+ @@ -374,7 +383,12 @@ llama_kv_cache::llama_kv_cache(
68
+ // when gpu_heads == 0 so headinfer_split_active() can test
69
+ // by vector size.
70
+ if (gpu_heads > 0) {
71
+ - ggml_context * ctx_cpu_s = ctx_for_buft(ggml_backend_cpu_buffer_type(), s);
72
+ + // opencoti F5 M7-A (0034) β€” was ggml_backend_cpu_buffer_type()
73
+ + // (pageable). Now uses the layer-scope cpu_buft resolved above:
74
+ + // pinned host buffer when CUDA is the layer dev, plain CPU buft
75
+ + // otherwise. Pinned unlocks full PCIe DMA for M2's CPU-half
76
+ + // stream-back AND is the foundation Rolling KV (M7) builds on.
77
+ + ggml_context * ctx_cpu_s = ctx_for_buft(cpu_buft, s);
78
+ if (!ctx_cpu_s) {
79
+ throw std::runtime_error("failed to create CPU ggml context for headinfer kv cache");
80
+ }
patches/0035-concat-q-block-aware.patch ADDED
@@ -0,0 +1,209 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ opencoti F5 bug-226 β€” block-aware ggml-cpu concat for quantized types
2
+
3
+ At 32k+q8_0 KV with M2 head split (--headinfer-gpu-heads-frac < 1.0)
4
+ without NEO orchestration, the M2 head-axis concat path in get_k/get_v
5
+ (llama-kv-cache.cpp:1881-1910) produced semantically corrupt K/V that
6
+ the downstream flash-attn read as garbage, scoring 0.00% on RULER vt
7
+ with completely wrong output (e.g. predicting 'SHRUU' instead of the
8
+ five expected variables). 5-cell bisection at 32k+q8_0 isolated M2 alone
9
+ (--headinfer-gpu-heads-frac 0.5) as the failing config; M0, M1, M3, and
10
+ vanilla scored 100.
11
+
12
+ Root cause: ggml_concat on block-quantized tensors falls through
13
+ multiple broken paths.
14
+
15
+ The M2 non-NEO get_k/get_v branch returns:
16
+
17
+ ggml_concat(ctx, ggml_view_4d(k_s, head_dim, n_head_gpu, n_kv, 1),
18
+ ggml_view_4d(kc_s, head_dim, n_head_cpu, n_kv, 1), 1)
19
+
20
+ both views are q8_0. The CUDA backend rejects this (concat.cu:165-167
21
+ asserts F32-only β€” task #253's earlier supports_op tightening was
22
+ correct), so the scheduler dispatches to CPU. CPU concat falls into
23
+ ggml_compute_forward_concat_any (ops.cpp:1894-1935) for any non-
24
+ F16/BF16/I16/I8/F32/I32 type. concat_any iterates i0 over LOGICAL
25
+ elements (0..ne0) with stride nb00, calling memcpy(len = type_size).
26
+ For q8_0, nb00 = type_size = 36 bytes (one block of 32 elements), but
27
+ the code treats it as a per-element stride β€” reading blocks at offsets
28
+ 0, 36, 72, ..., 127Β·36 = 4572 bytes from each row start, well past
29
+ the actual row of (ne0/32)Β·36 = 144 bytes. Result: q8_0 blocks
30
+ scrambled into uninterpretable data β†’ FA reads garbage K β†’ output is
31
+ task-format-correct but value-wrong.
32
+
33
+ At f16 KV (block size 1) this latent bug never triggered because
34
+ concat dispatches to ggml_compute_forward_concat_f16 which correctly
35
+ handles fp16 per-element. The full F5 stack at f16 + 32k scored 100
36
+ in the morning RULER comparison, masking the q8_0 case.
37
+
38
+ Fix: a new ggml_compute_forward_concat_q that copies entire dim-0 rows
39
+ at once for block-quantized types. Dispatched from
40
+ ggml_compute_forward_concat's default branch when ggml_blck_size > 1.
41
+
42
+ For dim != 0 (the M2 head-axis case): each (i1, i2, i3) maps to either
43
+ src0 or src1 (mutually exclusive); one memcpy of (ne0/blck)Β·type_size
44
+ bytes per row. For dim == 0: dst's dim-0 row = src0_row || src1_row,
45
+ asserted block-aligned at the split; two memcpys per row.
46
+
47
+ Non-block-quantized (F32/F16/BF16/I8/I16/I32) paths unchanged. The
48
+ ggml-cuda CUDA path stays F32-only β€” that's correct since this fix
49
+ lands at the CPU fallback the scheduler already routes to.
50
+
51
+ Lives in ggml-cpu only β€” no CUDA DSO rebuild needed. Compatible with
52
+ incremental cosmocc rebuild via `OPENCOTI_NO_CCACHE=1 bun run
53
+ build:llamafile:make`.
54
+
55
+ Verification:
56
+ - T2 config smoke (M2-only, no NEO, q8_0, 32k, vt n=1) β€” bug repro
57
+ pre-fix scored 0.00% with pred='SHRUU'; post-fix expected 100.00%.
58
+ - Full F5 stack (M0+M1+M2+M3+NEO+Phase4) at q8_0 + 32k β€” unchanged
59
+ from morning baseline (100%).
60
+ - All M0/M1/M2/M3/M5 unified-mode regression-shield benches β€” unchanged
61
+ (cosine β‰₯ 0.999); concat_q only fires on block-quantized types, and
62
+ f16/bf16/f32 KV paths route to their own concat_fN as before.
63
+
64
+ apply order: after 0034-m2-pinned-host (this is a ggml-cpu fix; the
65
+ two are independent so order is bookkeeping only).
66
+
67
+ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
68
+
69
+ diff --git a/llama.cpp/ggml/src/ggml-cpu/ops.cpp b/llama.cpp/ggml/src/ggml-cpu/ops.cpp
70
+ --- a/llama.cpp/ggml/src/ggml-cpu/ops.cpp
71
+ +++ b/llama.cpp/ggml/src/ggml-cpu/ops.cpp
72
+ @@ -2078,6 +2078,119 @@ static void ggml_compute_forward_concat_f32(
73
+ }
74
+ }
75
+
76
+ +// opencoti F5 bug-226 fix (0035) β€” block-quantized concat.
77
+ +//
78
+ +// concat_any (above) does per-logical-element memcpy(len = type_size),
79
+ +// stepping i0 from 0..ne0 with stride nb00. For block-quantized types
80
+ +// (q8_0, q4_0, …) the parent tensor stores ne0 LOGICAL elements packed
81
+ +// in ne0/blck blocks of type_size bytes each, with nb00 = type_size
82
+ +// (one block, not one element). concat_any's loop therefore reads
83
+ +// blocks at offsets 0, type_size, 2Β·type_size, … up to ne0Β·type_size β€”
84
+ +// well past the actual row of (ne0/blck)Β·type_size bytes. The result
85
+ +// is scrambled block data, which the downstream FA reads as garbage K
86
+ +// (root cause of bug-226 at M2 head-axis q8_0 K/V concat).
87
+ +//
88
+ +// Fix: for block-quantized types, copy entire dim-0 rows at once.
89
+ +// Each dim-0 row of src0/src1/dst is contiguous (nb00 = type_size,
90
+ +// ne0/blck blocks per row, no gaps), so a single memcpy per
91
+ +// (i1, i2, i3) selection on each source captures the right bytes.
92
+ +//
93
+ +// - dim != 0 (M2 head-axis case): per (i1, i2, i3), the whole dim-0
94
+ +// row comes from src0 or src1 (mutually exclusive). One memcpy.
95
+ +// - dim == 0 (split on dim 0): dst's dim-0 row = src0's dim-0 row ||
96
+ +// src1's dim-0 row, concatenated head-to-tail. The split MUST land
97
+ +// on a block boundary (ne00 % blck == 0); when it does, two memcpys.
98
+ +//
99
+ +// The M2 split in llama-kv-cache.cpp:get_k/get_v concats on dim 1
100
+ +// (head axis), so the dim != 0 branch is the hot path. dim 0 is
101
+ +// covered for completeness β€” any future quantized-feature-axis split
102
+ +// pattern lands correctly without re-discovering this bug.
103
+ +static void ggml_compute_forward_concat_q(
104
+ + const ggml_compute_params * params,
105
+ + ggml_tensor * dst) {
106
+ +
107
+ + const ggml_tensor * src0 = dst->src[0];
108
+ + const ggml_tensor * src1 = dst->src[1];
109
+ +
110
+ + const size_t type_size = ggml_type_size(src0->type);
111
+ + const int64_t blck = ggml_blck_size(src0->type);
112
+ +
113
+ + GGML_ASSERT(blck > 1 && "concat_q is for block-quantized types");
114
+ + GGML_ASSERT(ggml_type_size(src1->type) == type_size);
115
+ + GGML_ASSERT(ggml_blck_size(src1->type) == blck);
116
+ + GGML_ASSERT(ggml_type_size(dst->type) == type_size);
117
+ + GGML_ASSERT(ggml_blck_size(dst->type) == blck);
118
+ +
119
+ + const int ith = params->ith;
120
+ + const int nth = params->nth;
121
+ +
122
+ + GGML_TENSOR_BINARY_OP_LOCALS
123
+ +
124
+ + const int32_t dim = ggml_get_op_params_i32(dst, 0);
125
+ +
126
+ + GGML_ASSERT(dim >= 0 && dim < 4);
127
+ +
128
+ + // Both srcs must have block-aligned dim-0 sizes β€” guaranteed by ggml
129
+ + // for any well-formed quantized tensor. We assert anyway because if
130
+ + // a caller violates this, the row math below silently corrupts data.
131
+ + GGML_ASSERT((ne00 % blck) == 0);
132
+ + GGML_ASSERT((ne10 % blck) == 0);
133
+ +
134
+ + // Bytes per dim-0 row of each source / dst.
135
+ + const size_t row_bytes_src0 = (ne00 / blck) * type_size;
136
+ + const size_t row_bytes_src1 = (ne10 / blck) * type_size;
137
+ + const size_t row_bytes_dst = (ne0 / blck) * type_size;
138
+ +
139
+ + if (dim == 0) {
140
+ + // dst row = src0 row (head) || src1 row (tail). Both fill at the
141
+ + // block boundary. For non-concat dims i1/i2/i3 must match between
142
+ + // src0 and src1 (ggml concat invariant), so no per-dim mux logic.
143
+ + GGML_ASSERT(row_bytes_src0 + row_bytes_src1 == row_bytes_dst);
144
+ + for (int i3 = 0; i3 < ne3; i3++) {
145
+ + for (int i2 = ith; i2 < ne2; i2 += nth) {
146
+ + for (int i1 = 0; i1 < ne1; i1++) {
147
+ + const char * x0 = (const char *)src0->data
148
+ + + i1*nb01 + i2*nb02 + i3*nb03;
149
+ + const char * x1 = (const char *)src1->data
150
+ + + i1*nb11 + i2*nb12 + i3*nb13;
151
+ + char * y = (char *)dst->data
152
+ + + i1*nb1 + i2*nb2 + i3*nb3;
153
+ + memcpy(y, x0, row_bytes_src0);
154
+ + memcpy(y + row_bytes_src0, x1, row_bytes_src1);
155
+ + }
156
+ + }
157
+ + }
158
+ + return;
159
+ + }
160
+ +
161
+ + // dim != 0: each (i1, i2, i3) maps to either src0 or src1, never both.
162
+ + // row_bytes is shared (src0/src1/dst all have the same dim-0 row).
163
+ + GGML_ASSERT(row_bytes_src0 == row_bytes_src1);
164
+ + GGML_ASSERT(row_bytes_src0 == row_bytes_dst);
165
+ +
166
+ + int64_t o[4] = {0, 0, 0, 0};
167
+ + o[dim] = src0->ne[dim];
168
+ +
169
+ + // TODO: smarter multi-threading
170
+ + for (int i3 = 0; i3 < ne3; i3++) {
171
+ + for (int i2 = ith; i2 < ne2; i2 += nth) {
172
+ + for (int i1 = 0; i1 < ne1; i1++) {
173
+ + const char * x;
174
+ + if (i1 < ne01 && i2 < ne02 && i3 < ne03) {
175
+ + x = (const char *)src0->data
176
+ + + (i1 )*nb01 + (i2 )*nb02 + (i3 )*nb03;
177
+ + } else {
178
+ + x = (const char *)src1->data
179
+ + + (i1 - o[1])*nb11 + (i2 - o[2])*nb12 + (i3 - o[3])*nb13;
180
+ + }
181
+ + char * y = (char *)dst->data
182
+ + + i1*nb1 + i2*nb2 + i3*nb3;
183
+ + memcpy(y, x, row_bytes_dst);
184
+ + }
185
+ + }
186
+ + }
187
+ +}
188
+ +
189
+ void ggml_compute_forward_concat(
190
+ const ggml_compute_params * params,
191
+ ggml_tensor * dst) {
192
+ @@ -2102,7 +2215,16 @@ void ggml_compute_forward_concat(
193
+ } break;
194
+ default:
195
+ {
196
+ - ggml_compute_forward_concat_any(params, dst);
197
+ + // opencoti F5 bug-226 fix (0035): block-quantized types
198
+ + // (q8_0, q4_0, q5_0, …) route to concat_q which iterates
199
+ + // over BLOCKS, not logical elements. Without this they
200
+ + // hit concat_any whose per-element memcpy with type_size
201
+ + // = block-size scrambles q8_0 K/V at M2 head-axis concat.
202
+ + if (ggml_blck_size(src0->type) > 1) {
203
+ + ggml_compute_forward_concat_q(params, dst);
204
+ + } else {
205
+ + ggml_compute_forward_concat_any(params, dst);
206
+ + }
207
+ }
208
+ }
209
+ }
patches/0036-pcie-probe-consume.patch ADDED
@@ -0,0 +1,364 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ opencoti F5-opt W1 (#293) β€” PCIe/ReBAR profile boot-time reader + autodetect flags
2
+
3
+ WHAT
4
+ Consumes the shipped rebar/PCIe probe (perf/llamafile/rebar-probe.{cu,sh}) at
5
+ server boot: reads the newest probe JSON (or $OPENCOTI_PCIE_PROFILE) for the
6
+ measured pinned H2D bandwidth + PCIe link width/speed, falling back to a
7
+ one-shot nvidia-smi query, then a conservative PCIe3 x8 default. The result
8
+ (effective GB/s + link geometry) is cached in a process-global pcie_profile,
9
+ logged at startup, and consumed later by M7 Rolling KV (#296) for streaming
10
+ tile sizing (tile_bytes_max = compute_ms * eff_bw_GBps * 0.8).
11
+
12
+ Adds:
13
+ - common/pcie-profile.{h,cpp} (NEW β€” detection + global)
14
+ - common_params.pcie_autodetect/pcie_bw_gbps (common.h)
15
+ - --pcie-autodetect / --pcie-bw-gbps (common/arg.cpp)
16
+ - common/pcie-profile.cpp TU (BUILD.mk COMMON_SRCS_CPP)
17
+ - pcie_profile_init() + boot log (tools/server/server.cpp β€” hook)
18
+
19
+ PROVENANCE (per opencoti upstream-sync directive)
20
+ opencoti-ORIGINAL β€” no code copied from upstream llama.cpp. The PCIe-link
21
+ sysfs read + ReBAR-active detection technique derives from lightseek's
22
+ tools/windows/rebar_test.cu (Windows NVML/VirtualAlloc original), Linux-ported
23
+ by opencoti in perf/llamafile/rebar-probe.cu (#293). The server-side reader
24
+ here parses that probe's JSON.
25
+ Upstream base: ggml-org/llama.cpp via Mozilla-Ocho/llamafile submodule
26
+ 5e9c63546 (llamafile 0.10.1 pin). Apply order: after 0035 (independent β€”
27
+ common/ + server additive). Surgical hook: tools/server/server.cpp boot call
28
+ β€” registered in docs/protocols/UPSTREAM_SYNC.md.
29
+
30
+ Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
31
+
32
+ diff -ruN state-pre-0036/llama.cpp/BUILD.mk state-post-0036/llama.cpp/BUILD.mk
33
+ diff --git a/llama.cpp/BUILD.mk b/llama.cpp/BUILD.mk
34
+ --- a/llama.cpp/BUILD.mk
35
+ +++ b/llama.cpp/BUILD.mk
36
+ @@ -242,6 +242,7 @@ COMMON_SRCS_CPP := \
37
+ llama.cpp/common/ngram-cache.cpp \
38
+ llama.cpp/common/ngram-map.cpp \
39
+ llama.cpp/common/ngram-mod.cpp \
40
+ + llama.cpp/common/pcie-profile.cpp \
41
+ llama.cpp/common/peg-parser.cpp \
42
+ llama.cpp/common/preset.cpp \
43
+ llama.cpp/common/reasoning-budget.cpp \
44
+ diff --git a/llama.cpp/common/arg.cpp b/llama.cpp/common/arg.cpp
45
+ --- a/llama.cpp/common/arg.cpp
46
+ +++ b/llama.cpp/common/arg.cpp
47
+ @@ -1440,6 +1440,23 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
48
+ else throw std::invalid_argument("--neo-pipeline must be off|on|auto");
49
+ }
50
+ ).set_examples({LLAMA_EXAMPLE_SERVER}).set_env("LLAMA_ARG_NEO_PIPELINE"));
51
+ + // opencoti F5-opt W1 (#293) pcie profile β€” see docs/features/rolling_kv.md
52
+ + add_opt(common_arg(
53
+ + {"--pcie-autodetect"}, "MODE",
54
+ + "pcie profile: auto-detect host<->device bandwidth + PCIe link at boot (on (default), off). Reads the rebar-probe JSON / nvidia-smi; the result feeds M7 Rolling KV tile sizing.",
55
+ + [](common_params & params, const std::string & value) {
56
+ + if (value == "on" || value == "true" || value == "1") params.pcie_autodetect = true;
57
+ + else if (value == "off" || value == "false" || value == "0") params.pcie_autodetect = false;
58
+ + else throw std::invalid_argument("--pcie-autodetect must be on|off");
59
+ + }
60
+ + ).set_examples({LLAMA_EXAMPLE_SERVER}).set_env("LLAMA_ARG_PCIE_AUTODETECT"));
61
+ + add_opt(common_arg(
62
+ + {"--pcie-bw-gbps"}, "F",
63
+ + string_format("pcie profile: manual override of effective host->device bandwidth in GB/s (default: %.1f = autodetect). > 0 skips detection.", (double) params.pcie_bw_gbps),
64
+ + [](common_params & params, const std::string & value) {
65
+ + params.pcie_bw_gbps = std::stof(value);
66
+ + }
67
+ + ).set_examples({LLAMA_EXAMPLE_SERVER}).set_env("LLAMA_ARG_PCIE_BW_GBPS"));
68
+ add_opt(common_arg(
69
+ {"--chunks"}, "N",
70
+ string_format("max number of chunks to process (default: %d, -1 = all)", params.n_chunks),
71
+ diff --git a/llama.cpp/common/common.h b/llama.cpp/common/common.h
72
+ --- a/llama.cpp/common/common.h
73
+ +++ b/llama.cpp/common/common.h
74
+ @@ -445,6 +445,12 @@ struct common_params {
75
+ // active for the layer), 2 = auto (reserved for future load-based
76
+ // engagement; same as `on` today).
77
+ uint32_t neo_pipeline_mode = 0;
78
+ + // opencoti F5-opt W1 (#293) pcie profile β€” see docs/features/rolling_kv.md
79
+ + // Boot-time host<->device bandwidth + PCIe link detection, consumed by M7
80
+ + // Rolling KV (#296) tile sizing. autodetect reads the rebar-probe JSON /
81
+ + // nvidia-smi; bw_gbps > 0 forces a manual override (0 = autodetect/default).
82
+ + bool pcie_autodetect = true;
83
+ + float pcie_bw_gbps = 0.0f;
84
+ int32_t n_batch = 2048; // logical batch size for prompt processing (must be >=32 to use BLAS)
85
+ int32_t n_ubatch = 512; // physical batch size for prompt processing (must be >=32 to use BLAS)
86
+ int32_t n_keep = 0; // number of tokens to keep from initial prompt
87
+ diff --git a/llama.cpp/common/pcie-profile.cpp b/llama.cpp/common/pcie-profile.cpp
88
+ new file mode 100644
89
+ --- /dev/null
90
+ +++ b/llama.cpp/common/pcie-profile.cpp
91
+ @@ -0,0 +1,201 @@
92
+ +// opencoti F5-opt W1 (#293) β€” PCIe link + host<->device bandwidth profile.
93
+ +// See pcie-profile.h for the contract. CPU-side only; never throws.
94
+ +
95
+ +#include "pcie-profile.h"
96
+ +
97
+ +#include <algorithm>
98
+ +#include <cmath>
99
+ +#include <cstdio>
100
+ +#include <cstdlib>
101
+ +#include <cstring>
102
+ +#include <dirent.h>
103
+ +#include <fstream>
104
+ +#include <sstream>
105
+ +#include <string>
106
+ +#include <vector>
107
+ +
108
+ +namespace {
109
+ +
110
+ +// PCIe generation -> per-lane raw transfer rate (GT/s).
111
+ +float gen_to_gtps(int gen) {
112
+ + switch (gen) {
113
+ + case 1: return 2.5f;
114
+ + case 2: return 5.0f;
115
+ + case 3: return 8.0f;
116
+ + case 4: return 16.0f;
117
+ + case 5: return 32.0f;
118
+ + case 6: return 64.0f;
119
+ + default: return 0.0f;
120
+ + }
121
+ +}
122
+ +
123
+ +// Effective usable H2D bandwidth from link geometry. The 0.8 factor folds in
124
+ +// 128b/130b (gen3+) encoding overhead + DMA/protocol inefficiency; matches the
125
+ +// rolling_kv.md auto-adapt formula. width lanes * GT/s / 8 bits-per-byte * 0.8.
126
+ +float geometry_bw_gbps(int width, float gtps) {
127
+ + if (width <= 0 || gtps <= 0.0f) return 0.0f;
128
+ + return (float) width * gtps * 0.8f / 8.0f;
129
+ +}
130
+ +
131
+ +// "8.0 GT/s PCIe" / "16.0 GT/s PCIe" -> leading float. Empty/garbage -> 0.
132
+ +float parse_leading_float(const std::string & s) {
133
+ + return s.empty() ? 0.0f : (float) atof(s.c_str());
134
+ +}
135
+ +
136
+ +// Minimal value-after-key extraction for our OWN well-formed probe JSON.
137
+ +// Returns the substring between key's ':' and the next ',' or '}' / newline.
138
+ +bool json_raw_after(const std::string & body, const std::string & key, std::string & out) {
139
+ + const size_t k = body.find(key);
140
+ + if (k == std::string::npos) return false;
141
+ + size_t c = body.find(':', k + key.size());
142
+ + if (c == std::string::npos) return false;
143
+ + ++c;
144
+ + while (c < body.size() && (body[c] == ' ' || body[c] == '\t')) ++c;
145
+ + size_t e = c;
146
+ + while (e < body.size() && body[e] != ',' && body[e] != '\n' && body[e] != '}') ++e;
147
+ + out = body.substr(c, e - c);
148
+ + return true;
149
+ +}
150
+ +
151
+ +float json_float_after(const std::string & body, const std::string & key) {
152
+ + std::string raw;
153
+ + if (!json_raw_after(body, key, raw)) return 0.0f;
154
+ + return (float) atof(raw.c_str());
155
+ +}
156
+ +
157
+ +// Quoted-string value: trims surrounding quotes/spaces.
158
+ +std::string json_string_after(const std::string & body, const std::string & key) {
159
+ + std::string raw;
160
+ + if (!json_raw_after(body, key, raw)) return {};
161
+ + size_t a = raw.find('"');
162
+ + if (a == std::string::npos) return {};
163
+ + size_t b = raw.find('"', a + 1);
164
+ + if (b == std::string::npos) return {};
165
+ + return raw.substr(a + 1, b - a - 1);
166
+ +}
167
+ +
168
+ +std::string read_file(const std::string & path) {
169
+ + std::ifstream f(path, std::ios::binary);
170
+ + if (!f.is_open()) return {};
171
+ + std::ostringstream ss;
172
+ + ss << f.rdbuf();
173
+ + return ss.str();
174
+ +}
175
+ +
176
+ +// Newest ./.opencoti/rebar-probe-*.json. Filenames embed a YYYYMMDD-HHMMSS
177
+ +// stamp, so lexicographic max == newest. Empty if none.
178
+ +std::string newest_probe_json() {
179
+ + const char * env = getenv("OPENCOTI_PCIE_PROFILE");
180
+ + if (env && env[0]) return std::string(env);
181
+ +
182
+ + const char * dirs[] = {"./.opencoti", ".opencoti"};
183
+ + std::string best;
184
+ + for (const char * d : dirs) {
185
+ + DIR * dp = opendir(d);
186
+ + if (!dp) continue;
187
+ + struct dirent * ent;
188
+ + while ((ent = readdir(dp)) != nullptr) {
189
+ + const std::string name = ent->d_name;
190
+ + if (name.rfind("rebar-probe-", 0) != 0) continue;
191
+ + if (name.size() < 5 || name.compare(name.size() - 5, 5, ".json") != 0) continue;
192
+ + const std::string full = std::string(d) + "/" + name;
193
+ + if (best.empty() || name > best.substr(best.find_last_of('/') + 1)) best = full;
194
+ + }
195
+ + closedir(dp);
196
+ + if (!best.empty()) break;
197
+ + }
198
+ + return best;
199
+ +}
200
+ +
201
+ +// Parse a probe JSON into a profile. Prefers the MEASURED pinned H2D bandwidth
202
+ +// over the geometry formula; still records link width/speed for logging.
203
+ +bool profile_from_probe_json(const std::string & path, pcie_profile & out) {
204
+ + const std::string body = read_file(path);
205
+ + if (body.empty()) return false;
206
+ +
207
+ + // measured pinned H2D: locate the "pinned" object, then its "h2d".
208
+ + float pinned_h2d = 0.0f;
209
+ + const size_t p = body.find("\"pinned\"");
210
+ + if (p != std::string::npos) {
211
+ + pinned_h2d = json_float_after(body.substr(p), "\"h2d\"");
212
+ + }
213
+ +
214
+ + const int width = atoi(json_string_after(body, "\"pci_link_width_current\"").c_str());
215
+ + const float gtps = parse_leading_float(json_string_after(body, "\"pci_link_speed_current\""));
216
+ +
217
+ + if (pinned_h2d <= 0.0f && width <= 0) return false;
218
+ +
219
+ + out.link_width = width;
220
+ + out.link_gtps = gtps;
221
+ + out.effective_bw_gbps = pinned_h2d > 0.0f ? pinned_h2d : geometry_bw_gbps(width, gtps);
222
+ + out.source = "probe-json";
223
+ + out.detail = "from " + path + (pinned_h2d > 0.0f ? " (measured pinned H2D)" : " (link geometry)");
224
+ + return out.effective_bw_gbps > 0.0f;
225
+ +}
226
+ +
227
+ +// One-shot nvidia-smi query for link gen + width. CPU-side, no CUDA link dep.
228
+ +bool profile_from_nvidia_smi(pcie_profile & out) {
229
+ + FILE * pp = popen("nvidia-smi --query-gpu=pcie.link.gen.current,pcie.link.width.current "
230
+ + "--format=csv,noheader,nounits -i 0 2>/dev/null", "r");
231
+ + if (!pp) return false;
232
+ + char buf[256] = {};
233
+ + const bool got = fgets(buf, sizeof(buf), pp) != nullptr;
234
+ + pclose(pp);
235
+ + if (!got) return false;
236
+ +
237
+ + int gen = 0, width = 0;
238
+ + // format: "<gen>, <width>"
239
+ + if (sscanf(buf, " %d , %d", &gen, &width) < 2) return false;
240
+ + const float gtps = gen_to_gtps(gen);
241
+ + const float bw = geometry_bw_gbps(width, gtps);
242
+ + if (bw <= 0.0f) return false;
243
+ +
244
+ + out.link_width = width;
245
+ + out.link_gtps = gtps;
246
+ + out.effective_bw_gbps = bw;
247
+ + out.source = "nvidia-smi";
248
+ + out.detail = "gen" + std::to_string(gen) + " x" + std::to_string(width) + " (link geometry)";
249
+ + return true;
250
+ +}
251
+ +
252
+ +pcie_profile g_pcie_profile;
253
+ +bool g_pcie_inited = false;
254
+ +
255
+ +} // namespace
256
+ +
257
+ +pcie_profile pcie_profile_detect(bool autodetect, float manual_gbps) {
258
+ + pcie_profile prof; // conservative default below
259
+ +
260
+ + if (manual_gbps > 0.0f) {
261
+ + prof.effective_bw_gbps = manual_gbps;
262
+ + prof.source = "manual";
263
+ + prof.detail = "--pcie-bw-gbps override";
264
+ + return prof;
265
+ + }
266
+ +
267
+ + if (autodetect) {
268
+ + const std::string js = newest_probe_json();
269
+ + if (!js.empty() && profile_from_probe_json(js, prof)) return prof;
270
+ + if (profile_from_nvidia_smi(prof)) return prof;
271
+ + }
272
+ +
273
+ + // Conservative default: PCIe 3.0 x8 (common on Ryzen 5600G/5700G hosts where
274
+ + // the iGPU absorbs 8 lanes β€” see rolling_kv.md). ~6.4 GB/s.
275
+ + prof.link_width = 8;
276
+ + prof.link_gtps = 8.0f;
277
+ + prof.effective_bw_gbps = geometry_bw_gbps(8, 8.0f);
278
+ + prof.source = "default";
279
+ + prof.detail = autodetect ? "no probe JSON / nvidia-smi; assuming PCIe3 x8"
280
+ + : "autodetect disabled; assuming PCIe3 x8";
281
+ + return prof;
282
+ +}
283
+ +
284
+ +const pcie_profile & pcie_profile_init(bool autodetect, float manual_gbps) {
285
+ + g_pcie_profile = pcie_profile_detect(autodetect, manual_gbps);
286
+ + g_pcie_inited = true;
287
+ + return g_pcie_profile;
288
+ +}
289
+ +
290
+ +const pcie_profile & pcie_profile_get() {
291
+ + return g_pcie_profile;
292
+ +}
293
+ diff --git a/llama.cpp/common/pcie-profile.h b/llama.cpp/common/pcie-profile.h
294
+ new file mode 100644
295
+ --- /dev/null
296
+ +++ b/llama.cpp/common/pcie-profile.h
297
+ @@ -0,0 +1,38 @@
298
+ +#pragma once
299
+ +
300
+ +// opencoti F5-opt W1 (#293) β€” PCIe link + host<->device bandwidth profile.
301
+ +//
302
+ +// Boot-time detection of the GPU's effective host-to-device transfer bandwidth
303
+ +// and PCIe link geometry. This is a foundation diagnostic and, more
304
+ +// importantly, the input M7 Rolling KV (#296) uses to size streaming KV tiles:
305
+ +// tile_bytes_max = compute_ms_per_layer * effective_bw_gbps * 0.8
306
+ +// (see docs/features/rolling_kv.md, "Auto-adapt loop", step 1).
307
+ +//
308
+ +// Detection is CPU-side only (no CUDA dependency in common/). Sources, in
309
+ +// preference order:
310
+ +// 1. manual override (--pcie-bw-gbps F, F > 0)
311
+ +// 2. explicit probe JSON via $OPENCOTI_PCIE_PROFILE
312
+ +// 3. newest ./.opencoti/rebar-probe-*.json (measured pinned H2D + link)
313
+ +// 4. one-shot `nvidia-smi` query (pcie.link.gen.current + width.current)
314
+ +// 5. conservative default (PCIe 3.0 x8 ~= 6.4 GB/s)
315
+ +// Never throws; never fails server boot.
316
+ +
317
+ +#include <string>
318
+ +
319
+ +struct pcie_profile {
320
+ + float effective_bw_gbps = 0.0f; // usable H2D bandwidth estimate (GB/s)
321
+ + int link_width = 0; // current PCIe lane count (e.g. 8, 16)
322
+ + float link_gtps = 0.0f; // current per-lane transfer rate (GT/s)
323
+ + std::string source = "default"; // manual|probe-json|nvidia-smi|default
324
+ + std::string detail; // human-readable note for logging
325
+ +};
326
+ +
327
+ +// Run detection once. manual_gbps > 0 forces source="manual"; autodetect=false
328
+ +// skips probe/nvidia-smi discovery and returns the conservative default.
329
+ +pcie_profile pcie_profile_detect(bool autodetect, float manual_gbps);
330
+ +
331
+ +// Process-global: pcie_profile_init() runs detection, caches, and returns the
332
+ +// result; pcie_profile_get() returns the cached value (defaults until init
333
+ +// runs). Consumed by M7 Rolling KV (#296) at server boot.
334
+ +const pcie_profile & pcie_profile_init(bool autodetect, float manual_gbps);
335
+ +const pcie_profile & pcie_profile_get();
336
+ diff --git a/llama.cpp/tools/server/server.cpp b/llama.cpp/tools/server/server.cpp
337
+ --- a/llama.cpp/tools/server/server.cpp
338
+ +++ b/llama.cpp/tools/server/server.cpp
339
+ @@ -7,6 +7,7 @@
340
+ #include "arg.h"
341
+ #include "build-info.h"
342
+ #include "common.h"
343
+ +#include "pcie-profile.h" // opencoti F5-opt W1 (#293)
344
+ #include "fit.h"
345
+ #include "llama.h"
346
+ #include "log.h"
347
+ @@ -135,6 +136,17 @@ int server_main(int argc, char ** argv,
348
+ // struct that contains llama context and inference
349
+ server_context ctx_server;
350
+
351
+ + // opencoti F5-opt W1 (#293): detect host<->device bandwidth + PCIe link
352
+ + // once at boot; logged here and cached (pcie_profile_get) for M7 Rolling KV
353
+ + // (#296) streaming-tile sizing. Never fails boot β€” falls back to a
354
+ + // conservative PCIe3 x8 default on any detection miss.
355
+ + {
356
+ + const pcie_profile & pp = pcie_profile_init(params.pcie_autodetect, params.pcie_bw_gbps);
357
+ + LOG_INF("%s: pcie profile: %.1f GB/s effective (x%d @ %.1f GT/s, source=%s) β€” %s\n",
358
+ + __func__, (double) pp.effective_bw_gbps, pp.link_width, (double) pp.link_gtps,
359
+ + pp.source.c_str(), pp.detail.c_str());
360
+ + }
361
+ +
362
+ server_http_context ctx_http;
363
+ if (!ctx_http.init(params)) {
364
+ SRV_ERR("%s", "failed to initialize HTTP server\n");
patches/0040-iqk-flash-attn.patch ADDED
The diff for this file is too large to render. See raw diff
 
patches/0042-fused-moe.patch ADDED
@@ -0,0 +1,527 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ opencoti F5-opt W3 (#292) β€” op-level fused MoE up+gate+GLU (GGML_OP_MOE_FUSED_UP_GATE)
2
+
3
+ Adds a new ggml op, GGML_OP_MOE_FUSED_UP_GATE, that collapses a MoE FFN's two
4
+ separate per-expert projections (up = up_exps @ cur, gate = gate_exps @ cur)
5
+ plus the GLU activation into ONE op at decode (n_tokens == 1). The CUDA backend
6
+ dispatches it straight to the existing fused mmvq kernel (ggml_cuda_mul_mat_vec_q
7
+ with a fusion arg carrying the gate weight + glu_op), eliminating the gate_up
8
+ intermediate's HBM round-trip and the standalone GLU kernel launch per MoE layer
9
+ per decode token.
10
+
11
+ Off by default. Engaged with --fused-moe-up-gate on (server) / cparams
12
+ .fused_moe_up_gate / adapter fusedMoeUpGate. The build_moe_ffn graph hook only
13
+ emits the op when the model presents SEPARATE up/gate expert weights
14
+ (gate_exps && !gate_up_exps), same type, SILU|GELU activation, at decode β€” i.e.
15
+ Qwen2-MoE / Qwen3-MoE / OLMoE / Mixtral-style layouts. Models that fuse gate+up
16
+ at the weight level (a single ffn_gate_up_exps β€” Gemma-4 A4B, Qwen3.5-MoE)
17
+ never match the hook and run unchanged. Flag-OFF is byte-identical to the pre-W3
18
+ binary (op never emitted); flag-ON on a matching model is byte-identical to the
19
+ unfused two-matmul + GLU path. Validated on OLMoE-1B-7B-0924-Instruct-Q4_K_M:
20
+ graph-dump engagement confirmed (34 MOE_FUSED_UP_GATE nodes on CUDA0), greedy
21
+ decode byte-identical OFF vs ON, +2.4% decode tok/s (265.9 -> 272.2).
22
+
23
+ opencoti-hook: f5-opt-fmoe β€” see docs/features/advanced_kv.md
24
+
25
+ UPSTREAM PROVENANCE (for future re-sync)
26
+ Idea source : ik_llama.cpp op-level MoE fusion
27
+ https://github.com/ikawrakow/ik_llama.cpp
28
+ PR #229 (fused MoE mul_mat_id path), PR #520 (OOAE expert eval).
29
+ Commit context HEAD 8960c5ba5ee9db30ba838304373aa4dbec9f7cbd.
30
+ License MIT, Author Iwan Kawrakow.
31
+ Code vendored: NONE. This op is original opencoti code. ik_llama's
32
+ ggml_cuda_moe_up_gate_unary dispatches a fused mmvq-id kernel;
33
+ our tree already carries the equivalent primitive
34
+ (ggml_cuda_mul_mat_vec_q with ggml_cuda_mm_fusion_args.gate +
35
+ glu_op β€” ggml-cuda/mmvq.cu:555-582). W3 takes only the
36
+ op-level-fusion IDEA and routes our existing kernel through a
37
+ new ggml op + a build_moe_ffn graph hook. No ik_llama source is
38
+ copied into this patch.
39
+ ABI note : GGML_OP_COUNT 96 -> 97. The cosmocc binary AND the ggml-cuda.so
40
+ DSO must be rebuilt together and paired (asymmetric DSO cache
41
+ hygiene). The CPU backend deliberately aborts the op (CUDA-only;
42
+ CPU MoE fusion is out of scope β€” only the CPU embedder matters
43
+ for CPU speed, and that is the separate W2 iqk FA engine).
44
+ Op convention (locked from ggml-cuda/mmvq.cu):
45
+ out = up_proj(src0=up_exps @ src1=cur) (.) act(gate_proj=fusion.gate @ cur)
46
+ == ggml_geglu_split(gate, up); the kernel reads the GLU variant from
47
+ fusion.glu_op (NOT dst op_params). New op srcs: src0=up_exps, src1=gate_exps,
48
+ src2=cur, src3=ids; op_params[0]=glu_op. CUDA dispatch passes src0=up_exps,
49
+ src1=cur(=src2), ids=src3, fusion.gate=gate_exps(=src1), glu_op=op_params[0].
50
+
51
+ Files touched (all surgical; no new vendored files):
52
+ ggml/include/ggml.h enum GGML_OP_MOE_FUSED_UP_GATE + ggml_moe_up_gate decl
53
+ ggml/src/ggml.c NAME/SYMBOL entries, COUNT 96->97 asserts, creation fn
54
+ ggml/src/ggml-cpu/ggml-cpu.c forward-dispatch + n_tasks: CUDA-only ABORT stub
55
+ ggml/src/ggml-cuda/ggml-cuda.cu eval dispatch (reuse fused mmvq) + supports_op gate
56
+ src/llama-cparams.h bool fused_moe_up_gate
57
+ include/llama.h bool fused_moe_up_gate (llama_context_params)
58
+ src/llama-context.cpp param copy + default-params initializer
59
+ common/common.h bool fused_moe_up_gate = false
60
+ common/common.cpp cparams copy
61
+ common/arg.cpp --fused-moe-up-gate on|off (LLAMA_ARG_FUSED_MOE_UP_GATE)
62
+ src/llama-graph.cpp build_moe_ffn hook: emit op for separate-up/gate decode
63
+
64
+ ---
65
+ opencoti note (2026-05-31, bug-250): hunks regenerated via snapshot-diff against the post-0040 chain so the patch strict git-applies (3 common/* hunks had whitespace context drift). Content unchanged.
66
+ diff --git a/llama.cpp/common/arg.cpp b/llama.cpp/common/arg.cpp
67
+ --- a/llama.cpp/common/arg.cpp
68
+ +++ b/llama.cpp/common/arg.cpp
69
+ @@ -1375,6 +1375,91 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
70
+ params.slot_initial_ctx = value;
71
+ }
72
+ ).set_env("LLAMA_ARG_SLOT_INITIAL_CTX").set_examples({LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_EMBEDDING, LLAMA_EXAMPLE_PARALLEL}));
73
+ + // opencoti F5 M1 rest-kv-eviction β€” see docs/features/advanced_kv.md
74
+ + add_opt(common_arg(
75
+ + {"--rest-kv-eviction"},
76
+ + {"--no-rest-kv-eviction"},
77
+ + string_format("when context shift fires, evict the lowest-retention (KeyDiff key-similarity) contiguous window instead of the positional middle (default: %s)", params.rest_kv_eviction ? "enabled" : "disabled"),
78
+ + [](common_params & params, bool value) {
79
+ + params.rest_kv_eviction = value;
80
+ + }
81
+ + ).set_examples({LLAMA_EXAMPLE_SERVER}).set_env("LLAMA_ARG_REST_KV_EVICTION"));
82
+ + add_opt(common_arg(
83
+ + {"--rest-kv-recent"}, "N",
84
+ + string_format("rest-kv: protect the most-recent N tokens from eviction (default: %d)", params.rest_kv_recent),
85
+ + [](common_params & params, int value) {
86
+ + params.rest_kv_recent = value;
87
+ + }
88
+ + ).set_examples({LLAMA_EXAMPLE_SERVER}).set_env("LLAMA_ARG_REST_KV_RECENT"));
89
+ + add_opt(common_arg(
90
+ + {"--rest-kv-layer"}, "N",
91
+ + string_format("rest-kv: model layer whose keys score retention, -1 = auto/middle (default: %d)", params.rest_kv_layer),
92
+ + [](common_params & params, int value) {
93
+ + params.rest_kv_layer = value;
94
+ + }
95
+ + ).set_examples({LLAMA_EXAMPLE_SERVER}).set_env("LLAMA_ARG_REST_KV_LAYER"));
96
+ + // opencoti F5 M2 headinfer β€” see docs/features/advanced_kv.md
97
+ + add_opt(common_arg(
98
+ + {"--headinfer-gpu-heads-frac"}, "F",
99
+ + string_format("headinfer: fraction of KV heads kept GPU-resident; the rest are offloaded to host memory and streamed back per step (default: %.2f, 1.0 = off). Only effective for GPU-offloaded layers with flash-attention.", (double) params.headinfer_gpu_heads_frac),
100
+ + [](common_params & params, const std::string & value) {
101
+ + params.headinfer_gpu_heads_frac = std::stof(value);
102
+ + }
103
+ + ).set_examples({LLAMA_EXAMPLE_SERVER}).set_env("LLAMA_ARG_HEADINFER_GPU_HEADS_FRAC"));
104
+ + // opencoti F5 M3 neo β€” see docs/features/advanced_kv.md
105
+ + add_opt(common_arg(
106
+ + {"--neo-pipeline"}, "MODE",
107
+ + "neo (asymmetric GPU/CPU attention pipelining): off (default), on (engage when headinfer split is active for the layer), auto (reserved, same as on). Requires --headinfer-gpu-heads-frac < 1.0 to do anything.",
108
+ + [](common_params & params, const std::string & value) {
109
+ + if (value == "off") params.neo_pipeline_mode = 0;
110
+ + else if (value == "on") params.neo_pipeline_mode = 1;
111
+ + else if (value == "auto") params.neo_pipeline_mode = 2;
112
+ + else throw std::invalid_argument("--neo-pipeline must be off|on|auto");
113
+ + }
114
+ + ).set_examples({LLAMA_EXAMPLE_SERVER}).set_env("LLAMA_ARG_NEO_PIPELINE"));
115
+ + // opencoti F5-opt W2 (#290) iqk CPU flash-attention β€” see docs/features/advanced_kv.md
116
+ + add_opt(common_arg(
117
+ + {"--iqk-flash-attn"}, "MODE",
118
+ + "iqk CPU flash-attention (vendored ik_llama.cpp engine): off (default), on. "
119
+ + "Speeds up the M2 CPU-half attention on the token-generation path; falls back "
120
+ + "to the mainline kernel for unsupported (type, shape) cases. x86_64-only.",
121
+ + [](common_params & params, const std::string & value) {
122
+ + if (value == "off" || value == "false" || value == "0") params.iqk_flash_attn = false;
123
+ + else if (value == "on" || value == "true" || value == "1") params.iqk_flash_attn = true;
124
+ + else throw std::invalid_argument("--iqk-flash-attn must be on|off");
125
+ + ggml_iqk_flash_attn_set_enabled(params.iqk_flash_attn);
126
+ + }
127
+ + ).set_examples({LLAMA_EXAMPLE_SERVER}).set_env("LLAMA_ARG_IQK_FLASH_ATTN"));
128
+ + // opencoti F5-opt W3 (#292) fused-MoE β€” see docs/features/advanced_kv.md
129
+ + add_opt(common_arg(
130
+ + {"--fused-moe-up-gate"}, "MODE",
131
+ + "fused MoE up+gate+GLU (CUDA decode): off (default), on. Collapses the "
132
+ + "separate-up/gate expert matmuls + GLU into one op at decode so the CUDA "
133
+ + "backend runs the fused mmvq kernel without graph-pattern fusion. Only "
134
+ + "engages for separate-up/gate MoE models (e.g. Gemma-4 A4B) at n_tokens==1.",
135
+ + [](common_params & params, const std::string & value) {
136
+ + if (value == "off" || value == "false" || value == "0") params.fused_moe_up_gate = false;
137
+ + else if (value == "on" || value == "true" || value == "1") params.fused_moe_up_gate = true;
138
+ + else throw std::invalid_argument("--fused-moe-up-gate must be on|off");
139
+ + }
140
+ + ).set_examples({LLAMA_EXAMPLE_SERVER}).set_env("LLAMA_ARG_FUSED_MOE_UP_GATE"));
141
+ + // opencoti F5-opt W1 (#293) pcie profile β€” see docs/features/rolling_kv.md
142
+ + add_opt(common_arg(
143
+ + {"--pcie-autodetect"}, "MODE",
144
+ + "pcie profile: auto-detect host<->device bandwidth + PCIe link at boot (on (default), off). Reads the rebar-probe JSON / nvidia-smi; the result feeds M7 Rolling KV tile sizing.",
145
+ + [](common_params & params, const std::string & value) {
146
+ + if (value == "on" || value == "true" || value == "1") params.pcie_autodetect = true;
147
+ + else if (value == "off" || value == "false" || value == "0") params.pcie_autodetect = false;
148
+ + else throw std::invalid_argument("--pcie-autodetect must be on|off");
149
+ + }
150
+ + ).set_examples({LLAMA_EXAMPLE_SERVER}).set_env("LLAMA_ARG_PCIE_AUTODETECT"));
151
+ + add_opt(common_arg(
152
+ + {"--pcie-bw-gbps"}, "F",
153
+ + string_format("pcie profile: manual override of effective host->device bandwidth in GB/s (default: %.1f = autodetect). > 0 skips detection.", (double) params.pcie_bw_gbps),
154
+ + [](common_params & params, const std::string & value) {
155
+ + params.pcie_bw_gbps = std::stof(value);
156
+ + }
157
+ + ).set_examples({LLAMA_EXAMPLE_SERVER}).set_env("LLAMA_ARG_PCIE_BW_GBPS"));
158
+ add_opt(common_arg(
159
+ // opencoti F4 M3 Phase 3 β€” see docs/decisions/0001-lazy-slot-context.md
160
+ {"--slot-shrink-idle-ms"}, "N",
161
+ diff --git a/llama.cpp/common/common.cpp b/llama.cpp/common/common.cpp
162
+ --- a/llama.cpp/common/common.cpp
163
+ +++ b/llama.cpp/common/common.cpp
164
+ @@ -1636,6 +1636,8 @@ struct llama_context_params common_context_params_to_llama(const common_params &
165
+ cparams.headinfer_gpu_heads_frac = params.headinfer_gpu_heads_frac;
166
+ // opencoti F5 M3 neo β€” see docs/features/advanced_kv.md
167
+ cparams.neo_pipeline_mode = params.neo_pipeline_mode;
168
+ + // opencoti F5 W3 #292 fused-MoE β€” see docs/features/advanced_kv.md
169
+ + cparams.fused_moe_up_gate = params.fused_moe_up_gate;
170
+ cparams.n_rs_seq = params.speculative.need_n_rs_seq();
171
+ cparams.n_batch = params.n_batch;
172
+ cparams.n_ubatch = params.n_ubatch;
173
+ diff --git a/llama.cpp/common/common.h b/llama.cpp/common/common.h
174
+ --- a/llama.cpp/common/common.h
175
+ +++ b/llama.cpp/common/common.h
176
+ @@ -445,6 +445,41 @@ struct common_params {
177
+ // active for the layer), 2 = auto (reserved for future load-based
178
+ // engagement; same as `on` today).
179
+ uint32_t neo_pipeline_mode = 0;
180
+ + // opencoti F5 W3 #292 fused-MoE β€” see docs/features/advanced_kv.md
181
+ + // false = off (default; unfused 3-node path). true = fuse decode MoE
182
+ + // up+gate+GLU into one GGML_OP_MOE_FUSED_UP_GATE op for the CUDA backend.
183
+ + bool fused_moe_up_gate = false;
184
+ + // opencoti F5-opt W1 (#293) pcie profile β€” see docs/features/rolling_kv.md
185
+ + // Boot-time host<->device bandwidth + PCIe link detection, consumed by M7
186
+ + // Rolling KV (#296) tile sizing. autodetect reads the rebar-probe JSON /
187
+ + // nvidia-smi; bw_gbps > 0 forces a manual override (0 = autodetect/default).
188
+ + bool pcie_autodetect = true;
189
+ + // opencoti F5-opt W2 (#290) iqk CPU flash-attention β€” see docs/features/advanced_kv.md
190
+ + // Engage the vendored ik_llama.cpp CPU Flash-Attention engine on the M2
191
+ + // CPU-half (token-generation path). Off by default; flag-off is byte-identical.
192
+ + // x86_64-only at runtime (the engine compiles only on the AVX2 x86_64 slice).
193
+ + bool iqk_flash_attn = false;
194
+ + float pcie_bw_gbps = 0.0f;
195
+ + // opencoti F4 M3 Phase 2 β€” see docs/decisions/0001-lazy-slot-context.md
196
+ + // Initial KV-cell allocation per sequence. 0 = full n_ctx_seq allocation
197
+ + // (Phase 1 behavior β€” only zero-fill is deferred). Positive values cap
198
+ + // the initial allocation and enable grow-on-demand up to n_ctx_seq.
199
+ + int32_t slot_initial_ctx = 0;
200
+ + // opencoti F4 M3 Phase 3 β€” see docs/decisions/0001-lazy-slot-context.md
201
+ + // Idle threshold (ms) after which a grown soft cap is shrunk back to
202
+ + // slot_initial_ctx and the over-cap pages are decommitted via
203
+ + // posix_madvise. 0 disables shrink-on-idle (Phase 2 behavior).
204
+ + int32_t slot_shrink_idle_ms = 0;
205
+ + // opencoti F5 M2 headinfer β€” see docs/features/advanced_kv.md
206
+ + // Fraction of KV heads kept GPU-resident; the rest are offloaded to host
207
+ + // memory and streamed back per step. 1.0 = off (no split). Only active for
208
+ + // GPU-offloaded layers with flash-attention (non-transposed V cache).
209
+ + float headinfer_gpu_heads_frac = 1.0f;
210
+ + // opencoti F5 M3 neo β€” see docs/features/advanced_kv.md
211
+ + // 0 = off (default; M2 concat path runs), 1 = on (engage when split
212
+ + // active for the layer), 2 = auto (reserved for future load-based
213
+ + // engagement; same as `on` today).
214
+ + uint32_t neo_pipeline_mode = 0;
215
+ // opencoti F5-opt W1 (#293) pcie profile β€” see docs/features/rolling_kv.md
216
+ // Boot-time host<->device bandwidth + PCIe link detection, consumed by M7
217
+ // Rolling KV (#296) tile sizing. autodetect reads the rebar-probe JSON /
218
+ diff --git a/llama.cpp/ggml/include/ggml.h b/llama.cpp/ggml/include/ggml.h
219
+ --- a/llama.cpp/ggml/include/ggml.h
220
+ +++ b/llama.cpp/ggml/include/ggml.h
221
+ @@ -583,6 +583,8 @@ extern "C" {
222
+
223
+ GGML_OP_GLU,
224
+
225
+ + GGML_OP_MOE_FUSED_UP_GATE, // opencoti-hook: f5-opt-fmoe β€” W3 #292
226
+ +
227
+ GGML_OP_COUNT,
228
+ };
229
+
230
+ @@ -1437,6 +1439,22 @@ extern "C" {
231
+ struct ggml_tensor * b,
232
+ struct ggml_tensor * ids);
233
+
234
+ + // opencoti-hook: f5-opt-fmoe β€” W3 #292: op-level fused MoE up+gate+GLU.
235
+ + // build_moe_ffn otherwise emits {mul_mat_id(up), mul_mat_id(gate), glu}
236
+ + // as 3 nodes that the CUDA backend can only fuse via the fragile
237
+ + // ggml_can_fuse_subgraph graph-pattern path (which does not engage at
238
+ + // decode for Gemma-4). This single op lets the backend run the already
239
+ + // existing fused mmvq kernel directly. Output is GLU(gate@cur) (.) (up@cur),
240
+ + // matching ggml_geglu_split(gate, up). up_exps/gate_exps: expert weight
241
+ + // stacks (same type); cur: input activations; ids: expert selection.
242
+ + GGML_API struct ggml_tensor * ggml_moe_up_gate(
243
+ + struct ggml_context * ctx,
244
+ + struct ggml_tensor * up_exps,
245
+ + struct ggml_tensor * gate_exps,
246
+ + struct ggml_tensor * cur,
247
+ + struct ggml_tensor * ids,
248
+ + enum ggml_glu_op glu_op);
249
+ +
250
+ // A: m columns, n rows,
251
+ // B: p columns, n rows,
252
+ // result is m columns, p rows
253
+ diff --git a/llama.cpp/ggml/src/ggml-cpu/ggml-cpu.c b/llama.cpp/ggml/src/ggml-cpu/ggml-cpu.c
254
+ --- a/llama.cpp/ggml/src/ggml-cpu/ggml-cpu.c
255
+ +++ b/llama.cpp/ggml/src/ggml-cpu/ggml-cpu.c
256
+ @@ -2066,6 +2066,13 @@ static void ggml_compute_forward(struct ggml_compute_params * params, struct ggm
257
+ {
258
+ ggml_compute_forward_glu(params, tensor);
259
+ } break;
260
+ + case GGML_OP_MOE_FUSED_UP_GATE:
261
+ + {
262
+ + // opencoti-hook: f5-opt-fmoe β€” W3 #292. CUDA-only op. The
263
+ + // build_moe_ffn graph hook emits it only when the CUDA backend
264
+ + // owns the FFN nodes, so the CPU scheduler never reaches here.
265
+ + GGML_ABORT("MOE_FUSED_UP_GATE has no CPU implementation (CUDA-only op)");
266
+ + } break;
267
+ case GGML_OP_GET_REL_POS:
268
+ {
269
+ ggml_compute_forward_get_rel_pos(params, tensor);
270
+ @@ -2333,6 +2340,12 @@ static int ggml_get_n_tasks(struct ggml_tensor * node, int n_threads) {
271
+ GGML_ABORT("fatal error");
272
+ }
273
+ break;
274
+ + case GGML_OP_MOE_FUSED_UP_GATE:
275
+ + {
276
+ + // opencoti-hook: f5-opt-fmoe β€” W3 #292. CUDA-only; never
277
+ + // scheduled on CPU. Single task keeps the planner well-formed.
278
+ + n_tasks = 1;
279
+ + } break;
280
+ case GGML_OP_SILU_BACK:
281
+ case GGML_OP_MUL:
282
+ case GGML_OP_DIV:
283
+ diff --git a/llama.cpp/ggml/src/ggml-cuda/ggml-cuda.cu b/llama.cpp/ggml/src/ggml-cuda/ggml-cuda.cu
284
+ --- a/llama.cpp/ggml/src/ggml-cuda/ggml-cuda.cu
285
+ +++ b/llama.cpp/ggml/src/ggml-cuda/ggml-cuda.cu
286
+ @@ -3005,6 +3005,25 @@ static bool ggml_cuda_compute_forward(ggml_backend_cuda_context & ctx, struct gg
287
+ case GGML_OP_MUL_MAT_ID:
288
+ ggml_cuda_mul_mat_id(ctx, dst);
289
+ break;
290
+ + case GGML_OP_MOE_FUSED_UP_GATE:
291
+ + {
292
+ + // opencoti-hook: f5-opt-fmoe β€” W3 #292. Drive the existing fused
293
+ + // mmvq-id kernel directly: up = src0 @ src1, gate = fusion.gate @
294
+ + // src1, out = act(gate) (.) up. Byte-identical to the graph-pattern
295
+ + // fusion path (see the { MUL_MAT_ID, MUL_MAT_ID, GLU } branch in
296
+ + // the eval fusion loop). Decode-only (ne[2]==1): the build_moe_ffn
297
+ + // hook only emits this at n_tokens==1 and supports_op enforces it.
298
+ + const ggml_tensor * up_exps = dst->src[0];
299
+ + const ggml_tensor * gate_exps = dst->src[1];
300
+ + const ggml_tensor * cur = dst->src[2];
301
+ + const ggml_tensor * ids = dst->src[3];
302
+ + GGML_ASSERT(dst->ne[2] == 1 && "MOE_FUSED_UP_GATE is decode-only (n_tokens==1)");
303
+ + ggml_cuda_mm_fusion_args_host fusion_data{};
304
+ + fusion_data.gate = gate_exps;
305
+ + fusion_data.glu_op = (ggml_glu_op) dst->op_params[0];
306
+ + ggml_cuda_mul_mat_vec_q(ctx, up_exps, cur, ids, dst, &fusion_data);
307
+ + }
308
+ + break;
309
+ case GGML_OP_OUT_PROD:
310
+ ggml_cuda_out_prod(ctx, dst);
311
+ break;
312
+ @@ -5210,6 +5229,44 @@ static bool GGML_CALL ggml_backend_cuda_device_supports_op(ggml_backend_dev_t de
313
+ return false;
314
+ }
315
+ break;
316
+ + case GGML_OP_MOE_FUSED_UP_GATE:
317
+ + {
318
+ + // opencoti-hook: f5-opt-fmoe β€” W3 #292. Supported only in the
319
+ + // exact shape the fused mmvq-id decode kernel handles: quantized
320
+ + // up/gate expert stacks of identical type, F32 activations, I32
321
+ + // ids, single-token (decode) output, non-split weights, and a GLU
322
+ + // op the kernel implements explicitly (GEGLU/SWIGLU/SWIGLU_OAI β€”
323
+ + // others silently degrade to a plain product). build_moe_ffn only
324
+ + // emits the op when all of these hold, so supports_op and the hook
325
+ + // stay consistent (the scheduler never strands it on the CPU).
326
+ + const ggml_tensor * up_exps = op->src[0];
327
+ + const ggml_tensor * gate_exps = op->src[1];
328
+ + const ggml_tensor * cur = op->src[2];
329
+ + const ggml_tensor * ids = op->src[3];
330
+ + if (!up_exps || !gate_exps || !cur || !ids) {
331
+ + return false;
332
+ + }
333
+ + if (up_exps->buffer && ggml_backend_buft_is_cuda_split(up_exps->buffer->buft)) {
334
+ + return false;
335
+ + }
336
+ + if (!ggml_is_quantized(up_exps->type) || up_exps->type != gate_exps->type) {
337
+ + return false;
338
+ + }
339
+ + if (cur->type != GGML_TYPE_F32 || ids->type != GGML_TYPE_I32) {
340
+ + return false;
341
+ + }
342
+ + if (op->ne[2] != 1) {
343
+ + return false;
344
+ + }
345
+ + switch ((ggml_glu_op) op->op_params[0]) {
346
+ + case GGML_GLU_OP_GEGLU:
347
+ + case GGML_GLU_OP_SWIGLU:
348
+ + case GGML_GLU_OP_SWIGLU_OAI:
349
+ + return true;
350
+ + default:
351
+ + return false;
352
+ + }
353
+ + }
354
+ case GGML_OP_MUL_MAT:
355
+ case GGML_OP_MUL_MAT_ID:
356
+ {
357
+ diff --git a/llama.cpp/ggml/src/ggml.c b/llama.cpp/ggml/src/ggml.c
358
+ --- a/llama.cpp/ggml/src/ggml.c
359
+ +++ b/llama.cpp/ggml/src/ggml.c
360
+ @@ -1078,9 +1078,10 @@ static const char * GGML_OP_NAME[GGML_OP_COUNT] = {
361
+ "OPT_STEP_SGD",
362
+
363
+ "GLU",
364
+ + "MOE_FUSED_UP_GATE",
365
+ };
366
+
367
+ -static_assert(GGML_OP_COUNT == 96, "GGML_OP_COUNT != 96");
368
+ +static_assert(GGML_OP_COUNT == 97, "GGML_OP_COUNT != 97");
369
+
370
+ static const char * GGML_OP_SYMBOL[GGML_OP_COUNT] = {
371
+ "none",
372
+ @@ -1188,9 +1189,10 @@ static const char * GGML_OP_SYMBOL[GGML_OP_COUNT] = {
373
+ "sgd(x)",
374
+
375
+ "glu(x)",
376
+ + "moe_up_gate(up,gate,x)",
377
+ };
378
+
379
+ -static_assert(GGML_OP_COUNT == 96, "GGML_OP_COUNT != 96");
380
+ +static_assert(GGML_OP_COUNT == 97, "GGML_OP_COUNT != 97");
381
+
382
+ static_assert(GGML_OP_POOL_COUNT == 2, "GGML_OP_POOL_COUNT != 2");
383
+
384
+ @@ -3314,6 +3316,53 @@ struct ggml_tensor * ggml_mul_mat_id(
385
+ return result;
386
+ }
387
+
388
+ +// ggml_moe_up_gate
389
+ +// opencoti-hook: f5-opt-fmoe β€” W3 #292. Op-level fusion of the two parallel
390
+ +// expert matmuls (up = up_exps @ cur, gate = gate_exps @ cur) plus the GLU
391
+ +// activation that build_moe_ffn otherwise emits as 3 separate graph nodes.
392
+ +// Output == ggml_geglu_split(gate, up) == act(gate) (.) up, where the CUDA
393
+ +// backend runs the existing fused mmvq kernel (act on the gate projection,
394
+ +// multiply by the up projection β€” see ggml-cuda/mmvq.cu). Mirrors
395
+ +// ggml_mul_mat_id's shape contract: up_exps is the primary "as" weight, cur
396
+ +// the "b" activations shared by both projections, ids the expert selection.
397
+ +
398
+ +struct ggml_tensor * ggml_moe_up_gate(
399
+ + struct ggml_context * ctx,
400
+ + struct ggml_tensor * up_exps,
401
+ + struct ggml_tensor * gate_exps,
402
+ + struct ggml_tensor * cur,
403
+ + struct ggml_tensor * ids,
404
+ + enum ggml_glu_op glu_op) {
405
+ + GGML_ASSERT(!ggml_is_transposed(up_exps));
406
+ + GGML_ASSERT(!ggml_is_transposed(gate_exps));
407
+ + GGML_ASSERT(ids->type == GGML_TYPE_I32);
408
+ +
409
+ + // up and gate are parallel expert stacks: identical shape + type so one
410
+ + // shared-activation fused mmvq can drive both weight sets.
411
+ + GGML_ASSERT(ggml_are_same_shape(up_exps, gate_exps));
412
+ + GGML_ASSERT(up_exps->type == gate_exps->type);
413
+ +
414
+ + GGML_ASSERT(up_exps->ne[3] == 1); // 3d (one matrix per expert)
415
+ + GGML_ASSERT(cur->ne[3] == 1); // cur is 3d
416
+ + GGML_ASSERT(ids->ne[2] == 1 && ids->ne[3] == 1); // ids is 2d
417
+ + GGML_ASSERT(ids->ne[1] == cur->ne[2]); // one expert list per cur row
418
+ + GGML_ASSERT(up_exps->ne[0] == cur->ne[0]); // can_mul_mat
419
+ + GGML_ASSERT(ids->ne[0] % cur->ne[1] == 0); // can broadcast
420
+ +
421
+ + const int64_t ne[4] = { up_exps->ne[1], ids->ne[0], cur->ne[2], 1 };
422
+ + struct ggml_tensor * result = ggml_new_tensor(ctx, GGML_TYPE_F32, 4, ne);
423
+ +
424
+ + ggml_set_op_params_i32(result, 0, (int32_t) glu_op);
425
+ +
426
+ + result->op = GGML_OP_MOE_FUSED_UP_GATE;
427
+ + result->src[0] = up_exps;
428
+ + result->src[1] = gate_exps;
429
+ + result->src[2] = cur;
430
+ + result->src[3] = ids;
431
+ +
432
+ + return result;
433
+ +}
434
+ +
435
+ // ggml_out_prod
436
+
437
+ static inline bool ggml_can_out_prod(const struct ggml_tensor * t0, const struct ggml_tensor * t1) {
438
+ diff --git a/llama.cpp/include/llama.h b/llama.cpp/include/llama.h
439
+ --- a/llama.cpp/include/llama.h
440
+ +++ b/llama.cpp/include/llama.h
441
+ @@ -362,6 +362,9 @@ extern "C" {
442
+ // 0 = off (default), 1 = on, 2 = auto. Effective only when the
443
+ // M2 head split is active for a given layer.
444
+ uint32_t neo_pipeline_mode;
445
+ + // opencoti F5 W3 #292 fused-MoE β€” see docs/features/advanced_kv.md
446
+ + // true = fuse decode MoE up+gate+GLU into one op. false = off (default).
447
+ + bool fused_moe_up_gate;
448
+ uint32_t n_rs_seq; // number of recurrent-state snapshots per seq for rollback (0 = no rollback) [EXPERIMENTAL]
449
+ int32_t n_threads; // number of threads to use for generation
450
+ int32_t n_threads_batch; // number of threads to use for batch processing
451
+ diff --git a/llama.cpp/src/llama-context.cpp b/llama.cpp/src/llama-context.cpp
452
+ --- a/llama.cpp/src/llama-context.cpp
453
+ +++ b/llama.cpp/src/llama-context.cpp
454
+ @@ -66,6 +66,8 @@ llama_context::llama_context(
455
+ cparams.headinfer_gpu_heads_frac = params.headinfer_gpu_heads_frac;
456
+ // opencoti F5 M3 neo β€” see docs/features/advanced_kv.md
457
+ cparams.neo_pipeline_mode = params.neo_pipeline_mode;
458
+ + // opencoti F5 W3 #292 fused-MoE β€” see docs/features/advanced_kv.md
459
+ + cparams.fused_moe_up_gate = params.fused_moe_up_gate;
460
+
461
+ cparams.n_threads = params.n_threads;
462
+ cparams.n_threads_batch = params.n_threads_batch;
463
+ @@ -3359,6 +3361,7 @@ llama_context_params llama_context_default_params() {
464
+ /*.slot_shrink_idle_ms =*/ 0,
465
+ /*.headinfer_gpu_heads_frac =*/ 1.0f,
466
+ /*.neo_pipeline_mode =*/ 0, // opencoti F5 M3 neo β€” off by default
467
+ + /*.fused_moe_up_gate =*/ false, // opencoti F5 W3 #292 β€” off by default
468
+ /*.n_rs_seq =*/ 0,
469
+ /*.n_threads =*/ GGML_DEFAULT_N_THREADS, // TODO: better default
470
+ /*.n_threads_batch =*/ GGML_DEFAULT_N_THREADS,
471
+ diff --git a/llama.cpp/src/llama-cparams.h b/llama.cpp/src/llama-cparams.h
472
+ --- a/llama.cpp/src/llama-cparams.h
473
+ +++ b/llama.cpp/src/llama-cparams.h
474
+ @@ -32,6 +32,10 @@ struct llama_cparams {
475
+ // 2 = auto (same engagement as `on` for now β€” reserved for future
476
+ // load-based engagement heuristics).
477
+ uint32_t neo_pipeline_mode;
478
+ + // opencoti F5 W3 #292 fused-MoE β€” see docs/features/advanced_kv.md.
479
+ + // true = build_moe_ffn collapses the up/gate expert matmuls + GLU into one
480
+ + // GGML_OP_MOE_FUSED_UP_GATE op at decode (n_tokens==1). false = unfused path.
481
+ + bool fused_moe_up_gate;
482
+ uint32_t n_rs_seq; // number of recurrent-state snapshots per seq for rollback
483
+ int32_t n_threads; // number of threads to use for generation
484
+ int32_t n_threads_batch; // number of threads to use for batch processing
485
+ diff --git a/llama.cpp/src/llama-graph.cpp b/llama.cpp/src/llama-graph.cpp
486
+ --- a/llama.cpp/src/llama-graph.cpp
487
+ +++ b/llama.cpp/src/llama-graph.cpp
488
+ @@ -1534,7 +1534,28 @@ ggml_tensor * llm_graph_context::build_moe_ffn(
489
+ ggml_tensor * up = nullptr;
490
+ ggml_tensor * experts = nullptr;
491
+
492
+ - if (gate_up_exps) {
493
+ + // opencoti-hook: f5-opt-fmoe β€” W3 #292: collapse the separate-up/gate
494
+ + // expert matmuls + the GLU activation into ONE GGML_OP_MOE_FUSED_UP_GATE
495
+ + // op at decode, so the CUDA backend runs the existing fused mmvq kernel
496
+ + // without relying on graph-pattern fusion (ggml_can_fuse_subgraph, which
497
+ + // does not engage for Gemma-4 at decode). Guards mirror supports_op:
498
+ + // decode only (n_tokens==1), separate up/gate of identical type, no
499
+ + // bias/scale, and GELU→GEGLU or SILU→SWIGLU. Off path is byte-identical.
500
+ + bool fused_up_gate = false;
501
+ + if (cparams.fused_moe_up_gate && gate_exps && !gate_up_exps &&
502
+ + !up_exps_b && !gate_exps_b && !up_exps_s && !gate_exps_s &&
503
+ + up_exps->type == gate_exps->type && n_tokens == 1 &&
504
+ + (type_op == LLM_FFN_GELU || type_op == LLM_FFN_SILU)) {
505
+ + const enum ggml_glu_op glu_op = (type_op == LLM_FFN_GELU)
506
+ + ? GGML_GLU_OP_GEGLU : GGML_GLU_OP_SWIGLU;
507
+ + cur = ggml_moe_up_gate(ctx0, up_exps, gate_exps, cur, selected_experts, glu_op);
508
+ + cb(cur, "ffn_moe_fused_up_gate", il);
509
+ + fused_up_gate = true;
510
+ + }
511
+ +
512
+ + if (fused_up_gate) {
513
+ + // cur already holds act(gate) (.) up from the fused op
514
+ + } else if (gate_up_exps) {
515
+ // merged gate_up path: one mul_mat_id, then split into gate and up views
516
+ ggml_tensor * gate_up = build_lora_mm_id(gate_up_exps, cur, selected_experts); // [n_ff*2, n_expert_used, n_tokens]
517
+ cb(gate_up, "ffn_moe_gate_up", il);
518
+ @@ -1601,6 +1622,9 @@ ggml_tensor * llm_graph_context::build_moe_ffn(
519
+
520
+ const bool has_gate = gate_exps || gate_up_exps;
521
+
522
+ + // opencoti-hook: f5-opt-fmoe β€” W3 #292: the fused op already applied the
523
+ + // GLU activation; skip the separate activation pass.
524
+ + if (!fused_up_gate)
525
+ switch (type_op) {
526
+ case LLM_FFN_SILU:
527
+ if (gate_exps) {
patches/0070-rolling-kv.patch ADDED
The diff for this file is too large to render. See raw diff
 
patches/0071-state-io-cosmocc.patch ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ From: opencoti
2
+ Subject: [PATCH 0071] cosmocc state-IO open fix (bug-269)
3
+
4
+ opencoti F5 M7 (#349, bug-269). Orthogonal to Rolling KV (0070) β€” captured as
5
+ its own slot. Fixes ALL llama_state_*_save/load_file in the cosmocc/llamafile
6
+ build, which were broken before this: llama.cpp/src/llama-mmap.cpp's COSMOCC
7
+ llama_file constructor routed every open() through llamafile_open_gguf(), which
8
+ pread()s the GGUF magic at offset 0 (EBADF on a write-only "wb" fd) and
9
+ zip-routes any non-GGUF read; and write_raw's fwrite(fp) ran with fp==NULL.
10
+ KV-state files are neither GGUF nor zip and are opened for write, so model
11
+ load was unaffected but state save/load never worked through the binary (the
12
+ M2 bug-222 "state round-trip" was thus never actually exercised end-to-end).
13
+
14
+ FIX (cosmocc/host-only; no CUDA, no kernel):
15
+ * vendors/sources/llamafile/llamafile/llamafile.{c,h} (Mozilla-Ocho llamafile,
16
+ Apache-2.0): add llamafile_open_plain() β€” a plain fopen-backed handle with
17
+ no GGUF magic probe and no zip routing, for non-model files. It delegates
18
+ to the existing static llamafile_open_file().
19
+ * llama.cpp/src/llama-mmap.cpp: the COSMOCC llama_file ctor falls back to
20
+ llamafile_open_plain() when llamafile_open_gguf() returns NULL (models are
21
+ unaffected β€” they still open via the gguf path) and sets fp =
22
+ llamafile_fp(lfile) so write_raw has a valid stream.
23
+
24
+ Provenance: opencoti-original. Touches Mozilla-Ocho llamafile.{c,h}
25
+ (Apache-2.0) and upstream llama.cpp llama-mmap.cpp. Applies after 0070
26
+ (independent file set β€” order immaterial); reverse-applies clean.
27
+ diff --git a/llama.cpp/src/llama-mmap.cpp b/llama.cpp/src/llama-mmap.cpp
28
+ --- a/llama.cpp/src/llama-mmap.cpp
29
+ +++ b/llama.cpp/src/llama-mmap.cpp
30
+ @@ -186,11 +186,25 @@ struct llama_file::impl {
31
+ #else
32
+ impl(const char * fname, const char * mode, [[maybe_unused]] const bool use_direct_io = false) : fname(fname) {
33
+ #ifdef COSMOCC
34
+ - // [llamafile] Use llamafile_open_gguf for all file opening
35
+ + // [llamafile] Use llamafile_open_gguf for model files (raw .gguf,
36
+ + // foo.zip@weights, /zip/ paths, and .llamafile archives via zip routing).
37
+ lfile = llamafile_open_gguf(fname, mode);
38
+ + // opencoti bug-269: llamafile_open_gguf is model-oriented β€” it pread()s the
39
+ + // GGUF magic at offset 0 and routes non-GGUF files into a zip parser. KV
40
+ + // state files (llama_state_*_save_file) are neither GGUF nor zip and are
41
+ + // opened for write ("wb" β†’ pread EBADF). Both made every state save/restore
42
+ + // fail in the cosmocc build. When the model opener declines, fall back to a
43
+ + // plain fopen-backed handle (model loads are unaffected β€” they succeed above).
44
+ + if (lfile == NULL) {
45
+ + lfile = llamafile_open_plain(fname, mode);
46
+ + }
47
+ if (lfile == NULL) {
48
+ throw std::runtime_error(format("failed to open %s: %s", fname, strerror(errno)));
49
+ }
50
+ + // write_raw() writes via std::fwrite(fp); the cosmocc open path left fp NULL
51
+ + // (read-only design). Expose the underlying FILE* so state-save writes land.
52
+ + // Harmless for reads (read_raw_unsafe uses llamafile_read(lfile)).
53
+ + fp = llamafile_fp(lfile);
54
+ size = llamafile_size(lfile);
55
+ return;
56
+ #endif
57
+ diff --git a/llamafile/llamafile.c b/llamafile/llamafile.c
58
+ --- a/llamafile/llamafile.c
59
+ +++ b/llamafile/llamafile.c
60
+ @@ -318,6 +318,15 @@ struct llamafile *llamafile_open_gguf(const char *fname, const char *mode) {
61
+ return llamafile_open_zip(fname, 0, mode);
62
+ }
63
+
64
+ +// opencoti bug-269: plain fopen-backed handle for non-model files (KV state
65
+ +// save/load). llamafile_open_gguf() is model-oriented β€” it pread()s the GGUF
66
+ +// magic at offset 0 (which fails EBADF on a write-only "wb" fd) and routes any
67
+ +// non-GGUF file into a zip parser. State files are neither GGUF nor zip and are
68
+ +// opened for write, so they need a path with no magic probe and no zip routing.
69
+ +struct llamafile *llamafile_open_plain(const char *fname, const char *mode) {
70
+ + return llamafile_open_file(fname, mode);
71
+ +}
72
+ +
73
+ FILE *llamafile_fp(struct llamafile *file) {
74
+ return file->fp;
75
+ }
76
+ diff --git a/llamafile/llamafile.h b/llamafile/llamafile.h
77
+ --- a/llamafile/llamafile.h
78
+ +++ b/llamafile/llamafile.h
79
+ @@ -46,6 +46,9 @@ extern int FLAG_verbose; // Verbose output (chatbot_main.cpp, metal.c, cu
80
+
81
+ struct llamafile;
82
+ struct llamafile *llamafile_open_gguf(const char *, const char *); // UNUSED externally
83
+ +// opencoti bug-269: plain fopen-backed handle (no GGUF magic probe, no zip
84
+ +// routing) for non-model files such as KV state save/load.
85
+ +struct llamafile *llamafile_open_plain(const char *, const char *);
86
+ void llamafile_close(struct llamafile *); // UNUSED externally
87
+ long llamafile_read(struct llamafile *, void *, size_t); // UNUSED externally
88
+ long llamafile_write(struct llamafile *, const void *, size_t); // UNUSED externally
patches/0072-poly-kv-pool.patch ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ From: opencoti
2
+ Subject: [PATCH 0072] F5 M6 PolyKV S1 β€” SharedKVPool shared read-only prefix
3
+
4
+ Server-side SharedKVPool (F5 M6 S1, #370): a read-only KV prefix shared across
5
+ sequences/slots so N agents over one base context pay O(1) shared-prefix memory
6
+ instead of O(N). Per-request opt-in via the `shared_pool_slot` (>=0 selects the
7
+ pool donor slot) and `shared_prefix_n_tokens` task params; default-off (slot -1)
8
+ is byte-identical to upstream slot handling. Pure server/task plumbing β€” no ggml
9
+ or kernel change.
10
+
11
+ Bench: perf/llamafile/advanced-kv-stack.bench.ts (A4/S'); gate .opencoti/m6-s1-shared-pool-gate.sh
12
+ Milestone: F5 M6 PolyKV S1 β€” see docs/features/poly_kv.md
13
+ Applies after: 0071 (numbered 0072 so lexical apply order lands it after the M7
14
+ rolling-KV 0070/0071 it composes with; the original 0060 slot predates 0070).
15
+
16
+ diff --git a/llama.cpp/tools/server/server-context.cpp b/llama.cpp/tools/server/server-context.cpp
17
+ --- a/llama.cpp/tools/server/server-context.cpp
18
+ +++ b/llama.cpp/tools/server/server-context.cpp
19
+ @@ -2907,6 +2907,48 @@ private:
20
+ }
21
+ }
22
+
23
+ + // opencoti-hook: poly-kv-pool β€” see docs/features/poly_kv.md
24
+ + // F5 M6 PolyKV S1: bit-share a pool slot's read-only prefix into this seq
25
+ + // (zero-copy cells.seq_add in unified mode) and skip re-processing it.
26
+ + // Caller contract: this request's prompt MUST begin with exactly the pool
27
+ + // slot's first shared_prefix_n_tokens tokens, and the pool slot must remain
28
+ + // resident while sharers reference it. RESIDENCY PREREQUISITE: the server
29
+ + // must be booted with --no-clear-idle. The upstream idle-slot prompt-cache
30
+ + // offload (clear_idle, default-on with --kv-unified + --cache-ram; PR #16391)
31
+ + // serializes an idle slot's KV to a host blob and CLEARS its device cells the
32
+ + // moment a new task launches β€” which would evict the pool prefix before any
33
+ + // sharer's seq_cp runs (seq would be empty β†’ silent miss). We guard against
34
+ + // that below: if the pool's resident range does not cover [0,P), we skip the
35
+ + // share and fall through to normal full re-processing (correct, just slower).
36
+ + // We mirror the copy_state_to() precedent: after seq_cp we also set this
37
+ + // slot's token bookkeeping to the first P tokens, else keep_first(n_past)
38
+ + // asserts on the (empty) slot token list (server-common.cpp:397).
39
+ + if (slot.task->params.shared_pool_slot >= 0 &&
40
+ + slot.task->params.shared_prefix_n_tokens > 0 &&
41
+ + slot.task->params.shared_pool_slot != slot.id) {
42
+ + const int pool = slot.task->params.shared_pool_slot;
43
+ + int32_t P = std::min<int32_t>(slot.task->params.shared_prefix_n_tokens,
44
+ + (int32_t) input_tokens.size());
45
+ + auto * mem = llama_get_memory(ctx_tgt);
46
+ + // residency guard: the pool seq must actually hold cells covering [0,P)
47
+ + const llama_pos pool_max = llama_memory_seq_pos_max(mem, pool);
48
+ + if (pool_max < (llama_pos) (P - 1)) {
49
+ + SLT_WRN(slot, "poly-kv-pool: pool slot %d not resident to %d (max pos %d) β€” "
50
+ + "skipping share, full reprocess (boot with --no-clear-idle)\n",
51
+ + pool, (int) (P - 1), (int) pool_max);
52
+ + } else {
53
+ + // clear any stale KV on this slot, then bit-share the pool's [0,P) prefix
54
+ + llama_memory_seq_rm(mem, slot.id, -1, -1);
55
+ + llama_memory_seq_cp(mem, pool, slot.id, 0, P);
56
+ + // mirror token bookkeeping so keep_first(n_past)/pos_next stay consistent
57
+ + server_tokens shared = input_tokens.clone();
58
+ + shared.keep_first(P);
59
+ + slot.prompt.tokens = std::move(shared);
60
+ + n_past = P;
61
+ + SLT_INF(slot, "poly-kv-pool: shared %d-token prefix from slot %d (n_past -> %d)\n", (int) P, pool, n_past);
62
+ + }
63
+ + }
64
+ +
65
+ // [TAG_PROMPT_LOGITS]
66
+ if (n_past == slot.task->n_tokens() && n_past > 0) {
67
+ SLT_WRN(slot, "need to evaluate at least 1 token for each active slot (n_past = %d, task.n_tokens() = %d)\n", n_past, slot.task->n_tokens());
68
+ diff --git a/llama.cpp/tools/server/server-task.cpp b/llama.cpp/tools/server/server-task.cpp
69
+ --- a/llama.cpp/tools/server/server-task.cpp
70
+ +++ b/llama.cpp/tools/server/server-task.cpp
71
+ @@ -273,6 +273,8 @@ task_params server_task::params_from_json_cmpl(
72
+ params.n_cmpl = json_value(data, "n_cmpl", json_value(data, "n", 1));
73
+ params.n_cache_reuse = json_value(data, "n_cache_reuse", defaults.n_cache_reuse);
74
+ params.session_id = json_value(data, "session_id", std::string("")); // opencoti F5 M0 kv-reuse β€” see docs/features/advanced_kv.md
75
+ + params.shared_pool_slot = json_value(data, "shared_pool_slot", -1); // opencoti-hook: poly-kv-pool β€” see docs/features/poly_kv.md
76
+ + params.shared_prefix_n_tokens = json_value(data, "shared_prefix_n_tokens", 0); // opencoti-hook: poly-kv-pool β€” see docs/features/poly_kv.md
77
+ //params.t_max_prompt_ms = json_value(data, "t_max_prompt_ms", defaults.t_max_prompt_ms); // TODO: implement
78
+ params.t_max_predict_ms = json_value(data, "t_max_predict_ms", defaults.t_max_predict_ms);
79
+ params.response_fields = json_value(data, "response_fields", std::vector<std::string>());
80
+ diff --git a/llama.cpp/tools/server/server-task.h b/llama.cpp/tools/server/server-task.h
81
+ --- a/llama.cpp/tools/server/server-task.h
82
+ +++ b/llama.cpp/tools/server/server-task.h
83
+ @@ -72,6 +72,15 @@ struct task_params {
84
+ // number of prompt tokens before the latest user message
85
+ int32_t n_before_user = -1;
86
+
87
+ + // opencoti-hook: poly-kv-pool β€” see docs/features/poly_kv.md
88
+ + // F5 M6 PolyKV S1 (SharedKVPool). When shared_pool_slot >= 0, the server
89
+ + // bit-shares this request's [0, shared_prefix_n_tokens) KV with the pool
90
+ + // slot's already-resident read-only prefix (zero-copy cells.seq_add in
91
+ + // unified mode), so N agents share ONE physical copy of the prefix and the
92
+ + // prefix is neither re-stored nor re-processed. <0 (default) is fully inert.
93
+ + int32_t shared_pool_slot = -1;
94
+ + int32_t shared_prefix_n_tokens = 0;
95
+ +
96
+ int64_t t_max_prompt_ms = -1; // TODO: implement
97
+ int64_t t_max_predict_ms = -1; // if positive, limit the generation phase to this time limit
98
+
patches/0073-turboquant-kv.patch ADDED
The diff for this file is too large to render. See raw diff
 
patches/0075-d512-turbo-vec.patch ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ From: opencoti
2
+ Subject: [PATCH 0075] F5 M6 PolyKV S2 β€” D=512 fused FA-VEC turbo2/turbo3 (Gemma-4 GLOBAL)
3
+
4
+ #411. The Level-B fused FA-VEC turbo read (0073) covered head_dim {128,256} only;
5
+ the Gemma-4 A4B GLOBAL layers use head_dim 512 (key_length=512) and therefore fell
6
+ back to the Level-A turbo->f16 materialize β€” a decode tax that scales with the
7
+ global-cache n_kv. This patch adds the head_dim-512 fused FA-VEC instances for
8
+ turbo2/turbo3 so the global cache stays IN-REGISTER:
9
+
10
+ - fattn-vec.cuh: extern DECL_FATTN_VEC_CASE(512, turbo2/3) (the kernel is already
11
+ D-generic: nthreads/nthreads_V derive from D arithmetically, KQ smem ~4-8KB,
12
+ VKQ ~16 regs at ncols=1 β€” well within budget; verified compile + run clean).
13
+ - fattn.cu: turbo kernel-selection relax (Q->ne[0]==512 routes to VEC) + the two
14
+ D=512 FATTN_VEC_CASE dispatch lines for turbo2/turbo3.
15
+ - template-instances/fattn-vec-instance-turbo{2,3}_0: emit the D=512 case; the
16
+ autogenerator (generate_cu_files.py) emits the same line so re-gen stays exact.
17
+ - llama-graph.cpp: build_attn_mha fuse gate extended (fused_turbo_512) so D=512
18
+ turbo2/turbo3 decode ubatches fuse; turbo4/turbo8 @ 512 keep Level-A.
19
+
20
+ Correctness is Parseval-EXACT: the WHT is block-diagonal over 128-blocks, so D=512
21
+ decomposes into 4 independent Parseval-exact 128-blocks (WHT(Q/s).K_stored ==
22
+ Q.K_true). Acceptance = LOGIT-EQUIVALENCE fused-vs-materialize (#355 harness),
23
+ real_frac=0.0 for BOTH turbo3 and turbo2 (bit-equivalent, not merely close).
24
+ Perf (the point): turbo3 @ ctx 27.7k, fused 41.0 vs materialize 29.7 tok/s = +38%,
25
+ scaling with n_kv. Gate: .opencoti/p411-d512-gate.sh (+ p411-perf.sh).
26
+
27
+ diff --git a/llama.cpp/ggml/src/ggml-cuda/fattn-vec.cuh b/llama.cpp/ggml/src/ggml-cuda/fattn-vec.cuh
28
+ --- a/llama.cpp/ggml/src/ggml-cuda/fattn-vec.cuh
29
+ +++ b/llama.cpp/ggml/src/ggml-cuda/fattn-vec.cuh
30
+ @@ -618,3 +618,8 @@ EXTERN_DECL_FATTN_VEC_CASES(256, GGML_TYPE_BF16)
31
+ EXTERN_DECL_FATTN_VEC_TURBO(GGML_TYPE_TURBO2_0)
32
+ EXTERN_DECL_FATTN_VEC_TURBO(GGML_TYPE_TURBO3_0)
33
+ EXTERN_DECL_FATTN_VEC_TURBO(GGML_TYPE_TURBO4_0)
34
+ +
35
+ +// opencoti #411 (F5 M6-S2 Level-B): head_dim 512 fused VEC for turbo2/3 ONLY β€” Gemma-4 GLOBAL
36
+ +// layers (key_length=512). Definitions in the turbo2/turbo3 instance .cu files. turbo4 stays ≀256.
37
+ +extern DECL_FATTN_VEC_CASE(512, GGML_TYPE_TURBO2_0, GGML_TYPE_TURBO2_0);
38
+ +extern DECL_FATTN_VEC_CASE(512, GGML_TYPE_TURBO3_0, GGML_TYPE_TURBO3_0);
39
+ diff --git a/llama.cpp/ggml/src/ggml-cuda/fattn.cu b/llama.cpp/ggml/src/ggml-cuda/fattn.cu
40
+ --- a/llama.cpp/ggml/src/ggml-cuda/fattn.cu
41
+ +++ b/llama.cpp/ggml/src/ggml-cuda/fattn.cu
42
+ @@ -337,6 +337,12 @@ static void ggml_cuda_flash_attn_ext_vec(ggml_backend_cuda_context & ctx, ggml_t
43
+ FATTN_VEC_CASES_ALL_D(GGML_TYPE_TURBO3_0, GGML_TYPE_TURBO3_0)
44
+ FATTN_VEC_CASES_ALL_D(GGML_TYPE_TURBO4_0, GGML_TYPE_TURBO4_0)
45
+
46
+ + // opencoti #411 (F5 M6-S2 Level-B): head_dim 512 fused VEC for turbo2/3 ONLY (Gemma-4 GLOBAL
47
+ + // layers). turbo4/turbo8 keep no D=512 instance (Level-A materialize). Matches the fattn.cu
48
+ + // kernel-selection relax + the L3 graph gate (both turbo2/3 + D==512 only).
49
+ + FATTN_VEC_CASE(512, GGML_TYPE_TURBO2_0, GGML_TYPE_TURBO2_0)
50
+ + FATTN_VEC_CASE(512, GGML_TYPE_TURBO3_0, GGML_TYPE_TURBO3_0)
51
+ +
52
+ GGML_ABORT("fatal error");
53
+ }
54
+
55
+ @@ -474,7 +480,13 @@ static best_fattn_kernel ggml_cuda_get_best_fattn_kernel(const int device, const
56
+ // read turbo blocks. The graph (build_attn_mha, L3) only emits turbo K/V to FLASH_ATTN_EXT when
57
+ // it intends this path (head_dim ≀ 256, decode); turbo8 / head_dim 512 take the materialize lift.
58
+ if (K->type == GGML_TYPE_TURBO2_0 || K->type == GGML_TYPE_TURBO3_0 || K->type == GGML_TYPE_TURBO4_0) {
59
+ - return can_use_vector_kernel ? BEST_FATTN_KERNEL_VEC : BEST_FATTN_KERNEL_NONE;
60
+ + // opencoti #411 (F5 M6-S2 Level-B): turbo2/3 also ship a D=512 fused VEC instance for Gemma-4
61
+ + // GLOBAL layers (key_length=512) so the 256k global cache reads in-register instead of the
62
+ + // Level-A turboβ†’f16 materialize. turbo4/turbo8 have no D=512 instance β†’ keep the ≀256 cap
63
+ + // (Level-A fallback at 512). The L3 graph gate only emits turbo2/3 K/V to FA at 512 to match.
64
+ + const bool turbo512_vec = Q->ne[0] == 512 && Q->ne[0] % 64 == 0 && K->ne[1] % FATTN_KQ_STRIDE == 0 &&
65
+ + (K->type == GGML_TYPE_TURBO2_0 || K->type == GGML_TYPE_TURBO3_0);
66
+ + return (can_use_vector_kernel || turbo512_vec) ? BEST_FATTN_KERNEL_VEC : BEST_FATTN_KERNEL_NONE;
67
+ }
68
+
69
+ // If Turing tensor cores are available, use them:
70
+ diff --git a/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-vec-instance-turbo2_0-turbo2_0.cu b/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-vec-instance-turbo2_0-turbo2_0.cu
71
+ --- a/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-vec-instance-turbo2_0-turbo2_0.cu
72
+ +++ b/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-vec-instance-turbo2_0-turbo2_0.cu
73
+ @@ -6,3 +6,4 @@
74
+ DECL_FATTN_VEC_CASE( 64, GGML_TYPE_TURBO2_0, GGML_TYPE_TURBO2_0);
75
+ DECL_FATTN_VEC_CASE(128, GGML_TYPE_TURBO2_0, GGML_TYPE_TURBO2_0);
76
+ DECL_FATTN_VEC_CASE(256, GGML_TYPE_TURBO2_0, GGML_TYPE_TURBO2_0);
77
+ +DECL_FATTN_VEC_CASE(512, GGML_TYPE_TURBO2_0, GGML_TYPE_TURBO2_0); // opencoti #411 (F5 M6-S2 Level-B): Gemma-4 GLOBAL head_dim 512
78
+ diff --git a/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-vec-instance-turbo3_0-turbo3_0.cu b/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-vec-instance-turbo3_0-turbo3_0.cu
79
+ --- a/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-vec-instance-turbo3_0-turbo3_0.cu
80
+ +++ b/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-vec-instance-turbo3_0-turbo3_0.cu
81
+ @@ -6,3 +6,4 @@
82
+ DECL_FATTN_VEC_CASE( 64, GGML_TYPE_TURBO3_0, GGML_TYPE_TURBO3_0);
83
+ DECL_FATTN_VEC_CASE(128, GGML_TYPE_TURBO3_0, GGML_TYPE_TURBO3_0);
84
+ DECL_FATTN_VEC_CASE(256, GGML_TYPE_TURBO3_0, GGML_TYPE_TURBO3_0);
85
+ +DECL_FATTN_VEC_CASE(512, GGML_TYPE_TURBO3_0, GGML_TYPE_TURBO3_0); // opencoti #411 (F5 M6-S2 Level-B): Gemma-4 GLOBAL head_dim 512
86
+ diff --git a/llama.cpp/ggml/src/ggml-cuda/template-instances/generate_cu_files.py b/llama.cpp/ggml/src/ggml-cuda/template-instances/generate_cu_files.py
87
+ --- a/llama.cpp/ggml/src/ggml-cuda/template-instances/generate_cu_files.py
88
+ +++ b/llama.cpp/ggml/src/ggml-cuda/template-instances/generate_cu_files.py
89
+ @@ -79,6 +79,10 @@ for type_k in TYPES_KV:
90
+ for type_t in ["GGML_TYPE_TURBO2_0", "GGML_TYPE_TURBO3_0", "GGML_TYPE_TURBO4_0"]:
91
+ with open(f"fattn-vec-instance-{get_short_name(type_t)}-{get_short_name(type_t)}.cu", "w") as f:
92
+ f.write(SOURCE_FATTN_VEC.format(type_k=type_t, type_v=type_t))
93
+ + # opencoti #411 (F5 M6-S2 Level-B): head_dim 512 fused VEC for turbo2/3 ONLY (Gemma-4 GLOBAL
94
+ + # layers, key_length=512). turbo4 has no D=512 instance (Level-A materialize fallback).
95
+ + if type_t in ("GGML_TYPE_TURBO2_0", "GGML_TYPE_TURBO3_0"):
96
+ + f.write(f"DECL_FATTN_VEC_CASE(512, {type_t}, {type_t}); // opencoti #411 (F5 M6-S2 Level-B): Gemma-4 GLOBAL head_dim 512\n")
97
+
98
+ for ncols in [8, 16, 32, 64]:
99
+ for ncols2 in [1, 2, 4, 8, 16, 32]:
100
+ diff --git a/llama.cpp/src/llama-graph.cpp b/llama.cpp/src/llama-graph.cpp
101
+ --- a/llama.cpp/src/llama-graph.cpp
102
+ +++ b/llama.cpp/src/llama-graph.cpp
103
+ @@ -2019,10 +2019,17 @@ ggml_tensor * llm_graph_context::build_attn_mha(
104
+ }();
105
+ const int64_t tbv_n_stream = k->ne[3] > 0 ? k->ne[3] : 1;
106
+ const int64_t tbv_nq_per_strm = q->ne[2] / tbv_n_stream;
107
+ + // opencoti F5 M6-S2 Level-B #411: head_dim 512 (Gemma-4 GLOBAL layers, key_length=512) fuses
108
+ + // for turbo2/turbo3 ONLY β€” the D=512 fused FA-VEC turbo instances added in #411 keep the 256k
109
+ + // global cache in-register instead of the Level-A turbo→f16 materialize (a ~0.55× decode tax that
110
+ + // scales with n_kv). turbo4/turbo8 at 512 still take the materialize fallback (no D=512 instance).
111
+ + // The WHT is block-diagonal over 128-blocks, so 512 = 4 Parseval-exact 128-blocks β†’ exact.
112
+ + const bool fused_turbo_512 =
113
+ + k->ne[0] == 512 && (k->type == GGML_TYPE_TURBO2_0 || k->type == GGML_TYPE_TURBO3_0);
114
+ const bool fused_turbo =
115
+ cparams.flash_attn && kq_b == nullptr &&
116
+ is_turbo_vec(k->type) && k->type == v->type &&
117
+ - (k->ne[0] == 128 || k->ne[0] == 256) &&
118
+ + (k->ne[0] == 128 || k->ne[0] == 256 || fused_turbo_512) &&
119
+ tbv_nq_per_strm <= tbv_fuse_max_nq;
120
+
121
+ if (fused_turbo) {
patches/0078-dca.patch ADDED
The diff for this file is too large to render. See raw diff
 
patches/0079-gemma-assistant-mtp.patch ADDED
The diff for this file is too large to render. See raw diff
 
patches/0080-mtp-d512-fa.patch ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ From: opencoti
2
+ Subject: [PATCH 0080] Gemma-4 assistant-MTP D512/gqa<=2 flash-attn β€” real f16 VEC kernel (#491+#493, bug-567)
3
+
4
+ The Gemma-4 E4B assistant-MTP drafter's single GLOBAL (non-SWA) layer is head_count 4 /
5
+ head_count_kv 2 -> gqa_ratio 2 at head_dim 512. On the 0.10.3 base NEITHER the MMA
6
+ (switch_ncols2, fattn.cu) NOR the TILE (switch_ncols2, fattn-tile.cuh) D512 path instantiates
7
+ ncols2 in {1,2} (gated DV<=256 for binary size), so a D512/gqa<=2 flash-attn op aborts at
8
+ runtime (bug-567). Every other Gemma-4 config takes ncols2 in {4,8} and is unaffected
9
+ (A4B/12B/31B and the E4B base are gqa>=4; A4B globals are head16/kv2 -> gqa 8).
10
+
11
+ #491 (host stop-gap): a guard in build_attn_mha (llama-graph.cpp) routed the D512/gqa<=2 layer
12
+ to the EXISTING non-FA explicit-softmax branch. Correct but not a real flash-attn β€” "fa off" on
13
+ the hot path.
14
+
15
+ #493 (this patch, the real fix): add a REAL D512 f16 flash-attn via the VEC kernel, and narrow
16
+ the #491 guard so f16/f32 K/V take it; the guard now fires ONLY for the residual with no D512
17
+ kernel (quantized / bf16 K/V), which the assistant drafter never hits.
18
+ - template-instances/fattn-vec-instance-f16-f16.cu + generate_cu_files.py:
19
+ DECL_FATTN_VEC_CASE(512, GGML_TYPE_F16, GGML_TYPE_F16) instance (the autogen line + the file).
20
+ - ggml-cuda/fattn-vec.cuh: extern decl for the D512 f16 instance.
21
+ - ggml-cuda/fattn.cu: (a) selector route in ggml_cuda_get_best_fattn_kernel β€” D512 && gqa_ratio<=2
22
+ && gqa_opt_applies && K/V f16 -> BEST_FATTN_KERNEL_VEC; (b) the matching VEC dispatch case
23
+ FATTN_VEC_CASE(512, F16, F16) in ggml_cuda_flash_attn_ext_vec, BEFORE the GGML_ABORT.
24
+ NOTE: fattn.cu has TWO independent head-size switches (selector + VEC dispatch); both need the
25
+ D512 case or the selector picks VEC but the dispatch falls through to a decode-time abort.
26
+ - llama.cpp/src/llama-graph.cpp: narrow the #491 guard β€” fa_no_gpu_kernel fires only when K/V are
27
+ NOT f16/f32 (fa_d512_f16_vec), so f16 D512/gqa<=2 now flash-attns on-GPU.
28
+
29
+ The VEC kernel tiles Q->ne[1] (cols_per_block 1 decode / 2 prefill+verify) so the one f16 D512
30
+ instance is a real flash-attn at ANY n_q (covers decode, prefill, and the spec-verify batch).
31
+ logit_softcap is forced off for D!=128,256 in the VEC kernel; safe because Gemma-4 attn_soft_cap
32
+ is false (softcap 0 -> use_logit_softcap false).
33
+
34
+ Gate (.opencoti/m493-fa-d512-gate.sh): E4B -fa on assistant-MTP decodes 147.86 tps with NO abort;
35
+ the verified-greedy output is BYTE-IDENTICAL to the #491 non-FA soft_max path and draft acceptance
36
+ is unchanged (0.67910 both), so the new VEC FA is numerically correct (exact spec-decode validates
37
+ every token, and a wrong FA would collapse acceptance). A4B (gqa 8, untouched MMA path) regresses
38
+ nowhere. Both DSO paths rebuilt byte-identical (sha256 a8ddcd82).
39
+
40
+ Marker convention: every changed file carries an `opencoti #491`/`opencoti #493` marker; see
41
+ docs/protocols/UPSTREAM_SYNC.md. All hunks are nested (llama.cpp/<path>); no outer-vendor delta.
42
+
43
+ diff --git a/llama.cpp/src/llama-graph.cpp b/llama.cpp/src/llama-graph.cpp
44
+ --- a/llama.cpp/src/llama-graph.cpp
45
+ +++ b/llama.cpp/src/llama-graph.cpp
46
+ @@ -2122,7 +2122,29 @@
47
+
48
+ ggml_tensor * cur;
49
+
50
+ - const bool use_flash_attn = cparams.flash_attn && kq_b == nullptr;
51
+ + // opencoti F5 M6-S4 mtp (#441/#491): head_dim 512 has NO GPU flash-attn kernel for gqa_ratio ≀ 2.
52
+ + // Both the MMA (ggml_cuda_flash_attn_ext_mma_f16_switch_ncols2, fattn.cu) and TILE
53
+ + // (launch_fattn_tile_switch_ncols2, fattn-tile.cuh) D512 paths only instantiate ncols2 ∈ {4,8}
54
+ + // (gqa_ratio > 2); the ncols2 ∈ {1,2} variants are gated out for binary size (DV<=256 only), so a
55
+ + // D512/gqa≀2 flash-attn op aborts at runtime (fattn.cu:132 / fattn-tile.cuh:1322). That is exactly
56
+ + // the Gemma-4 E4B assistant-MTP drafter's single GLOBAL layer: head_count 4 / head_count_kv 2 β†’
57
+ + // gqa_ratio 2. Route it through the EXISTING non-FA explicit-softmax branch below (any head_dim/gqa
58
+ + // β€” see the "non-flash-attn branch" fallback note at the top of this fn). q/k here are post-permute
59
+ + // (flash-attn layout), so q->ne[2]/k->ne[2] == the CUDA kernel's gqa_ratio and k->ne[0] == head_dim.
60
+ + // NARROW: every working D512 model (A4B/E2B/12B/31B + the E4B base, gqa_ratio β‰₯ 4) keeps flash-attn,
61
+ + // byte-unchanged; only the gqa≀2 case is special.
62
+ + // opencoti #493 (F5 M6-S4): D512/gqa≀2 with f16 (or f32β†’f16-cast) K AND V now has a REAL GPU
63
+ + // flash-attn β€” the f16 D512 VEC instance, which tiles Q->ne[1] (cols_per_block 1 decode / 2
64
+ + // prefill+verify) so it computes at ANY n_q (see ggml_cuda_get_best_fattn_kernel, fattn.cu). So
65
+ + // -fa on COMPUTES on-GPU for the E4B drafter (its KV is f16) instead of diverting to soft_max; no
66
+ + // global fa-off, no non-FA fallback for the real path. The guard now fires ONLY for the residual
67
+ + // with no D512/gqa≀2 kernel: QUANTIZED or bf16 K/V (the VEC D512 instance is f16-only; MMA/TILE
68
+ + // abort, bug-567) β€” an exotic config the assistant drafter never hits. f16/f32 K/V β†’ flash-attn.
69
+ + const bool fa_d512_f16_vec = (k->type == GGML_TYPE_F16 || k->type == GGML_TYPE_F32) &&
70
+ + (v->type == GGML_TYPE_F16 || v->type == GGML_TYPE_F32);
71
+ + const bool fa_no_gpu_kernel = k->ne[0] == 512 && k->ne[2] > 0 && (q->ne[2] / k->ne[2]) <= 2 &&
72
+ + !fa_d512_f16_vec;
73
+ + const bool use_flash_attn = cparams.flash_attn && kq_b == nullptr && !fa_no_gpu_kernel;
74
+ if (use_flash_attn) {
75
+ GGML_ASSERT(kq_b == nullptr && "Flash attention does not support KQ bias yet");
76
+
77
+ diff --git a/llama.cpp/ggml/src/ggml-cuda/fattn.cu b/llama.cpp/ggml/src/ggml-cuda/fattn.cu
78
+ --- a/llama.cpp/ggml/src/ggml-cuda/fattn.cu
79
+ +++ b/llama.cpp/ggml/src/ggml-cuda/fattn.cu
80
+ @@ -360,6 +360,13 @@
81
+ FATTN_VEC_CASE(512, GGML_TYPE_TURBO2_0, GGML_TYPE_TURBO2_0)
82
+ FATTN_VEC_CASE(512, GGML_TYPE_TURBO3_0, GGML_TYPE_TURBO3_0)
83
+
84
+ + // opencoti #493 (F5 M6-S4): head_dim 512 f16 VEC dispatch β€” the runtime counterpart to the
85
+ + // selector's D512/gqa<=2 f16 VEC route (ggml_cuda_get_best_fattn_kernel) + the f16-f16 D512
86
+ + // instance. Without this case the selector picks VEC but the dispatch falls through to the abort
87
+ + // below (bug-567 reappeared as a *decode-time* fatal at fattn.cu:363). The macro accepts F16 and
88
+ + // F32β†’F16 for both K and V, matching the graph guard. Gemma-4 E4B assistant-MTP drafter global layer.
89
+ + FATTN_VEC_CASE(512, GGML_TYPE_F16, GGML_TYPE_F16)
90
+ +
91
+ GGML_ABORT("fatal error");
92
+ }
93
+
94
+ @@ -513,6 +520,20 @@
95
+ return (can_use_vector_kernel || turbo512_vec) ? BEST_FATTN_KERNEL_VEC : BEST_FATTN_KERNEL_NONE;
96
+ }
97
+
98
+ + // opencoti #493 (F5 M6-S4): D512 (Gemma-4 GLOBAL layer) at gqa_ratio <= 2 β€” the E4B assistant-MTP
99
+ + // drafter (head_count 4 / head_count_kv 2 β†’ gqa 2) β€” has NO MMA/TILE kernel: switch_ncols2 aborts
100
+ + // for DKQ>256 && gqa<=2 (the ncols2 ∈ {1,2} D512 instances are if-constexpr'd out, bug-567). The
101
+ + // f16 D512 VEC instance (#493) tiles Q->ne[1] (cols_per_block 1 for decode, 2 for prefill/verify β€”
102
+ + // ggml_cuda_flash_attn_ext_vec_case) so it is a REAL flash-attn at ANY n_q. Route f16/f16 D512 here
103
+ + // when gqa<=2 so -fa on COMPUTES instead of diverting to the #491 non-FA host guard. D512/gqa>2
104
+ + // (A4B / 12B / 31B / E4B base) keeps the faster MMA ncols2 ∈ {4,8} path β€” byte-unchanged. Past the
105
+ + // case-512 check above, gqa_opt_applies already holds (so gqa_ratio>=2); gqa_ratio<=2 β‡’ exactly 2.
106
+ + if (Q->ne[0] == 512 && gqa_ratio <= 2 && gqa_opt_applies &&
107
+ + K->type == GGML_TYPE_F16 && V->type == GGML_TYPE_F16 &&
108
+ + K->ne[1] % FATTN_KQ_STRIDE == 0) {
109
+ + return BEST_FATTN_KERNEL_VEC;
110
+ + }
111
+ +
112
+ // If Turing tensor cores are available, use them:
113
+ if (turing_mma_available(cc) && Q->ne[0] != 40 && Q->ne[0] != 72) {
114
+ if (can_use_vector_kernel) {
115
+ diff --git a/llama.cpp/ggml/src/ggml-cuda/fattn-vec.cuh b/llama.cpp/ggml/src/ggml-cuda/fattn-vec.cuh
116
+ --- a/llama.cpp/ggml/src/ggml-cuda/fattn-vec.cuh
117
+ +++ b/llama.cpp/ggml/src/ggml-cuda/fattn-vec.cuh
118
+ @@ -631,3 +631,8 @@
119
+ // layers (key_length=512). Definitions in the turbo2/turbo3 instance .cu files. turbo4 stays ≀256.
120
+ extern DECL_FATTN_VEC_CASE(512, GGML_TYPE_TURBO2_0, GGML_TYPE_TURBO2_0);
121
+ extern DECL_FATTN_VEC_CASE(512, GGML_TYPE_TURBO3_0, GGML_TYPE_TURBO3_0);
122
+ +
123
+ +// opencoti #493 (F5 M6-S4): head_dim 512 f16 VEC β€” Gemma-4 GLOBAL layer at gqa_ratio<=2 (the E4B
124
+ +// assistant-MTP drafter), where MMA/TILE abort (DKQ>256 && gqa<=2, bug-567). The VEC kernel tiles
125
+ +// Q->ne[1] so it is a real flash-attn at ANY n_q. Definition in fattn-vec-instance-f16-f16.cu.
126
+ +extern DECL_FATTN_VEC_CASE(512, GGML_TYPE_F16, GGML_TYPE_F16);
127
+ diff --git a/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-vec-instance-f16-f16.cu b/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-vec-instance-f16-f16.cu
128
+ --- a/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-vec-instance-f16-f16.cu
129
+ +++ b/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-vec-instance-f16-f16.cu
130
+ @@ -5,3 +5,4 @@
131
+ DECL_FATTN_VEC_CASE( 64, GGML_TYPE_F16, GGML_TYPE_F16);
132
+ DECL_FATTN_VEC_CASE(128, GGML_TYPE_F16, GGML_TYPE_F16);
133
+ DECL_FATTN_VEC_CASE(256, GGML_TYPE_F16, GGML_TYPE_F16);
134
+ +DECL_FATTN_VEC_CASE(512, GGML_TYPE_F16, GGML_TYPE_F16); // opencoti #493: Gemma-4 GLOBAL head_dim 512 f16, gqa<=2
135
+ diff --git a/llama.cpp/ggml/src/ggml-cuda/template-instances/generate_cu_files.py b/llama.cpp/ggml/src/ggml-cuda/template-instances/generate_cu_files.py
136
+ --- a/llama.cpp/ggml/src/ggml-cuda/template-instances/generate_cu_files.py
137
+ +++ b/llama.cpp/ggml/src/ggml-cuda/template-instances/generate_cu_files.py
138
+ @@ -74,6 +74,17 @@
139
+ with open(f"fattn-vec-instance-{get_short_name(type_k)}-{get_short_name(type_v)}.cu", "w") as f:
140
+ f.write(SOURCE_FATTN_VEC.format(type_k=type_k, type_v=type_v))
141
+
142
+ +# opencoti #493 (F5 M6-S4): head_dim 512 f16 VEC for Gemma-4 GLOBAL layers at gqa_ratio<=2 (the E4B
143
+ +# assistant-MTP drafter's single global layer is head_count 4 / head_count_kv 2 β†’ gqa 2). The MMA and
144
+ +# TILE D512 paths only instantiate ncols2 ∈ {4,8} (gqa>2); ncols2 ∈ {1,2} are if-constexpr'd out, so a
145
+ +# D512/gqa<=2 op aborts at runtime (fattn.cu switch_ncols2, bug-567). The VEC kernel tiles Q->ne[1]
146
+ +# (cols_per_block 1 for decode, 2 for prefill/verify β€” ggml_cuda_flash_attn_ext_vec_case) so it is a
147
+ +# REAL flash-attn at ANY n_q for D512. Mirrors #411's turbo512 but for plain f16 K/V β†’ -fa on COMPUTES
148
+ +# instead of falling back to the #491 non-FA host guard. f16/f16 only (drafter KV is f16); quantized
149
+ +# D512/gqa<=2 keeps the guard (no VEC D512 quant instance).
150
+ +with open("fattn-vec-instance-f16-f16.cu", "a") as f:
151
+ + f.write("DECL_FATTN_VEC_CASE(512, GGML_TYPE_F16, GGML_TYPE_F16); // opencoti #493: Gemma-4 GLOBAL head_dim 512 f16, gqa<=2\n")
152
+ +
153
+ # opencoti-hook: turboquant perf-lift (Level B) β€” F5 M6-S2 #392. Fused turbo K/V vec instances
154
+ # (K==V same tier only; the in-register readers live in fattn-turbo.cuh).
155
+ for type_t in ["GGML_TYPE_TURBO2_0", "GGML_TYPE_TURBO3_0", "GGML_TYPE_TURBO4_0"]:
patches/0081-tcq-q6-asym-kv.patch ADDED
The diff for this file is too large to render. See raw diff
 
patches/0082-cuda-build-parallelism.patch ADDED
@@ -0,0 +1,320 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ diff --git a/llamafile/cuda.sh b/llamafile/cuda.sh
2
+ --- a/llamafile/cuda.sh
3
+ +++ b/llamafile/cuda.sh
4
+ @@ -259,6 +259,22 @@
5
+ COMMON_FLAGS="$COMMON_FLAGS -DGGML_CUDA_FA_ALL_QUANTS"
6
+ fi
7
+
8
+ +# opencoti-hook: cuda-build-parallelism (#531) β€” nvcc intra-TU arch threading. Each nvcc TU compiles
9
+ +# the gencode arches (sm_75/80/86/89/90/120f); without --threads they run serially, so any TU left
10
+ +# alone at the build tail uses one core while the rest idle. --threads compiles the arches
11
+ +# concurrently. To avoid oversubscribing the bulk (JOBS nvcc already saturate the cores), we divide
12
+ +# the outer job-slot count by NVCC_THREADS so PEAK concurrency stays β‰ˆ JOBS (slots Γ— threads). The
13
+ +# Windows cuda.bat already passes --threads 5; the Linux path lacked it. Tunable: NVCC_THREADS=1
14
+ +# disables (restores pre-#531 behavior). See docs/protocols/LLAMAFILE_UPGRADE.md.
15
+ +NVCC_THREADS="${NVCC_THREADS:-2}"
16
+ +JOBS_EFF="$JOBS"
17
+ +if [ "${NVCC_THREADS}" -gt 1 ] 2>/dev/null; then
18
+ + COMMON_FLAGS="$COMMON_FLAGS --threads ${NVCC_THREADS}"
19
+ + JOBS_EFF=$(( JOBS / NVCC_THREADS ))
20
+ + [ "$JOBS_EFF" -lt 1 ] && JOBS_EFF=1
21
+ +fi
22
+ +echo " nvcc --threads ${NVCC_THREADS} β†’ ${JOBS_EFF} job-slots Γ— ${NVCC_THREADS} arch-threads (#531)"
23
+ +
24
+ # Collect sources
25
+ collect_gpu_sources "$GGML_CUDA_DIR" "$EXTRA_SOURCES" "$NO_IQ_QUANTS" "$FA_ALL_QUANTS"
26
+ echo " Sources: $NUM_SOURCES .cu files"
27
+ @@ -267,7 +283,7 @@
28
+ START_TIME=$(date +%s)
29
+
30
+ # Compile GPU sources
31
+ -compile_gpu_sources_parallel "$NVCC" "$ARCH_FLAGS" "$COMMON_FLAGS" "$BUILD_DIR" "$JOBS"
32
+ +compile_gpu_sources_parallel "$NVCC" "$ARCH_FLAGS" "$COMMON_FLAGS" "$BUILD_DIR" "$JOBS_EFF"
33
+
34
+ COMPILE_TIME=$(date +%s)
35
+ echo "Compilation took $((COMPILE_TIME - START_TIME)) seconds"
36
+ diff --git a/llama.cpp/ggml/src/ggml-cuda/fattn-mma-f16.cuh b/llama.cpp/ggml/src/ggml-cuda/fattn-mma-f16.cuh
37
+ --- a/llama.cpp/ggml/src/ggml-cuda/fattn-mma-f16.cuh
38
+ +++ b/llama.cpp/ggml/src/ggml-cuda/fattn-mma-f16.cuh
39
+ @@ -2600,3 +2600,41 @@
40
+ extern DECL_FATTN_MMA_F16_CASE(576, 512, 16, 4);
41
+ extern DECL_FATTN_MMA_F16_CASE(576, 512, 1, 32);
42
+ extern DECL_FATTN_MMA_F16_CASE(576, 512, 2, 32);
43
+ +
44
+ +// opencoti-hook: cuda-build-parallelism (#531) β€” shard the DCA-fused MMA case into parallel
45
+ +// instance TUs (template-instances/fattn-mma-f16-dca-fused-instance-*.cu), mirroring the stock
46
+ +// DECL_FATTN_MMA_F16_CASE sharding above. Before this, fattn.cu's dca-fused dispatch
47
+ +// (ggml_cuda_flash_attn_ext_mma_f16_dca_fused_switch) instantiated all 48
48
+ +// ggml_cuda_flash_attn_ext_mma_f16_dca_fused_case<DKQ,DV,ncols1,ncols2> INLINE β€” DKQ==DV ∈ {128,256,512}
49
+ +// Γ— ncols2 ∈ {1,2,4,8} Γ— ncols1 ∈ {8,16,32,64}/ncols2 β†’ 576 device kernels in ONE TU = a ~52-min
50
+ +// SOLO compile tail (fattn.o was 29 MB, the largest object, finishing ~52 min after every other TU).
51
+ +// These extern decls move the instantiation to the parallel bulk; fattn.cu now just references the
52
+ +// symbols (the regular MMA/TILE/VEC cases were already sharded this way; only our DCA kernel wasn't).
53
+ +// The instance set matches the dispatch 1:1 β€” every reachable _case<DKQ,DV,ncols1,ncols2> has a
54
+ +// definition, none extra β€” so the linked DSO is byte-for-byte equivalent (same 576 kernels, relocated).
55
+ +#define DECL_FATTN_MMA_F16_DCA_FUSED_CASE(DKQ, DV, ncols1, ncols2) \
56
+ + template void ggml_cuda_flash_attn_ext_mma_f16_dca_fused_case \
57
+ + <DKQ, DV, ncols1, ncols2>(ggml_backend_cuda_context & ctx, ggml_tensor * dst) \
58
+ +
59
+ +// DCA always uses DKQ==DV ∈ {128,256,512}; one extern per (ncols1,ncols2) across all three head dims.
60
+ +#define EXTERN_DECL_FATTN_MMA_F16_DCA_FUSED_ALL_DKQ(ncols1, ncols2) \
61
+ + extern DECL_FATTN_MMA_F16_DCA_FUSED_CASE(128, 128, ncols1, ncols2); \
62
+ + extern DECL_FATTN_MMA_F16_DCA_FUSED_CASE(256, 256, ncols1, ncols2); \
63
+ + extern DECL_FATTN_MMA_F16_DCA_FUSED_CASE(512, 512, ncols1, ncols2); \
64
+ +
65
+ +EXTERN_DECL_FATTN_MMA_F16_DCA_FUSED_ALL_DKQ( 8, 1)
66
+ +EXTERN_DECL_FATTN_MMA_F16_DCA_FUSED_ALL_DKQ(16, 1)
67
+ +EXTERN_DECL_FATTN_MMA_F16_DCA_FUSED_ALL_DKQ(32, 1)
68
+ +EXTERN_DECL_FATTN_MMA_F16_DCA_FUSED_ALL_DKQ(64, 1)
69
+ +EXTERN_DECL_FATTN_MMA_F16_DCA_FUSED_ALL_DKQ( 4, 2)
70
+ +EXTERN_DECL_FATTN_MMA_F16_DCA_FUSED_ALL_DKQ( 8, 2)
71
+ +EXTERN_DECL_FATTN_MMA_F16_DCA_FUSED_ALL_DKQ(16, 2)
72
+ +EXTERN_DECL_FATTN_MMA_F16_DCA_FUSED_ALL_DKQ(32, 2)
73
+ +EXTERN_DECL_FATTN_MMA_F16_DCA_FUSED_ALL_DKQ( 2, 4)
74
+ +EXTERN_DECL_FATTN_MMA_F16_DCA_FUSED_ALL_DKQ( 4, 4)
75
+ +EXTERN_DECL_FATTN_MMA_F16_DCA_FUSED_ALL_DKQ( 8, 4)
76
+ +EXTERN_DECL_FATTN_MMA_F16_DCA_FUSED_ALL_DKQ(16, 4)
77
+ +EXTERN_DECL_FATTN_MMA_F16_DCA_FUSED_ALL_DKQ( 1, 8)
78
+ +EXTERN_DECL_FATTN_MMA_F16_DCA_FUSED_ALL_DKQ( 2, 8)
79
+ +EXTERN_DECL_FATTN_MMA_F16_DCA_FUSED_ALL_DKQ( 4, 8)
80
+ +EXTERN_DECL_FATTN_MMA_F16_DCA_FUSED_ALL_DKQ( 8, 8)
81
+ diff --git a/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-mma-f16-dca-fused-instance-ncols1_16-ncols2_1.cu b/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-mma-f16-dca-fused-instance-ncols1_16-ncols2_1.cu
82
+ new file mode 100644
83
+ --- /dev/null
84
+ +++ b/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-mma-f16-dca-fused-instance-ncols1_16-ncols2_1.cu
85
+ @@ -0,0 +1,10 @@
86
+ +// opencoti-hook: cuda-build-parallelism (#531) β€” DCA-fused MMA case instance, relocated out of
87
+ +// fattn.cu so it compiles in the parallel bulk instead of inflating fattn.cu's solo tail.
88
+ +// Mirrors template-instances/fattn-mma-f16-instance-*.cu. extern decls live in ../fattn-mma-f16.cuh.
89
+ +// DKQ==DV ∈ {128,256,512} (the only head dims DCA dispatches: Gemma-4 512, qwen35moe 256, qwen* 128).
90
+ +
91
+ +#include "../fattn-mma-f16.cuh"
92
+ +
93
+ +DECL_FATTN_MMA_F16_DCA_FUSED_CASE(128, 128, 16, 1);
94
+ +DECL_FATTN_MMA_F16_DCA_FUSED_CASE(256, 256, 16, 1);
95
+ +DECL_FATTN_MMA_F16_DCA_FUSED_CASE(512, 512, 16, 1);
96
+ diff --git a/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-mma-f16-dca-fused-instance-ncols1_16-ncols2_2.cu b/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-mma-f16-dca-fused-instance-ncols1_16-ncols2_2.cu
97
+ new file mode 100644
98
+ --- /dev/null
99
+ +++ b/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-mma-f16-dca-fused-instance-ncols1_16-ncols2_2.cu
100
+ @@ -0,0 +1,10 @@
101
+ +// opencoti-hook: cuda-build-parallelism (#531) β€” DCA-fused MMA case instance, relocated out of
102
+ +// fattn.cu so it compiles in the parallel bulk instead of inflating fattn.cu's solo tail.
103
+ +// Mirrors template-instances/fattn-mma-f16-instance-*.cu. extern decls live in ../fattn-mma-f16.cuh.
104
+ +// DKQ==DV ∈ {128,256,512} (the only head dims DCA dispatches: Gemma-4 512, qwen35moe 256, qwen* 128).
105
+ +
106
+ +#include "../fattn-mma-f16.cuh"
107
+ +
108
+ +DECL_FATTN_MMA_F16_DCA_FUSED_CASE(128, 128, 16, 2);
109
+ +DECL_FATTN_MMA_F16_DCA_FUSED_CASE(256, 256, 16, 2);
110
+ +DECL_FATTN_MMA_F16_DCA_FUSED_CASE(512, 512, 16, 2);
111
+ diff --git a/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-mma-f16-dca-fused-instance-ncols1_16-ncols2_4.cu b/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-mma-f16-dca-fused-instance-ncols1_16-ncols2_4.cu
112
+ new file mode 100644
113
+ --- /dev/null
114
+ +++ b/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-mma-f16-dca-fused-instance-ncols1_16-ncols2_4.cu
115
+ @@ -0,0 +1,10 @@
116
+ +// opencoti-hook: cuda-build-parallelism (#531) β€” DCA-fused MMA case instance, relocated out of
117
+ +// fattn.cu so it compiles in the parallel bulk instead of inflating fattn.cu's solo tail.
118
+ +// Mirrors template-instances/fattn-mma-f16-instance-*.cu. extern decls live in ../fattn-mma-f16.cuh.
119
+ +// DKQ==DV ∈ {128,256,512} (the only head dims DCA dispatches: Gemma-4 512, qwen35moe 256, qwen* 128).
120
+ +
121
+ +#include "../fattn-mma-f16.cuh"
122
+ +
123
+ +DECL_FATTN_MMA_F16_DCA_FUSED_CASE(128, 128, 16, 4);
124
+ +DECL_FATTN_MMA_F16_DCA_FUSED_CASE(256, 256, 16, 4);
125
+ +DECL_FATTN_MMA_F16_DCA_FUSED_CASE(512, 512, 16, 4);
126
+ diff --git a/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-mma-f16-dca-fused-instance-ncols1_1-ncols2_8.cu b/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-mma-f16-dca-fused-instance-ncols1_1-ncols2_8.cu
127
+ new file mode 100644
128
+ --- /dev/null
129
+ +++ b/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-mma-f16-dca-fused-instance-ncols1_1-ncols2_8.cu
130
+ @@ -0,0 +1,10 @@
131
+ +// opencoti-hook: cuda-build-parallelism (#531) β€” DCA-fused MMA case instance, relocated out of
132
+ +// fattn.cu so it compiles in the parallel bulk instead of inflating fattn.cu's solo tail.
133
+ +// Mirrors template-instances/fattn-mma-f16-instance-*.cu. extern decls live in ../fattn-mma-f16.cuh.
134
+ +// DKQ==DV ∈ {128,256,512} (the only head dims DCA dispatches: Gemma-4 512, qwen35moe 256, qwen* 128).
135
+ +
136
+ +#include "../fattn-mma-f16.cuh"
137
+ +
138
+ +DECL_FATTN_MMA_F16_DCA_FUSED_CASE(128, 128, 1, 8);
139
+ +DECL_FATTN_MMA_F16_DCA_FUSED_CASE(256, 256, 1, 8);
140
+ +DECL_FATTN_MMA_F16_DCA_FUSED_CASE(512, 512, 1, 8);
141
+ diff --git a/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-mma-f16-dca-fused-instance-ncols1_2-ncols2_4.cu b/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-mma-f16-dca-fused-instance-ncols1_2-ncols2_4.cu
142
+ new file mode 100644
143
+ --- /dev/null
144
+ +++ b/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-mma-f16-dca-fused-instance-ncols1_2-ncols2_4.cu
145
+ @@ -0,0 +1,10 @@
146
+ +// opencoti-hook: cuda-build-parallelism (#531) β€” DCA-fused MMA case instance, relocated out of
147
+ +// fattn.cu so it compiles in the parallel bulk instead of inflating fattn.cu's solo tail.
148
+ +// Mirrors template-instances/fattn-mma-f16-instance-*.cu. extern decls live in ../fattn-mma-f16.cuh.
149
+ +// DKQ==DV ∈ {128,256,512} (the only head dims DCA dispatches: Gemma-4 512, qwen35moe 256, qwen* 128).
150
+ +
151
+ +#include "../fattn-mma-f16.cuh"
152
+ +
153
+ +DECL_FATTN_MMA_F16_DCA_FUSED_CASE(128, 128, 2, 4);
154
+ +DECL_FATTN_MMA_F16_DCA_FUSED_CASE(256, 256, 2, 4);
155
+ +DECL_FATTN_MMA_F16_DCA_FUSED_CASE(512, 512, 2, 4);
156
+ diff --git a/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-mma-f16-dca-fused-instance-ncols1_2-ncols2_8.cu b/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-mma-f16-dca-fused-instance-ncols1_2-ncols2_8.cu
157
+ new file mode 100644
158
+ --- /dev/null
159
+ +++ b/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-mma-f16-dca-fused-instance-ncols1_2-ncols2_8.cu
160
+ @@ -0,0 +1,10 @@
161
+ +// opencoti-hook: cuda-build-parallelism (#531) β€” DCA-fused MMA case instance, relocated out of
162
+ +// fattn.cu so it compiles in the parallel bulk instead of inflating fattn.cu's solo tail.
163
+ +// Mirrors template-instances/fattn-mma-f16-instance-*.cu. extern decls live in ../fattn-mma-f16.cuh.
164
+ +// DKQ==DV ∈ {128,256,512} (the only head dims DCA dispatches: Gemma-4 512, qwen35moe 256, qwen* 128).
165
+ +
166
+ +#include "../fattn-mma-f16.cuh"
167
+ +
168
+ +DECL_FATTN_MMA_F16_DCA_FUSED_CASE(128, 128, 2, 8);
169
+ +DECL_FATTN_MMA_F16_DCA_FUSED_CASE(256, 256, 2, 8);
170
+ +DECL_FATTN_MMA_F16_DCA_FUSED_CASE(512, 512, 2, 8);
171
+ diff --git a/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-mma-f16-dca-fused-instance-ncols1_32-ncols2_1.cu b/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-mma-f16-dca-fused-instance-ncols1_32-ncols2_1.cu
172
+ new file mode 100644
173
+ --- /dev/null
174
+ +++ b/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-mma-f16-dca-fused-instance-ncols1_32-ncols2_1.cu
175
+ @@ -0,0 +1,10 @@
176
+ +// opencoti-hook: cuda-build-parallelism (#531) β€” DCA-fused MMA case instance, relocated out of
177
+ +// fattn.cu so it compiles in the parallel bulk instead of inflating fattn.cu's solo tail.
178
+ +// Mirrors template-instances/fattn-mma-f16-instance-*.cu. extern decls live in ../fattn-mma-f16.cuh.
179
+ +// DKQ==DV ∈ {128,256,512} (the only head dims DCA dispatches: Gemma-4 512, qwen35moe 256, qwen* 128).
180
+ +
181
+ +#include "../fattn-mma-f16.cuh"
182
+ +
183
+ +DECL_FATTN_MMA_F16_DCA_FUSED_CASE(128, 128, 32, 1);
184
+ +DECL_FATTN_MMA_F16_DCA_FUSED_CASE(256, 256, 32, 1);
185
+ +DECL_FATTN_MMA_F16_DCA_FUSED_CASE(512, 512, 32, 1);
186
+ diff --git a/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-mma-f16-dca-fused-instance-ncols1_32-ncols2_2.cu b/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-mma-f16-dca-fused-instance-ncols1_32-ncols2_2.cu
187
+ new file mode 100644
188
+ --- /dev/null
189
+ +++ b/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-mma-f16-dca-fused-instance-ncols1_32-ncols2_2.cu
190
+ @@ -0,0 +1,10 @@
191
+ +// opencoti-hook: cuda-build-parallelism (#531) β€” DCA-fused MMA case instance, relocated out of
192
+ +// fattn.cu so it compiles in the parallel bulk instead of inflating fattn.cu's solo tail.
193
+ +// Mirrors template-instances/fattn-mma-f16-instance-*.cu. extern decls live in ../fattn-mma-f16.cuh.
194
+ +// DKQ==DV ∈ {128,256,512} (the only head dims DCA dispatches: Gemma-4 512, qwen35moe 256, qwen* 128).
195
+ +
196
+ +#include "../fattn-mma-f16.cuh"
197
+ +
198
+ +DECL_FATTN_MMA_F16_DCA_FUSED_CASE(128, 128, 32, 2);
199
+ +DECL_FATTN_MMA_F16_DCA_FUSED_CASE(256, 256, 32, 2);
200
+ +DECL_FATTN_MMA_F16_DCA_FUSED_CASE(512, 512, 32, 2);
201
+ diff --git a/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-mma-f16-dca-fused-instance-ncols1_4-ncols2_2.cu b/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-mma-f16-dca-fused-instance-ncols1_4-ncols2_2.cu
202
+ new file mode 100644
203
+ --- /dev/null
204
+ +++ b/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-mma-f16-dca-fused-instance-ncols1_4-ncols2_2.cu
205
+ @@ -0,0 +1,10 @@
206
+ +// opencoti-hook: cuda-build-parallelism (#531) β€” DCA-fused MMA case instance, relocated out of
207
+ +// fattn.cu so it compiles in the parallel bulk instead of inflating fattn.cu's solo tail.
208
+ +// Mirrors template-instances/fattn-mma-f16-instance-*.cu. extern decls live in ../fattn-mma-f16.cuh.
209
+ +// DKQ==DV ∈ {128,256,512} (the only head dims DCA dispatches: Gemma-4 512, qwen35moe 256, qwen* 128).
210
+ +
211
+ +#include "../fattn-mma-f16.cuh"
212
+ +
213
+ +DECL_FATTN_MMA_F16_DCA_FUSED_CASE(128, 128, 4, 2);
214
+ +DECL_FATTN_MMA_F16_DCA_FUSED_CASE(256, 256, 4, 2);
215
+ +DECL_FATTN_MMA_F16_DCA_FUSED_CASE(512, 512, 4, 2);
216
+ diff --git a/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-mma-f16-dca-fused-instance-ncols1_4-ncols2_4.cu b/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-mma-f16-dca-fused-instance-ncols1_4-ncols2_4.cu
217
+ new file mode 100644
218
+ --- /dev/null
219
+ +++ b/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-mma-f16-dca-fused-instance-ncols1_4-ncols2_4.cu
220
+ @@ -0,0 +1,10 @@
221
+ +// opencoti-hook: cuda-build-parallelism (#531) β€” DCA-fused MMA case instance, relocated out of
222
+ +// fattn.cu so it compiles in the parallel bulk instead of inflating fattn.cu's solo tail.
223
+ +// Mirrors template-instances/fattn-mma-f16-instance-*.cu. extern decls live in ../fattn-mma-f16.cuh.
224
+ +// DKQ==DV ∈ {128,256,512} (the only head dims DCA dispatches: Gemma-4 512, qwen35moe 256, qwen* 128).
225
+ +
226
+ +#include "../fattn-mma-f16.cuh"
227
+ +
228
+ +DECL_FATTN_MMA_F16_DCA_FUSED_CASE(128, 128, 4, 4);
229
+ +DECL_FATTN_MMA_F16_DCA_FUSED_CASE(256, 256, 4, 4);
230
+ +DECL_FATTN_MMA_F16_DCA_FUSED_CASE(512, 512, 4, 4);
231
+ diff --git a/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-mma-f16-dca-fused-instance-ncols1_4-ncols2_8.cu b/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-mma-f16-dca-fused-instance-ncols1_4-ncols2_8.cu
232
+ new file mode 100644
233
+ --- /dev/null
234
+ +++ b/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-mma-f16-dca-fused-instance-ncols1_4-ncols2_8.cu
235
+ @@ -0,0 +1,10 @@
236
+ +// opencoti-hook: cuda-build-parallelism (#531) β€” DCA-fused MMA case instance, relocated out of
237
+ +// fattn.cu so it compiles in the parallel bulk instead of inflating fattn.cu's solo tail.
238
+ +// Mirrors template-instances/fattn-mma-f16-instance-*.cu. extern decls live in ../fattn-mma-f16.cuh.
239
+ +// DKQ==DV ∈ {128,256,512} (the only head dims DCA dispatches: Gemma-4 512, qwen35moe 256, qwen* 128).
240
+ +
241
+ +#include "../fattn-mma-f16.cuh"
242
+ +
243
+ +DECL_FATTN_MMA_F16_DCA_FUSED_CASE(128, 128, 4, 8);
244
+ +DECL_FATTN_MMA_F16_DCA_FUSED_CASE(256, 256, 4, 8);
245
+ +DECL_FATTN_MMA_F16_DCA_FUSED_CASE(512, 512, 4, 8);
246
+ diff --git a/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-mma-f16-dca-fused-instance-ncols1_64-ncols2_1.cu b/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-mma-f16-dca-fused-instance-ncols1_64-ncols2_1.cu
247
+ new file mode 100644
248
+ --- /dev/null
249
+ +++ b/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-mma-f16-dca-fused-instance-ncols1_64-ncols2_1.cu
250
+ @@ -0,0 +1,10 @@
251
+ +// opencoti-hook: cuda-build-parallelism (#531) β€” DCA-fused MMA case instance, relocated out of
252
+ +// fattn.cu so it compiles in the parallel bulk instead of inflating fattn.cu's solo tail.
253
+ +// Mirrors template-instances/fattn-mma-f16-instance-*.cu. extern decls live in ../fattn-mma-f16.cuh.
254
+ +// DKQ==DV ∈ {128,256,512} (the only head dims DCA dispatches: Gemma-4 512, qwen35moe 256, qwen* 128).
255
+ +
256
+ +#include "../fattn-mma-f16.cuh"
257
+ +
258
+ +DECL_FATTN_MMA_F16_DCA_FUSED_CASE(128, 128, 64, 1);
259
+ +DECL_FATTN_MMA_F16_DCA_FUSED_CASE(256, 256, 64, 1);
260
+ +DECL_FATTN_MMA_F16_DCA_FUSED_CASE(512, 512, 64, 1);
261
+ diff --git a/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-mma-f16-dca-fused-instance-ncols1_8-ncols2_1.cu b/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-mma-f16-dca-fused-instance-ncols1_8-ncols2_1.cu
262
+ new file mode 100644
263
+ --- /dev/null
264
+ +++ b/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-mma-f16-dca-fused-instance-ncols1_8-ncols2_1.cu
265
+ @@ -0,0 +1,10 @@
266
+ +// opencoti-hook: cuda-build-parallelism (#531) β€” DCA-fused MMA case instance, relocated out of
267
+ +// fattn.cu so it compiles in the parallel bulk instead of inflating fattn.cu's solo tail.
268
+ +// Mirrors template-instances/fattn-mma-f16-instance-*.cu. extern decls live in ../fattn-mma-f16.cuh.
269
+ +// DKQ==DV ∈ {128,256,512} (the only head dims DCA dispatches: Gemma-4 512, qwen35moe 256, qwen* 128).
270
+ +
271
+ +#include "../fattn-mma-f16.cuh"
272
+ +
273
+ +DECL_FATTN_MMA_F16_DCA_FUSED_CASE(128, 128, 8, 1);
274
+ +DECL_FATTN_MMA_F16_DCA_FUSED_CASE(256, 256, 8, 1);
275
+ +DECL_FATTN_MMA_F16_DCA_FUSED_CASE(512, 512, 8, 1);
276
+ diff --git a/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-mma-f16-dca-fused-instance-ncols1_8-ncols2_2.cu b/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-mma-f16-dca-fused-instance-ncols1_8-ncols2_2.cu
277
+ new file mode 100644
278
+ --- /dev/null
279
+ +++ b/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-mma-f16-dca-fused-instance-ncols1_8-ncols2_2.cu
280
+ @@ -0,0 +1,10 @@
281
+ +// opencoti-hook: cuda-build-parallelism (#531) β€” DCA-fused MMA case instance, relocated out of
282
+ +// fattn.cu so it compiles in the parallel bulk instead of inflating fattn.cu's solo tail.
283
+ +// Mirrors template-instances/fattn-mma-f16-instance-*.cu. extern decls live in ../fattn-mma-f16.cuh.
284
+ +// DKQ==DV ∈ {128,256,512} (the only head dims DCA dispatches: Gemma-4 512, qwen35moe 256, qwen* 128).
285
+ +
286
+ +#include "../fattn-mma-f16.cuh"
287
+ +
288
+ +DECL_FATTN_MMA_F16_DCA_FUSED_CASE(128, 128, 8, 2);
289
+ +DECL_FATTN_MMA_F16_DCA_FUSED_CASE(256, 256, 8, 2);
290
+ +DECL_FATTN_MMA_F16_DCA_FUSED_CASE(512, 512, 8, 2);
291
+ diff --git a/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-mma-f16-dca-fused-instance-ncols1_8-ncols2_4.cu b/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-mma-f16-dca-fused-instance-ncols1_8-ncols2_4.cu
292
+ new file mode 100644
293
+ --- /dev/null
294
+ +++ b/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-mma-f16-dca-fused-instance-ncols1_8-ncols2_4.cu
295
+ @@ -0,0 +1,10 @@
296
+ +// opencoti-hook: cuda-build-parallelism (#531) β€” DCA-fused MMA case instance, relocated out of
297
+ +// fattn.cu so it compiles in the parallel bulk instead of inflating fattn.cu's solo tail.
298
+ +// Mirrors template-instances/fattn-mma-f16-instance-*.cu. extern decls live in ../fattn-mma-f16.cuh.
299
+ +// DKQ==DV ∈ {128,256,512} (the only head dims DCA dispatches: Gemma-4 512, qwen35moe 256, qwen* 128).
300
+ +
301
+ +#include "../fattn-mma-f16.cuh"
302
+ +
303
+ +DECL_FATTN_MMA_F16_DCA_FUSED_CASE(128, 128, 8, 4);
304
+ +DECL_FATTN_MMA_F16_DCA_FUSED_CASE(256, 256, 8, 4);
305
+ +DECL_FATTN_MMA_F16_DCA_FUSED_CASE(512, 512, 8, 4);
306
+ diff --git a/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-mma-f16-dca-fused-instance-ncols1_8-ncols2_8.cu b/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-mma-f16-dca-fused-instance-ncols1_8-ncols2_8.cu
307
+ new file mode 100644
308
+ --- /dev/null
309
+ +++ b/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-mma-f16-dca-fused-instance-ncols1_8-ncols2_8.cu
310
+ @@ -0,0 +1,10 @@
311
+ +// opencoti-hook: cuda-build-parallelism (#531) β€” DCA-fused MMA case instance, relocated out of
312
+ +// fattn.cu so it compiles in the parallel bulk instead of inflating fattn.cu's solo tail.
313
+ +// Mirrors template-instances/fattn-mma-f16-instance-*.cu. extern decls live in ../fattn-mma-f16.cuh.
314
+ +// DKQ==DV ∈ {128,256,512} (the only head dims DCA dispatches: Gemma-4 512, qwen35moe 256, qwen* 128).
315
+ +
316
+ +#include "../fattn-mma-f16.cuh"
317
+ +
318
+ +DECL_FATTN_MMA_F16_DCA_FUSED_CASE(128, 128, 8, 8);
319
+ +DECL_FATTN_MMA_F16_DCA_FUSED_CASE(256, 256, 8, 8);
320
+ +DECL_FATTN_MMA_F16_DCA_FUSED_CASE(512, 512, 8, 8);
patches/0083-tcq-warp-encode.patch ADDED
@@ -0,0 +1,868 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ diff --git a/llama.cpp/ggml/src/ggml-cuda/set-rows.cu b/llama.cpp/ggml/src/ggml-cuda/set-rows.cu
2
+ --- a/llama.cpp/ggml/src/ggml-cuda/set-rows.cu
3
+ +++ b/llama.cpp/ggml/src/ggml-cuda/set-rows.cu
4
+ @@ -748,33 +748,17 @@
5
+ const int64_t s10_i = nb10/sizeof(idx_t), s11_i = nb11/sizeof(idx_t), s12_i = nb12/sizeof(idx_t);
6
+ const int iq_is_k = (strncmp(dst->name, "cache_k_", 8) == 0) ? 1 : 0;
7
+
8
+ - // one-time per-device: prefer the 8 KB/block shared-memory backtrace (opt-in); else global fallback buffer
9
+ - static int tcq3_use_shared_bt[GGML_CUDA_MAX_DEVICES] = {};
10
+ - static bool tcq3_bt_checked [GGML_CUDA_MAX_DEVICES] = {};
11
+ - constexpr int tcq3_bt_shared_bytes = 128 * 64;
12
+ - if (!tcq3_bt_checked[ctx.device]) {
13
+ - tcq3_bt_checked[ctx.device] = true;
14
+ - const char * env = getenv("TURBO_TCQ_SHARED_BT");
15
+ - if (!env || atoi(env) != 0) {
16
+ - int max_shared_optin = 0;
17
+ - CUDA_CHECK(cudaDeviceGetAttribute(&max_shared_optin, cudaDevAttrMaxSharedMemoryPerBlockOptin, ctx.device));
18
+ - if (max_shared_optin >= tcq3_bt_shared_bytes) {
19
+ - if (tcq_fp64_enabled())
20
+ - CUDA_SET_SHARED_MEMORY_LIMIT((k_set_rows_turbo3_tcq<idx_t, double>), tcq3_bt_shared_bytes);
21
+ - else
22
+ - CUDA_SET_SHARED_MEMORY_LIMIT(k_set_rows_turbo3_tcq<idx_t>, tcq3_bt_shared_bytes);
23
+ - tcq3_use_shared_bt[ctx.device] = 1;
24
+ - }
25
+ - }
26
+ - }
27
+ - if (!tcq3_use_shared_bt[ctx.device]) ensure_tcq_bt_buf(ctx.device, ne_total_groups * 128 * 64);
28
+ + // #533 warp-synchronous encode: bt is always a global per-group slab. With TCQ3_WPB groups/block a shared-bt
29
+ + // would cost WPB*8KB; and the warp kernel is no longer barrier-bound, so global bt is free here. One 8KB slab
30
+ + // per group (indexed by `group`, not blockIdx.x). tcq_bt_buf grows monotonically and is reused across launches.
31
+ + ensure_tcq_bt_buf(ctx.device, ne_total_groups * 128 * 64);
32
+
33
+ const uint3 ne00_fd = init_fastdiv_values((uint32_t) ne00);
34
+ const uint3 ne01_fd = init_fastdiv_values((uint32_t) ne01);
35
+ const uint3 ne02_fd = init_fastdiv_values((uint32_t) ne02);
36
+ const uint3 ne11_fd = init_fastdiv_values((uint32_t) ne11);
37
+ const uint3 ne12_fd = init_fastdiv_values((uint32_t) ne12);
38
+ - const int shared_bytes = tcq3_use_shared_bt[ctx.device] ? tcq3_bt_shared_bytes : 0;
39
+ + const int tcq3_grid = (int)((ne_total_groups + TCQ3_WPB - 1) / TCQ3_WPB); // #533 warp-packed: TCQ3_WPB groups/block, 32 thr/group
40
+ // opencoti-hook: TCQ activation-dump (F-D, #515) β€” bind the staging buffer for this launch (no-op unless
41
+ // TURBO_TCQ_DUMP_ACTS is set). tcq_dump_init() is idempotent; dump_dev is nullptr when inactive/cap-reached,
42
+ // in which case d_tcq_dump_buf is left at its nullptr default and the kernel's dump branch is a uniform no-op.
43
+ @@ -782,15 +766,15 @@
44
+ float * dump_dev = tcq_dump_pre_launch((long) ne_total_groups);
45
+ if (dump_dev) CUDA_CHECK(cudaMemcpyToSymbol(d_tcq_dump_buf, &dump_dev, sizeof(float*)));
46
+ if (tcq_fp64_enabled())
47
+ - k_set_rows_turbo3_tcq<idx_t, double><<<(int)ne_total_groups, 512, shared_bytes, stream>>>(
48
+ + k_set_rows_turbo3_tcq<idx_t, double><<<tcq3_grid, TCQ3_WPB*32, 0, stream>>>(
49
+ src0_d, src1_d, (block_turbo3_tcq *)dst->data,
50
+ - ne_total_groups, tcq_bt_buf[ctx.device], tcq3_use_shared_bt[ctx.device], ne00, ne01, ne02, ne10, ne11, ne12, ne13,
51
+ + ne_total_groups, tcq_bt_buf[ctx.device], 0, ne00, ne01, ne02, ne10, ne11, ne12, ne13,
52
+ s01_f, s02_f, s03_f, s10_i, s11_i, s12_i, iq_is_k, nb1, nb2, nb3,
53
+ ne00_fd, ne01_fd, ne02_fd, ne11_fd, ne12_fd);
54
+ else
55
+ - k_set_rows_turbo3_tcq<idx_t><<<(int)ne_total_groups, 512, shared_bytes, stream>>>(
56
+ + k_set_rows_turbo3_tcq<idx_t><<<tcq3_grid, TCQ3_WPB*32, 0, stream>>>(
57
+ src0_d, src1_d, (block_turbo3_tcq *)dst->data,
58
+ - ne_total_groups, tcq_bt_buf[ctx.device], tcq3_use_shared_bt[ctx.device], ne00, ne01, ne02, ne10, ne11, ne12, ne13,
59
+ + ne_total_groups, tcq_bt_buf[ctx.device], 0, ne00, ne01, ne02, ne10, ne11, ne12, ne13,
60
+ s01_f, s02_f, s03_f, s10_i, s11_i, s12_i, iq_is_k, nb1, nb2, nb3,
61
+ ne00_fd, ne01_fd, ne02_fd, ne11_fd, ne12_fd);
62
+ if (dump_dev) tcq_dump_post_launch((long) ne_total_groups, stream);
63
+ @@ -827,42 +811,26 @@
64
+ const int64_t s10_i = nb10/sizeof(idx_t), s11_i = nb11/sizeof(idx_t), s12_i = nb12/sizeof(idx_t);
65
+ const int iq_is_k = (strncmp(dst->name, "cache_k_", 8) == 0) ? 1 : 0;
66
+
67
+ - static int tcq2_use_shared_bt[GGML_CUDA_MAX_DEVICES] = {};
68
+ - static bool tcq2_bt_checked [GGML_CUDA_MAX_DEVICES] = {};
69
+ - constexpr int tcq2_bt_shared_bytes = 128 * 64;
70
+ - if (!tcq2_bt_checked[ctx.device]) {
71
+ - tcq2_bt_checked[ctx.device] = true;
72
+ - const char * env = getenv("TURBO_TCQ_SHARED_BT");
73
+ - if (!env || atoi(env) != 0) {
74
+ - int max_shared_optin = 0;
75
+ - CUDA_CHECK(cudaDeviceGetAttribute(&max_shared_optin, cudaDevAttrMaxSharedMemoryPerBlockOptin, ctx.device));
76
+ - if (max_shared_optin >= tcq2_bt_shared_bytes) {
77
+ - if (tcq_fp64_enabled())
78
+ - CUDA_SET_SHARED_MEMORY_LIMIT((k_set_rows_turbo2_tcq<idx_t, double>), tcq2_bt_shared_bytes);
79
+ - else
80
+ - CUDA_SET_SHARED_MEMORY_LIMIT(k_set_rows_turbo2_tcq<idx_t>, tcq2_bt_shared_bytes);
81
+ - tcq2_use_shared_bt[ctx.device] = 1;
82
+ - }
83
+ - }
84
+ - }
85
+ - if (!tcq2_use_shared_bt[ctx.device]) ensure_tcq_bt_buf(ctx.device, ne_total_groups * 128 * 64);
86
+ + // #533 warp-synchronous encode: bt always a global per-group slab (TCQ2_WPB groups/block); warp kernel is
87
+ + // no longer barrier-bound, so global bt is free. One 8KB slab per group, indexed by `group`.
88
+ + ensure_tcq_bt_buf(ctx.device, ne_total_groups * 128 * 64);
89
+
90
+ const uint3 ne00_fd = init_fastdiv_values((uint32_t) ne00);
91
+ const uint3 ne01_fd = init_fastdiv_values((uint32_t) ne01);
92
+ const uint3 ne02_fd = init_fastdiv_values((uint32_t) ne02);
93
+ const uint3 ne11_fd = init_fastdiv_values((uint32_t) ne11);
94
+ const uint3 ne12_fd = init_fastdiv_values((uint32_t) ne12);
95
+ - const int shared_bytes = tcq2_use_shared_bt[ctx.device] ? tcq2_bt_shared_bytes : 0;
96
+ + const int tcq2_grid = (int)((ne_total_groups + TCQ2_WPB - 1) / TCQ2_WPB); // #533 warp-packed: TCQ2_WPB groups/block, 32 thr/group
97
+ if (tcq_fp64_enabled())
98
+ - k_set_rows_turbo2_tcq<idx_t, double><<<(int)ne_total_groups, 256, shared_bytes, stream>>>(
99
+ + k_set_rows_turbo2_tcq<idx_t, double><<<tcq2_grid, TCQ2_WPB*32, 0, stream>>>(
100
+ src0_d, src1_d, (block_turbo2_tcq *)dst->data,
101
+ - ne_total_groups, tcq_bt_buf[ctx.device], tcq2_use_shared_bt[ctx.device], ne00, ne01, ne02, ne10, ne11, ne12, ne13,
102
+ + ne_total_groups, tcq_bt_buf[ctx.device], 0, ne00, ne01, ne02, ne10, ne11, ne12, ne13,
103
+ s01_f, s02_f, s03_f, s10_i, s11_i, s12_i, iq_is_k, nb1, nb2, nb3,
104
+ ne00_fd, ne01_fd, ne02_fd, ne11_fd, ne12_fd);
105
+ else
106
+ - k_set_rows_turbo2_tcq<idx_t><<<(int)ne_total_groups, 256, shared_bytes, stream>>>(
107
+ + k_set_rows_turbo2_tcq<idx_t><<<tcq2_grid, TCQ2_WPB*32, 0, stream>>>(
108
+ src0_d, src1_d, (block_turbo2_tcq *)dst->data,
109
+ - ne_total_groups, tcq_bt_buf[ctx.device], tcq2_use_shared_bt[ctx.device], ne00, ne01, ne02, ne10, ne11, ne12, ne13,
110
+ + ne_total_groups, tcq_bt_buf[ctx.device], 0, ne00, ne01, ne02, ne10, ne11, ne12, ne13,
111
+ s01_f, s02_f, s03_f, s10_i, s11_i, s12_i, iq_is_k, nb1, nb2, nb3,
112
+ ne00_fd, ne01_fd, ne02_fd, ne11_fd, ne12_fd);
113
+ }
114
+ diff --git a/llama.cpp/ggml/src/ggml-cuda/turbo-tcq-cuda.cuh b/llama.cpp/ggml/src/ggml-cuda/turbo-tcq-cuda.cuh
115
+ --- a/llama.cpp/ggml/src/ggml-cuda/turbo-tcq-cuda.cuh
116
+ +++ b/llama.cpp/ggml/src/ggml-cuda/turbo-tcq-cuda.cuh
117
+ @@ -109,11 +109,16 @@
118
+ static __device__ __forceinline__ float tcq_acc_sqrt(float v) { return sqrtf(v); }
119
+ static __device__ __forceinline__ double tcq_acc_sqrt(double v) { return sqrt(v); }
120
+
121
+ -// TCQ SET_ROWS encode: Viterbi optimal path with right-shift trellis
122
+ -// 512 threads per block (one per trellis state), one block per 128-element group
123
+ -// Double-buffered cost arrays + global memory backtrace (128 syncs/group, was 384)
124
+ +// TCQ SET_ROWS encode: warp-synchronous Viterbi (#533). ncu proved the old 512-thread/__syncthreads design was
125
+ +// LATENCY-bound on the 128-step block-barrier chain (25% SM, 0.1% DRAM, 33% occ); #538's occupancy/shuffle tweak
126
+ +// couldn't help that. This collapses each 128-element group to ONE 32-thread warp (16 states/lane, thread-local
127
+ +// predecessor-min, __syncwarp broadcast, bt in global), packs TCQ3_WPB warps/block, and breaks the warp lockstep
128
+ +// so per-group serial latency is hidden by other warps. Validated byte-equivalent (round-trip decode cosine
129
+ +// 1.00000 vs the old design β€” .opencoti/tcq-warp-enc-full.cu) + 3.74x on the full encode. Reuses #538's warp-
130
+ +// shuffle min primitive. acc_t fp64 path (#506) preserved; bt is always global (use_shared_bt ignored).
131
+ +#define TCQ3_WPB 8
132
+ template<typename idx_t, typename acc_t = float>
133
+ -static __global__ void __launch_bounds__(512, 1) k_set_rows_turbo3_tcq(
134
+ +static __global__ void __launch_bounds__(TCQ3_WPB*32, 3) k_set_rows_turbo3_tcq(
135
+ const float * __restrict__ src0, const idx_t * __restrict__ src1,
136
+ block_turbo3_tcq * __restrict__ dst, const int64_t ne_total_groups,
137
+ uint8_t * __restrict__ bt_buf,
138
+ @@ -127,12 +132,13 @@
139
+ const uint3 ne00_fd, const uint3 ne01_fd, const uint3 ne02_fd,
140
+ const uint3 ne11_fd, const uint3 ne12_fd) {
141
+
142
+ - const int64_t group = blockIdx.x;
143
+ + const int warp = threadIdx.x >> 5;
144
+ + const int lane = threadIdx.x & 31;
145
+ + const int64_t group = (int64_t)blockIdx.x * TCQ3_WPB + warp;
146
+ if (group >= ne_total_groups) return;
147
+ + (void) use_shared_bt; (void) ne10; (void) ne13; // bt is always global in the warp design
148
+
149
+ - const int sid = threadIdx.x; // state index 0..511
150
+ -
151
+ - // Compute source and destination pointers (same index math as turbo3)
152
+ + // Compute source and destination pointers (same index math as the 512-thread design; per-warp group)
153
+ const int64_t i_base = group * QK_TURBO3_TCQ;
154
+ uint32_t tmp = (uint32_t)i_base; uint2 div_mod;
155
+ div_mod = fast_div_modulo(tmp, ne00_fd); const int64_t i00 = div_mod.y; tmp = div_mod.x;
156
+ @@ -145,244 +151,130 @@
157
+ block_turbo3_tcq * dst_blk = (block_turbo3_tcq *)((char *)dst + dst_row*s1 + i02*s2 + i03*s3)
158
+ + (i00 / QK_TURBO3_TCQ);
159
+
160
+ - // Shared memory layout (~5KB, was ~35KB before global bt optimization):
161
+ - // x[128] : rotated+normalized input (also reused as outputs[] after Viterbi)
162
+ - // cost[512] : path costs buffer A (also reused for reductions)
163
+ - // cost_b[512]: path costs buffer B (double-buffering eliminates 2/3 of syncs)
164
+ - // Backtrace: one predecessor byte for each of the 64 low-state groups per
165
+ - // step. The predecessor is independent of the output bits in sid[8:6], so
166
+ - // storing 128Γ—64 bytes is equivalent to the older 128Γ—512 layout.
167
+ - extern __shared__ uint8_t bt_shared[];
168
+ - __shared__ acc_t x[128];
169
+ - __shared__ acc_t cost[512];
170
+ - __shared__ acc_t cost_b[512];
171
+ - __shared__ int warp_min_idx[16];
172
+ - __shared__ acc_t warp_min_cost[16];
173
+ - __shared__ acc_t pred_min_cost[64];
174
+ - __shared__ int shared_initial_state;
175
+ -
176
+ - if (sid < 128) x[sid] = grp_src[sid];
177
+ - __syncthreads();
178
+ -
179
+ - __syncthreads();
180
+ -
181
+ - // Norm reduction
182
+ - cost[sid] = (sid < 128) ? x[sid] * x[sid] : 0.0f;
183
+ - __syncthreads();
184
+ - for (int stride = 256; stride >= 32; stride >>= 1) {
185
+ - if (sid < stride) cost[sid] += cost[sid + stride];
186
+ - __syncthreads();
187
+ - }
188
+ - if (sid < 32) {
189
+ - acc_t v = cost[sid];
190
+ - v += __shfl_down_sync(0xFFFFFFFFULL, v, 16);
191
+ - v += __shfl_down_sync(0xFFFFFFFFULL, v, 8);
192
+ - v += __shfl_down_sync(0xFFFFFFFFULL, v, 4);
193
+ - v += __shfl_down_sync(0xFFFFFFFFULL, v, 2);
194
+ - v += __shfl_down_sync(0xFFFFFFFFULL, v, 1);
195
+ - if (sid == 0) cost[0] = v;
196
+ - }
197
+ - __syncthreads();
198
+ - acc_t grp_norm = tcq_acc_sqrt(cost[0]);
199
+ - acc_t inv_norm = grp_norm > 1e-10f ? 1.0f / grp_norm : 0.0f;
200
+ -
201
+ - if (sid < 128) x[sid] *= inv_norm;
202
+ - __syncthreads();
203
+ -
204
+ - // FWHT. The first five stages are contained within each 32-lane warp, so
205
+ - // use warp shuffles and only synchronize for the two cross-warp stages.
206
+ - if (sid < 128) {
207
+ - acc_t v = x[sid] * TURBO3_WHT_SIGNS1[sid];
208
+ - const int lane = sid & 31;
209
+ -#pragma unroll
210
+ - for (int h = 1; h < 32; h <<= 1) {
211
+ - const acc_t other = __shfl_xor_sync(0xFFFFFFFFULL, v, h);
212
+ - v = (lane & h) ? (other - v) : (v + other);
213
+ - }
214
+ - x[sid] = v;
215
+ - }
216
+ - __syncthreads();
217
+ - if (sid < 64) {
218
+ - const int j = ((sid >> 5) << 6) + (sid & 31);
219
+ - acc_t a = x[j], b = x[j + 32];
220
+ - x[j] = a + b; x[j + 32] = a - b;
221
+ - }
222
+ - __syncthreads();
223
+ - if (sid < 64) {
224
+ - acc_t a = x[sid], b = x[sid + 64];
225
+ - x[sid] = a + b; x[sid + 64] = a - b;
226
+ + // Per-warp smem: whitened vec x[128] (reused as outputs[] after Viterbi) + pred_min_cost[64] broadcast +
227
+ + // initial-state scratch. The Viterbi path-cost lives in registers (c[16], 16 states/lane). bt is global.
228
+ + __shared__ acc_t x_s[TCQ3_WPB][128];
229
+ + __shared__ acc_t pm_s[TCQ3_WPB][64];
230
+ + __shared__ int ini_s[TCQ3_WPB];
231
+ + acc_t * x = x_s[warp];
232
+ + acc_t * pred_min_cost = pm_s[warp];
233
+ + uint8_t * bt = bt_buf + group * (int64_t)(128 * 64); // global per-group backtrace slab
234
+ +
235
+ + // --- load 4 elements/lane (stride-32 layout: lane owns positions L, L+32, L+64, L+96) ---
236
+ + acc_t e0 = grp_src[lane], e1 = grp_src[lane+32], e2 = grp_src[lane+64], e3 = grp_src[lane+96];
237
+ + // --- norm (warp-shuffle sum of squares) ---
238
+ + acc_t ss = e0*e0 + e1*e1 + e2*e2 + e3*e3;
239
+ +#pragma unroll
240
+ + for (int o = 16; o > 0; o >>= 1) ss += __shfl_xor_sync(0xFFFFFFFFu, ss, o);
241
+ + acc_t grp_norm = tcq_acc_sqrt(ss);
242
+ + acc_t inv_norm = grp_norm > 1e-10f ? (acc_t)1.0 / grp_norm : (acc_t)0.0;
243
+ + e0 *= inv_norm; e1 *= inv_norm; e2 *= inv_norm; e3 *= inv_norm;
244
+ + // --- FWHT-128 (stride-32 layout: 5 shuffle-butterfly stages + 2 intra-thread cross stages,
245
+ + // byte-faithful to the 512-thread block FWHT) ---
246
+ + e0 *= TURBO3_WHT_SIGNS1[lane]; e1 *= TURBO3_WHT_SIGNS1[lane+32];
247
+ + e2 *= TURBO3_WHT_SIGNS1[lane+64]; e3 *= TURBO3_WHT_SIGNS1[lane+96];
248
+ +#pragma unroll
249
+ + for (int h = 1; h < 32; h <<= 1) {
250
+ + acc_t o0 = __shfl_xor_sync(0xFFFFFFFFu, e0, h); e0 = (lane & h) ? o0 - e0 : e0 + o0;
251
+ + acc_t o1 = __shfl_xor_sync(0xFFFFFFFFu, e1, h); e1 = (lane & h) ? o1 - e1 : e1 + o1;
252
+ + acc_t o2 = __shfl_xor_sync(0xFFFFFFFFu, e2, h); e2 = (lane & h) ? o2 - e2 : e2 + o2;
253
+ + acc_t o3 = __shfl_xor_sync(0xFFFFFFFFu, e3, h); e3 = (lane & h) ? o3 - e3 : e3 + o3;
254
+ }
255
+ - __syncthreads();
256
+ + { acc_t a = e0, b = e1; e0 = a + b; e1 = a - b; acc_t c = e2, d = e3; e2 = c + d; e3 = c - d; } // cross-32: blk0<->1, blk2<->3
257
+ + { acc_t a = e0, b = e2; e0 = a + b; e2 = a - b; acc_t c = e1, d = e3; e1 = c + d; e3 = c - d; } // cross-64: blk0<->2, blk1<->3
258
+ constexpr float inv_sqrt_128 = 0.08838834764831845f;
259
+ - if (sid < 128) x[sid] *= inv_sqrt_128 * TURBO3_WHT_SIGNS2[sid];
260
+ - __syncthreads();
261
+ -
262
+ - // opencoti-hook: TCQ activation-dump (F-D, #515). x[0..127] is now the post-FWHT unit-norm whitened vec β€”
263
+ - // exactly the xt the trellis quantizes against d_turbo3_tcq_codebook below, and x stays read-only through the
264
+ - // forward pass. Stage it for the F-E codebook retrain when enabled; nullptr (default) is a uniform no-op.
265
+ - // group == blockIdx.x < ne_total_groups, so [group*128 + sid] is in-bounds of the (ne_total_groups*128) buffer.
266
+ - if (d_tcq_dump_buf != nullptr && sid < 128) {
267
+ - d_tcq_dump_buf[group * 128 + sid] = (float) x[sid];
268
+ - }
269
+ -
270
+ - if (sid == 0) cost[0] = grp_norm;
271
+ - __syncthreads();
272
+ -
273
+ - acc_t saved_norm = cost[0];
274
+ -
275
+ - // Viterbi forward pass: double-buffered cost (1 sync/step, was 3)
276
+ - uint8_t * bt = use_shared_bt ? bt_shared : bt_buf + (int64_t)blockIdx.x * (128 * 64);
277
+ - cost[sid] = 0.0f;
278
+ - __syncthreads();
279
+ -
280
+ + e0 *= inv_sqrt_128 * TURBO3_WHT_SIGNS2[lane]; e1 *= inv_sqrt_128 * TURBO3_WHT_SIGNS2[lane+32];
281
+ + e2 *= inv_sqrt_128 * TURBO3_WHT_SIGNS2[lane+64]; e3 *= inv_sqrt_128 * TURBO3_WHT_SIGNS2[lane+96];
282
+ + x[lane] = e0; x[lane+32] = e1; x[lane+64] = e2; x[lane+96] = e3;
283
+ + __syncwarp();
284
+ +
285
+ + // opencoti-hook: TCQ activation-dump (F-D, #515). e0..e3 are the post-FWHT unit-norm whitened vec.
286
+ + if (d_tcq_dump_buf != nullptr) {
287
+ + d_tcq_dump_buf[group*128 + lane] = (float) e0;
288
+ + d_tcq_dump_buf[group*128 + lane+32] = (float) e1;
289
+ + d_tcq_dump_buf[group*128 + lane+64] = (float) e2;
290
+ + d_tcq_dump_buf[group*128 + lane+96] = (float) e3;
291
+ + }
292
+ + acc_t saved_norm = grp_norm;
293
+ +
294
+ + // --- Viterbi forward DP: warp-synchronous (16 states/lane, thread-local predecessor-min, __syncwarp). ---
295
+ + // lane L owns states {16L..16L+15} = trellis-groups 2L (states 16L..16L+7) and 2L+1 (states 16L+8..16L+15);
296
+ + // each group's 8 predecessors (g*8..g*8+7) fall entirely within lane g/2, so the predecessor-min is
297
+ + // thread-local (no shuffle). A 64-entry per-warp smem broadcast under __syncwarp replaces the block barrier.
298
+ + acc_t c[16];
299
+ +#pragma unroll
300
+ + for (int i = 0; i < 16; i++) c[i] = 0.0f;
301
+ for (int t = 0; t < 128; t++) {
302
+ - // Double-buffer: even steps read cost/write cost_b, odd steps read cost_b/write cost
303
+ - acc_t * cost_rd = (t & 1) ? cost_b : cost;
304
+ - acc_t * cost_wr = (t & 1) ? cost : cost_b;
305
+ -
306
+ + acc_t pm0 = c[0]; int pp0 = 0;
307
+ +#pragma unroll
308
+ + for (int j = 1; j < 8; j++) { if (c[j] < pm0) { pm0 = c[j]; pp0 = j; } }
309
+ + acc_t pm1 = c[8]; int pp1 = 0;
310
+ +#pragma unroll
311
+ + for (int j = 1; j < 8; j++) { if (c[8+j] < pm1) { pm1 = c[8+j]; pp1 = j; } }
312
+ + pred_min_cost[2*lane] = pm0; pred_min_cost[2*lane+1] = pm1;
313
+ + bt[t*64 + 2*lane] = (uint8_t) pp0; bt[t*64 + 2*lane+1] = (uint8_t) pp1;
314
+ + __syncwarp();
315
+ acc_t xt = x[t];
316
+ -
317
+ - // Right-shift trellis: ns = (prev >> 3) | (out << 6). The best
318
+ - // predecessor depends only on sid's low 6 bits, so compute those 64
319
+ - // minima once instead of repeating the same 8-way scan for each out.
320
+ - if (sid < 64) {
321
+ - const int base_prev = sid << 3;
322
+ - acc_t best = cost_rd[base_prev];
323
+ - int best_p = 0;
324
+ -#pragma unroll
325
+ - for (int p = 1; p < 8; p++) {
326
+ - acc_t c = cost_rd[base_prev | p];
327
+ - if (c < best) {
328
+ - best = c;
329
+ - best_p = p;
330
+ - }
331
+ - }
332
+ - pred_min_cost[sid] = best;
333
+ - bt[t * 64 + sid] = (uint8_t) best_p;
334
+ - }
335
+ - __syncthreads();
336
+ -
337
+ - const int pred_idx = sid & 0x3F;
338
+ - acc_t dist = xt - d_turbo3_tcq_codebook[sid];
339
+ - dist = dist * dist;
340
+ -
341
+ - cost_wr[sid] = pred_min_cost[pred_idx] + dist;
342
+ - __syncthreads();
343
+ - }
344
+ - // After 128 steps (even count): final costs are in cost[] (step 127 writes to cost)
345
+ -
346
+ - // Warp argmin over 512 costs
347
+ - {
348
+ - acc_t my_cost = cost[sid];
349
+ - int my_idx = sid;
350
+ - #pragma unroll
351
+ - for (int offset = 16; offset > 0; offset >>= 1) {
352
+ - acc_t other_cost = __shfl_xor_sync(0xFFFFFFFFULL, my_cost, offset);
353
+ - int other_idx = __shfl_xor_sync(0xFFFFFFFFULL, my_idx, offset);
354
+ - if (other_cost < my_cost) { my_cost = other_cost; my_idx = other_idx; }
355
+ - }
356
+ - if (sid % 32 == 0) {
357
+ - warp_min_cost[sid / 32] = my_cost;
358
+ - warp_min_idx[sid / 32] = my_idx;
359
+ - }
360
+ +#pragma unroll
361
+ + for (int i = 0; i < 16; i++) { int s = 16*lane + i; acc_t d = xt - d_turbo3_tcq_codebook[s]; c[i] = pred_min_cost[s & 0x3F] + d * d; }
362
+ + __syncwarp();
363
+ }
364
+ - __syncthreads();
365
+ - if (sid < 32) {
366
+ - acc_t best = (sid < 16) ? warp_min_cost[sid] : 3.4028234663852886e38f;
367
+ - int best_idx = (sid < 16) ? warp_min_idx[sid] : 0;
368
+ -#pragma unroll
369
+ - for (int offset = 16; offset > 0; offset >>= 1) {
370
+ - acc_t other_cost = __shfl_down_sync(0xFFFFFFFFULL, best, offset);
371
+ - int other_idx = __shfl_down_sync(0xFFFFFFFFULL, best_idx, offset);
372
+ - if (other_cost < best) {
373
+ - best = other_cost;
374
+ - best_idx = other_idx;
375
+ - }
376
+ - }
377
+ - if (sid == 0) {
378
+ - shared_initial_state = best_idx; // temporarily: best final state (becomes initial after backtrack)
379
+ - }
380
+ + // --- argmin over 512 states (per-lane min over 16, then warp-shuffle) ---
381
+ + acc_t mc = c[0]; int mi = 16*lane;
382
+ +#pragma unroll
383
+ + for (int i = 1; i < 16; i++) { if (c[i] < mc) { mc = c[i]; mi = 16*lane + i; } }
384
+ +#pragma unroll
385
+ + for (int o = 16; o > 0; o >>= 1) {
386
+ + acc_t oc = __shfl_xor_sync(0xFFFFFFFFu, mc, o); int oi = __shfl_xor_sync(0xFFFFFFFFu, mi, o);
387
+ + if (oc < mc || (oc == mc && oi < mi)) { mc = oc; mi = oi; }
388
+ }
389
+ - __syncthreads();
390
+ -
391
+ - // Save x[] to global buffer before backtrack overwrites it
392
+ -
393
+ - // Backtrack (inherently sequential, reads global bt)
394
+ + // --- backtrack (lane 0; outputs reuse the x slab β€” x no longer needed) ---
395
+ uint8_t * outputs = (uint8_t *)x;
396
+ - if (sid == 0) {
397
+ - int state = shared_initial_state;
398
+ + if (lane == 0) {
399
+ + int state = mi;
400
+ for (int t = 127; t >= 0; t--) {
401
+ outputs[t] = (uint8_t)(state >> 6);
402
+ - int p = bt[t * 64 + (state & 0x3F)];
403
+ + int p = bt[t*64 + (state & 0x3F)];
404
+ state = ((state & 0x3F) << 3) | p;
405
+ }
406
+ - shared_initial_state = state;
407
+ + ini_s[warp] = state;
408
+ }
409
+ - __syncthreads();
410
+ + __syncwarp();
411
+ + int initial_state = ini_s[warp];
412
+
413
+ - // Save output symbols to global buffer
414
+ -
415
+ - // Parallel recon norm: t>=2 can compute state directly from 3 outputs (3 shifts of 3 = 9 bits)
416
+ - acc_t my_recon_sq = 0.0f;
417
+ - if (sid < 128) {
418
+ - int cur_state;
419
+ - if (sid < 2) {
420
+ - cur_state = shared_initial_state;
421
+ - for (int t = 0; t <= sid; t++)
422
+ - cur_state = (cur_state >> 3) | (((int)outputs[t]) << 6);
423
+ - } else {
424
+ - cur_state = ((int)outputs[sid - 2] & 0x7)
425
+ - | (((int)outputs[sid - 1] & 0x7) << 3)
426
+ - | (((int)outputs[sid] & 0x7) << 6);
427
+ - }
428
+ - acc_t c = d_turbo3_tcq_codebook[cur_state];
429
+ - my_recon_sq = c * c;
430
+ - }
431
+ - cost[sid] = my_recon_sq;
432
+ - __syncthreads();
433
+ - for (int stride = 256; stride >= 32; stride >>= 1) {
434
+ - if (sid < stride) cost[sid] += cost[sid + stride];
435
+ - __syncthreads();
436
+ - }
437
+ - if (sid < 32) {
438
+ - acc_t v = cost[sid];
439
+ - v += __shfl_down_sync(0xFFFFFFFFULL, v, 16);
440
+ - v += __shfl_down_sync(0xFFFFFFFFULL, v, 8);
441
+ - v += __shfl_down_sync(0xFFFFFFFFULL, v, 4);
442
+ - v += __shfl_down_sync(0xFFFFFFFFULL, v, 2);
443
+ - v += __shfl_down_sync(0xFFFFFFFFULL, v, 1);
444
+ - if (sid == 0) cost[0] = v;
445
+ + // --- recon norm (warp-shuffle sum; lane owns recon positions L, L+32, L+64, L+96) ---
446
+ + acc_t recon_sq = 0.0f;
447
+ +#pragma unroll
448
+ + for (int b = 0; b < 4; b++) {
449
+ + int pos = lane + 32*b; int cur_state;
450
+ + if (pos < 2) { cur_state = initial_state; for (int t = 0; t <= pos; t++) cur_state = (cur_state >> 3) | (((int)outputs[t]) << 6); }
451
+ + else { cur_state = ((int)outputs[pos-2] & 0x7) | (((int)outputs[pos-1] & 0x7) << 3) | (((int)outputs[pos] & 0x7) << 6); }
452
+ + acc_t cc = d_turbo3_tcq_codebook[cur_state]; recon_sq += cc * cc;
453
+ }
454
+ - __syncthreads();
455
+ - acc_t recon_norm = tcq_acc_sqrt(cost[0]);
456
+ +#pragma unroll
457
+ + for (int o = 16; o > 0; o >>= 1) recon_sq += __shfl_xor_sync(0xFFFFFFFFu, recon_sq, o);
458
+ + acc_t recon_norm = tcq_acc_sqrt(recon_sq);
459
+ acc_t corrected_norm = (recon_norm > 1e-10f) ? saved_norm / recon_norm : saved_norm;
460
+ corrected_norm *= innerq_is_k ? d_tcq_norm_alpha : d_tcq_norm_alpha_v;
461
+
462
+ - // Parallel bitpack: qs stores 6 initial-state bits followed by 128 3-bit
463
+ - // output symbols. Each byte is independent, so avoid the old serial OR loop.
464
+ - if (sid < 49) {
465
+ - const int init_bits = (shared_initial_state >> 3) & 0x3F;
466
+ - uint8_t packed = 0;
467
+ -#pragma unroll
468
+ - for (int bit = 0; bit < 8; bit++) {
469
+ - const int pos = sid * 8 + bit;
470
+ - int v = 0;
471
+ - if (pos < 6) {
472
+ - v = (init_bits >> pos) & 1;
473
+ - } else {
474
+ - const int sym_bit_pos = pos - 6;
475
+ - const int sym_idx = sym_bit_pos / 3;
476
+ - if (sym_idx < 128) {
477
+ - v = (outputs[sym_idx] >> (sym_bit_pos % 3)) & 1;
478
+ - }
479
+ + // --- bitpack: lane handles bytes {lane, lane+32} that are < 49 ---
480
+ + const int init_bits = (initial_state >> 3) & 0x3F;
481
+ +#pragma unroll
482
+ + for (int bb = 0; bb < 2; bb++) {
483
+ + int byte = lane + 32*bb;
484
+ + if (byte < 49) {
485
+ + uint8_t packed = 0;
486
+ +#pragma unroll
487
+ + for (int bit = 0; bit < 8; bit++) {
488
+ + const int pos = byte*8 + bit; int v = 0;
489
+ + if (pos < 6) v = (init_bits >> pos) & 1;
490
+ + else { const int sym_bit_pos = pos - 6; const int sym_idx = sym_bit_pos / 3; if (sym_idx < 128) v = (outputs[sym_idx] >> (sym_bit_pos % 3)) & 1; }
491
+ + packed |= (uint8_t)(v << bit);
492
+ }
493
+ - packed |= (uint8_t)(v << bit);
494
+ + dst_blk->qs[byte] = packed;
495
+ }
496
+ - dst_blk->qs[sid] = packed;
497
+ - }
498
+ - if (sid == 0) {
499
+ - dst_blk->norm = __float2half((float)corrected_norm);
500
+ }
501
+ + if (lane == 0) dst_blk->norm = __float2half((float)corrected_norm);
502
+ }
503
+
504
+ // TCQ GET_ROWS dequantize (for non-FA paths)
505
+ @@ -455,10 +347,13 @@
506
+ -0.13432936f, -0.05269006f, +0.03536416f, +0.117640756f, -0.022776067f, +0.042032316f, +0.10472976f, +0.18042557f
507
+ };
508
+
509
+ -// 2-bit TCQ SET_ROWS encode: Viterbi optimal path with right-shift trellis (k=2, L=8)
510
+ -// Double-buffered cost arrays + global memory backtrace (128 syncs/group, was 384)
511
+ +// 2-bit TCQ SET_ROWS encode: warp-synchronous Viterbi (#533, k=2, L=8, 256 states). Same warp redesign as
512
+ +// turbo3_tcq: 1 group / 32-thread warp (8 states/lane, thread-local quartet predecessor-min, __syncwarp
513
+ +// broadcast, bt in global), TCQ2_WPB warps/block. Replaces the old 256-thread/__syncthreads block (latency-
514
+ +// bound on the 128-step barrier chain). bt always global (use_shared_bt ignored); acc_t fp64 path preserved.
515
+ +#define TCQ2_WPB 8
516
+ template<typename idx_t, typename acc_t = float>
517
+ -static __global__ void __launch_bounds__(256, 1) k_set_rows_turbo2_tcq(
518
+ +static __global__ void __launch_bounds__(TCQ2_WPB*32, 3) k_set_rows_turbo2_tcq(
519
+ const float * __restrict__ src0, const idx_t * __restrict__ src1,
520
+ block_turbo2_tcq * __restrict__ dst, const int64_t ne_total_groups,
521
+ uint8_t * __restrict__ bt_buf,
522
+ @@ -472,12 +367,14 @@
523
+ const uint3 ne00_fd, const uint3 ne01_fd, const uint3 ne02_fd,
524
+ const uint3 ne11_fd, const uint3 ne12_fd) {
525
+
526
+ - const int grp = blockIdx.x;
527
+ - if (grp >= ne_total_groups) return;
528
+ - const int sid = threadIdx.x; // 0..255 = trellis state
529
+ + const int warp = threadIdx.x >> 5;
530
+ + const int lane = threadIdx.x & 31;
531
+ + const int64_t group = (int64_t)blockIdx.x * TCQ2_WPB + warp;
532
+ + if (group >= ne_total_groups) return;
533
+ + (void) use_shared_bt; (void) ne10; (void) ne13; // bt is always global in the warp design
534
+
535
+ - // Compute source and destination pointers (all threads, used by thread 0)
536
+ - const int64_t i_base = int64_t(grp) * QK_TURBO2_TCQ;
537
+ + // Compute source and destination pointers (same index math as the 256-thread design; per-warp group)
538
+ + const int64_t i_base = group * QK_TURBO2_TCQ;
539
+ uint32_t tmp = (uint32_t)i_base; uint2 div_mod;
540
+ div_mod = fast_div_modulo(tmp, ne00_fd); const int64_t i00 = div_mod.y; tmp = div_mod.x;
541
+ div_mod = fast_div_modulo(tmp, ne01_fd); const int64_t i01 = div_mod.y; tmp = div_mod.x;
542
+ @@ -489,232 +386,120 @@
543
+ block_turbo2_tcq * dst_blk = (block_turbo2_tcq *)((char *)dst + dst_row*s1 + i02*s2 + i03*s3)
544
+ + (i00 / QK_TURBO2_TCQ);
545
+
546
+ - // Backtrace: one predecessor byte per 64 low-state groups per step.
547
+ - // The predecessor depends only on sid's low 6 bits (same as turbo3_tcq).
548
+ - extern __shared__ uint8_t bt_shared[];
549
+ - __shared__ acc_t x[128];
550
+ - __shared__ acc_t cost[256];
551
+ - __shared__ acc_t cost_b[256]; // double-buffering for Viterbi
552
+ - __shared__ int warp_min_idx[8];
553
+ - __shared__ acc_t warp_min_cost[8];
554
+ - __shared__ acc_t pred_min_cost[64];
555
+ - __shared__ int shared_initial_state;
556
+ -
557
+ - if (sid < 128) x[sid] = grp_src[sid];
558
+ - __syncthreads();
559
+ -
560
+ - __syncthreads();
561
+ -
562
+ - // Norm reduction
563
+ - cost[sid] = (sid < 128) ? x[sid] * x[sid] : 0.0f;
564
+ - __syncthreads();
565
+ - for (int stride = 128; stride >= 32; stride >>= 1) {
566
+ - if (sid < stride) cost[sid] += cost[sid + stride];
567
+ - __syncthreads();
568
+ - }
569
+ - if (sid < 32) {
570
+ - acc_t v = cost[sid];
571
+ - v += __shfl_down_sync(0xFFFFFFFFULL, v, 16);
572
+ - v += __shfl_down_sync(0xFFFFFFFFULL, v, 8);
573
+ - v += __shfl_down_sync(0xFFFFFFFFULL, v, 4);
574
+ - v += __shfl_down_sync(0xFFFFFFFFULL, v, 2);
575
+ - v += __shfl_down_sync(0xFFFFFFFFULL, v, 1);
576
+ - if (sid == 0) cost[0] = v;
577
+ - }
578
+ - __syncthreads();
579
+ - acc_t grp_norm = tcq_acc_sqrt(cost[0]);
580
+ - acc_t inv_norm = grp_norm > 1e-10f ? 1.0f / grp_norm : 0.0f;
581
+ -
582
+ - if (sid < 128) x[sid] *= inv_norm;
583
+ - __syncthreads();
584
+ -
585
+ - // FWHT. The first five stages use warp shuffles, the two cross-warp stages
586
+ - // use shared memory (same approach as turbo3_tcq).
587
+ - if (sid < 128) {
588
+ - acc_t v = x[sid] * TURBO3_WHT_SIGNS1[sid];
589
+ - const int lane = sid & 31;
590
+ -#pragma unroll
591
+ - for (int h = 1; h < 32; h <<= 1) {
592
+ - const acc_t other = __shfl_xor_sync(0xFFFFFFFFULL, v, h);
593
+ - v = (lane & h) ? (other - v) : (v + other);
594
+ - }
595
+ - x[sid] = v;
596
+ - }
597
+ - __syncthreads();
598
+ - if (sid < 64) {
599
+ - const int j = ((sid >> 5) << 6) + (sid & 31);
600
+ - acc_t a = x[j], b = x[j + 32];
601
+ - x[j] = a + b; x[j + 32] = a - b;
602
+ - }
603
+ - __syncthreads();
604
+ - if (sid < 64) {
605
+ - acc_t a = x[sid], b = x[sid + 64];
606
+ - x[sid] = a + b; x[sid + 64] = a - b;
607
+ + // Per-warp smem: whitened vec x[128] (reused as outputs[] after Viterbi) + pred_min_cost[64] broadcast +
608
+ + // initial-state scratch. Path-cost in registers (c[8], 8 states/lane). bt is global.
609
+ + __shared__ acc_t x_s[TCQ2_WPB][128];
610
+ + __shared__ acc_t pm_s[TCQ2_WPB][64];
611
+ + __shared__ int ini_s[TCQ2_WPB];
612
+ + acc_t * x = x_s[warp];
613
+ + acc_t * pred_min_cost = pm_s[warp];
614
+ + uint8_t * bt = bt_buf + group * (int64_t)(128 * 64); // global per-group backtrace slab
615
+ +
616
+ + // --- load 4 elements/lane (stride-32 layout: lane owns positions L, L+32, L+64, L+96) ---
617
+ + acc_t e0 = grp_src[lane], e1 = grp_src[lane+32], e2 = grp_src[lane+64], e3 = grp_src[lane+96];
618
+ + // --- norm (warp-shuffle sum of squares) ---
619
+ + acc_t ss = e0*e0 + e1*e1 + e2*e2 + e3*e3;
620
+ +#pragma unroll
621
+ + for (int o = 16; o > 0; o >>= 1) ss += __shfl_xor_sync(0xFFFFFFFFu, ss, o);
622
+ + acc_t grp_norm = tcq_acc_sqrt(ss);
623
+ + acc_t inv_norm = grp_norm > 1e-10f ? (acc_t)1.0 / grp_norm : (acc_t)0.0;
624
+ + e0 *= inv_norm; e1 *= inv_norm; e2 *= inv_norm; e3 *= inv_norm;
625
+ + // --- FWHT-128 (stride-32 layout: 5 shuffle-butterfly stages + 2 intra-thread cross stages) ---
626
+ + e0 *= TURBO3_WHT_SIGNS1[lane]; e1 *= TURBO3_WHT_SIGNS1[lane+32];
627
+ + e2 *= TURBO3_WHT_SIGNS1[lane+64]; e3 *= TURBO3_WHT_SIGNS1[lane+96];
628
+ +#pragma unroll
629
+ + for (int h = 1; h < 32; h <<= 1) {
630
+ + acc_t o0 = __shfl_xor_sync(0xFFFFFFFFu, e0, h); e0 = (lane & h) ? o0 - e0 : e0 + o0;
631
+ + acc_t o1 = __shfl_xor_sync(0xFFFFFFFFu, e1, h); e1 = (lane & h) ? o1 - e1 : e1 + o1;
632
+ + acc_t o2 = __shfl_xor_sync(0xFFFFFFFFu, e2, h); e2 = (lane & h) ? o2 - e2 : e2 + o2;
633
+ + acc_t o3 = __shfl_xor_sync(0xFFFFFFFFu, e3, h); e3 = (lane & h) ? o3 - e3 : e3 + o3;
634
+ }
635
+ - __syncthreads();
636
+ + { acc_t a = e0, b = e1; e0 = a + b; e1 = a - b; acc_t c = e2, d = e3; e2 = c + d; e3 = c - d; } // cross-32
637
+ + { acc_t a = e0, b = e2; e0 = a + b; e2 = a - b; acc_t c = e1, d = e3; e1 = c + d; e3 = c - d; } // cross-64
638
+ constexpr float inv_sqrt_128 = 0.08838834764831845f;
639
+ - if (sid < 128) x[sid] *= inv_sqrt_128 * TURBO3_WHT_SIGNS2[sid];
640
+ - __syncthreads();
641
+ -
642
+ - if (sid == 0) cost[0] = grp_norm;
643
+ - __syncthreads();
644
+ -
645
+ - acc_t saved_norm = cost[0];
646
+ -
647
+ - // Viterbi forward pass: double-buffered cost (1 sync/step, was 3)
648
+ - uint8_t * bt = use_shared_bt ? bt_shared : bt_buf + (int64_t)blockIdx.x * (128 * 64);
649
+ - cost[sid] = 0.0f;
650
+ - __syncthreads();
651
+ -
652
+ + e0 *= inv_sqrt_128 * TURBO3_WHT_SIGNS2[lane]; e1 *= inv_sqrt_128 * TURBO3_WHT_SIGNS2[lane+32];
653
+ + e2 *= inv_sqrt_128 * TURBO3_WHT_SIGNS2[lane+64]; e3 *= inv_sqrt_128 * TURBO3_WHT_SIGNS2[lane+96];
654
+ + x[lane] = e0; x[lane+32] = e1; x[lane+64] = e2; x[lane+96] = e3;
655
+ + __syncwarp();
656
+ + acc_t saved_norm = grp_norm;
657
+ +
658
+ + // --- Viterbi forward DP: warp-synchronous (8 states/lane, thread-local quartet pred-min, __syncwarp). ---
659
+ + // lane L owns states {8L..8L+7} = trellis-groups 2L (states 8L..8L+3) and 2L+1 (states 8L+4..8L+7); each
660
+ + // group's 4 predecessors (g*4..g*4+3) fall within lane g/2, so predecessor-min is thread-local (no shuffle).
661
+ + acc_t c[8];
662
+ +#pragma unroll
663
+ + for (int i = 0; i < 8; i++) c[i] = 0.0f;
664
+ for (int t = 0; t < 128; t++) {
665
+ - acc_t * cost_rd = (t & 1) ? cost_b : cost;
666
+ - acc_t * cost_wr = (t & 1) ? cost : cost_b;
667
+ -
668
+ + acc_t pm0 = c[0]; int pp0 = 0;
669
+ +#pragma unroll
670
+ + for (int j = 1; j < 4; j++) { if (c[j] < pm0) { pm0 = c[j]; pp0 = j; } }
671
+ + acc_t pm1 = c[4]; int pp1 = 0;
672
+ +#pragma unroll
673
+ + for (int j = 1; j < 4; j++) { if (c[4+j] < pm1) { pm1 = c[4+j]; pp1 = j; } }
674
+ + pred_min_cost[2*lane] = pm0; pred_min_cost[2*lane+1] = pm1;
675
+ + bt[t*64 + 2*lane] = (uint8_t) pp0; bt[t*64 + 2*lane+1] = (uint8_t) pp1;
676
+ + __syncwarp();
677
+ acc_t xt = x[t];
678
+ -
679
+ - // Right-shift trellis (k=2, L=8): ns = (prev >> 2) | (out << 6).
680
+ - // The best predecessor depends only on sid's low 6 bits, so compute
681
+ - // those 64 minima once instead of repeating the same 4-way scan per state.
682
+ - if (sid < 64) {
683
+ - const int base_prev = sid << 2;
684
+ - acc_t best = cost_rd[base_prev];
685
+ - int best_p = 0;
686
+ -#pragma unroll
687
+ - for (int p = 1; p < 4; p++) {
688
+ - acc_t c = cost_rd[base_prev | p];
689
+ - if (c < best) {
690
+ - best = c;
691
+ - best_p = p;
692
+ - }
693
+ - }
694
+ - pred_min_cost[sid] = best;
695
+ - bt[t * 64 + sid] = (uint8_t) best_p;
696
+ - }
697
+ - __syncthreads();
698
+ -
699
+ - const int pred_idx = sid & 0x3F;
700
+ - acc_t dist = xt - d_turbo2_tcq_codebook[sid];
701
+ - dist = dist * dist;
702
+ -
703
+ - cost_wr[sid] = pred_min_cost[pred_idx] + dist;
704
+ - __syncthreads();
705
+ - }
706
+ - // After 128 steps (even count): final costs in cost[]
707
+ -
708
+ - // Warp argmin over 256 costs
709
+ - {
710
+ - acc_t my_cost = cost[sid];
711
+ - int my_idx = sid;
712
+ - #pragma unroll
713
+ - for (int offset = 16; offset > 0; offset >>= 1) {
714
+ - acc_t other_cost = __shfl_xor_sync(0xFFFFFFFFULL, my_cost, offset);
715
+ - int other_idx = __shfl_xor_sync(0xFFFFFFFFULL, my_idx, offset);
716
+ - if (other_cost < my_cost) { my_cost = other_cost; my_idx = other_idx; }
717
+ - }
718
+ - if (sid % 32 == 0) {
719
+ - warp_min_cost[sid / 32] = my_cost;
720
+ - warp_min_idx[sid / 32] = my_idx;
721
+ - }
722
+ +#pragma unroll
723
+ + for (int i = 0; i < 8; i++) { int s = 8*lane + i; acc_t d = xt - d_turbo2_tcq_codebook[s]; c[i] = pred_min_cost[s & 0x3F] + d * d; }
724
+ + __syncwarp();
725
+ }
726
+ - __syncthreads();
727
+ - if (sid < 32) {
728
+ - acc_t best = (sid < 8) ? warp_min_cost[sid] : 3.4028234663852886e38f;
729
+ - int best_idx = (sid < 8) ? warp_min_idx[sid] : 0;
730
+ -#pragma unroll
731
+ - for (int offset = 16; offset > 0; offset >>= 1) {
732
+ - acc_t other_cost = __shfl_down_sync(0xFFFFFFFFULL, best, offset);
733
+ - int other_idx = __shfl_down_sync(0xFFFFFFFFULL, best_idx, offset);
734
+ - if (other_cost < best) {
735
+ - best = other_cost;
736
+ - best_idx = other_idx;
737
+ - }
738
+ - }
739
+ - if (sid == 0) {
740
+ - shared_initial_state = best_idx;
741
+ - }
742
+ + // --- argmin over 256 states (per-lane min over 8, then warp-shuffle) ---
743
+ + acc_t mc = c[0]; int mi = 8*lane;
744
+ +#pragma unroll
745
+ + for (int i = 1; i < 8; i++) { if (c[i] < mc) { mc = c[i]; mi = 8*lane + i; } }
746
+ +#pragma unroll
747
+ + for (int o = 16; o > 0; o >>= 1) {
748
+ + acc_t oc = __shfl_xor_sync(0xFFFFFFFFu, mc, o); int oi = __shfl_xor_sync(0xFFFFFFFFu, mi, o);
749
+ + if (oc < mc || (oc == mc && oi < mi)) { mc = oc; mi = oi; }
750
+ }
751
+ - __syncthreads();
752
+ -
753
+ - // Save x[] to global buffer before backtrack overwrites it
754
+ -
755
+ - // Backtrack (inherently sequential, reads compressed bt)
756
+ + // --- backtrack (lane 0; outputs reuse the x slab) β€” turbo2: 2-bit output (state>>6), <<2 trellis shift ---
757
+ uint8_t * outputs = (uint8_t *)x;
758
+ - if (sid == 0) {
759
+ - int state = shared_initial_state;
760
+ + if (lane == 0) {
761
+ + int state = mi;
762
+ for (int t = 127; t >= 0; t--) {
763
+ outputs[t] = (uint8_t)(state >> 6);
764
+ - int p = bt[t * 64 + (state & 0x3F)];
765
+ + int p = bt[t*64 + (state & 0x3F)];
766
+ state = ((state & 0x3F) << 2) | p;
767
+ }
768
+ - shared_initial_state = state;
769
+ + ini_s[warp] = state;
770
+ }
771
+ - __syncthreads();
772
+ -
773
+ - // Save output symbols to global buffer
774
+ + __syncwarp();
775
+ + int initial_state = ini_s[warp];
776
+
777
+ - // Parallel recon norm: t>=3 can compute state directly from 4 outputs (4 shifts of 2 = 8 bits)
778
+ - acc_t my_recon_sq = 0.0f;
779
+ - if (sid < 128) {
780
+ - int cur_state;
781
+ - if (sid < 3) {
782
+ - cur_state = shared_initial_state;
783
+ - for (int t = 0; t <= sid; t++)
784
+ - cur_state = (cur_state >> 2) | (((int)outputs[t]) << 6);
785
+ - } else {
786
+ - cur_state = ((int)outputs[sid - 3] & 0x3)
787
+ - | (((int)outputs[sid - 2] & 0x3) << 2)
788
+ - | (((int)outputs[sid - 1] & 0x3) << 4)
789
+ - | (((int)outputs[sid] & 0x3) << 6);
790
+ - }
791
+ - acc_t c = d_turbo2_tcq_codebook[cur_state];
792
+ - my_recon_sq = c * c;
793
+ - }
794
+ - cost[sid] = my_recon_sq;
795
+ - __syncthreads();
796
+ - for (int stride = 128; stride >= 32; stride >>= 1) {
797
+ - if (sid < stride) cost[sid] += cost[sid + stride];
798
+ - __syncthreads();
799
+ - }
800
+ - if (sid < 32) {
801
+ - acc_t v = cost[sid];
802
+ - v += __shfl_down_sync(0xFFFFFFFFULL, v, 16);
803
+ - v += __shfl_down_sync(0xFFFFFFFFULL, v, 8);
804
+ - v += __shfl_down_sync(0xFFFFFFFFULL, v, 4);
805
+ - v += __shfl_down_sync(0xFFFFFFFFULL, v, 2);
806
+ - v += __shfl_down_sync(0xFFFFFFFFULL, v, 1);
807
+ - if (sid == 0) cost[0] = v;
808
+ + // --- recon norm (warp-shuffle sum; turbo2: pos<3 walk, else 4 symbols x 2-bit) ---
809
+ + acc_t recon_sq = 0.0f;
810
+ +#pragma unroll
811
+ + for (int b = 0; b < 4; b++) {
812
+ + int pos = lane + 32*b; int cur_state;
813
+ + if (pos < 3) { cur_state = initial_state; for (int t = 0; t <= pos; t++) cur_state = (cur_state >> 2) | (((int)outputs[t]) << 6); }
814
+ + else { cur_state = ((int)outputs[pos-3] & 0x3) | (((int)outputs[pos-2] & 0x3) << 2) | (((int)outputs[pos-1] & 0x3) << 4) | (((int)outputs[pos] & 0x3) << 6); }
815
+ + acc_t cc = d_turbo2_tcq_codebook[cur_state]; recon_sq += cc * cc;
816
+ }
817
+ - __syncthreads();
818
+ - acc_t recon_norm = tcq_acc_sqrt(cost[0]);
819
+ +#pragma unroll
820
+ + for (int o = 16; o > 0; o >>= 1) recon_sq += __shfl_xor_sync(0xFFFFFFFFu, recon_sq, o);
821
+ + acc_t recon_norm = tcq_acc_sqrt(recon_sq);
822
+ acc_t corrected_norm = (recon_norm > 1e-10f) ? saved_norm / recon_norm : saved_norm;
823
+ corrected_norm *= iq_is_k ? d_tcq_norm_alpha : d_tcq_norm_alpha_v;
824
+
825
+ - // Parallel bitpack: qs stores 6 initial-state bits followed by 128 2-bit
826
+ - // output symbols. Each byte is independent (2-bit symbols never cross byte
827
+ - // boundaries after the 6-bit prefix), so avoid the serial OR loop.
828
+ - if (sid < 33) {
829
+ - const int init_bits = (shared_initial_state >> 2) & 0x3F;
830
+ - uint8_t packed = 0;
831
+ -#pragma unroll
832
+ - for (int bit = 0; bit < 8; bit++) {
833
+ - const int pos = sid * 8 + bit;
834
+ - int v = 0;
835
+ - if (pos < 6) {
836
+ - v = (init_bits >> pos) & 1;
837
+ - } else {
838
+ - const int sym_bit_pos = pos - 6;
839
+ - const int sym_idx = sym_bit_pos / 2;
840
+ - if (sym_idx < 128) {
841
+ - v = (outputs[sym_idx] >> (sym_bit_pos % 2)) & 1;
842
+ - }
843
+ + // --- bitpack: 6 init bits + 128 x 2-bit symbols = 33 bytes; lane handles bytes {lane, lane+32} < 33 ---
844
+ + const int init_bits = (initial_state >> 2) & 0x3F;
845
+ +#pragma unroll
846
+ + for (int bb = 0; bb < 2; bb++) {
847
+ + int byte = lane + 32*bb;
848
+ + if (byte < 33) {
849
+ + uint8_t packed = 0;
850
+ +#pragma unroll
851
+ + for (int bit = 0; bit < 8; bit++) {
852
+ + const int pos = byte*8 + bit; int v = 0;
853
+ + if (pos < 6) v = (init_bits >> pos) & 1;
854
+ + else { const int sym_bit_pos = pos - 6; const int sym_idx = sym_bit_pos / 2; if (sym_idx < 128) v = (outputs[sym_idx] >> (sym_bit_pos % 2)) & 1; }
855
+ + packed |= (uint8_t)(v << bit);
856
+ }
857
+ - packed |= (uint8_t)(v << bit);
858
+ + dst_blk->qs[byte] = packed;
859
+ }
860
+ - dst_blk->qs[sid] = packed;
861
+ - }
862
+ - if (sid == 0) {
863
+ - dst_blk->norm = __float2half((float)corrected_norm);
864
+ }
865
+ + if (lane == 0) dst_blk->norm = __float2half((float)corrected_norm);
866
+ }
867
+
868
+ // 2-bit TCQ GET_ROWS dequantize
patches/0084-cuda-graphs.patch ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ diff --git a/llamafile/cuda.sh b/llamafile/cuda.sh
2
+ --- a/llamafile/cuda.sh
3
+ +++ b/llamafile/cuda.sh
4
+ @@ -259,6 +259,19 @@
5
+ COMMON_FLAGS="$COMMON_FLAGS -DGGML_CUDA_FA_ALL_QUANTS"
6
+ fi
7
+
8
+ +# opencoti-hook: cuda-graphs (#533 decode-tps lever L6) β€” enable CUDA graph capture+replay.
9
+ +# Upstream llama.cpp's CMake defines GGML_CUDA_USE_GRAPHS by default; the llamafile cuda.sh build
10
+ +# omitted it, so the entire `#ifdef USE_CUDA_GRAPH` capture/replay path (ggml-cuda.cu:3318/4478/
11
+ +# 4515/4593/4698; common.cuh:1218) was compiled OUT β€” every decode step launched hundreds of
12
+ +# kernels individually with full per-launch CPU overhead on a latency-bound (~4.5% SM) decode.
13
+ +# This flag turns it on; the runtime guards (split buffers, unsupported node types, the opencoti
14
+ +# NEO head-split force-disable) fall back to eager capture-by-capture when a graph isn't viable, so
15
+ +# it composes with the opencoti stack. Tunable: OPENCOTI_NO_CUDA_GRAPHS=1 omits it (pre-#533).
16
+ +# See docs/evaluations/decode-levers.md (L6).
17
+ +if [ "${OPENCOTI_NO_CUDA_GRAPHS:-0}" != "1" ]; then
18
+ + COMMON_FLAGS="$COMMON_FLAGS -DGGML_CUDA_USE_GRAPHS"
19
+ +fi
20
+ +
21
+ # opencoti-hook: cuda-build-parallelism (#531) β€” nvcc intra-TU arch threading. Each nvcc TU compiles
22
+ # the gencode arches (sm_75/80/86/89/90/120f); without --threads they run serially, so any TU left
23
+ # alone at the build tail uses one core while the rest idle. --threads compiles the arches
patches/0085-dca-quant-prefill-inlaunch.patch ADDED
@@ -0,0 +1,237 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ From: opencoti
2
+ Subject: [PATCH 0085] DCA quant-KV prefill β€” in-launch single-pass dequant (#554)
3
+
4
+ Move scalar/turbo K/V dequant for the multi-chunk DCA fused flash-attn op out of the
5
+ two-stage whole-cache graph lift (dca_lift_to_f16: q->f32->f16 materialized every
6
+ forward, GPU-memory-bound and O(cache) β€” catastrophic at >=512k) and into a single-pass
7
+ to_fp16_nc pool convert inside the fused launch (mirrors the non-DCA launch_fattn fast
8
+ path). Byte-identical to the prior lift (both compute d*q in f32 then round to f16);
9
+ restores DCA + quantized-KV as a first-class long-context path (~12x at 85k, ~30x at
10
+ 512k: 46 -> 1379+ tok/s, the 1M q5_0/q4_0 path). Also moves the regime==3 fused
11
+ early-out above the f16-K asserts in ggml_cuda_flash_attn_ext_lse so quant-K cells reach
12
+ the fused path instead of asserting.
13
+
14
+ dca_is_inkernel_liftable() gates the defer to Q8_0/Q4_0/Q4_1/Q5_0/Q5_1/Q6_0 (+ the
15
+ turbo/TCQ families via the existing to_fp16_nc table); f16/bf16 keep the stock path.
16
+ See .wolf/buglog.json bug-720.
17
+
18
+ diff --git a/llama.cpp/src/dca.cpp b/llama.cpp/src/dca.cpp
19
+ --- a/llama.cpp/src/dca.cpp
20
+ +++ b/llama.cpp/src/dca.cpp
21
+ @@ -204,6 +204,20 @@
22
+ return ggml_cast(ctx0, x, GGML_TYPE_F16);
23
+ }
24
+
25
+ +// opencoti F5 dca (DCA all-KV C-perf, bug-720): can the FUSED multi-chunk kernel dequant this cache type
26
+ +// to f16 INSIDE its launch (single-pass to_fp16_nc into a transient pool buffer, mirroring launch_fattn),
27
+ +// instead of dca_lift_to_f16 whole-cache materializing it to f16 in the GRAPH every forward? That graph
28
+ +// lift is the prefill cliff: at 98k/q5_0 the two-stage q->f32->f16 cast ran every ubatch and dragged
29
+ +// DCA-on prefill to ~138 tps vs ~1212 for the same scalar-quant cache DCA-off (which already converts
30
+ +// per-op via launch_fattn). Restrict to the SCALAR quants with a to_fp16_nc converter (convert.cu:843);
31
+ +// turbo/TCQ/bf16 keep dca_lift_to_f16 (already a single-pass DIRECT ->f16 cast, not the slow two-stage),
32
+ +// f16 is a no-op. The in-op convert is byte-IDENTICAL to the lift (both compute d*q in f32 then round to
33
+ +// f16), so this is a pure perf change, gateable by logit-equivalence.
34
+ +static bool dca_is_inkernel_liftable(enum ggml_type t) {
35
+ + return t == GGML_TYPE_Q8_0 || t == GGML_TYPE_Q4_0 || t == GGML_TYPE_Q4_1 ||
36
+ + t == GGML_TYPE_Q5_0 || t == GGML_TYPE_Q5_1 || t == GGML_TYPE_Q6_0;
37
+ +}
38
+ +
39
+ ggml_tensor * llm_graph_context::build_attn_dca_core(
40
+ const llama_kv_cache_context * mctx_cur,
41
+ llm_graph_input_dca * inp_dca,
42
+ @@ -226,24 +240,53 @@
43
+ // the kernel). At f16 this is a no-op (byte-identical to the prior assert path). Context-shift
44
+ // stays correct independently: the modular K-shift re-ropes the STORED, still-quantized cache
45
+ // via build_rope_shift's existing dequant->rope->requant quantized branch (llama-kv-cache.cpp).
46
+ - ggml_tensor * k_dca = dca_lift_to_f16(ctx0, mctx_cur->get_k(ctx0, il));
47
+ + ggml_tensor * k_raw = mctx_cur->get_k(ctx0, il);
48
+ + ggml_tensor * v_raw = mctx_cur->get_v(ctx0, il);
49
+ +
50
+ + // opencoti F5 dca (DCA all-KV C-perf, bug-720): on the analytical-band (multi-chunk) path the fused
51
+ + // kernel dequants scalar-quant K/V to f16 IN ITS LAUNCH (single-pass to_fp16_nc, see
52
+ + // ggml_cuda_flash_attn_ext_mma_f16_dca_fused_case), so pass the quantized cache THROUGH instead of
53
+ + // whole-cache materializing it to f16 via dca_lift_to_f16 every forward (the prefill cliff). Gate:
54
+ + // - !single_chunk: only the fused op carries the in-launch convert; single_chunk uses stock
55
+ + // ggml_flash_attn_ext (launch_fattn already converts scalar-quant natively) and runs only while
56
+ + // n_kv <= chunk (a tiny cache) so its lift cost is negligible β€” keep it on the safe lift path.
57
+ + // - dca_is_inkernel_liftable: scalar quants the fused launch's to_fp16_nc can read.
58
+ + // - !v_trans: a transposed quant V view breaks the dim-0 (block) contiguity to_fp16_nc requires; the
59
+ + // permute(0,2,1,3) below keeps dim-0 intact, but a transpose would not β€” fall back to the lift.
60
+ + // The in-op convert is byte-identical to dca_lift_to_f16, so deferred output == lifted output.
61
+ + const bool v_trans_raw = v_raw->nb[1] > v_raw->nb[2];
62
+ + const bool defer_quant_lift =
63
+ + !inp_dca->single_chunk &&
64
+ + dca_is_inkernel_liftable(k_raw->type) &&
65
+ + dca_is_inkernel_liftable(v_raw->type) &&
66
+ + !v_trans_raw;
67
+
68
+ // (2) read V once; permute K + V to the flash-attn layout [n_embd_head, seq, n_head, n_stream].
69
+ // opencoti F5 dca #444 (DCA all-KV C1): lift a block/scalar-quantized V (q8_0/turbo/...) to f16
70
+ // while it is still CONTIGUOUS β€” a permuted view of a block type is not cleanly cpy-able (the
71
+ // block stride spans dim-0), mirroring build_attn_mha's pre-permute turbo cast. f32 V keeps the
72
+ // post-permute cast below (unchanged); f16 V is untouched (byte-identical).
73
+ - ggml_tensor * v = mctx_cur->get_v(ctx0, il);
74
+ - if (v->type != GGML_TYPE_F16 && v->type != GGML_TYPE_F32) {
75
+ - v = dca_lift_to_f16(ctx0, v);
76
+ - }
77
+ - const bool v_trans = v->nb[1] > v->nb[2];
78
+ - ggml_tensor * vp = ggml_permute(ctx0, v, 0, 2, 1, 3);
79
+ - if (v_trans) {
80
+ - vp = ggml_transpose(ctx0, vp);
81
+ - }
82
+ - if (vp->type == GGML_TYPE_F32) {
83
+ - vp = ggml_cast(ctx0, vp, GGML_TYPE_F16);
84
+ + ggml_tensor * k_dca;
85
+ + ggml_tensor * vp;
86
+ + if (defer_quant_lift) {
87
+ + // pass scalar-quant K/V straight to the fused op; permute(0,2,1,3) is a clean view (dim-0 = the
88
+ + // block axis, untouched) that the launch's to_fp16_nc dequants with the permuted strides.
89
+ + k_dca = k_raw;
90
+ + vp = ggml_permute(ctx0, v_raw, 0, 2, 1, 3);
91
+ + } else {
92
+ + k_dca = dca_lift_to_f16(ctx0, k_raw);
93
+ + ggml_tensor * v = v_raw;
94
+ + if (v->type != GGML_TYPE_F16 && v->type != GGML_TYPE_F32) {
95
+ + v = dca_lift_to_f16(ctx0, v);
96
+ + }
97
+ + const bool v_trans = v->nb[1] > v->nb[2];
98
+ + vp = ggml_permute(ctx0, v, 0, 2, 1, 3);
99
+ + if (v_trans) {
100
+ + vp = ggml_transpose(ctx0, vp);
101
+ + }
102
+ + if (vp->type == GGML_TYPE_F32) {
103
+ + vp = ggml_cast(ctx0, vp, GGML_TYPE_F16);
104
+ + }
105
+ }
106
+ ggml_tensor * kp = ggml_permute(ctx0, k_dca, 0, 2, 1, 3);
107
+
108
+ diff --git a/llama.cpp/ggml/src/ggml-cuda/fattn.cu b/llama.cpp/ggml/src/ggml-cuda/fattn.cu
109
+ --- a/llama.cpp/ggml/src/ggml-cuda/fattn.cu
110
+ +++ b/llama.cpp/ggml/src/ggml-cuda/fattn.cu
111
+ @@ -788,6 +788,17 @@
112
+ const int64_t n_batch = Q->ne[3];
113
+ const int64_t n_rows = n_head * n_q * n_batch;
114
+ GGML_ASSERT(dst->ne[0] == DV + 1 && "flash_attn_ext_lse dst must pack [O|lse] on ne[0]");
115
+ +
116
+ + // opencoti F5 dca (DCA all-KV C-perf, bug-720): the FUSED regime (op_params[4]==3) self-contains its
117
+ + // attention + normalization and now dequant-on-lifts a scalar-quantized K/V INSIDE its own launch
118
+ + // (ggml_cuda_flash_attn_ext_mma_f16_dca_fused_case). It never runs the streaming_lse recompute below, so
119
+ + // route it out BEFORE the recompute-only f32-Q / f16-K asserts (the streaming_lse_kernel reads K as f16,
120
+ + // hence those gates). Without this early-out a quantized DCA cache aborts here at line ~792.
121
+ + if (ggml_get_op_params_i32(dst, 4) == 3) {
122
+ + ggml_cuda_flash_attn_ext_mma_f16_dca_fused_switch(ctx, dst);
123
+ + return;
124
+ + }
125
+ +
126
+ GGML_ASSERT(Q->type == GGML_TYPE_F32 && "flash_attn_ext_lse expects f32 Q for the lse recompute");
127
+ GGML_ASSERT(K->type == GGML_TYPE_F16 && "flash_attn_ext_lse expects f16 K for the lse recompute");
128
+
129
+ @@ -820,19 +831,11 @@
130
+ const int dca_chunk = ggml_get_op_params_i32(dst, 3);
131
+ const int dca_regime = ggml_get_op_params_i32(dst, 4);
132
+
133
+ - // opencoti F5 dca perf (T87.pD Branch B): FUSED single-pass DCA. The three band passes (INTER/SUCC/
134
+ - // INTRA) are folded into ONE block-per-tile kernel that streams K/V once and swaps the pre-roped Q
135
+ - // variant at the 2 phase boundaries β€” no per-regime LSE emit, no host combine, no streaming_lse
136
+ - // fallback. The fused case reads its inputs straight off dst (src[0]=Q_intra, src[1]=K, src[2]=V,
137
+ - // src[3]=mask_intra, src[4]=pos_q_real, src[5]=Q_succ, src[6]=Q_inter, op_params[3]=chunk) and writes
138
+ - // normalized O into the leading DV*n_rows of dst's [O|lse] buffer (the trailing lse row is left
139
+ - // untouched β€” dca.cpp slices it off). Bands partition the causal range [0,i] β‡’ one online-softmax
140
+ - // pass is bit-faithful to the 3-op + host LSE combine it replaces.
141
+ - if (dca_regime == 3) {
142
+ - ggml_cuda_flash_attn_ext_mma_f16_dca_fused_switch(ctx, dst);
143
+ - return;
144
+ - }
145
+ -
146
+ + // opencoti F5 dca perf (T87.pD Branch B): the FUSED single-pass DCA regime (op_params[4]==3) β€” three
147
+ + // band passes (INTER/SUCC/INTRA) folded into ONE block-per-tile kernel, no per-regime LSE emit / host
148
+ + // combine / streaming_lse fallback β€” is dispatched at the TOP of this function (bug-720), before the
149
+ + // f32-Q / f16-K recompute asserts, so a quantized DCA cache reaches its in-launch dequant. Only the
150
+ + // non-fused (regime 0/1/2) lse-recompute path continues below.
151
+ float * lse_out = (float *) dst->data + DV * n_rows;
152
+ opencoti_fattn_dst_lse = lse_out;
153
+ opencoti_fattn_dst_lse_written = false;
154
+ diff --git a/llama.cpp/ggml/src/ggml-cuda/fattn-mma-f16.cuh b/llama.cpp/ggml/src/ggml-cuda/fattn-mma-f16.cuh
155
+ --- a/llama.cpp/ggml/src/ggml-cuda/fattn-mma-f16.cuh
156
+ +++ b/llama.cpp/ggml/src/ggml-cuda/fattn-mma-f16.cuh
157
+ @@ -2360,7 +2360,14 @@
158
+ const ggml_tensor * Qi = dst->src[6]; // Q_inter
159
+ const ggml_tensor * Ms = dst->src[7]; // opencoti F5 dca (#617B): dense SUCC mask (NULL β‡’ fast path)
160
+ const ggml_tensor * Mi = dst->src[8]; // opencoti F5 dca (#617B): dense INTER mask (NULL β‡’ fast path)
161
+ - GGML_ASSERT(K->type == GGML_TYPE_F16 && V->type == GGML_TYPE_F16 && "dca fused: f16 K/V required");
162
+ + // opencoti F5 dca (DCA all-KV C-perf, bug-720): the fused kernel reads f16 K/V, but a scalar-quantized
163
+ + // cache is now dequant'd to f16 IN THIS LAUNCH below (single-pass to_fp16_nc into a transient pool
164
+ + // buffer, mirroring launch_fattn) instead of being whole-cache materialized to f16 in the graph every
165
+ + // forward (the prefill cliff: ~138 vs ~1212 tps at 98k/q5_0). Accept f16, or any type with a to_fp16_nc
166
+ + // converter; reject anything else (the kernel would have no f16 source).
167
+ + GGML_ASSERT((K->type == GGML_TYPE_F16 || ggml_get_to_fp16_nc_cuda(K->type) != nullptr) &&
168
+ + (V->type == GGML_TYPE_F16 || ggml_get_to_fp16_nc_cuda(V->type) != nullptr) &&
169
+ + "dca fused: K/V must be f16 or have a to_fp16_nc converter");
170
+ GGML_ASSERT(PQ && Qs && Qi && mask && "dca fused: missing src tensors");
171
+ // opencoti F5 dca (#617B): FRAGMENTED fallback engaged iff the host attached the dense SUCC/INTER
172
+ // masks (only on a non-contiguous, post-context-shift cache). Both present or both absent.
173
+ @@ -2410,6 +2417,44 @@
174
+ ggml_cuda_pool & pool = ctx.pool();
175
+ cudaStream_t main_stream = ctx.stream();
176
+
177
+ + // opencoti F5 dca (DCA all-KV C-perf, bug-720): dequant-on-lift a scalar-quantized K/V into a transient
178
+ + // f16 pool buffer here β€” the SAME single-pass to_fp16_nc convert the stock MMA path uses (launch_fattn,
179
+ + // fattn-common.cuh) β€” replacing the old two-stage whole-cache graph lift (dca.cpp dca_lift_to_f16,
180
+ + // q->f32->f16) that ran every forward and dominated quant-KV DCA-on prefill. K/V are PERMUTED views
181
+ + // (kp/vp, dim-0 = the contiguous block axis), so use the non-contiguous (strided) converter and pack to
182
+ + // a contiguous f16 slot; the kernel then reads the slot with contiguous strides. f16 K/V skip the
183
+ + // convert (k_data/v_data alias the tensor, strides unchanged β‡’ byte-identical to the prior f16 path).
184
+ + ggml_cuda_pool_alloc<half> K_f16(pool);
185
+ + ggml_cuda_pool_alloc<half> V_f16(pool);
186
+ + const char * k_data = (const char *) K->data;
187
+ + size_t nb_k1 = K->nb[1], nb_k2 = K->nb[2], nb_k3 = K->nb[3];
188
+ + const char * v_data = (const char *) V->data;
189
+ + size_t nb_v1 = V->nb[1], nb_v2 = V->nb[2], nb_v3 = V->nb[3];
190
+ + if (K->type != GGML_TYPE_F16) {
191
+ + const size_t ts = ggml_type_size(K->type);
192
+ + GGML_ASSERT(K->nb[0] == ts && "dca fused: quant K must have contiguous dim-0 (block axis)");
193
+ + to_fp16_nc_cuda_t to_fp16 = ggml_get_to_fp16_nc_cuda(K->type);
194
+ + K_f16.alloc(ggml_nelements(K));
195
+ + to_fp16(k_data, K_f16.ptr, K->ne[0], K->ne[1], K->ne[2], K->ne[3],
196
+ + (int64_t) (nb_k1 / ts), (int64_t) (nb_k2 / ts), (int64_t) (nb_k3 / ts), main_stream);
197
+ + k_data = (const char *) K_f16.ptr;
198
+ + nb_k1 = K->ne[0] * sizeof(half);
199
+ + nb_k2 = K->ne[1] * nb_k1;
200
+ + nb_k3 = K->ne[2] * nb_k2;
201
+ + }
202
+ + if (V->type != GGML_TYPE_F16) {
203
+ + const size_t ts = ggml_type_size(V->type);
204
+ + GGML_ASSERT(V->nb[0] == ts && "dca fused: quant V must have contiguous dim-0 (block axis)");
205
+ + to_fp16_nc_cuda_t to_fp16 = ggml_get_to_fp16_nc_cuda(V->type);
206
+ + V_f16.alloc(ggml_nelements(V));
207
+ + to_fp16(v_data, V_f16.ptr, V->ne[0], V->ne[1], V->ne[2], V->ne[3],
208
+ + (int64_t) (nb_v1 / ts), (int64_t) (nb_v2 / ts), (int64_t) (nb_v3 / ts), main_stream);
209
+ + v_data = (const char *) V_f16.ptr;
210
+ + nb_v1 = V->ne[0] * sizeof(half);
211
+ + nb_v2 = V->ne[1] * nb_v1;
212
+ + nb_v3 = V->ne[2] * nb_v2;
213
+ + }
214
+ +
215
+ const int ntiles_x = (Q->ne[1] + ncols1 - 1) / ncols1;
216
+ const int gqa_ratio = Q->ne[2] / K->ne[2];
217
+ const int ntiles_z_gqa = (gqa_ratio + ncols2 - 1) / ncols2;
218
+ @@ -2490,7 +2535,7 @@
219
+
220
+ const dim3 grid(nblocks, 1, 1);
221
+ kernel<<<grid, block_dim, nbytes_shared_total, main_stream>>>(
222
+ - (const char *) Q->data, (const char *) K->data, (const char *) V->data,
223
+ + (const char *) Q->data, k_data, v_data, // opencoti dca bug-720: f16 slot (or aliased f16 tensor)
224
+ (const char *) mask->data,
225
+ Ms ? (const char *) Ms->data : nullptr, // opencoti F5 dca (#617B): dense SUCC mask (null β‡’ fast path)
226
+ Mi ? (const char *) Mi->data : nullptr, // opencoti F5 dca (#617B): dense INTER mask (null β‡’ fast path)
227
+ @@ -2499,8 +2544,8 @@
228
+ (float *) KQV->data, dst_meta_ptr,
229
+ scale, 0.0f /*max_bias*/, 1.0f /*m0*/, 1.0f /*m1*/, n_head_log2, logit_softcap,
230
+ Q->ne[0], ne01, Q->ne[2], Q->ne[3], Q->nb[1], Q->nb[2], Q->nb[3],
231
+ - K->ne[0], K->ne[1], K->ne[2], K->ne[3], K->nb[1], K->nb[2], K->nb[3],
232
+ - V->nb[1], V->nb[2], V->nb[3],
233
+ + K->ne[0], K->ne[1], K->ne[2], K->ne[3], nb_k1, nb_k2, nb_k3, // opencoti dca bug-720: slot strides
234
+ + nb_v1, nb_v2, nb_v3,
235
+ mask->ne[1], mask->ne[2], mask->ne[3], mask->nb[1], mask->nb[2], mask->nb[3]);
236
+ CUDA_CHECK(cudaGetLastError());
237
+
patches/0086-f16-moe-mmf-prefill.patch ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ From: opencoti
2
+ Subject: [PATCH 0086] F16 MoE prefill β€” route large-batch f16 mul_mat_id to MMF (#555)
3
+
4
+ ggml_cuda_mul_mat_id had no fast CUDA path for f16/bf16/f32 experts at prefill batch:
5
+ MMQ is quantized-only, and ggml_cuda_should_use_mmf rejected mul_mat_id whenever
6
+ src1_ncols (= n_tokens) exceeded 512 (or 128 when the expert dim > 1024). At prefill
7
+ (~32k tokens) that gate always failed, so f16 MoE fell into the host-side sorted
8
+ fallback in ggml_cuda_mul_mat_id: a per-call cudaStreamSynchronize (which also disables
9
+ CUDA graphs) + an O(n_expert*n_tokens*n_used) host sort + a per-expert cuBLAS GEMM loop.
10
+ Quantized MoE dodges all of it via the single fused MMQ-id kernel. Net: f16 128e prefill
11
+ ran ~12x slower than Q6_K (690 vs 8857 tok/s on Gemma-4-A4B 128e).
12
+
13
+ The MMF kernel's `ncols_dst > 16` branch (ggml_cuda_mul_mat_f, this file) already performs
14
+ GPU-side expert compaction (ggml_cuda_launch_mm_ids_helper, mmid.cuh) entirely on-stream
15
+ and handles arbitrary n_tokens -- the gate was a conservative tuning heuristic, not a
16
+ capability limit. This compiles out that mul_mat_id gate so large-batch f16/bf16/f32 MoE
17
+ rides the fast GPU-compacted MMF path. Measured on a real 128e-F16 (bs2 RTX PRO 6000):
18
+ prefill 690 -> 3081 tok/s (4.47x); RULER-vt@32k retrieval 100.0 (== Q6_K, correctness-clean).
19
+ The residual gap to Q6_K is just the legitimate f16-vs-q6 weight-bandwidth ratio (~2.2x).
20
+ Quantized MoE (MMQ path) and dense f16 mul_mat are untouched -- both are rejected/handled
21
+ before this gate. The upstream gate is preserved, compiled out, behind
22
+ -DOPENCOTI_MMF_LEGACY_GATE.
23
+
24
+ diff --git a/llama.cpp/ggml/src/ggml-cuda/mmf.cu b/llama.cpp/ggml/src/ggml-cuda/mmf.cu
25
+ --- a/llama.cpp/ggml/src/ggml-cuda/mmf.cu
26
+ +++ b/llama.cpp/ggml/src/ggml-cuda/mmf.cu
27
+ @@ -160,11 +160,22 @@
28
+ }
29
+
30
+ if (mul_mat_id) {
31
+ + // opencoti-hook: f16-moe-prefill (#555) β€” the upstream heuristic below rejected
32
+ + // large-batch (prefill) f16/bf16/f32 mul_mat_id, sending it to the host-sync sorted
33
+ + // fallback in ggml_cuda_mul_mat_id (per-call cudaStreamSynchronize + per-expert cuBLAS
34
+ + // loop + disabled CUDA graphs => measured 12x slower than the quantized MMQ-id path:
35
+ + // 690 vs 8857 tok/s prefill on Gemma-4-A4B 128e). The MMF kernel's `ncols_dst > 16`
36
+ + // branch already performs GPU-side expert compaction (ggml_cuda_launch_mm_ids_helper,
37
+ + // mmid.cuh) entirely on-stream and handles arbitrary n_tokens, so keep f16 MoE on the
38
+ + // fast fused MMF path for any batch size. Upstream gate preserved (disabled) for ref;
39
+ + // re-enable with -DOPENCOTI_MMF_LEGACY_GATE.
40
+ +#ifdef OPENCOTI_MMF_LEGACY_GATE
41
+ if (src0_ne[1] <= 1024 && src1_ncols > 512) {
42
+ return false;
43
+ } else if(src0_ne[1] > 1024 && src1_ncols > 128) {
44
+ return false;
45
+ }
46
+ +#endif
47
+ } else {
48
+ if (GGML_CUDA_CC_IS_RDNA3_0(cc) && src1_ncols > 8) {
49
+ return false;
patches/0087-dca-yarn-factor.patch ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ From: opencoti
2
+ Subject: [PATCH 0087] DCA: wire --dca-yarn-factor into build_dca_rope mscale (bug-740 / #550)
3
+
4
+ --dca-yarn-factor was fully plumbed (arg.cpp -> params -> cparams.dca_yarn_factor,
5
+ public C API, TS adapter) but never READ by the rope path, so the flag was inert
6
+ (bug-740). build_dca_rope now composes it into the attention scaling:
7
+ dca_attn_factor = attn_factor * cparams.dca_yarn_factor
8
+ and passes that to both ggml_rope_multi (MROPE) and ggml_rope_ext (NEOX) in place of
9
+ the bare attn_factor. This is the YaRN mscale piece of the DCA+YaRN composition: DCA
10
+ already keeps q-k distances in-distribution via the per-regime chunk remap, so only the
11
+ softmax temperature (mscale) needs stretching for ctx past the chunk grid -- freq_scale
12
+ / freq_base stay the model's baked values. Host-only (graph construction); the existing
13
+ CUDA rope kernel reads attn_factor from op_params unchanged, so no DSO rebuild.
14
+
15
+ Default 1.0 => dca_attn_factor == attn_factor exactly (x1.0f is bit-identity for finite
16
+ floats), so the flag-unset path is byte-identical to upstream -- the fix only makes a
17
+ previously-inert flag live; it cannot regress default behavior.
18
+
19
+ Empirical characterization (Qwen3-4B, native 40960, DCA chunk 16384, RULER 5-needle
20
+ substring under ignore_eos; NOT greedy-first-token):
21
+ f16 KV @ 2.4x native: yarn 1.0->5/5 1.25->4/5 1.5->1/5 2.0->0/5 (monotonic: flag live)
22
+ q8_0 KV ladder yarn=1.0: 2.4x->4 4.0x->5 5.0x->3 6.0x->4 (plain-DCA holds deep)
23
+ q8_0 KV @ 6.0x native: yarn 1.0->4 1.1->4 1.25->4 (flat; 1.18 = principled mscale, no win)
24
+ Verdict: the mscale lever has NO beneficial regime for DCA retrieval -- it only
25
+ over-sharpens already-concentrated attention. DEFAULT 1.0 IS CORRECT. Ships as a
26
+ power-user knob (raising it degrades retrieval); documented in docs/evaluations/context.md.
27
+
28
+ diff --git a/llama.cpp/src/dca.cpp b/llama.cpp/src/dca.cpp
29
+ --- a/llama.cpp/src/dca.cpp
30
+ +++ b/llama.cpp/src/dca.cpp
31
+ @@ -164,15 +164,23 @@
32
+ // Rope the DCA per-regime Q (or insert-K) faithfully for the arch. `pos` must be the width
33
+ // build_attn_inp_dca allocated: 4*n_tokens for MROPE (section split below), n_tokens for NEOX.
34
+ ggml_tensor * llm_graph_context::build_dca_rope(ggml_tensor * x, ggml_tensor * pos, const dca_rope & rp) const {
35
+ + // opencoti-hook: dca-yarn-factor (bug-740 / #550) β€” compose a YaRN mscale stretch with DCA.
36
+ + // cparams.dca_yarn_factor (1.0 = none; set by --dca-yarn-factor) multiplies the attention scaling
37
+ + // (mscale / attn_factor) so the extending-regime softmax is re-tempered for ctx past the chunk grid
38
+ + // (the final 2-4x of the DCA+YaRN sub-native extension recipe; see docs/features/gemma4_dca.md). DCA
39
+ + // already keeps q-k distances in-distribution via the per-regime chunk remap, so only the mscale needs
40
+ + // stretching β€” freq_scale/freq_base stay the model's baked values. Default 1.0 => this rope call is
41
+ + // byte-identical to upstream (inert unless the flag is set).
42
+ + const float dca_attn_factor = attn_factor * cparams.dca_yarn_factor;
43
+ if (dca_is_mrope(rope_type)) {
44
+ int sections[GGML_MROPE_SECTIONS] = { rp.sections[0], rp.sections[1], rp.sections[2], rp.sections[3] };
45
+ return ggml_rope_multi(ctx0, x, pos, rp.freq_factors,
46
+ rp.n_rot, sections, rope_type, n_ctx_orig, rp.freq_base, rp.freq_scale,
47
+ - ext_factor, attn_factor, beta_fast, beta_slow);
48
+ + ext_factor, dca_attn_factor, beta_fast, beta_slow);
49
+ }
50
+ return ggml_rope_ext(ctx0, x, pos, rp.freq_factors,
51
+ rp.n_rot, rope_type, n_ctx_orig, rp.freq_base, rp.freq_scale,
52
+ - ext_factor, attn_factor, beta_fast, beta_slow);
53
+ + ext_factor, dca_attn_factor, beta_fast, beta_slow);
54
+ }
55
+
56
+ // opencoti F5 dca #444 (DCA all-KV C1): dequant-on-lift a cached K/V tensor to f16 for the f16-only
patches/0088-sparsev-autopolicy.patch ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ From: opencoti
2
+ Subject: [PATCH 0088] sparse-v auto-policy: self-configure quantized-V skip from model arch (#565)
3
+
4
+ The FA-VEC quantized-V skip (sparse-V, #546) is a MEASURED lossless decode win on
5
+ interleaved-SWA models (Gemma-4: the small, concentrated global V-cache lets a
6
+ peak-relative static tau=0.05 harvest the sub-mean V tail β€” RULER niah 100 at
7
+ 64k/128k/256k on A4B-62e AND official Gemma-4-31B), but a no-win on full-attention
8
+ models (Qwen: diffuse weights cluster near the mean, nothing safely skippable). The
9
+ kernel reads its threshold from the TURBO_SPARSE_V_TAU env (ggml-cuda is a runtime
10
+ DSO, so env is the cross-DSO channel). Until now a user had to know to set that env.
11
+
12
+ This puts the intelligence in the BINARY so a STANDALONE `llamafile --server` self-
13
+ configures: at llama_context construction, when the user has NOT set TURBO_SPARSE_V_TAU
14
+ and the model is iSWA (hparams.swa_type != NONE) with a quantized V cache, default the
15
+ env to 0.05 (overwrite=0 β€” an explicit env still wins). Full-attention models leave it
16
+ unset -> kernel tau=0 -> dense, byte-identical. f16/bf16 V is inert via the kernel's own
17
+ `if constexpr type_V != F16/BF16` guard. Host-only (no DSO change); the existing kernel
18
+ getenv reads the pre-populated value. Announced once at WARN (the llamafile server
19
+ suppresses the model-load INFO block, and a binary that auto-changes inference should
20
+ say so + tell the user how to override).
21
+
22
+ Verified (3090): decision table PASS (policy fires iff iSWA+quant-V+no-env: A4B q8_0
23
+ auto=engaged, override/f16/Qwen-full-attn=off) + A/B at 24k lossless + non-slower.
24
+ ---
25
+ diff --git a/llama.cpp/src/llama-context.cpp b/llama.cpp/src/llama-context.cpp
26
+ index 722bd7f..2d87414 100644
27
+ --- a/llama.cpp/src/llama-context.cpp
28
+ +++ b/llama.cpp/src/llama-context.cpp
29
+ @@ -54,6 +54,34 @@ llama_context::llama_context(
30
+
31
+ const auto & hparams = model.hparams;
32
+
33
+ + // opencoti-hook: sparse-v auto-policy (#565) β€” see docs/features/sparse_attn.md
34
+ + // Self-configure the FA-VEC quantized-V skip (#546) from the model's OWN architecture, so a
35
+ + // STANDALONE `llamafile --server` gets the win with no flags (the intelligence lives in the binary,
36
+ + // not the opencoti TS adapter). The skip is a MEASURED lossless decode win on interleaved-SWA models
37
+ + // (Gemma-4: the tiny, concentrated global V-cache lets a peak-relative static Ο„=0.05 harvest the
38
+ + // sub-mean V tail β€” niah 100 at 64k/128k/256k on A4B-62e AND official Gemma-4-31B), but a no-win on
39
+ + // full-attention models (Qwen: diffuse weights cluster near the mean, nothing safely skippable). It
40
+ + // only engages for a QUANTIZED V cache (the kernel's `if constexpr type_V != F16/BF16` guard; -ctv
41
+ + // f16 is inert). ggml-cuda is a runtime-loaded DSO, so the kernel reads its threshold from the
42
+ + // TURBO_SPARSE_V_TAU env (the existing cross-DSO channel) β€” we just pre-populate a smart default when
43
+ + // the user has not, with overwrite=0 so an explicit TURBO_SPARSE_V_TAU (or eps/sink/recent) still
44
+ + // wins. Set as early as possible (before any graph_reserve / decode) so the kernel's lazy getenv
45
+ + // caches the policy value. Full-attention models leave the env unset β†’ kernel Ο„=0 β†’ dense, byte-identical.
46
+ + if (!hparams.vocab_only && getenv("TURBO_SPARSE_V_TAU") == nullptr) {
47
+ + const bool is_iswa = hparams.swa_type != LLAMA_SWA_TYPE_NONE;
48
+ + // Mirror the kernel's engage guard exactly: any non-float V cache (q8_0/q4_0/turbo*/tcq) is eligible.
49
+ + const bool quant_v = params.type_v != GGML_TYPE_F16 && params.type_v != GGML_TYPE_BF16 &&
50
+ + params.type_v != GGML_TYPE_F32;
51
+ + if (is_iswa && quant_v) {
52
+ + setenv("TURBO_SPARSE_V_TAU", "0.05", /*overwrite=*/0);
53
+ + // WARN level (not INFO): the llamafile server suppresses the model-load INFO block, and a binary
54
+ + // that auto-changes inference behavior should announce it audibly + tell the user how to override.
55
+ + LLAMA_LOG_WARN("%s: sparse-v auto-policy: iSWA + quantized V (%s) -> TURBO_SPARSE_V_TAU=0.05 "
56
+ + "(lossless decode win; export TURBO_SPARSE_V_TAU to override)\n",
57
+ + __func__, ggml_type_name(params.type_v));
58
+ + }
59
+ + }
60
+ +
61
+ cparams.n_seq_max = std::max(1u, params.n_seq_max);
62
+ if (cparams.n_seq_max > LLAMA_MAX_SEQ) {
63
+ throw std::runtime_error("n_seq_max must be <= " + std::to_string(LLAMA_MAX_SEQ));
patches/0089-sparse-attn-vslash-foundation.patch ADDED
The diff for this file is too large to render. See raw diff
 
patches/0090-rolling-kv-compute-reserve.patch ADDED
@@ -0,0 +1,333 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ From: opencoti
2
+ Subject: [PATCH 0089] rolling-KV two-pass compute-buffer reserve (bug-1342)
3
+
4
+ The POSITION_WINDOW resident-window sizer (opencoti_compute_resident_window_cells)
5
+ held back a FIXED OPENCOTI_HEADINFER_AUTO_RESERVE_BYTES = 1536 MiB for the compute
6
+ scratch that graph_reserve allocates AFTER the KV cache. But that compute buffer
7
+ SCALES with context β€” measured 3.63 GiB (3 800 045 568 B) at 262 144-524 288 ctx on
8
+ an RTX PRO 6000 β€” so the sizer kept ~2.1 GiB too much KV resident and graph_reserve
9
+ OOM'd at boot:
10
+
11
+ ggml_gallocr_reserve_n_impl: failed to allocate CUDA0 buffer of size 3800045568
12
+ graph_reserve: failed to allocate compute buffers
13
+
14
+ Fix: measure the real compute buffer instead of guessing it. Two-pass context init,
15
+ gated to the GPU flash-attn residency-managed path so the common resident case stays
16
+ single-pass / byte-identical:
17
+
18
+ Pass 1 (measure): create_memory with kv_window_measure_pass=true -> the window sizer
19
+ returns a MINIMAL resident window (tiny device KV, boots for ANY compute size).
20
+ sched_reserve() runs the pp worst-case reserve; sum the GPU backends' compute
21
+ buffer via backend_buf_exp_size.
22
+ Pass 2 (real): re-create_memory with kv_compute_reserve_mib = measured + 256 MiB
23
+ safety; the sizer now holds back the true compute buffer and sizes the window
24
+ against (budget - model - measured). sched_need_reserve=true; sched_reserve() again.
25
+
26
+ The compute buffer is independent of the window/tail split (it depends on
27
+ n_ubatch x n_kv x n_embd, and n_kv = full ctx regardless of the resident split), so
28
+ one measurement suffices. kv_compute_reserve_mib / kv_window_measure_pass are INTERNAL
29
+ cparams (set by the context two-pass, not from llama_context_params); hybrid-memory
30
+ paths pass 0/false (two-pass disabled). Host-only change β€” see
31
+ docs/features/rolling_kv_compute_reserve.md.
32
+
33
+ diff --git a/llama.cpp/src/llama-cparams.h b/llama.cpp/src/llama-cparams.h
34
+ --- a/llama.cpp/src/llama-cparams.h
35
+ +++ b/llama.cpp/src/llama-cparams.h
36
+ @@ -35,6 +35,16 @@
37
+ // opencoti F5 M7 Rolling KV β€” Stage 4 (#338) residency-tactic knob.
38
+ // 0 = auto (POSITION_WINDOW when eligible, else M2 head split), 1 = head, 2 = window.
39
+ uint32_t kv_residency_mode;
40
+ + // opencoti F5 M7 Rolling KV β€” two-pass compute-buffer reserve (bug-1342) β€”
41
+ + // see docs/features/rolling_kv_compute_reserve.md. INTERNAL (not user params):
42
+ + // set by the llama_context two-pass measurement, NOT from llama_context_params.
43
+ + // kv_compute_reserve_mib = MEASURED GPU compute-buffer size to hold back from the
44
+ + // resident-window budget (0 = use the fixed OPENCOTI_HEADINFER_AUTO_RESERVE_BYTES
45
+ + // default, i.e. pass 1 / two-pass disabled). kv_window_measure_pass forces the
46
+ + // window sizer to a minimal resident window (tiny device KV, boots for ANY compute
47
+ + // size) so pass 1 can measure the real compute buffer without OOM.
48
+ + uint32_t kv_compute_reserve_mib;
49
+ + bool kv_window_measure_pass;
50
+ // opencoti F5 M3 neo β€” asymmetric GPU/CPU pipelining of attention.
51
+ // 0 = off (M2 concat path runs; binary equivalent to upstream when
52
+ // headinfer_gpu_heads_frac == 1.0).
53
+ diff --git a/llama.cpp/src/llama-context.cpp b/llama.cpp/src/llama-context.cpp
54
+ --- a/llama.cpp/src/llama-context.cpp
55
+ +++ b/llama.cpp/src/llama-context.cpp
56
+ @@ -106,6 +106,12 @@
57
+ cparams.pcie_bw_gbps = params.pcie_bw_gbps;
58
+ // opencoti F5 M7 Rolling KV β€” Stage 4 (#338) residency knob β€” see docs/features/rolling_kv.md
59
+ cparams.kv_residency_mode = params.kv_residency_mode;
60
+ + // opencoti F5 M7 Rolling KV β€” two-pass compute-buffer reserve (bug-1342). INTERNAL:
61
+ + // the memory init below runs a measurement pass, then re-creates with the measured
62
+ + // reserve; these start at the "pass 1 / default reserve" values. See
63
+ + // docs/features/rolling_kv_compute_reserve.md.
64
+ + cparams.kv_compute_reserve_mib = 0;
65
+ + cparams.kv_window_measure_pass = false;
66
+ // opencoti F5 M3 neo β€” see docs/features/advanced_kv.md
67
+ cparams.neo_pipeline_mode = params.neo_pipeline_mode;
68
+ // opencoti F5 W3 #292 fused-MoE β€” see docs/features/advanced_kv.md
69
+ @@ -373,6 +379,18 @@
70
+ }
71
+ }
72
+
73
+ + // opencoti F5 M7 Rolling KV β€” two-pass compute-buffer reserve (bug-1342) β€”
74
+ + // see docs/features/rolling_kv_compute_reserve.md. The resident-window sizer must
75
+ + // hold back the compute buffer (allocated AFTER the KV by sched_reserve), but its
76
+ + // size scales with context and is only known post-reserve. So: PASS 1 creates a
77
+ + // MINIMAL resident window (tiny device KV, boots for any compute size), sched_reserve
78
+ + // measures the real GPU compute buffer, then PASS 2 re-creates the KV sized against
79
+ + // (free - model - measured). Scoped to the GPU flash-attn residency-managed path so
80
+ + // the common resident case is unaffected (the pass-2 result is identical there).
81
+ + const bool opencoti_two_pass = !hparams.vocab_only
82
+ + && cparams.flash_attn && cparams.offload_kqv
83
+ + && !model.devices.empty() && cparams.kv_residency_mode != 1;
84
+ +
85
+ // init the memory module
86
+ if (!hparams.vocab_only) {
87
+ llama_memory_params params_mem = {
88
+ @@ -382,6 +400,8 @@
89
+ /*.ctx_type= */ cparams.ctx_type,
90
+ };
91
+
92
+ + // opencoti bug-1342: pass 1 forces a minimal resident window (measure mode).
93
+ + cparams.kv_window_measure_pass = opencoti_two_pass;
94
+ memory.reset(model.create_memory(params_mem, cparams));
95
+ }
96
+
97
+ @@ -450,6 +470,41 @@
98
+
99
+ sched_reserve();
100
+
101
+ + // opencoti F5 M7 Rolling KV β€” two-pass compute-buffer reserve (bug-1342). Pass 1
102
+ + // (above) sized a minimal resident window so sched_reserve could not OOM; the real
103
+ + // GPU compute buffer is now in backend_buf_exp_size. Sum it over the GPU backends,
104
+ + // re-create the KV with that measured reserve, and re-reserve. The compute buffer is
105
+ + // independent of the window/tail split, so the measurement is exact for pass 2. See
106
+ + // docs/features/rolling_kv_compute_reserve.md.
107
+ + if (opencoti_two_pass && !model.hparams.no_alloc) {
108
+ + size_t gpu_compute = 0;
109
+ + for (size_t i = 0; i < backend_ptrs.size(); ++i) {
110
+ + if (ggml_backend_dev_type(ggml_backend_get_device(backend_ptrs[i])) == GGML_BACKEND_DEVICE_TYPE_GPU) {
111
+ + gpu_compute += backend_buf_exp_size[i];
112
+ + }
113
+ + }
114
+ + // + 256 MiB safety margin over the measured worst-case pp compute buffer.
115
+ + const uint32_t measured_mib = (uint32_t) (gpu_compute / (1024 * 1024)) + 256;
116
+ + LLAMA_LOG_INFO("%s: rolling-kv two-pass β€” measured GPU compute buffer = %u MiB; "
117
+ + "re-sizing resident window against it\n", __func__, measured_mib);
118
+ + cparams.kv_compute_reserve_mib = measured_mib;
119
+ + cparams.kv_window_measure_pass = false;
120
+ +
121
+ + // free the pass-1 KV (incl. its host tail) BEFORE creating pass 2 to avoid a
122
+ + // transient double host-tail allocation.
123
+ + memory.reset();
124
+ + llama_memory_params params_mem2 = {
125
+ + /*.type_k =*/ params.type_k,
126
+ + /*.type_v =*/ params.type_v,
127
+ + /*.swa_full =*/ params.swa_full,
128
+ + /*.ctx_type= */ cparams.ctx_type,
129
+ + };
130
+ + memory.reset(model.create_memory(params_mem2, cparams));
131
+ +
132
+ + sched_need_reserve = true;
133
+ + sched_reserve();
134
+ + }
135
+ +
136
+ if (!cparams.flash_attn) {
137
+ if (ggml_is_quantized(params.type_v)) {
138
+ throw std::runtime_error("quantized V cache was requested, but this requires Flash Attention");
139
+ diff --git a/llama.cpp/src/llama-kv-cache.h b/llama.cpp/src/llama-kv-cache.h
140
+ --- a/llama.cpp/src/llama-kv-cache.h
141
+ +++ b/llama.cpp/src/llama-kv-cache.h
142
+ @@ -131,6 +131,15 @@
143
+ // 0 = auto (POSITION_WINDOW when eligible, else M2 head split),
144
+ // 1 = head (force M2 split), 2 = window (force POSITION_WINDOW).
145
+ uint32_t kv_residency_mode,
146
+ + // opencoti F5 M7 Rolling KV β€” two-pass compute-buffer reserve
147
+ + // (bug-1342) β€” see docs/features/rolling_kv_compute_reserve.md.
148
+ + // compute_reserve_mib: MEASURED GPU compute-buffer size to hold
149
+ + // back from the resident-window budget (0 = fixed default).
150
+ + // window_measure_pass: force a MINIMAL resident window (tiny
151
+ + // device KV) so the context two-pass can measure the real
152
+ + // compute buffer without OOM.
153
+ + uint32_t compute_reserve_mib,
154
+ + bool window_measure_pass,
155
+ uint32_t n_seq_max,
156
+ uint32_t n_pad,
157
+ uint32_t n_swa,
158
+ diff --git a/llama.cpp/src/llama-kv-cache.cpp b/llama.cpp/src/llama-kv-cache.cpp
159
+ --- a/llama.cpp/src/llama-kv-cache.cpp
160
+ +++ b/llama.cpp/src/llama-kv-cache.cpp
161
+ @@ -138,6 +138,7 @@
162
+ uint32_t kv_size,
163
+ uint32_t n_stream,
164
+ uint32_t vram_target_mib,
165
+ + size_t compute_reserve_bytes, // opencoti bug-1342: measured (or default) compute hold-back
166
+ const std::function<bool(int32_t)> & filter) {
167
+ // No GPU offload β†’ nothing is "GPU-resident" and nothing to spill; the M2
168
+ // split is only meaningful for offloaded layers. Stay at vanilla.
169
+ @@ -187,9 +188,10 @@
170
+ if (vram_target_mib > 0) {
171
+ budget = std::min(budget, (size_t) vram_target_mib * 1024 * 1024);
172
+ }
173
+ - budget = budget > OPENCOTI_HEADINFER_AUTO_RESERVE_BYTES
174
+ - ? budget - OPENCOTI_HEADINFER_AUTO_RESERVE_BYTES
175
+ - : 0;
176
+ + // opencoti bug-1342: hold back the MEASURED compute buffer (two-pass) instead of the
177
+ + // fixed 1536 MiB guess when the context supplied it; 0 β†’ fall back to the default.
178
+ + const size_t reserve = compute_reserve_bytes ? compute_reserve_bytes : OPENCOTI_HEADINFER_AUTO_RESERVE_BYTES;
179
+ + budget = budget > reserve ? budget - reserve : 0;
180
+
181
+ if (kv_demand <= budget) {
182
+ LLAMA_LOG_INFO("%s: auto residency = GPU_RESIDENT (KV %.0f MiB fits budget "
183
+ @@ -235,10 +237,21 @@
184
+ uint32_t n_stream,
185
+ uint32_t vram_target_mib,
186
+ uint32_t n_pad,
187
+ + size_t compute_reserve_bytes, // opencoti bug-1342: measured (or default) compute hold-back
188
+ + bool measure_pass, // opencoti bug-1342: pass 1 β†’ minimal window (tiny device KV)
189
+ const std::function<bool(int32_t)> & filter) {
190
+ if (!offload || kv_size == 0) {
191
+ return kv_size; // no position-axis spill without GPU offload
192
+ }
193
+ + // opencoti bug-1342 two-pass: the MEASUREMENT pass forces a MINIMAL resident window
194
+ + // (one FA stride, ~0.5 MiB device KV) so pass 1 boots for ANY compute-buffer size.
195
+ + // The context then measures the real compute buffer and re-sizes in pass 2. Aligned
196
+ + // to lcm(n_pad, FATTN_KQ_STRIDE=256) so the tiny window is a valid FA tiling.
197
+ + if (measure_pass) {
198
+ + const uint32_t pad0 = n_pad ? n_pad : 32;
199
+ + const uint32_t walign0 = pad0 >= 256 ? pad0 : 256;
200
+ + return kv_size > walign0 ? walign0 : kv_size;
201
+ + }
202
+
203
+ size_t per_cell = 0; // bytes for ONE position cell across all this cache's KV layers
204
+ ggml_backend_dev_t dev = nullptr;
205
+ @@ -327,6 +340,8 @@
206
+ uint32_t vram_target_mib,
207
+ float pcie_bw_gbps,
208
+ uint32_t kv_residency_mode,
209
+ + uint32_t compute_reserve_mib,
210
+ + bool window_measure_pass,
211
+ uint32_t n_seq_max,
212
+ uint32_t n_pad,
213
+ uint32_t n_swa,
214
+ @@ -383,9 +398,14 @@
215
+ // A non-negative frac is the manual escape hatch / upstream default, used
216
+ // verbatim. Every split decision below reads eff_gpu_heads_frac, not the raw
217
+ // ctor argument, so the sentinel never leaks into the allocation math.
218
+ + // opencoti bug-1342: the compute-buffer reserve to hold back from the resident budget.
219
+ + // compute_reserve_mib > 0 = the MEASURED GPU compute buffer (context two-pass, pass 2);
220
+ + // 0 = pass 1 / two-pass disabled β†’ the sizers fall back to the fixed default.
221
+ + const size_t opencoti_compute_reserve_bytes = (size_t) compute_reserve_mib * 1024 * 1024;
222
+ const float eff_gpu_heads_frac = headinfer_gpu_heads_frac < 0.0f
223
+ ? opencoti_compute_auto_gpu_heads_frac(model, hparams, type_k, type_v,
224
+ - v_trans, offload, kv_size, n_stream, vram_target_mib, filter)
225
+ + v_trans, offload, kv_size, n_stream, vram_target_mib,
226
+ + opencoti_compute_reserve_bytes, filter)
227
+ : headinfer_gpu_heads_frac;
228
+
229
+ // opencoti F5 M7 Stage 3c β€” position-axis window sizing. Computed + logged now so the
230
+ @@ -395,7 +415,7 @@
231
+ // value is intentionally unused downstream at this sub-stage. See docs/features/advanced_kv.md.
232
+ const uint32_t window_cells = opencoti_compute_resident_window_cells(
233
+ model, hparams, type_k, type_v, v_trans, offload, kv_size, n_stream,
234
+ - vram_target_mib, n_pad, filter);
235
+ + vram_target_mib, n_pad, opencoti_compute_reserve_bytes, window_measure_pass, filter);
236
+ // opencoti F5 M7 Stage 4 (#338) β€” position-window mode is driven by the
237
+ // --kv-residency-mode knob (cparams.kv_residency_mode): 0=auto, 1=head, 2=window.
238
+ // The ELIGIBILITY predicate is the validated POSITION_WINDOW class: the sizer found a
239
+ diff --git a/llama.cpp/src/llama-kv-cache-iswa.h b/llama.cpp/src/llama-kv-cache-iswa.h
240
+ --- a/llama.cpp/src/llama-kv-cache-iswa.h
241
+ +++ b/llama.cpp/src/llama-kv-cache-iswa.h
242
+ @@ -41,6 +41,12 @@
243
+ // opencoti F5 M7 Rolling KV β€” Stage 4 (#338) residency-tactic knob;
244
+ // forwarded to both kv_base and kv_swa. 0 = auto, 1 = head, 2 = window.
245
+ uint32_t kv_residency_mode,
246
+ + // opencoti F5 M7 Rolling KV β€” two-pass compute-buffer reserve
247
+ + // (bug-1342); forwarded to both kv_base and kv_swa (only the
248
+ + // overflowing base cache windows). See
249
+ + // docs/features/rolling_kv_compute_reserve.md.
250
+ + uint32_t compute_reserve_mib,
251
+ + bool window_measure_pass,
252
+ uint32_t n_seq_max,
253
+ uint32_t n_ubatch,
254
+ uint32_t n_pad,
255
+ diff --git a/llama.cpp/src/llama-kv-cache-iswa.cpp b/llama.cpp/src/llama-kv-cache-iswa.cpp
256
+ --- a/llama.cpp/src/llama-kv-cache-iswa.cpp
257
+ +++ b/llama.cpp/src/llama-kv-cache-iswa.cpp
258
+ @@ -26,6 +26,8 @@
259
+ uint32_t vram_target_mib,
260
+ float pcie_bw_gbps,
261
+ uint32_t kv_residency_mode,
262
+ + uint32_t compute_reserve_mib,
263
+ + bool window_measure_pass,
264
+ uint32_t n_seq_max,
265
+ uint32_t n_ubatch,
266
+ uint32_t n_pad,
267
+ @@ -70,6 +72,7 @@
268
+ model, type_k, type_v,
269
+ v_trans, offload, unified, size_base, kv_size_initial, slot_shrink_idle_ms,
270
+ headinfer_gpu_heads_frac, vram_target_mib, pcie_bw_gbps, kv_residency_mode,
271
+ + compute_reserve_mib, window_measure_pass,
272
+ n_seq_max, n_pad, 0, LLAMA_SWA_TYPE_NONE, filter_base, reuse, sparse_attn_block_size);
273
+
274
+ LLAMA_LOG_INFO("%s: creating SWA KV cache, size = %u cells\n", __func__, size_swa);
275
+ @@ -78,6 +81,7 @@
276
+ model, type_k, type_v,
277
+ v_trans, offload, unified, size_swa, kv_size_initial, slot_shrink_idle_ms,
278
+ headinfer_gpu_heads_frac, vram_target_mib, pcie_bw_gbps, kv_residency_mode,
279
+ + compute_reserve_mib, window_measure_pass,
280
+ n_seq_max, n_pad, hparams.n_swa, hparams.swa_type, filter_swa, reuse, sparse_attn_block_size);
281
+ }
282
+
283
+ diff --git a/llama.cpp/src/llama-model.cpp b/llama.cpp/src/llama-model.cpp
284
+ --- a/llama.cpp/src/llama-model.cpp
285
+ +++ b/llama.cpp/src/llama-model.cpp
286
+ @@ -2100,6 +2100,10 @@
287
+ cparams.pcie_bw_gbps,
288
+ // opencoti F5 M7 Rolling KV β€” Stage 4 (#338) residency knob
289
+ cparams.kv_residency_mode,
290
+ + // opencoti F5 M7 Rolling KV β€” two-pass compute-buffer reserve
291
+ + // (bug-1342) β€” see docs/features/rolling_kv_compute_reserve.md
292
+ + cparams.kv_compute_reserve_mib,
293
+ + cparams.kv_window_measure_pass,
294
+ cparams.n_seq_max,
295
+ cparams.n_ubatch,
296
+ 1,
297
+ @@ -2130,6 +2134,10 @@
298
+ cparams.pcie_bw_gbps,
299
+ // opencoti F5 M7 Rolling KV β€” Stage 4 (#338) residency knob
300
+ cparams.kv_residency_mode,
301
+ + // opencoti F5 M7 Rolling KV β€” two-pass compute-buffer reserve
302
+ + // (bug-1342) β€” see docs/features/rolling_kv_compute_reserve.md
303
+ + cparams.kv_compute_reserve_mib,
304
+ + cparams.kv_window_measure_pass,
305
+ cparams.n_seq_max,
306
+ 1,
307
+ hparams.n_swa,
308
+ diff --git a/llama.cpp/src/llama-memory-hybrid.cpp b/llama.cpp/src/llama-memory-hybrid.cpp
309
+ --- a/llama.cpp/src/llama-memory-hybrid.cpp
310
+ +++ b/llama.cpp/src/llama-memory-hybrid.cpp
311
+ @@ -55,6 +55,9 @@
312
+ 0.0f,
313
+ // opencoti F5 M7 Stage 4 β€” hybrid memory does not thread the residency-mode knob; 0 = auto.
314
+ 0,
315
+ + // opencoti F5 M7 bug-1342 β€” hybrid memory does not use the window two-pass; 0 / false.
316
+ + 0,
317
+ + false,
318
+ n_seq_max,
319
+ n_pad,
320
+ n_swa,
321
+ diff --git a/llama.cpp/src/llama-memory-hybrid-iswa.cpp b/llama.cpp/src/llama-memory-hybrid-iswa.cpp
322
+ --- a/llama.cpp/src/llama-memory-hybrid-iswa.cpp
323
+ +++ b/llama.cpp/src/llama-memory-hybrid-iswa.cpp
324
+ @@ -56,6 +56,9 @@
325
+ 0.0f,
326
+ // opencoti F5 M7 Stage 4 β€” hybrid-iswa does not thread the residency-mode knob; 0 = auto.
327
+ 0,
328
+ + // opencoti F5 M7 bug-1342 β€” hybrid-iswa does not use the window two-pass; 0 / false.
329
+ + 0,
330
+ + false,
331
+ n_seq_max,
332
+ n_ubatch,
333
+ n_pad,
patches/0091-multi-session-graph-nodes.patch ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ From: opencoti
2
+ Subject: [PATCH 0090] multi-session graph-node budget for --parallel >=2 (bug-2094)
3
+
4
+ graph_max_nodes() sized the compute-graph meta pool as a single-stream budget
5
+ (8*n_tensors for dense archs; max(n_tokens*40, 32*n_tensors) for the qwen3next/
6
+ qwen35/kimi linear-attn branch), INDEPENDENT of the stream count. But with a
7
+ non-unified KV cache (the default; --parallel N => kv_unified=false) the graph
8
+ builds per-stream KV views/masks/copies (the `for s < n_stream` loops throughout
9
+ llama-kv-cache.cpp), so the actual node count scales with the number of streams.
10
+ On a 48-layer model (Qwen2.5-14B-Instruct-1M) --parallel 2 therefore overflowed
11
+ the pool at graph build:
12
+
13
+ llama.cpp/ggml/src/ggml.c:1790: not enough space in the context's memory pool
14
+
15
+ (server aborts at boot; --parallel 1 was fine; independent of --kv-residency-mode
16
+ and of ctx). Fix: scale the node budget by n_stream = (kv_unified ? 1 : n_seq_max),
17
+ applied to both branches. Tied to the exact per-stream-KV-graph mechanism; a
18
+ UNIFIED cache keeps n_stream=1 -> byte-identical to upstream, so the factor
19
+ self-disables when there is no per-stream graph.
20
+
21
+ Measured actual n_nodes (RTX 3090, ctx 8192): dense Qwen3-8B 1267/2131/3571 at
22
+ --parallel 1/2/4 (~776 nodes/stream); iSWA Gemma-4-A4B 2647/3367/4567 (~660/stream);
23
+ qwen35 Qwen3.5-9B 1856/2048/2368 (~170/stream). All boot with 2.5-4x headroom.
24
+ Verified on the original repro (bs2 RTX PRO 6000, 14B-1M-Q8_0 --parallel 2): CRASH
25
+ -> BOOT. Host-only (build:llamafile:make); no CUDA DSO, no restamp.
26
+
27
+ --- a/llama.cpp/src/llama-context.cpp
28
+ +++ b/llama.cpp/src/llama-context.cpp
29
+ @@ -2585,10 +2585,19 @@
30
+ //
31
+
32
+ uint32_t llama_context::graph_max_nodes(uint32_t n_tokens) const {
33
+ + // opencoti-hook: bug-2094 multi-session graph-node budget β€” see docs/features/rolling_kv_compute_reserve.md
34
+ + // With a non-unified KV cache (--parallel N β‡’ kv_unified=false) the graph builds
35
+ + // per-stream KV views/masks/copies (the `for s < n_stream` loops throughout
36
+ + // llama-kv-cache.cpp), so the node count scales with the stream count. The upstream
37
+ + // budgets below are single-stream; without this factor --parallel β‰₯2 overflows the
38
+ + // graph-meta pool (ggml.c:1790 "not enough space in the context's memory pool"). A
39
+ + // UNIFIED cache keeps n_stream=1 β†’ byte-identical to upstream, so the factor
40
+ + // self-disables exactly when there is no per-stream graph.
41
+ + const uint32_t n_stream = cparams.kv_unified ? 1u : std::max<uint32_t>(1u, cparams.n_seq_max);
42
+ if (model.arch == LLM_ARCH_QWEN3NEXT || model.arch == LLM_ARCH_KIMI_LINEAR || model.arch == LLM_ARCH_QWEN35 || model.arch == LLM_ARCH_QWEN35MOE) {
43
+ - return std::max<uint32_t>(n_tokens * 40, 32u * model.n_tensors());
44
+ + return std::max<uint32_t>(n_tokens * 40, 32u * model.n_tensors() * n_stream);
45
+ }
46
+ - uint32_t res = std::max<uint32_t>(1024u, 8u*model.n_tensors());
47
+ + uint32_t res = std::max<uint32_t>(1024u, 8u*model.n_tensors()) * n_stream;
48
+ for (const auto & lora : model.loras) {
49
+ res += lora->get_n_nodes();
50
+ }
patches/0092-cuda-build-ccache.patch ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ diff --git a/llamafile/build-functions.sh b/llamafile/build-functions.sh
2
+ index 4781516..5b1c26c 100644
3
+ --- a/llamafile/build-functions.sh
4
+ +++ b/llamafile/build-functions.sh
5
+ @@ -238,8 +238,15 @@ compile_gpu_sources_parallel() {
6
+ obj="$build_dir/${base}.o"
7
+ fi
8
+
9
+ - # Skip if object file is newer than source
10
+ - if [ -f "$obj" ] && [ "$obj" -nt "$src" ]; then
11
+ + # opencoti-hook: cuda-build-ccache (bug-858) β€” HEADER-AWARE freshness. The CUDA kernels
12
+ + # live in .cuh HEADERS; template-instance .cu stubs never change, so comparing the object
13
+ + # only to its .cu left STALE objects on header edits β†’ stale DSO. Also require the object
14
+ + # to be newer than the newest ggml-tree header ($OPENCOTI_CUDA_DEPS_NEWEST, exported by
15
+ + # cuda.sh). Compilation runs through ccache (see cuda.sh), so a header edit then recompiles
16
+ + # only the TUs whose expanded includes actually changed. The `-z` guard preserves the old
17
+ + # .cu-only behaviour for any caller that doesn't export the var.
18
+ + if [ -f "$obj" ] && [ "$obj" -nt "$src" ] \
19
+ + && { [ -z "$OPENCOTI_CUDA_DEPS_NEWEST" ] || [ "$obj" -nt "$OPENCOTI_CUDA_DEPS_NEWEST" ]; }; then
20
+ echo "[$count/$total] Skipping: $base.cu (up to date)"
21
+ continue
22
+ fi
23
+ @@ -298,8 +305,11 @@ compile_ggml_core() {
24
+ local name="${base%.*}"
25
+ local obj="$build_dir/ggml-core-${name}.o"
26
+
27
+ - # Skip if object file is newer than source
28
+ - if [ -f "$obj" ] && [ "$obj" -nt "$src" ]; then
29
+ + # opencoti-hook: cuda-build-ccache (bug-858) β€” HEADER-AWARE freshness (see cuda.sh). Skip
30
+ + # only when the object is newer than the source AND every ggml-tree header, so a header
31
+ + # edit correctly forces a recompile. `-z` guard keeps the old behaviour if the var is unset.
32
+ + if [ -f "$obj" ] && [ "$obj" -nt "$src" ] \
33
+ + && { [ -z "$OPENCOTI_CUDA_DEPS_NEWEST" ] || [ "$obj" -nt "$OPENCOTI_CUDA_DEPS_NEWEST" ]; }; then
34
+ echo " Skipping: $base (up to date)"
35
+ continue
36
+ fi
37
+ diff --git a/llamafile/cuda.sh b/llamafile/cuda.sh
38
+ index 18d4357..8e38209 100755
39
+ --- a/llamafile/cuda.sh
40
+ +++ b/llamafile/cuda.sh
41
+ @@ -293,10 +293,29 @@ collect_gpu_sources "$GGML_CUDA_DIR" "$EXTRA_SOURCES" "$NO_IQ_QUANTS" "$FA_ALL_Q
42
+ echo " Sources: $NUM_SOURCES .cu files"
43
+ echo ""
44
+
45
+ +# opencoti-hook: cuda-build-ccache (bug-858) β€” see docs/protocols/LLAMAFILE_UPGRADE.md.
46
+ +# The CUDA kernels live in .cuh HEADERS (fattn-vec.cuh, fattn-mma-f16.cuh, ...); the
47
+ +# template-instance .cu stubs that #include them essentially never change. The per-TU
48
+ +# incremental skip in build-functions.sh compared each object ONLY to its .cu file, so editing a
49
+ +# .cuh recompiled NOTHING β†’ a silently STALE DSO (bug-858; same class as the host bug-167 -MMD
50
+ +# fix, which was never applied to this hand-rolled CUDA loop). Fix: (1) route every TU compile
51
+ +# through ccache, whose include-manifest invalidates the cached object when the .cu OR any
52
+ +# included header changes; (2) export the newest header mtime in the ggml tree so the no-ccache
53
+ +# fallback skip is header-aware. Linking (link_shared_library) stays on raw $NVCC β€” ccache caches
54
+ +# compiles only.
55
+ +CUDA_CC="$NVCC"
56
+ +if command -v ccache >/dev/null 2>&1; then
57
+ + CUDA_CC="ccache $NVCC"
58
+ + echo " ccache: ON for CUDA compilation (header-aware via include manifest)"
59
+ +else
60
+ + echo " ccache: NOT FOUND β€” install ccache for fast + correct incremental CUDA rebuilds"
61
+ +fi
62
+ +export OPENCOTI_CUDA_DEPS_NEWEST=$(find "$GGML_CUDA_DIR/../.." \( -name '*.cuh' -o -name '*.h' -o -name '*.hpp' \) -printf '%T@ %p\n' 2>/dev/null | sort -rn | head -1 | cut -d' ' -f2-)
63
+ +
64
+ START_TIME=$(date +%s)
65
+
66
+ # Compile GPU sources
67
+ -compile_gpu_sources_parallel "$NVCC" "$ARCH_FLAGS" "$COMMON_FLAGS" "$BUILD_DIR" "$JOBS_EFF"
68
+ +compile_gpu_sources_parallel "$CUDA_CC" "$ARCH_FLAGS" "$COMMON_FLAGS" "$BUILD_DIR" "$JOBS_EFF"
69
+
70
+ COMPILE_TIME=$(date +%s)
71
+ echo "Compilation took $((COMPILE_TIME - START_TIME)) seconds"
patches/0093-mtp-dualctx-fused-nextn.patch ADDED
The diff for this file is too large to render. See raw diff
 
patches/0094-tinyblas-small-gemm.patch ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ From: opencoti
2
+ Subject: [PATCH 0094] tinyblas small-GEMM second config (bug-2104, #614)
3
+
4
+ opencoti-hook: tinyblas-small-gemm β€” skinny f32 GEMMs (Gemma MoE router
5
+ ffn_gate_inp [2816x128], qwen35 DeltaNet chunk GEMMs) hit a single 128x64-tile
6
+ config = 8 blocks on a 142-SM RTX 6000 (5% occupancy, 274us). Add a second
7
+ <32,32,64,...> config selected when the big config yields <24 blocks (90.7us;
8
+ prefill +25-35% on MoE models).
9
+
10
+ diff --git a/llamafile/tinyblas.cu b/llamafile/tinyblas.cu
11
+ index 758f68a..76ba099 100644
12
+ --- a/llamafile/tinyblas.cu
13
+ +++ b/llamafile/tinyblas.cu
14
+ @@ -539,6 +539,17 @@ tinyblasStatus_t tinyblasGE_launch(tinyblasHandle_t handle, tinyblasOperation_t
15
+ int ldc) {
16
+ if (can_use_matvec(aT, bT, m, n, k, alpha, beta))
17
+ return matvec_launch<WORD>(handle, m, k, A, lda, B, C);
18
+ + // opencoti-hook: tinyblas-small-gemm (#614/bug-2104) β€” the single 128x64-tile config
19
+ + // launches only ceil(m/64)*ceil(n/128) blocks after the row-major swap below; a skinny
20
+ + // GEMM like the Gemma MoE router ([2816x128] x [2816x512] per layer per ubatch) gets 8
21
+ + // blocks on a 142-SM GPU (~6% occupancy, ~274 us vs cuBLAS ~8 us) and was 35% of A4B
22
+ + // prefill GPU time. When the big config cannot fill the GPU, use a 32x32-tile config
23
+ + // (same deterministic FFMA warp2d kernel, 4x1 thread tiles, 2 warps) so the block
24
+ + // count grows 8x. Threshold 24 keeps every launch that already saturates on the big
25
+ + // tiles untouched.
26
+ + if ((long long)CEIL_DIV(m, 64) * CEIL_DIV(n, 128) < 24)
27
+ + return tinyblasGE_launcher<32, 32, 64, 8, 32, 16, 1, 4, 4, 64>(
28
+ + handle, bT, aT, n, m, k, alpha, B, ldb, A, lda, beta, C, ldc);
29
+ constexpr int TT = 256;
30
+ constexpr int BM = 128;
31
+ constexpr int BN = 64;
patches/0095-prefill-checkpoint-align.patch ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ From: opencoti
2
+ Subject: [PATCH 0095] prefill checkpoint alignment: min-step 8192 + state-buffer recycle pool (bug-2105/2107, #614)
3
+
4
+ - checkpoint_min_step 256 -> 8192 (align with b9859; stale port default caused
5
+ several SWA/recurrent state checkpoints per request instead of <=1).
6
+ - opencoti-hook: ckpt-buf-noinit β€” common_state_buf type + server-side recycle
7
+ pool (ckpt_buf_pool, cap 4) so steady-state requests reuse warm pages: no
8
+ GB-class value-init memset and no page-fault storm inside the state D2H copy.
9
+ (Measured dead end: a non-zeroing allocator alone regressed all models 8-13%.)
10
+
11
+ diff --git a/llama.cpp/common/common.h b/llama.cpp/common/common.h
12
+ index b688492..3bb6aa2 100644
13
+ --- a/llama.cpp/common/common.h
14
+ +++ b/llama.cpp/common/common.h
15
+ @@ -670,7 +673,7 @@ struct common_params {
16
+ bool cache_prompt = true; // whether to enable prompt caching
17
+ bool cache_idle_slots = true; // save and clear idle slots upon starting a new task
18
+ int32_t n_ctx_checkpoints = 32; // max number of context checkpoints per slot
19
+ - int32_t checkpoint_min_step = 256; // minimum spacing between context checkpoints
20
+ + int32_t checkpoint_min_step = 8192; // minimum spacing between context checkpoints
21
+ int32_t cache_ram_mib = 8192; // -1 = no limit, 0 - disable, 1 = 1 MiB, etc.
22
+
23
+ std::string hostname = "127.0.0.1";
24
+ @@ -1124,14 +1127,21 @@ enum ggml_opt_optimizer_type common_opt_get_optimizer(const char *);
25
+ // prompt utils
26
+ //
27
+
28
+ +// opencoti-hook: ckpt-buf-noinit (#614/bug-2107) β€” named buffer type for state snapshots so
29
+ +// the server can recycle them across requests (warm pages: no memset, no page-fault storm).
30
+ +// Measured dead end kept for the record: a non-zeroing allocator here made the COLD path
31
+ +// slower (page faults land inside the state D2H copy loop instead of a fast sequential
32
+ +// memset prefault) β€” plain vector cold + recycled warm is the optimum.
33
+ +using common_state_buf = std::vector<uint8_t>;
34
+ +
35
+ struct common_prompt_checkpoint {
36
+ int64_t n_tokens;
37
+
38
+ llama_pos pos_min;
39
+ llama_pos pos_max;
40
+
41
+ - std::vector<uint8_t> data_tgt;
42
+ - std::vector<uint8_t> data_dft;
43
+ + common_state_buf data_tgt;
44
+ + common_state_buf data_dft;
45
+
46
+ size_t size() const;
47
+
48
+ diff --git a/llama.cpp/tools/server/server-context.cpp b/llama.cpp/tools/server/server-context.cpp
49
+ index 7bad046..34b50b1 100644
50
+ --- a/llama.cpp/tools/server/server-context.cpp
51
+ +++ b/llama.cpp/tools/server/server-context.cpp
52
+ @@ -2119,20 +2158,50 @@ private:
53
+ return true;
54
+ }
55
+
56
+ + // opencoti-hook: ckpt-buf-noinit (#614/bug-2107) β€” recycle retired checkpoint state
57
+ + // buffers so the pages stay mapped and warm; a fresh GB-class allocation per request
58
+ + // pays a page-fault storm inside the state D2H copy (worse than the memset it replaced)
59
+ + std::vector<common_state_buf> ckpt_buf_pool;
60
+ +
61
+ + void ckpt_buf_recycle(common_prompt_checkpoint & ckpt) {
62
+ + if (ckpt_buf_pool.size() < 4 && ckpt.data_tgt.capacity() > 0) {
63
+ + ckpt_buf_pool.push_back(std::move(ckpt.data_tgt));
64
+ + }
65
+ + if (ckpt_buf_pool.size() < 4 && ckpt.data_dft.capacity() > 0) {
66
+ + ckpt_buf_pool.push_back(std::move(ckpt.data_dft));
67
+ + }
68
+ + }
69
+ +
70
+ + common_state_buf ckpt_buf_acquire() {
71
+ + if (ckpt_buf_pool.empty()) {
72
+ + return {};
73
+ + }
74
+ + common_state_buf buf = std::move(ckpt_buf_pool.back());
75
+ + ckpt_buf_pool.pop_back();
76
+ + return buf;
77
+ + }
78
+ +
79
+ // n_tokens_cur: the number of tokens added to the batch for the current slot
80
+ void create_checkpoint(server_slot & slot, const int64_t n_tokens_cur, llama_pos pos_min, llama_pos pos_max) {
81
+ while (slot.prompt.checkpoints.size() >= (size_t) params_base.n_ctx_checkpoints) {
82
+ // make room for the new checkpoint, if needed
83
+ - const auto & cur = slot.prompt.checkpoints.front();
84
+ + auto & cur = slot.prompt.checkpoints.front();
85
+
86
+ SLT_WRN(slot, "erasing old context checkpoint (pos_min = %d, pos_max = %d, n_tokens = %" PRId64 ", size = %.3f MiB)\n",
87
+ cur.pos_min, cur.pos_max, cur.n_tokens, (float) cur.size() / 1024 / 1024);
88
+
89
+ + ckpt_buf_recycle(cur); // opencoti-hook: ckpt-buf-noinit (#614/bug-2107)
90
+ slot.prompt.checkpoints.erase(slot.prompt.checkpoints.begin());
91
+ }
92
+
93
+ auto & cur = slot.prompt.checkpoints.emplace_back();
94
+
95
+ + // opencoti-hook: ckpt-buf-noinit (#614/bug-2107) β€” seed with warm recycled buffers
96
+ + cur.data_tgt = ckpt_buf_acquire();
97
+ + if (ctx_dft) {
98
+ + cur.data_dft = ckpt_buf_acquire();
99
+ + }
100
+ +
101
+ cur.update_pos(slot.prompt.n_tokens() - n_tokens_cur, pos_min, pos_max);
102
+
103
+ cur.update_tgt(ctx_tgt, slot.id, LLAMA_STATE_SEQ_FLAGS_PARTIAL_ONLY);
104
+ @@ -2980,9 +3042,10 @@ private:
105
+ {
106
+ // erase any checkpoints with pos_max > pos_next
107
+ for (auto it = slot.prompt.checkpoints.begin(); it != slot.prompt.checkpoints.end();) {
108
+ - const auto & cur = *it;
109
+ + auto & cur = *it;
110
+ if (cur.pos_max > pos_next) {
111
+ SLT_WRN(slot, "erased invalidated context checkpoint (pos_min = %d, pos_max = %d, n_tokens = %" PRId64 ", n_swa = %d, pos_next = %d, size = %.3f MiB)\n", cur.pos_min, cur.pos_max, cur.n_tokens, n_swa, pos_next, (float) cur.size() / 1024 / 1024);
112
+ + ckpt_buf_recycle(cur); // opencoti-hook: ckpt-buf-noinit (#614/bug-2107)
113
+ it = slot.prompt.checkpoints.erase(it);
114
+ } else {
115
+ ++it;
116
+ diff --git a/llama.cpp/tools/server/server-task.cpp b/llama.cpp/tools/server/server-task.cpp
117
+ index 803e31f..70ca8dc 100644
118
+ --- a/llama.cpp/tools/server/server-task.cpp
119
+ +++ b/llama.cpp/tools/server/server-task.cpp
120
+ @@ -2025,8 +2025,8 @@ server_prompt * server_prompt_cache::alloc(const server_prompt & prompt, size_t
121
+ }
122
+ }
123
+
124
+ - std::vector<uint8_t> state_data_tgt;
125
+ - std::vector<uint8_t> state_data_dft;
126
+ + common_state_buf state_data_tgt; // opencoti-hook: ckpt-buf-noinit (#614/bug-2107)
127
+ + common_state_buf state_data_dft;
128
+
129
+ // check if we can allocate enough memory for the new state
130
+ try {
131
+ diff --git a/llama.cpp/tools/server/server-task.h b/llama.cpp/tools/server/server-task.h
132
+ index 933bd99..380dc27 100644
133
+ --- a/llama.cpp/tools/server/server-task.h
134
+ +++ b/llama.cpp/tools/server/server-task.h
135
+ @@ -582,8 +582,8 @@ struct server_task_result_apply_lora : server_task_result {
136
+ };
137
+
138
+ struct server_prompt_data {
139
+ - std::vector<uint8_t> main;
140
+ - std::vector<uint8_t> drft;
141
+ + common_state_buf main; // opencoti-hook: ckpt-buf-noinit (#614/bug-2107)
142
+ + common_state_buf drft;
143
+
144
+ size_t size() const {
145
+ return main.size() + drft.size();
patches/0096-remove-single-ctx-gemma-mtp.patch ADDED
The diff for this file is too large to render. See raw diff
 
patches/0097-native-elf-cmake.patch ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ opencoti patch 0097 β€” native-ELF CMake lane (#619, secondary artifact of #613)
2
+
3
+ Makes the vendored tree buildable as a standard Linux ELF llama-server via
4
+ plain cmake + g++/nvcc (rpath libggml-cuda.so; clean gdb/nsys), alongside the
5
+ cosmocc Makefile lane which is UNTOUCHED (BUILD.mk lists sources explicitly
6
+ and never reads CMakeLists; the new stub TU is CMake-lane-only).
7
+
8
+ - 5 CMakeLists surgical hooks: add opencoti TUs absent from upstream source
9
+ lists (ggml-turbo-quant.c, ggml-neo-pipeline.cpp, ggml-iqk-flash-attn.cpp
10
+ -> ggml-base; dca.cpp -> llama; pcie-profile.cpp -> common; ELF stubs ->
11
+ ggml-cpu) + compile the Mozilla-overlay server.cpp with -Dmain=llama_server
12
+ (upstream defines llama_server directly; the cosmo lane renames at build).
13
+ - NEW ggml/src/ggml-cpu/opencoti-elf-stubs.cpp: return-false stubs for the
14
+ cosmocc-bound llamafile/sgemm.cpp CPU fastpath dispatchers (mixmul,
15
+ mixmul_needs, mixmul_iqk, fa_* helpers) β€” every call site falls back to the
16
+ upstream generic path (same contract as fa_helpers_unsupported.cpp), so the
17
+ ELF build loses only CPU fastpaths, never correctness.
18
+
19
+ Build (bs2-proven 2026-07-05): cmake -S llama.cpp -B build-elf -DGGML_CUDA=ON
20
+ -DGGML_CUDA_FA_ALL_QUANTS=ON (REQUIRED β€” turbo/TCQ FA-VEC instances are only
21
+ globbed under this flag; cosmo lane always sets it) -DCMAKE_CUDA_ARCHITECTURES=120
22
+ -DCMAKE_BUILD_TYPE=Release -DLLAMA_CURL=OFF -DGGML_NATIVE=OFF.
23
+ Gate: A4B -ngl 99 boot, dual-ctx assistant-MTP accept 0.764, 310.2 tps decode.
24
+
25
+ --- a/llama.cpp/ggml/src/CMakeLists.txt
26
+ +++ b/llama.cpp/ggml/src/CMakeLists.txt
27
+ @@ -205,6 +205,9 @@
28
+ ggml-threading.cpp
29
+ ggml-threading.h
30
+ ggml-quants.c
31
+ + ggml-turbo-quant.c # opencoti-hook: native-elf-cmake (#619) β€” turbo/TCQ CPU quant, in cosmo BUILD.mk but absent here
32
+ + ggml-neo-pipeline.cpp # opencoti-hook: native-elf-cmake (#619) β€” NEO pipeline control (pure std)
33
+ + ggml-iqk-flash-attn.cpp # opencoti-hook: native-elf-cmake (#619) β€” iqk FA enable flag (pure ggml)
34
+ ggml-quants.h
35
+ gguf.cpp)
36
+
37
+ --- a/llama.cpp/src/CMakeLists.txt
38
+ +++ b/llama.cpp/src/CMakeLists.txt
39
+ @@ -18,6 +18,7 @@
40
+ llama-context.cpp
41
+ llama-cparams.cpp
42
+ llama-grammar.cpp
43
+ + dca.cpp # opencoti-hook: native-elf-cmake (#619) β€” DCA (matches neither llama-*.cpp list nor models/ glob)
44
+ llama-graph.cpp
45
+ llama-hparams.cpp
46
+ llama-impl.cpp
47
+ --- a/llama.cpp/common/CMakeLists.txt
48
+ +++ b/llama.cpp/common/CMakeLists.txt
49
+ @@ -66,6 +66,7 @@
50
+ chat.cpp
51
+ chat.h
52
+ common.cpp
53
+ + pcie-profile.cpp # opencoti-hook: native-elf-cmake (#619) β€” PCIe bandwidth profile (rolling-KV)
54
+ common.h
55
+ console.cpp
56
+ console.h
57
+ --- a/llama.cpp/ggml/src/ggml-cpu/CMakeLists.txt
58
+ +++ b/llama.cpp/ggml/src/ggml-cpu/CMakeLists.txt
59
+ @@ -52,6 +52,7 @@
60
+ ggml-cpu/vec.cpp
61
+ ggml-cpu/ops.h
62
+ ggml-cpu/ops.cpp
63
+ + ggml-cpu/opencoti-elf-stubs.cpp # opencoti-hook: native-elf-cmake (#619) β€” sgemm.cpp dispatcher stubs (cosmo lane uses real sgemm.cpp)
64
+ )
65
+
66
+ target_compile_features(${GGML_CPU_NAME} PRIVATE c_std_11 cxx_std_17)
67
+ --- a/llama.cpp/tools/server/CMakeLists.txt
68
+ +++ b/llama.cpp/tools/server/CMakeLists.txt
69
+ @@ -31,6 +31,11 @@
70
+
71
+ set(TARGET llama-server-impl)
72
+
73
+ +# opencoti-hook: native-elf-cmake (#619) β€” the Mozilla overlay's server.cpp defines
74
+ +# `int main` (the cosmocc Makefile lane renames it at compile time); upstream
75
+ +# defines `llama_server` directly. Rename here so main.cpp's wrapper links.
76
+ +set_source_files_properties(server.cpp PROPERTIES COMPILE_DEFINITIONS "main=llama_server")
77
+ +
78
+ add_library(${TARGET}
79
+ server.cpp
80
+ server-http.cpp
81
+ --- /dev/null
82
+ +++ b/llama.cpp/ggml/src/ggml-cpu/opencoti-elf-stubs.cpp
83
+ @@ -0,0 +1,57 @@
84
+ +// opencoti-hook: native-elf-cmake (#619) β€” see docs/features/llamafile_build.md
85
+ +//
86
+ +// CMake(native-ELF)-lane-ONLY stubs for the llamafile/sgemm.cpp CPU fastpath
87
+ +// dispatchers. sgemm.cpp includes <cosmo.h> (cosmocc runtime CPUID/dispatch)
88
+ +// and cannot compile under plain g++; every call site treats a false return
89
+ +// as "not handled, use the upstream ggml helper" (ggml-cpu.c:1723 mixmul_iqk,
90
+ +// ops.cpp:8519/8560 flash-attn helpers β€” same contract as Mozilla's own
91
+ +// fa_helpers_unsupported.cpp), so these stubs are behavior-safe: the native
92
+ +// ELF build loses only the iqk/tinyBLAS *CPU* fastpaths, never correctness.
93
+ +// The cosmocc Makefile lane never compiles this file (BUILD.mk lists sources
94
+ +// explicitly) and keeps the real sgemm.cpp implementations.
95
+ +
96
+ +#include <cstddef>
97
+ +#include <cstdint>
98
+ +
99
+ +struct ggml_compute_params;
100
+ +struct ggml_tensor;
101
+ +
102
+ +extern "C" {
103
+ +
104
+ +// Top-level tinyBLAS MoE mixmul (ggml-cpu.c:1592, GGML_USE_LLAMAFILE):
105
+ +// false -> generic mul_mat_id path. _needs sizes its work buffer
106
+ +// (ggml-cpu.c:3002): 0 is consistent with mixmul never running.
107
+ +bool llamafile_mixmul(const struct ggml_compute_params *,
108
+ + const struct ggml_tensor *,
109
+ + const struct ggml_tensor *,
110
+ + const struct ggml_tensor *,
111
+ + struct ggml_tensor *) {
112
+ + return false;
113
+ +}
114
+ +
115
+ +size_t llamafile_mixmul_needs(const struct ggml_tensor *,
116
+ + const struct ggml_tensor *,
117
+ + const struct ggml_tensor *) {
118
+ + return 0;
119
+ +}
120
+ +
121
+ +bool llamafile_mixmul_iqk(long, long, long, int, int,
122
+ + const void *, const void *, float *,
123
+ + long, long, const void *, int, int) {
124
+ + return false;
125
+ +}
126
+ +
127
+ +bool llamafile_fa_vec_dot_f16(int, float *, const void *, const void *) {
128
+ + return false;
129
+ +}
130
+ +
131
+ +bool llamafile_fa_fp16_to_fp32_row(const void *, float *, int64_t) {
132
+ + return false;
133
+ +}
134
+ +
135
+ +bool llamafile_fa_simd_gemm(float *, const float *, const float *,
136
+ + int, int, int) {
137
+ + return false;
138
+ +}
139
+ +
140
+ +} // extern "C"
patches/0098-rolling-kv-lse-decode.patch ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ opencoti patch 0098 β€” rolling-KV: dst_lse emission for head_dim<=256 decode (bug-1843, task #623)
2
+
3
+ Extends the Stage-3a LSE channel (opencoti_fattn_dst_lse, drained by
4
+ launch_fattn's flash_attn_combine_results / stream-k fixup) to head_dim<=256
5
+ n_q==1 decode at all three streaming-FA call sites (S3b overlap, S3a
6
+ single-stream, S2 resident-view). Previously only head_dim>256 armed the
7
+ channel; Qwen-class D128 decode fell through to the per-tile
8
+ streaming_lse_kernel recompute β€” a <<<n_rows,WARP_SIZE>>> full key-rescan per
9
+ tile that was MEASURED as ~80% of spill-decode wall (bug-1843 A/B on the 3090:
10
+ 1.14 tps base vs 6.05 tps OPENCOTI_LSE_NOOP, Qwen3-8B-Q8_0 ctx40960 vt14000).
11
+
12
+ Mechanism: after the per-tile FA call, `lse_emitted = decode_lse &&
13
+ opencoti_fattn_dst_lse_written` β€” the written flag reports whether a finalize
14
+ (stream-k fixup or parallel-blocks combine) actually ran; the recompute now
15
+ fires only when the dispatch resolved to a no-finalize single-block path. The
16
+ combine emits the identical per-row value (kqmax + logf(denominator), row
17
+ order h + n_head*(qi + n_q*b)) the recompute produced. The `this_kv >
18
+ 2*FATTN_KQ_STRIDE` guard is retained everywhere (bug-263 conservatism).
19
+ Prefill and D>256 decode are behavior-identical.
20
+
21
+ Gates (2026-07-05, 3090, DSO df46dc36):
22
+ - perf: spill_base 1.14 -> 6.04 tps (5.3x, at the NOOP ceiling), needle
23
+ PRESENT; resident 42.5 tps unchanged; OPENCOTI_LSE_NOOP now speed-inert
24
+ (6.02) proving the recompute is structurally off the path.
25
+ - correctness: teacher-forced logit-equiv (Qwen3-8B D128, ours-resident REF vs
26
+ POSITION_WINDOW 256/16384 cells resident): real_frac=0.0, divergences=0,
27
+ frac_full_agree=1.0, mean_tv 0.0068 -> PASS.
28
+
29
+ DSO-only (fattn.cu; no header change, no host rebuild needed).
30
+
31
+ --- a/llama.cpp/ggml/src/ggml-cuda/fattn.cu
32
+ +++ b/llama.cpp/ggml/src/ggml-cuda/fattn.cu
33
+ @@ -1596,11 +1596,20 @@
34
+ // ntiles_KV=16 at 512, not 2). `>` is kept only because it is no worse
35
+ // than `>=`; the real mechanism is unisolated. Real Stage-3c tail tiles
36
+ // can be ≀512 at decode β†’ MUST resolve before relying on this path.
37
+ - const bool decode_lse = (n_q == 1 && head_dim > 256 && this_kv > 2*FATTN_KQ_STRIDE);
38
+ + // opencoti-hook: f5-rolling-kv bug-1843 β€” head_dim<=256 (Qwen-class VEC
39
+ + // decode) now ALSO arms the channel: the parallel-blocks combine emits
40
+ + // the per-row lse (Stage-3a formula in flash_attn_combine_results), and
41
+ + // opencoti_fattn_dst_lse_written reports whether a finalize actually ran
42
+ + // (T87.pD-opt O2 pattern) so the streaming_lse recompute below fires only
43
+ + // when the dispatch resolved to a no-finalize single-block path. The
44
+ + // recompute was measured at ~80% of spill-decode cost (1.14 vs 6.05 tps
45
+ + // A/B via OPENCOTI_LSE_NOOP).
46
+ + const bool decode_lse = (n_q == 1 && this_kv > 2*FATTN_KQ_STRIDE);
47
+ if (decode_lse) opencoti_fattn_dst_lse = lse_t;
48
+ ggml_cuda_flash_attn_ext(ctx, &dst_t);
49
+ + const bool lse_emitted = decode_lse && opencoti_fattn_dst_lse_written;
50
+
51
+ - if (!decode_lse) {
52
+ + if (!lse_emitted) {
53
+ // opencoti F5 bug-1840 fix (b) DIAGNOSTIC β€” OPENCOTI_LSE_NOOP skips
54
+ // the per-tile streaming_lse recompute (a low-parallelism
55
+ // <<<n_rows,32>>> rescan run once per tile because Qwen/head_dim≀256
56
+ @@ -1727,11 +1736,14 @@
57
+ // (softcap-aware). The `> 2*FATTN_KQ_STRIDE` boundary is NOT a fix for
58
+ // bug-263 (UNRESOLVED: decode tile_kv_full==512 corrupts on either lse
59
+ // path) β€” kept only as no-worse-than-`>=`. See .wolf/buglog.json.
60
+ - const bool decode_lse = (n_q == 1 && head_dim > 256 && this_kv > 2*FATTN_KQ_STRIDE);
61
+ + // bug-1843: head_dim<=256 also arms the channel; recompute only when no
62
+ + // finalize ran (opencoti_fattn_dst_lse_written) β€” see the S3b note above.
63
+ + const bool decode_lse = (n_q == 1 && this_kv > 2*FATTN_KQ_STRIDE);
64
+ if (decode_lse) opencoti_fattn_dst_lse = lse_t;
65
+ ggml_cuda_flash_attn_ext(ctx, &dst_t);
66
+ + const bool lse_emitted = decode_lse && opencoti_fattn_dst_lse_written;
67
+
68
+ - if (!decode_lse) {
69
+ + if (!lse_emitted) {
70
+ streaming_lse_kernel<<<n_rows, WARP_SIZE, 0, stream>>>(
71
+ (const char *)Q->data, sK,
72
+ mask ? (const char *)mask->data + (size_t)kv0*mask->nb[0] : nullptr,
73
+ @@ -1764,7 +1776,10 @@
74
+ // boundary is NOT a fix for bug-263 (UNRESOLVED: decode tile_kv_full==512
75
+ // corrupts on either lse path) β€” kept only as no-worse-than-`>=`. See the
76
+ // S3b overlap path's full note + .wolf/buglog.json.
77
+ - const bool decode_lse = (n_q == 1 && head_dim > 256 && this_kv > 2*FATTN_KQ_STRIDE);
78
+ + // bug-1843: head_dim<=256 also arms the channel; recompute only when no
79
+ + // finalize ran (opencoti_fattn_dst_lse_written) β€” see the S3b note above.
80
+ + const bool decode_lse = (n_q == 1 && this_kv > 2*FATTN_KQ_STRIDE);
81
+ + bool lse_emitted = false;
82
+ if (this_kv > 0) {
83
+ // FA over the tile's key-slice β†’ O_t (stock kernel, untouched).
84
+ ggml_tensor Kt = *K; Kt.ne[1] = this_kv; Kt.data = (char *)K->data + (size_t)kv0*K->nb[1];
85
+ @@ -1784,10 +1799,11 @@
86
+ dst_t.data = O_tiles.ptr + (size_t)t * nel;
87
+ if (decode_lse) opencoti_fattn_dst_lse = lse_t;
88
+ ggml_cuda_flash_attn_ext(ctx, &dst_t);
89
+ + lse_emitted = decode_lse && opencoti_fattn_dst_lse_written;
90
+ }
91
+
92
+ // lse_t (this_kv==0 β†’ empty tile: kernel writes -inf, combine ignores it).
93
+ - if (!decode_lse) {
94
+ + if (!lse_emitted) {
95
+ streaming_lse_kernel<<<n_rows, WARP_SIZE, 0, stream>>>(
96
+ (const char *)Q->data,
97
+ (const char *)K->data + (size_t)kv0*K->nb[1],
patches/0099-dca-gemma4-wiring.patch ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ opencoti patch 0099 β€” DCA: wire Gemma-4 builders (bug-2118, task #626 unblock)
2
+
3
+ `--dca on` was a SILENT NO-OP on all Gemma-4 archs: 0078-dca.patch wired only
4
+ the Qwen builders; dca.cpp's iswa build_attn_dca overload sat uncalled by
5
+ gemma4.cpp / gemma4-assistant.cpp. Consequence: every historical Gemma x DCA
6
+ eval measured DCA-off (contamination catalog: docs/evaluations bug-2118 notes).
7
+
8
+ Wiring: build_attn_inp_dca on the iswa base cache; per-layer dca_l gates
9
+ GLOBAL (non-SWA) layers only; Q/K rope moves inside build_attn_dca via the
10
+ dca_rope bundle (chunked-regime rope must happen per-band); the assistant's
11
+ Q-only shared-KV path passes (Qcur, nullptr, nullptr). SWA layers and DCA-off
12
+ are byte-identical (else-branch keeps the original build_attn call).
13
+
14
+ Gate (bs2, A4B + assistant, 32k, 24k prompt): INTRA regime (chunk==ctx) text
15
+ byte-identical to DCA-off with identical accept (f16 0.677 both; q8q4 text
16
+ identical); cross-chunk f16 accept 0.939, needle PRESENT.
17
+
18
+ --- a/llama.cpp/src/models/gemma4.cpp
19
+ +++ b/llama.cpp/src/models/gemma4.cpp
20
+ @@ -1,4 +1,6 @@
21
+ #include "models.h"
22
+ +#include "dca.h" // opencoti F5 dca bug-2118
23
+ +#include "llama-kv-cache-iswa.h" // bug-2118: get_base() on the iswa mctx
24
+
25
+ void llama_model_gemma4::load_arch_hparams(llama_model_loader & ml) {
26
+ hparams.swa_type = LLAMA_SWA_TYPE_STANDARD;
27
+ @@ -161,6 +163,14 @@
28
+ // TODO: is causal == true correct? might need some changes
29
+ auto * inp_attn = build_attn_inp_kv_iswa();
30
+
31
+ + // opencoti F5 dca bug-2118 β€” Gemma-4 DCA wiring. 0078 wired only the Qwen builders;
32
+ + // the iswa build_attn_dca overload (dca.cpp) existed uncalled, so '--dca on' was a
33
+ + // silent no-op on Gemma. DCA applies to the GLOBAL (non-SWA) layers only (the SWA
34
+ + // window is <= chunk already); the overload stores PRE-rope K + handles has_kv==false
35
+ + // shared-KV globals (null k/v -> read the earlier layer's cache).
36
+ + const bool use_dca = cparams.dca_enabled;
37
+ + llm_graph_input_dca * inp_dca = use_dca ? build_attn_inp_dca(inp_attn->mctx->get_base()) : nullptr;
38
+ +
39
+ ggml_tensor * inp_out_ids = build_inp_out_ids();
40
+
41
+ // opencoti-hook: gemma4-mtp-hidden (bug-858 dual-context MTP) β€” when a context extracts the
42
+ @@ -202,6 +212,10 @@
43
+ freq_factors = model.layers[il].rope_freqs;
44
+ }
45
+
46
+ + // opencoti F5 dca bug-2118 β€” global layers route through build_attn_dca (which
47
+ + // ropes Q per regime and pre-ropes K at insert), so skip the standard pre-rope.
48
+ + const bool dca_l = use_dca && !hparams.is_swa(il);
49
+ +
50
+ // Q projection (shared for both non-KV and KV layers)
51
+ // this is to mirror Gemma4Attention in pytorch code
52
+ ggml_tensor * Qcur;
53
+ @@ -214,9 +228,11 @@
54
+ Qcur = build_norm(Qcur, model.layers[il].attn_q_norm, nullptr, LLM_NORM_RMS, il);
55
+ cb(Qcur, "Qcur_normed", il);
56
+
57
+ + if (!dca_l) { // bug-2118: DCA ropes Q per regime in build_attn_dca
58
+ Qcur = ggml_rope_ext(ctx0, Qcur, inp_pos, freq_factors, n_rot_l, rope_type, n_ctx_orig, freq_base_l, freq_scale_l,
59
+ ext_factor, attn_factor, beta_fast, beta_slow);
60
+ cb(Qcur, "Qcur_pos", il);
61
+ + }
62
+ }
63
+
64
+ // self-attention
65
+ @@ -238,14 +254,28 @@
66
+ cb(Kcur, "Kcur_normed", il);
67
+ cb(Vcur, "Vcur_normed", il);
68
+
69
+ + if (!dca_l) { // bug-2118: DCA pre-ropes K at insert in build_attn_dca
70
+ Kcur = ggml_rope_ext(ctx0, Kcur, inp_pos, freq_factors, n_rot_l, rope_type, n_ctx_orig, freq_base_l, freq_scale_l,
71
+ ext_factor, attn_factor, beta_fast, beta_slow);
72
+
73
+ cb(Kcur, "Kcur_pos", il);
74
+ + }
75
+
76
+ + if (dca_l) { // bug-2118 (wo_s dropped like the qwen3 DCA path β€” null on these GGUFs)
77
+ + const dca_rope rp = { freq_base_l, freq_scale_l, n_rot_l, freq_factors };
78
+ + cur = build_attn_dca(inp_attn, inp_dca, model.layers[il].wo, nullptr,
79
+ + Qcur, Kcur, Vcur, rp, hparams.f_attention_scale, il);
80
+ + } else {
81
+ cur = build_attn(inp_attn, model.layers[il].wo,
82
+ nullptr, model.layers[il].wo_s, Qcur, Kcur, Vcur, nullptr, nullptr, nullptr,
83
+ hparams.f_attention_scale, il);
84
+ + }
85
+ + } else if (dca_l) {
86
+ + // bug-2118: shared-KV global layer under DCA β€” Q-only read of the earlier
87
+ + // layer's DCA-formatted cache (null k/v -> no store).
88
+ + const dca_rope rp = { freq_base_l, freq_scale_l, n_rot_l, freq_factors };
89
+ + cur = build_attn_dca(inp_attn, inp_dca, model.layers[il].wo, nullptr,
90
+ + Qcur, nullptr, nullptr, rp, hparams.f_attention_scale, il);
91
+ } else {
92
+ // reuse KV cache of earlier layers
93
+ cur = build_attn(inp_attn,
94
+ --- a/llama.cpp/src/models/gemma4-assistant.cpp
95
+ +++ b/llama.cpp/src/models/gemma4-assistant.cpp
96
+ @@ -1,5 +1,7 @@
97
+ // opencoti F5 M6-S4 mtp
98
+ #include "models.h"
99
+ +#include "dca.h" // opencoti F5 dca bug-2118
100
+ +#include "llama-kv-cache-iswa.h" // bug-2118: get_base() on the iswa mctx
101
+
102
+ #include <cmath>
103
+ #include <vector>
104
+ @@ -190,6 +192,13 @@
105
+ ggml_tensor * inp_pos = build_inp_pos();
106
+ ggml_tensor * inp_out_ids = build_inp_out_ids();
107
+
108
+ + // opencoti F5 dca bug-2118 β€” when the TARGET runs DCA, the drafter's global layers
109
+ + // read the target's DCA-formatted (pre-roped, position-remapped) shared K/V, so its
110
+ + // Q must be roped per regime through the same build_attn_dca path. Without this the
111
+ + // drafter reads remapped keys with raw-extrapolated Q beyond native ctx -> accept 0.
112
+ + const bool use_dca = cparams.dca_enabled;
113
+ + llm_graph_input_dca * inp_dca = use_dca ? build_attn_inp_dca(inp_attn->mctx->get_base()) : nullptr;
114
+ +
115
+ ggml_tensor * inpL = cur;
116
+
117
+ for (int il = 0; il < n_layer; ++il) {
118
+ @@ -212,13 +221,22 @@
119
+ cb(Qcur, "Qcur_normed", il);
120
+
121
+ ggml_tensor * freq_factors = is_swa ? nullptr : model.layers[il].rope_freqs;
122
+ + const bool dca_l = use_dca && !is_swa; // bug-2118
123
+ + if (!dca_l) {
124
+ Qcur = ggml_rope_ext(ctx0, Qcur, inp_pos, freq_factors, n_rot_l, rope_type, n_ctx_orig,
125
+ freq_base_l, freq_scale_l, ext_factor, attn_factor, beta_fast, beta_slow);
126
+ cb(Qcur, "Qcur_pos", il);
127
+ + }
128
+
129
+ // KV-less cross-attention into the shared/aliased target K/V (null k_cur/v_cur -> no store).
130
+ + if (dca_l) { // bug-2118: Q-only per-regime read of the target's DCA cache
131
+ + const dca_rope rp = { freq_base_l, freq_scale_l, n_rot_l, freq_factors };
132
+ + cur = build_attn_dca(inp_attn, inp_dca, model.layers[il].wo, nullptr,
133
+ + Qcur, nullptr, nullptr, rp, hparams.f_attention_scale, il);
134
+ + } else {
135
+ cur = build_attn(inp_attn, model.layers[il].wo, nullptr, nullptr,
136
+ Qcur, nullptr, nullptr, nullptr, nullptr, nullptr, hparams.f_attention_scale, il);
137
+ + }
138
+
139
+ if (il == n_layer - 1 && inp_out_ids) {
140
+ cur = ggml_get_rows(ctx0, cur, inp_out_ids);
patches/0100-dca-single-chunk-defer.patch ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ opencoti patch 0100 β€” DCA: defer quant lift in the single-chunk regime (bug-2119)
2
+
3
+ dca.cpp gated defer_quant_lift on !single_chunk, so the single-chunk (INTRA)
4
+ regime whole-cache dca_lift_to_f16'd the quantized K/V EVERY forward β€” a
5
+ per-decode-step full-cache dequant that collapsed A4B 32k q8K/q4V decode to
6
+ 4.69 tps. The comment assumed n_kv<=chunk meant "tiny cache"; at chunk 32768
7
+ it is the entire context. Stock ggml_flash_attn_ext converts scalar-quant
8
+ natively, so the single-chunk branch defers exactly like analytical-bands.
9
+
10
+ Gate (bs2, A4B q8K/q4V 32k chunk-32768): decode 4.69 -> 73.74 tps
11
+ (DCA-off 77.13); INTRA text remains byte-identical to DCA-off.
12
+
13
+ --- a/llama.cpp/src/dca.cpp
14
+ +++ b/llama.cpp/src/dca.cpp
15
+ @@ -255,16 +255,16 @@
16
+ // kernel dequants scalar-quant K/V to f16 IN ITS LAUNCH (single-pass to_fp16_nc, see
17
+ // ggml_cuda_flash_attn_ext_mma_f16_dca_fused_case), so pass the quantized cache THROUGH instead of
18
+ // whole-cache materializing it to f16 via dca_lift_to_f16 every forward (the prefill cliff). Gate:
19
+ - // - !single_chunk: only the fused op carries the in-launch convert; single_chunk uses stock
20
+ - // ggml_flash_attn_ext (launch_fattn already converts scalar-quant natively) and runs only while
21
+ - // n_kv <= chunk (a tiny cache) so its lift cost is negligible β€” keep it on the safe lift path.
22
+ - // - dca_is_inkernel_liftable: scalar quants the fused launch's to_fp16_nc can read.
23
+ + // - dca_is_inkernel_liftable: scalar quants the fused launch's to_fp16_nc can read. bug-2119: the
24
+ + // single_chunk branch defers too β€” it uses stock ggml_flash_attn_ext, whose launch_fattn
25
+ + // converts scalar-quant natively (the ordinary DCA-off decode path). The old !single_chunk gate
26
+ + // assumed n_kv <= chunk means "tiny cache, lift is negligible", but chunk can be 32k+: the
27
+ + // whole-cache two-leg graph lift ran EVERY decode step (A4B 32k/chunk-32768 q8q4: 76 -> 4.7 tps).
28
+ // - !v_trans: a transposed quant V view breaks the dim-0 (block) contiguity to_fp16_nc requires; the
29
+ // permute(0,2,1,3) below keeps dim-0 intact, but a transpose would not β€” fall back to the lift.
30
+ // The in-op convert is byte-identical to dca_lift_to_f16, so deferred output == lifted output.
31
+ const bool v_trans_raw = v_raw->nb[1] > v_raw->nb[2];
32
+ const bool defer_quant_lift =
33
+ - !inp_dca->single_chunk &&
34
+ dca_is_inkernel_liftable(k_raw->type) &&
35
+ dca_is_inkernel_liftable(v_raw->type) &&
36
+ !v_trans_raw;
patches/0101-kv-attn-rot-inherit.patch ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ opencoti patch 0101 β€” shared draft cache inherits attention-rotation state (bug-2120)
2
+
3
+ llamafile 0.10.3 attention rotation (Hadamard-rotate K/V rows before quantized
4
+ store when the cache type is quantized && head_dim%64==0; readers rotate Q and
5
+ un-rotate the output via attn_inp_k_rot/v_rot) is computed per-cache from its
6
+ OWN type_k/type_v and accumulated head dims. The dual-context assistant draft
7
+ cache is created with f16 types AND its shared layers skip the head-dim
8
+ accumulation, so rotation silently disabled: the drafter read the target's
9
+ ROTATED quant K/V with an unrotated Q -> decorrelated attention, flat finite
10
+ logits, accept ~0.000 with ANY quant target KV (f16/f16 unaffected; Qwen NextN
11
+ unaffected β€” it reads through the target's own cache object).
12
+
13
+ Fix: in the ctor share branch, inherit attn_rot_k/v, n_embd_head_*_all and the
14
+ Hadamard tables from the source cache, overriding the local computation. The
15
+ generic iswa build_attn then applies the rotation on the drafter's Q-only path.
16
+
17
+ Gate (bs2, A4B + assistant Q8_0 drafter, 32k, 24k prompt): accept
18
+ k8_v16 0.000 -> 0.641; k8_v4 (production PolyKV) 0.000 -> 0.729; f16 control
19
+ 0.677 unchanged.
20
+
21
+ --- a/llama.cpp/src/llama-kv-cache.cpp
22
+ +++ b/llama.cpp/src/llama-kv-cache.cpp
23
+ @@ -1106,6 +1106,26 @@
24
+ }
25
+ }
26
+
27
+ + // opencoti bug-2120: a shared (dual-context drafter) cache must READ with the same
28
+ + // attention-rotation state the SOURCE cache used to WRITE. The local computation above
29
+ + // sees this cache's own type_k/type_v (f16 for the drafter) and n_embd_head_*_all == 0
30
+ + // (shared layers `continue` before accumulating), so it always disables rotation β€” the
31
+ + // drafter then reads Hadamard-rotated quant K/V with an unrotated Q and un-unrotated
32
+ + // output: decorrelated attention, accept ~0 with ANY quant target KV (f16/f16 unaffected
33
+ + // because no rotation is applied at all). Inherit flags + head-dim extents + Hadamard
34
+ + // tables from `other` so build_input_k_rot/v_rot emit the matching rotation inputs.
35
+ + if (share && other) {
36
+ + attn_rot_k = other->attn_rot_k;
37
+ + attn_rot_v = other->attn_rot_v;
38
+ + n_embd_head_k_all = other->n_embd_head_k_all;
39
+ + n_embd_head_v_all = other->n_embd_head_v_all;
40
+ + attn_rot_hadamard = other->attn_rot_hadamard;
41
+ + if (attn_rot_k || attn_rot_v) {
42
+ + LLAMA_LOG_INFO("%s: shared cache inherits attn_rot_k=%d attn_rot_v=%d from source (bug-2120)\n",
43
+ + __func__, (int) attn_rot_k, (int) attn_rot_v);
44
+ + }
45
+ + }
46
+ +
47
+ const char * LLAMA_KV_CACHE_DEBUG = getenv("LLAMA_KV_CACHE_DEBUG");
48
+ debug = LLAMA_KV_CACHE_DEBUG ? atoi(LLAMA_KV_CACHE_DEBUG) : 0;
49
+ }
patches/0102-fattn-bulk-h2d-scratch-ring.patch ADDED
@@ -0,0 +1,154 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ opencoti patch 0102 β€” streaming-FA bulk-H2D tail lift + persistent staging scratch ring (bug-2116, bug-2121)
2
+
3
+ Two changes to the rolling-KV streaming-FA lift in fattn.cu:
4
+
5
+ 1. bug-2116 (perf): for a HOST quant tail source at decode (n_b==1), bulk-copy
6
+ the RAW QUANT bytes H2D once at full DMA rate into device scratch, then run
7
+ the nc-converter from device memory. The converter reading the pinned-host
8
+ tail via UVA at word granularity measured 10-16 GB/s vs 57-65 GB/s bulk on
9
+ bs2 β€” the UVA cliff behind the S0 spill-decode curve. Measured after-curve
10
+ (14B-1M q8q4 window 131k, decode tps): 800MiB 9.05->12.23, 1600 3.96->10.28,
11
+ 6400 ~1->5.53.
12
+
13
+ 2. bug-2121 (correctness): both bulk paths (the new quant one and the #586 f16
14
+ one) staged through ggml_cuda_pool_alloc scratch FREED on scope exit while
15
+ the async memcpy+convert on the copy stream were still pending. Decode
16
+ (bug-1841 single stream) is stream-ordered and safe; PREFILL runs two
17
+ streams and the FA compute allocates from the SAME pool concurrently (MMA
18
+ stream-k fixup buffers, every tile) -> freed-but-pending scratch re-handed
19
+ cross-stream -> corrupted K/V tiles. Manifested with >=2 host-tail tiles
20
+ (>32768 exercised tail cells): garbage decode / needle-LOST + racy host
21
+ GPFs; <=1 tail tile clean. Fix: persistent per-slot scratch ring
22
+ (g_scrK/g_scrV[4], grow-only, worst-case tile_kv_full capacity so no
23
+ cudaMalloc inside captured decode graphs) β€” no pool sharing across streams.
24
+
25
+ Gate (bs2, 14B-1M q8q4 window 131k): tails 1600/3200/6400 all needle=PRESENT,
26
+ coherent text, zero crashes across three full 106k prefills (3200 previously
27
+ crashed 2x + garbage decode; 6400 previously needle-LOST).
28
+
29
+ --- a/llama.cpp/ggml/src/ggml-cuda/fattn.cu
30
+ +++ b/llama.cpp/ggml/src/ggml-cuda/fattn.cu
31
+ @@ -1350,6 +1350,30 @@
32
+ char * const slotK_base = g_slotK;
33
+ char * const slotV_base = g_slotV;
34
+
35
+ + // opencoti-hook: f5-rolling-kv bug-2121 β€” PERSISTENT per-slot H2D staging scratch.
36
+ + // The bulk-H2D lifts below (bug-2116 quant path + #586 f16 path) used
37
+ + // ggml_cuda_pool_alloc scratch FREED on lambda/loop exit while the
38
+ + // cudaMemcpyAsync + converter on the copy stream were still pending. On decode
39
+ + // (single stream, bug-1841) reuse is stream-ordered and safe; on PREFILL the FA
40
+ + // compute on ctx.stream() allocates from the SAME pool concurrently (MMA
41
+ + // stream-k fixup buffers, every tile), so a freed-but-pending scratch gets
42
+ + // re-handed to the other stream β†’ cross-stream aliasing β†’ corrupted K/V tiles.
43
+ + // Manifested only with β‰₯2 host-tail tiles interleaving with compute (>32768
44
+ + // exercised tail cells): 14B-1M q8q4 window-mode tail=3200/6400 MiB β†’ garbage
45
+ + // decode / needle-LOST (+ racy host GPFs); tail≀1600 (1 tail tile) clean. A
46
+ + // persistent ring keyed by slot removes pool sharing entirely; within-ring WAR
47
+ + // is stream-ordered (memcpy β†’ convert β†’ next lift's memcpy, all on cs).
48
+ + static char * g_scrK[4] = {}; static size_t g_scrK_cap[4] = {};
49
+ + static char * g_scrV[4] = {}; static size_t g_scrV_cap[4] = {};
50
+ + auto scr_get = [](char ** ring, size_t * cap, int slot, size_t need) -> char * {
51
+ + if (cap[slot] < need) {
52
+ + if (ring[slot]) CUDA_CHECK(cudaFree(ring[slot])); // cudaFree syncs pending users
53
+ + CUDA_CHECK(cudaMalloc((void **)&ring[slot], need));
54
+ + cap[slot] = need;
55
+ + }
56
+ + return ring[slot];
57
+ + };
58
+ +
59
+ // R2-b S3b (#312): with >=2 slots the op runs a double-buffer ping-pong β€” the
60
+ // per-tile lift is issued on a dedicated copy_stream while FA/lse for the
61
+ // current tile run on ctx.stream(), so tile t+1's DMA overlaps tile t's
62
+ @@ -1445,10 +1469,34 @@
63
+ // below are unchanged. Unifies device + pinned-host (S3c) sources
64
+ // (the kernel reads host via UVA). Slot is f16 β†’ lse half-cast OK.
65
+ // 3c-5: tK/tV + loc select the window (device) or tail (host) source.
66
+ - to_fp16_k((const char *)tK->data + (size_t)loc*tK->nb[1], (half *)sK,
67
+ + // opencoti-hook: f5-rolling-kv bug-2116 β€” for a HOST quant source,
68
+ + // bulk-H2D the RAW QUANT bytes (cell-major block, 2.5Γ— fewer bytes
69
+ + // than f16) into device scratch at full DMA rate, then convert from
70
+ + // device memory. Calling the converter on the host pointer made it
71
+ + // read the tail via UVA at word granularity (~10–16 GB/s measured
72
+ + // vs 57–65 GB/s bulk on bs2). n_b>1 keeps the UVA path (nb[3]
73
+ + // batch blocks are not spanned by the cell-major copy; unused in
74
+ + // the spill regime where ne[3]==1).
75
+ + const bool bulkq = tr.host && n_b == 1;
76
+ + const char * qsrcK = (const char *)tK->data + (size_t)loc*tK->nb[1];
77
+ + const char * qsrcV = (const char *)tV->data + (size_t)loc*tV->nb[1];
78
+ + if (bulkq) {
79
+ + // bug-2121: persistent per-slot scratch β€” NOT pool memory (see scr_get above).
80
+ + // Capacity = worst-case tile (tile_kv_full), not this_kv: this_kv grows per
81
+ + // decode token, and a realloc inside a captured decode graph would fail.
82
+ + const size_t kqblk = (size_t)this_kv * tK->nb[1];
83
+ + const size_t vqblk = (size_t)this_kv * tV->nb[1];
84
+ + char * qscrK = scr_get(g_scrK, g_scrK_cap, slot, (size_t)tile_kv_full * tK->nb[1]);
85
+ + char * qscrV = scr_get(g_scrV, g_scrV_cap, slot, (size_t)tile_kv_full * tV->nb[1]);
86
+ + CUDA_CHECK(cudaMemcpyAsync(qscrK, qsrcK, kqblk, cudaMemcpyDefault, cs));
87
+ + CUDA_CHECK(cudaMemcpyAsync(qscrV, qsrcV, vqblk, cudaMemcpyDefault, cs));
88
+ + qsrcK = qscrK;
89
+ + qsrcV = qscrV;
90
+ + }
91
+ + to_fp16_k(qsrcK, (half *)sK,
92
+ head_dim, this_kv, n_head_kv, n_b,
93
+ (int64_t)(tK->nb[1]/ts_k), (int64_t)(tK->nb[2]/ts_k), (int64_t)(tK->nb[3]/ts_k), cs);
94
+ - to_fp16_v((const char *)tV->data + (size_t)loc*tV->nb[1], (half *)sV,
95
+ + to_fp16_v(qsrcV, (half *)sV,
96
+ dv, this_kv, n_head_kv, n_b,
97
+ (int64_t)(tV->nb[1]/ts_v), (int64_t)(tV->nb[2]/ts_v), (int64_t)(tV->nb[3]/ts_v), cs);
98
+ } else
99
+ @@ -1468,17 +1516,20 @@
100
+ const bool bulk = use_2d && tr.host;
101
+ const size_t kblk = (size_t)this_kv * tK->nb[1]; // contiguous cell-major span
102
+ const size_t vblk = (size_t)this_kv * tV->nb[1];
103
+ - ggml_cuda_pool_alloc<char> scrK(pool, bulk ? kblk : 1);
104
+ - ggml_cuda_pool_alloc<char> scrV(pool, bulk ? vblk : 1);
105
+ const char * baseK;
106
+ const char * baseV;
107
+ if (bulk) {
108
+ + // bug-2121: persistent per-slot scratch β€” NOT pool memory (see scr_get above).
109
+ + // Sequential bb iterations reuse it WAR-safely (copy/repack both on cs).
110
+ + // Worst-case tile capacity: see the quant path above (capture-safe growth).
111
+ + char * scrK = scr_get(g_scrK, g_scrK_cap, slot, (size_t)tile_kv_full * tK->nb[1]);
112
+ + char * scrV = scr_get(g_scrV, g_scrV_cap, slot, (size_t)tile_kv_full * tV->nb[1]);
113
+ const char * hK = (const char *)tK->data + (size_t)bb*tK->nb[3] + (size_t)loc*tK->nb[1];
114
+ const char * hV = (const char *)tV->data + (size_t)bb*tV->nb[3] + (size_t)loc*tV->nb[1];
115
+ - CUDA_CHECK(cudaMemcpyAsync(scrK.ptr, hK, kblk, cudaMemcpyDefault, cs)); // one full-BW H2D
116
+ - CUDA_CHECK(cudaMemcpyAsync(scrV.ptr, hV, vblk, cudaMemcpyDefault, cs));
117
+ - baseK = scrK.ptr; // scratch is cell-major, cell `loc` at offset 0
118
+ - baseV = scrV.ptr;
119
+ + CUDA_CHECK(cudaMemcpyAsync(scrK, hK, kblk, cudaMemcpyDefault, cs)); // one full-BW H2D
120
+ + CUDA_CHECK(cudaMemcpyAsync(scrV, hV, vblk, cudaMemcpyDefault, cs));
121
+ + baseK = scrK; // scratch is cell-major, cell `loc` at offset 0
122
+ + baseV = scrV;
123
+ } else {
124
+ baseK = (const char *)tK->data + (size_t)bb*tK->nb[3] + (size_t)loc*tK->nb[1];
125
+ baseV = (const char *)tV->data + (size_t)bb*tV->nb[3] + (size_t)loc*tV->nb[1];
126
+ @@ -1677,10 +1728,26 @@
127
+ if (!all_f16) {
128
+ // S3d: one nc-converter launch per tile β†’ packed f16 slot (see
129
+ // the overlap path for the layout proof). Source strides s=nb/ts.
130
+ - to_fp16_k((const char *)K->data + (size_t)kv0*K->nb[1], (half *)sK,
131
+ + // bug-2116: host quant source β†’ bulk-H2D the raw quant block at
132
+ + // DMA rate + convert from device scratch; direct converter-on-host
133
+ + // reads via UVA at word granularity (see the overlap-path note).
134
+ + const bool bulkq = host_src && n_b == 1;
135
+ + const size_t kqblk = bulkq ? (size_t)this_kv * K->nb[1] : 1;
136
+ + const size_t vqblk = bulkq ? (size_t)this_kv * V->nb[1] : 1;
137
+ + ggml_cuda_pool_alloc<char> qscrK(pool, kqblk);
138
+ + ggml_cuda_pool_alloc<char> qscrV(pool, vqblk);
139
+ + const char * qsrcK = (const char *)K->data + (size_t)kv0*K->nb[1];
140
+ + const char * qsrcV = (const char *)V->data + (size_t)kv0*V->nb[1];
141
+ + if (bulkq) {
142
+ + CUDA_CHECK(cudaMemcpyAsync(qscrK.ptr, qsrcK, kqblk, cudaMemcpyDefault, stream));
143
+ + CUDA_CHECK(cudaMemcpyAsync(qscrV.ptr, qsrcV, vqblk, cudaMemcpyDefault, stream));
144
+ + qsrcK = qscrK.ptr;
145
+ + qsrcV = qscrV.ptr;
146
+ + }
147
+ + to_fp16_k(qsrcK, (half *)sK,
148
+ head_dim, this_kv, n_head_kv, n_b,
149
+ (int64_t)(K->nb[1]/ts_k), (int64_t)(K->nb[2]/ts_k), (int64_t)(K->nb[3]/ts_k), stream);
150
+ - to_fp16_v((const char *)V->data + (size_t)kv0*V->nb[1], (half *)sV,
151
+ + to_fp16_v(qsrcV, (half *)sV,
152
+ dv, this_kv, n_head_kv, n_b,
153
+ (int64_t)(V->nb[1]/ts_v), (int64_t)(V->nb[2]/ts_v), (int64_t)(V->nb[3]/ts_v), stream);
154
+ } else
patches/0103-kv-cpu-fa-probe-guard.patch ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ opencoti patch 0103 β€” rolling-KV CPU-FA cost-probe guard (bug-2125)
2
+
3
+ The Stage-3d-S2 rolling-KV tactic selector runs a synthetic CPU flash-attn
4
+ "companion probe" (opencoti_fa_compute_probe_ms with on_cpu=true) purely to
5
+ MODEL a CPU_FA_TAIL cost for its audit log. CPU_FA_TAIL is gated out of
6
+ auto-select (#354) β€” the tail always streams on the GPU (POSITION_WINDOW) β€” so
7
+ the probe's result is never used for the decision.
8
+
9
+ That probe builds a real ggml_flash_attn_ext over the actual cache K/V types and
10
+ runs it on the host CPU backend. For opencoti's CUDA-FA-VEC-only KV types (q6_0,
11
+ turbo{2,3,4,8}_0, turbo{2,3}_tcq) the CPU vec_dot trait is NULL, so the CPU
12
+ flash-attn dereferences a NULL fn-ptr β†’ SIGSEGV at context-init (before any
13
+ client request). Stock types (f16/q8_0/q4_0/q5_0/q5_1/bf16) carry a CPU vec_dot
14
+ and survive, which is why only the custom-K tiers crashed under DCA at 256k/512k.
15
+
16
+ Fix: gate the CPU probe on a host-CPU-FA-capability predicate
17
+ (opencoti_cpu_fa_capable_ktype) so it is never built for a K type the CPU
18
+ cannot process. Returning 0.0f just leaves CPU_FA_TAIL unmodeled; the selector
19
+ keeps its GPU-stream default. The rolling window stays a pure-GPU path for every
20
+ KV tier β€” no silent CPU fallback. Host-only change (no CUDA DSO impact).
21
+ --- a/llama.cpp/src/llama-kv-cache.cpp
22
+ +++ b/llama.cpp/src/llama-kv-cache.cpp
23
+ @@ -2795,6 +2795,28 @@
24
+ // build_attn_mha FA contract (post-permute q[hd,n_q,n_head], k/v[hd,n_kv,n_head_kv],
25
+ // F16 mask). Never aborts boot: every shortfall (no GPU, unsupported op, non-GQA
26
+ // geometry, alloc miss) returns 0.0f and the selector keeps the stream default.
27
+ +//
28
+ +// opencoti bug-2125 SAFEGUARD: can the HOST CPU flash-attn path
29
+ +// (ggml_compute_forward_flash_attn_ext_f16) process a cache of this K type? It reads K via the
30
+ +// K-type's CPU vec_dot trait, which is NULL for opencoti's CUDA-FA-VEC-only KV types (q6_0,
31
+ +// turbo{2,3,4,8}_0, turbo{2,3}_tcq β€” GPU in-register readers, no CPU vec_dot / dp kernel). Running
32
+ +// CPU flash-attn over such a K deref's a NULL fn-ptr β†’ SIGSEGV. The rolling-KV tail is ALWAYS
33
+ +// streamed on the GPU (POSITION_WINDOW; CPU_FA_TAIL is gated out of auto-select, #354), so the ONLY
34
+ +// place a host CPU flash-attn is ever built is the Stage-3d-S2 cost-model probe below β€” and its
35
+ +// result is audit-log only. Gate the CPU probe on this predicate so the rolling window stays a
36
+ +// pure-GPU path with NO silent CPU fallback for ANY KV tier. Stock ggml types (which DO carry a CPU
37
+ +// vec_dot) are the allow-list; every opencoti custom KV type is excluded.
38
+ +static bool opencoti_cpu_fa_capable_ktype(enum ggml_type t) {
39
+ + switch (t) {
40
+ + case GGML_TYPE_F32: case GGML_TYPE_F16: case GGML_TYPE_BF16:
41
+ + case GGML_TYPE_Q4_0: case GGML_TYPE_Q4_1:
42
+ + case GGML_TYPE_Q5_0: case GGML_TYPE_Q5_1:
43
+ + case GGML_TYPE_Q8_0:
44
+ + return true; // stock ggml types: have a host CPU vec_dot
45
+ + default:
46
+ + return false; // opencoti custom KV (q6_0 / turbo* / turbo*_tcq): CUDA-only, NULL CPU vec_dot
47
+ + }
48
+ +}
49
+ static float opencoti_fa_compute_probe_ms(
50
+ const llama_model & model, const llama_hparams & hparams,
51
+ int32_t il, ggml_type type_k, ggml_type type_v, int64_t bench_cells,
52
+ @@ -2823,6 +2845,15 @@
53
+ return 0.0f; // device-type mismatch; selector falls back (stream default / no CPU model)
54
+ }
55
+
56
+ + // opencoti bug-2125 SAFEGUARD: never build a host CPU flash-attn over a K type the CPU cannot
57
+ + // process (NULL vec_dot β†’ SIGSEGV). This is audit-only (the tail always streams on GPU), so
58
+ + // returning 0.0f just leaves CPU_FA_TAIL unmodeled β€” the selector keeps the GPU-stream default.
59
+ + // Keeps the rolling window a pure-GPU path for q6_0 / turbo* / turbo*_tcq (and any future
60
+ + // CUDA-only KV tier), with no silent CPU fallback.
61
+ + if (on_cpu && !opencoti_cpu_fa_capable_ktype(type_k)) {
62
+ + return 0.0f;
63
+ + }
64
+ +
65
+ const int64_t hd_k = (int64_t) hparams.n_embd_head_k(il);
66
+ const int64_t hd_v = (int64_t) hparams.n_embd_head_v(il);
67
+ const int64_t n_head = (int64_t) hparams.n_head(il);
patches/0104-fattn-q6q4-vec-instance.patch ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ opencoti patch 0104 β€” q6_0-K / q4_0-V FA-VEC instance (bug-2125 / #627)
2
+
3
+ q6_0-K + q4_0-V (strong-K, cheap-V β€” the design invariant K precision >= V) is a
4
+ first-class asymmetric KV combo. Under GGML_CUDA_FA_ALL_QUANTS the kernel
5
+ selector admits K!=V and picks the VEC path, but no (q6_0, q4_0) FA-VEC instance
6
+ existed, so the D<=256 dispatch (Gemma-4 local/SWA layers β€” the global layers
7
+ use the DCA fused kernel's in-launch to_fp16_nc) fell through to
8
+ GGML_ABORT("fatal error") at ggml_cuda_flash_attn_ext_vec, crashing at boot.
9
+
10
+ Fix (mirrors the q6_0-q5_0 pattern, #512/#81): add the instance TU
11
+ fattn-vec-instance-q6_0-q4_0.cu (head_dim 64/128/256), the
12
+ FATTN_VEC_CASES_ALL_D(Q6_0, Q4_0) dispatch case, and the build-functions.sh 3c
13
+ default-build inclusion (so the symbol is present without FA_ALL_QUANTS too).
14
+ Verified bs2 256k DCA-on: boots HEALTHY (was abort) + niah_single_1 = 100.0.
15
+ CUDA DSO change (needs a cuda rebuild). Only the q6_0/q4_0 direction is added β€”
16
+ reverse q4_0/q6_0 is intentionally NOT added (K must stay the higher-bit type).
17
+ --- a/llama.cpp/ggml/src/ggml-cuda/fattn.cu
18
+ +++ b/llama.cpp/ggml/src/ggml-cuda/fattn.cu
19
+ @@ -353,13 +353,17 @@
20
+ // ik_llama.cpp ships for q6_0 KV), registered OUTSIDE the FA_ALL_QUANTS block so they compile in
21
+ // the DEFAULT build β€” the matching instance .cu files are added unconditionally in
22
+ // collect_gpu_sources (build-functions.sh section 3c), like the turbo K==V instances below.
23
+ - // q6_0-q6_0 is the symmetric (default-reachable) cell; the 4 mixed cells are reachable only under
24
+ + // q6_0-q6_0 is the symmetric (default-reachable) cell; the 5 mixed cells are reachable only under
25
+ // FA_ALL_QUANTS (the K!=V early-out in ggml_cuda_get_best_fattn_kernel), same as q5_0's mixed pairs.
26
+ FATTN_VEC_CASES_ALL_D(GGML_TYPE_Q6_0, GGML_TYPE_Q6_0)
27
+ FATTN_VEC_CASES_ALL_D(GGML_TYPE_Q8_0, GGML_TYPE_Q6_0)
28
+ FATTN_VEC_CASES_ALL_D(GGML_TYPE_Q6_0, GGML_TYPE_Q5_0)
29
+ FATTN_VEC_CASES_ALL_D(GGML_TYPE_Q6_0, GGML_TYPE_F16)
30
+ FATTN_VEC_CASES_ALL_D(GGML_TYPE_F16, GGML_TYPE_Q6_0)
31
+ + // opencoti-hook: q6_0 KV (#627/bug-2125) β€” strong-K q6_0 + cheap-V q4_0 asymmetric cell. Under
32
+ + // FA_ALL_QUANTS the selector admits q6_0-K/q4_0-V, so the stock FA-VEC dispatch (local SWA layers)
33
+ + // needs this instance; without it the D<=256 dispatch fell through to the GGML_ABORT below.
34
+ + FATTN_VEC_CASES_ALL_D(GGML_TYPE_Q6_0, GGML_TYPE_Q4_0)
35
+
36
+ // opencoti-hook: turboquant perf-lift (Level B) β€” F5 M6-S2 #392. Fused turbo K/V vec path
37
+ // (always registered, independent of GGML_CUDA_FA_ALL_QUANTS). turbo2/3/4 at head_dim ∈
38
+ --- /dev/null
39
+ +++ b/llama.cpp/ggml/src/ggml-cuda/template-instances/fattn-vec-instance-q6_0-q4_0.cu
40
+ @@ -0,0 +1,11 @@
41
+ +// opencoti-hook: q6_0 KV (Anbeeld/beellama.cpp port) β€” strong-K + cheap-V asymmetric cell (#627/bug-2125).
42
+ +// q6_0-K / q4_0-V is a first-class KV combo (6-bit K precision, 4-bit V footprint); the local SWA layers
43
+ +// read it through the stock FA-VEC path (the global layers use the DCA fused kernel's to_fp16_nc dequant),
44
+ +// so this instance must exist or ggml_cuda_flash_attn_ext_vec_case<D,q6_0,q4_0> is missing and the D<=256
45
+ +// dispatch aborts at fattn.cu (GGML_ABORT "fatal error"). Mirrors fattn-vec-instance-q6_0-q5_0.cu.
46
+ +
47
+ +#include "../fattn-vec.cuh"
48
+ +
49
+ +DECL_FATTN_VEC_CASE( 64, GGML_TYPE_Q6_0, GGML_TYPE_Q4_0);
50
+ +DECL_FATTN_VEC_CASE(128, GGML_TYPE_Q6_0, GGML_TYPE_Q4_0);
51
+ +DECL_FATTN_VEC_CASE(256, GGML_TYPE_Q6_0, GGML_TYPE_Q4_0);
52
+ --- a/llamafile/build-functions.sh
53
+ +++ b/llamafile/build-functions.sh
54
+ @@ -162,7 +162,8 @@
55
+ fi
56
+
57
+ # 3c. opencoti-hook: q6_0 KV (Anbeeld/beellama.cpp port). q6_0 is a scalar KV type read by the
58
+ - # FA-VEC path; its 5 Anbeeld instances must compile in the DEFAULT build (same #468/bug-339
59
+ + # FA-VEC path; its 6 instances (5 Anbeeld + q6_0/q4_0 strong-K/cheap-V, #627) must compile in
60
+ + # the DEFAULT build (same #468/bug-339
61
+ # reasoning as the turbo block above β€” the default build does NOT glob fattn-vec, so without
62
+ # this the DSO lacks ggml_cuda_flash_attn_ext_vec_case<D,q6_0,*> and dlopen fails the instant
63
+ # q6_0 KV reaches flash-attention). No-op under FA_ALL_QUANTS=1 (already globbed) and on a
64
+ @@ -171,6 +172,7 @@
65
+ for f in "$ti_dir"/fattn-vec-instance-q6_0-q6_0.cu \
66
+ "$ti_dir"/fattn-vec-instance-q8_0-q6_0.cu \
67
+ "$ti_dir"/fattn-vec-instance-q6_0-q5_0.cu \
68
+ + "$ti_dir"/fattn-vec-instance-q6_0-q4_0.cu \
69
+ "$ti_dir"/fattn-vec-instance-q6_0-f16.cu \
70
+ "$ti_dir"/fattn-vec-instance-f16-q6_0.cu; do
71
+ [ -f "$f" ] && CUDA_SOURCES="$CUDA_SOURCES $f"