maxwelhelp commited on
Commit
7c97475
·
1 Parent(s): 72ab2b3

DFlash2 q4-mix draft for llama.cpp on Tesla P40 (Pascal)

Browse files
.gitattributes CHANGED
@@ -33,3 +33,6 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+
37
+ *.gguf filter=lfs diff=lfs merge=lfs -text
38
+ *.py filter=lfs diff=lfs merge=lfs -text
DFLASH2_RING_PORT.md ADDED
@@ -0,0 +1,1183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # DFlash2 draft KV-ring port to llama.cpp (рабочий проект llama-dflash3-pr)
2
+
3
+ > Цель: сжать draft-KV с ~1 ГБ до ~48 МБ БЕЗ потери acceptance (держать 79-80%),
4
+ > не повторив ошибку предыдущего агента (наивен seq_rm/урезание n_ctx -> 30% acceptance).
5
+ >
6
+ > Эталон для переноса: Luce `server/src/common/dflash_draft_kv.{h,cpp}` +
7
+ > `server/src/common/dflash_feature_ring.h` + драйвер `qwen35_backend.cpp`.
8
+ > База в llama.cpp: рабочий проект `llama-dflash3-pr` (acceptance 79.889%).
9
+ > Сломанная dflash2 удалена пользователем; работаем только в dflash3-pr.
10
+
11
+ ## TL;DR (для чтения перед правками)
12
+
13
+ Проблема не в "окне 4096" как таковом, а в том, КАК его реализовали.
14
+ Luce использует честный **mod-cap ring** (`slot = pos % cap`) + **явные attention-маски**
15
+ (вне-окна = -inf, в-окна = 0) + **абсолютные RoPE-позиции** + **пересчёт K/V только
16
+ для новых строк**. Это математически эквивалентно "полному кешу", поэтому acceptance
17
+ не падает. Предыдущий агент вместо этого урезал `n_ctx` и резал кеш через
18
+ `llama_memory_seq_rm`, что создаёт дырки/неконсистентные окна и ломает selector-lattice.
19
+
20
+ Колесо НЕ даёт >80% (80% это потолок самой пары модели). Оно даёт тот же 80% при
21
+ меньшей памяти. Если не нужно экономить ~1 ГБ - НЕ переносим ничего, просто
22
+ собираем рабочую dflash3 и живём с полным контекстом.
23
+
24
+ ---
25
+
26
+ ## 0. Карта изменений (файлы, которые отличают рабочую dflash3 от сломанной dflash2)
27
+
28
+ Единственное отличие между worktree dflash3 (рабочий) и dflash2 (сломанный) - это
29
+ последняя правка агента "draft-only KV window". Оно затрагивает ровно 3 файла:
30
+
31
+ | файл | что добавил сломанный агент | вердикт |
32
+ |------|------------------------------|---------|
33
+ | `common/arg.cpp` | флаг `--spec-draft-ctx N` (добавляет `n_ctx` в `params.speculative.draft`) | НЕ решил проблему; меняет конфиг без правильного ring |
34
+ | `common/common.h` | поле `int32_t n_ctx = 4096` в `common_params_speculative_draft` | см. выше |
35
+ | `common/speculative.cpp` | `draft_ctx_window=4096`, `llama_memory_seq_rm`-ролл в `process()`, урезание `cparams.n_ctx` в common_speculative_init_result | **корень бага** |
36
+
37
+ Всё остальное (dflash.cpp, qwen35.cpp, llama-model-loader, server-context и т.д.)
38
+ между worktree ИДЕНТИЧНО - это рабочая часть порта, её не трогаем.
39
+
40
+ ### Вывод по эталону
41
+ - Рабочая версия = **БЕЗ** `seq_rm`-ролла, БЕЗ урезания `n_ctx`, с полным контекстом.
42
+ - Сломанная версия = добавила только вышеуказанное и сломала acceptance.
43
+ - Правильный путь = перенести НАСТОЯЩИЙ ring (mod-cap + маски), а не наивный роллинг.
44
+
45
+ ---
46
+
47
+ ## 1. Что делает ring в Luce (механизм, код, математика)
48
+
49
+ Файлы-эталоны:
50
+ - `lucebox/server/src/common/dflash_draft_kv.h` (DraftKvState)
51
+ - `lucebox/server/src/common/dflash_draft_kv.cpp` (init / begin_step / masks)
52
+ - `lucebox/server/src/common/dflash_feature_ring.h` (DraftFeatureMirror - зеркало фич target)
53
+ - `lucebox/server/src/qwen35/qwen35_backend.cpp` (драйвер decode: строки ~2690-2785)
54
+
55
+ ### 1.1 Геометрия (draft_kv_init, dflash_draft_kv.cpp:16-130)
56
+
57
+ ```
58
+ cap = ring-вместимость контекстных слотов (последние `cap` позиций)
59
+ q_len = block_size (у нас из GGUF = 8)
60
+ a_step = 2 * q_len + 2 (сколько новых строк можно доложить за шаг)
61
+ trash_slot= cap + q_len (слот-приёмник для pad-строк)
62
+ kv_total = align_up(cap + q_len + 1, 32) (всего строк кеша: ring + шумовой блок + pad)
63
+ kv_row = head_dim * n_head_kv (128 * 8 = 1024 f16 на строку)
64
+ ```
65
+
66
+ K/V - это отдельные F16-тензоры `[kv_row, kv_total]`, НЕ llama.cpp KV-кеш. Живут в
67
+ `mem_buf` (persistent backend buffer, вне gallocr), обнулены один раз (пустые слоты
68
+ читаются FA, masked -inf, обязаны быть конечными).
69
+
70
+ ### 1.2 mod-cap отображение (критично!)
71
+
72
+ В `draft_kv_begin_step` (dflash_draft_kv.cpp:226-318):
73
+ ```cpp
74
+ ap_rows[i] = p % st.cap; // СЛОТ = абсолютная позиция % cap
75
+ st.slot_pos[p % st.cap] = p; // host-зеркало: слот -> его абсолютная позиция
76
+ ```
77
+ - Абсолютные позиции сохраняются (RoPE по абсолюту).
78
+ - Свободные слоты перезаписываются по кольцу, НЕ удаляются сдвигом.
79
+ - `slot_pos[]` хранит абсолютную позицию каждого слота (для построения масок).
80
+
81
+ ### 1.3 Окно и начало шага (begin_step)
82
+
83
+ ```
84
+ win = min(committed, cap) // окно = последние `cap` закоммиченных
85
+ lo = committed - win // нижняя граница окна
86
+ start = max(next_pos, lo) // доложить всё, чего ещё нет в кеше
87
+ n_new = committed - start // сколько новых строк добавляем
88
+ ```
89
+ - Если `n_new > a_step` -> `draft_kv_bulk_append` (разово заливает по 1024 строки).
90
+ - Иначе - fold-in append: реальные строки + pad-строки в `trash_slot`.
91
+
92
+ Шумовой блок:
93
+ ```cpp
94
+ pq[i] = committed + i; // абсолютные позиции noise-блока (q_len штук)
95
+ ```
96
+
97
+ ### 1.4 Attention-маски (критично! отличается от наивного ролла)
98
+
99
+ `mask_full` (полное-внимание слои), dflash_draft_kv.cpp:280-294:
100
+ ```cpp
101
+ для каждого слота s:
102
+ p = slot_pos[s]
103
+ mask[s] = (p >= lo && p < committed) ? 0 : -inf // только окно видно
104
+ для шумовых слотов (cap+j): 0 // noise виден (bidirectional)
105
+ ```
106
+ - строки за пределами окна = -inf (полностью исключены).
107
+ - шумовой блок виден всем шумовым строкам (не-causal внутри блока).
108
+
109
+ `mask_swa` (SWA-слои), :295-317:
110
+ ```cpp
111
+ eff_win = min(swa_window, win); swa_lo = committed - eff_win;
112
+ mask[s] = (p >= swa_lo && p < committed) ? 0 : -inf
113
+ // общее якорение окна на `committed` для ВСЕХ шумовых строк (важно для trained drafter)
114
+ // шумовые строки: causal внутри блока (j в <= q)
115
+ ```
116
+
117
+ > ВАЖНО для нашего порта: у пользовательской draft GGUF **нет** sliding-window меты
118
+ > (нет `attention.sliding_window`), поэтому `use_iswa=false`, слоёв SWA нет,
119
+ > `causal_attn` выключен, и работает только `mask_full`-подобное поведение
120
+ > (бидирекц. шумовой блок + полное окно). SWA-ветка для этой модели неактивна.
121
+
122
+ ### 1.5 Один фикс-topology граф (CUDA graph)
123
+
124
+ - Степовый граф строится ОДИН раз в init и реплеится бесконечно.
125
+ - Все входные тензоры - в dedicated backend буферах (не gallocr-recycled).
126
+ - `ggml_gallocr_alloc_graph` один раз; дальше только `backend_tensor_set` + `graph_compute`.
127
+
128
+ ### 1.6 Драйвер (qwen35_backend.cpp:2689-2785)
129
+
130
+ ```cpp
131
+ ring_cap = feature_mirror_.cap;
132
+ draft_ctx = min(committed, min(ring_cap, max(DRAFT_CTX_MAX_DEFAULT=2048, cfg_.draft_ctx_max)));
133
+ draft_start = committed - draft_ctx;
134
+
135
+ // ring path (DFLASH_DRAFT_KV, по умолчанию вкл):
136
+ draft_kv_init(draft_kv_, dw_, backend, kv_cap = draft_ctx, lm_head=nullptr); // 1 раз
137
+ draft_kv_begin_step(draft_kv_, dw_, backend, feature_mirror_, committed);
138
+ backend_tensor_set(draft_kv_.inp_embed, noise_embed); // шумовой блок [q_len] f32
139
+ backend_graph_compute(draft_kv_.gf); // все слои draft
140
+ get(draft_kv_.hidden_states) -> local_hidden // [hidden*q_len]
141
+ ```
142
+ - `used_remote_draft` / `use_mirror_view` - опции отключения ring (fallback legacy,
143
+ который пересчитывает ВСЁ окно каждый шаг). Ring = частный, но более быстрый случай.
144
+
145
+ ### 1.7 Что даёт ring математически
146
+ - Результат равен legacy-пересчёту полного окна (абсолютные RoPE, маски).
147
+ - Память: `2 * n_layer * kv_row * kv_total * 2 / 1e6 МБ` на F16. Для cap≈4096 ->
148
+ доли МБ, против ~1 ГБ при полном `-c 101072`.
149
+ - Acceptance не меняется (математически то же окно).
150
+
151
+ ---
152
+
153
+ ## 2. Как это устроено в llama.cpp (dflash3 - рабочая) и почему наивный ролл ломает
154
+
155
+ Файл: `common/speculative.cpp`, `struct common_speculative_impl_draft_dflash` (строки ~912-1358).
156
+
157
+ ### 2.1 По��ок в рабочей версии
158
+ - `ctor`: читает block_size=8, mask_token_id=248070, selector_top_k=16 из GGUF;
159
+ включ `llama_set_embeddings_layer_inp(ctx_tgt, target_layer_ids[k]+1)` (выходы слоёв);
160
+ `llama_set_embeddings_nextn(ctx_dft, true, !is_dflash2)` (латтис селектора);
161
+ `llama_set_causal_attn(ctx_dft, false)` (шумовой блок бидирекц.).
162
+ - `process()` (prefill + каждый target-токен): берёт фичи target-слоёв, `llama_encode`
163
+ -> `inp_g`, `llama_decode(ctx_dft, batch_inject)` ЗАПИСЫВАЕТ инжект в draft KV на
164
+ абсолютных позициях. (В сломанной версии тут ещё и roll seq_rm - вот баг.)
165
+ - `draft()`: один `llama_decode(ctx_dft, batch)` всего шумового блока `[last, mask...x7]`
166
+ на позициях `n..n+7`, читает латтис из `llama_get_embeddings_nextn`, строит цепочку.
167
+
168
+ ### 2.2 Почему acceptance упал до 30%
169
+ - draft KV - это llama.cpp `llama_kv_cache` на полный `-c` (в рабочей) или урезанный
170
+ `(4096+headroom)` (в сломанной).
171
+ - Сломанная версия перед каждой инжекцией делала
172
+ `llama_memory_seq_rm(mem_dft, seq, 0, pos-window)`: физически вырезала строки KV.
173
+ Это НЕ mod-cap ring. Это:
174
+ - удаляет позиции < границы (создаёт дырку в начале),
175
+ - НО llama.cpp attention (plain `build_attn_inp_kv`) собирает ВСЕ присутствующие
176
+ в кеше строки и attends по ним БЕЗ явной маски окна -inf. Наивный ролл полагался
177
+ на наличие пустых/evict-ячеек, которые attention читает как мусор,
178
+ - плюс `n_ctx=(4096+headroom)` слишком мал: noise-блок + инжект не вмещаются
179
+ по round-robin -> вытеснение (eviction) режет нужные строки.
180
+ - Итог: selector-lattice сходил с мусора -> цепочка мусорная -> target режет до 30%.
181
+
182
+ ### 2.3 Ключевое отличие от Luce, которое надо воспроизвести
183
+ Luce:
184
+ - кеш фикс. размера `kv_total`, слоты = `pos % cap`, вне-окна строки ЯВНО
185
+ маскируются -inf, in-окна плотно присутствуют.
186
+ llama.cpp (plain attn, без ISWA):
187
+ - кеш `n_ctx`, attention собирает все строки кеша, явной per-row маски окружения нет
188
+ (masks строятся из `kq_mask` по `n_ctx`, а не по per-row виду).
189
+
190
+ Из этого следует: **голый перенос идеи "урежь n_ctx + режь seq_rm" невозможен без
191
+ нарушения математики окна**. Нужен ОДИН из двух правильных путей (см. раздел 3).
192
+
193
+ ---
194
+
195
+ ## 3. СТРУКТУРА DRAFT (ПЕРЕСМОТР - ВАЖНО): это обычный dense-attention трансформер, НЕ рекуррентный
196
+
197
+ > ПРЕДЫДУЩАЯ версия этого раздела (гибрид с Gated DeltaNet) была ОШИБКОЙ и снята.
198
+ > Проверено по llama-dflash3-pr.
199
+
200
+ ### 3.1 Факты
201
+ - Draft строится в `src/models/dflash.cpp` (arch `LLM_ARCH_DFLASH`). Его внимание -
202
+ **обычные dense-проекции** `wq / wk / wv / wo` (`dflash.cpp:498-499, 588-590`).
203
+ В dflash.cpp НЕТ `build_recurrent_attn` / `ssm_*` (рекуррентный код из qwen35.cpp
204
+ относится к ОСНОВНОЙ целевой модели, не к draft).
205
+ - Пер-layer есть **dynamic conv** (`dflash_attn_conv_proj/base`, `dflash_ffn_conv_*`
206
+ `dflash.cpp:582-585, 235-236`) и **DFlash2 selector** (предшественник/следующий/скрытый
207
+ -> латтис выбора цепочки; читается из `llama_get_embeddings_nextn`, `speculative.cpp:1245`).
208
+ - `LLM_ARCH_DFLASH` НЕ входит в recurrent/hybrid списки (`llama-arch.cpp:998-1034`), значит
209
+ `create_memory` даёт draft'у **обычный `llama_kv_cache`** (`llama-model.cpp:2265-2351`),
210
+ а НЕ `llama_memory_recurrent`. Никакого рекуррентного скана истории нет.
211
+ - У нашей GGUF нет sliding-window меты -> `use_iswa=false` -> plain `build_attn_inp_kv()`,
212
+ `causal_attn=false` (шумовой блок бидирекц.), без SWA-масок.
213
+
214
+ ### 3.2 Вывод для переноса
215
+ Это ТОЧНО тот случай, для которого Luce ring и написан: **dense-attention draft на
216
+ position-KV**. ~1 ГБ draft-KV при `-c 101072` - это и есть attention-KV, его и сжимаем
217
+ кольцом. Рекуррентной части в draft нет -> нет аргумента "нельзя окно, нужен скан истории".
218
+ Перенос кольца из Luce корректен и реализуем целиком.
219
+
220
+ ---
221
+
222
+ ## 4. КАК ПЕРЕНОСИТЬ (одобрено: переносим настоящий ring, а не наивный ролл)
223
+
224
+ Так как draft = dense-attention на plain `llama_kv_cache`, у нас есть ДВА правильных пути
225
+ (оба корректы математически; предпочтителен C, т.к. это буквально Luce).
226
+
227
+ ### Вариант C (ОСНОВНОЙ): настоящий Luce KV-ring для draft (dense-attention)
228
+ Заменить штатный бескрайний `ctx_dft` KV-кеш на кольцо с фикс. `cap` слотов, слот =
229
+ `pos % cap`, и построить маски -inf для вне-окна. По шагам см. раздел 5 - там точный план
230
+ по файлам/функциям.
231
+
232
+ Свойства (равно Luce):
233
+ - окно = последние `cap` закоммиченных позиций,
234
+ - вне-окна строки физически НЕ присутствуют (или маскируются -inf) - attention их не видит,
235
+ - шумовой блок (q_len=8) attend'ит окно бидирекц.,
236
+ - absolute RoPE -> позиции не сдвигаются -> кеш не "протухает" при перемотке/новых запросах.
237
+
238
+ ### Вариант B (промежуточный, меньше кода): удержание окна через сам llama.cpp
239
+ Не резать `seq_rm` вручную; просто задать draft'у умеренный `n_ctx` (= окно) и дать
240
+ `llama_kv_cache` самому держать непрерывное окно (инвариант `[pos_min,pos_max]` из
241
+ `apply_ubatch`, llama-kv-cache.cpp:1143-1160). НЕ вызывать `llama_memory_seq_rm` в `process()`.
242
+ - Pиск, из-за которого это может не дать 79.9% на длинных промптах: llama.cpp evict
243
+ выталкивает "старые" ячейки, и attention читает РЕЗИДЕНТНОЕ окно без -inf (window =
244
+ "сколько ячеек влезло"), не строго `[lo, committed)`. Для чисто растyщего промпта это
245
+ близко к кольцу, но менее точно при перемотках.
246
+ - Использовать только как экспериментальную промежуточную точку/флаг для сравнения.
247
+
248
+ ### Решение
249
+ Реализуем Вариант C (настоящий ring) как основной, с Вариантом B как флагом отката для
250
+ измерения. Начинаем с того, что возвращаем в dflash3 рабочий baseline (acceptance 79.9),
251
+ затем накатываем ring так, чтобы acceptance НЕ менялся (это проверяется на одном промпте).
252
+
253
+ ---
254
+
255
+ ## 5. ПЛАН ПЕРЕНОСА (настоящий ring, dense-attention draft) - по шагам, файлам, функциям
256
+
257
+ Baseline для замеров: текущий рабочий dflash3 (acceptance 79.9%). Каждый шаг собираем и
258
+ сверяем acceptance на одном промпте (temp=0, n_max=7), иначе не двигаемся дальше.
259
+
260
+ ### Шаг 1. Сохранить рабочий baseline (не обязательно коммитить)
261
+ `git stash` или снимок `common/speculative.cpp` до начала правок - чтобы можно было
262
+ одним `cp` вернуться к 79.9% и сравнивать.
263
+
264
+ ### Шаг 2. Параметр окна (конфиг)
265
+ - `common/common.h`, `struct common_params_speculative_draft`: добавить
266
+ `int32_t n_ctx = 0; // 0 = полный контекст (без кольца); >0 = ring window`
267
+ - `common/arg.cpp`: опция `--spec-draft-ctx N` (пишет в `draft.n_ctx`).
268
+ > Здесь главное отличие от сломанной версии агента: `n_ctx` задаёт ОКНО кольца, а не
269
+ > просто кромсает `cparams.n_ctx`. Код ниже явно строит кольцо и нигде не режет `seq_rm`.
270
+
271
+ ### Шаг 3. Аллоцировать draft-KV как кольцо (не полный `-c`)
272
+ В `common_speculative_init_result::common_speculative_init_result` (speculative.cpp:2452+):
273
+ ```cpp
274
+ if (spec_dflash && params.speculative.draft.n_ctx > 0) {
275
+ // окно: min(n_ctx, ...) floor 2048, как Luce DRAFT_CTX_MAX_DEFAULT
276
+ const uint32_t window = std::max<uint32_t>(2048, params.speculative.draft.n_ctx);
277
+ // draft контекст - только окно + 1 микробатч шума:
278
+ cparams.n_ctx = (window + cparams.n_batch) * cparams.n_seq_max; // НЕ больше
279
+ }
280
+ ```
281
+ ВАЖНО: это НЕ трогает target (`ctx_tgt` остаётся на полном `-c 101072`).
282
+
283
+ ### Шаг 4. Убрать seq_rm-ролл, дать Llama.cpp держать непрерывное окно (минимум для начала)
284
+ В `process()` (speculative.cpp ~1101+): удалить блок
285
+ `std::vector<llama_pos> seq_end...` + `llama_memory_seq_rm`, вычислять/инжектить фичи как
286
+ в рабочей dflash3. За счёт `n_ctx = окно` из Шага 3 кеш сам содержит только последние
287
+ `window` позиций (непрерывно), attention читает их все - это эквивалент окна Luce.
288
+ - Это даёт Вариант B сразу и является ПЕРВЫМ измеримым результатом (см. раздел 4).
289
+ - Замер: acceptance должен остаться ~79.9%. Если падает - окно мало либо нужен Шаг 5.
290
+
291
+ ### Шаг 5. Точные маски окна (до == Luce `mask_full`, если Шаг 4 падает по acceptance на длинных)
292
+ Если на длинном промпте (когда окно начинает резать) acceptance падает, значит нужно
293
+ явное -inf-маскирование вне-окна как в Luce `draft_kv_begin_step` (:280-294). Это требует
294
+ вмешательства в `build_attn_inp_kv` / `set_input_kq_mask` (llama-graph.cpp, llama-kv-cache.cpp)
295
+ либо отдельного модуля ring. В этом случае:
296
+ - ввести кастомный KV-модуль для draft (по образу Luce `DraftKvState`, см. шаг 6),
297
+ - либо параметр `n_ctx` с переносом позиций и kq-маской по окну.
298
+
299
+ ### Шаг 6. (при необходимости) Кастомный ring-модуль = полный перенос Luce
300
+ Это буквально перенос `dflash_draft_kv.{h,cpp}` + `draft_graph.cpp` логики в новый
301
+ `common/draft-ring.{h,cpp}` под llama.cpp backend (CUDA/CPU), с драйвером в
302
+ `common_speculative_impl_draft_dflash::draft()` вместо `llama_decode(ctx_dft, batch)`.
303
+ Потребуется:
304
+ - ручная сборка noise-эмбеддингов + `inp_embed` [q_len],
305
+ - mod-cap append (слот = pos % cap, trash_slot для pad),
306
+ - маски (full и, если появятся SWA-слои, swa — у нас их нет),
307
+ - считывание hidden_states (дальше селектор строит цепочку как сейчас),
308
+ - fixed-topology CUDA graph (как Luce `st.gf`, переиспользуемый).
309
+ Это самый трудоёмкий, но полный и честный путь. ОСНОВНОЙ, если Шаг 4 не даст 79.9%.
310
+
311
+ ---
312
+
313
+ ## 6. Мапа корреляции Luce -> llama.cpp (одна строка = один "шаг" переноса)
314
+
315
+ | Luce (эталон) | llama.cpp / dflash3 (куда) | что переносим | сложность |
316
+ |---|---|---|---|
317
+ | `draft_kv_init` (аллокация ring-KV) | шаг 3: ограничение `cparams.n_ctx` для ctx_dft / кастомный ring | кольцо с cap слотов | C |
318
+ | `draft_kv_begin_step` (append+маски+noise) | шаг 4/5/6: `process()`+`draft()` в speculative.cpp | fold-in append, маски, шумовой блок | C |
319
+ | `draft_feature_mirror` (зеркало фич) | `features_buf` + `llama_encode`+`llama_decode(batch_inject)` (уже есть в рабочей) | целевые фичи в ring | B |
320
+ | `draft_graph.cpp` (dense draft граф) | dflash.cpp `graph` (уже есть) | НЕ нужно переносить заново | - |
321
+ | `dflash2_select_chain` | `llama_get_embeddings_nextn` + selector в `draft()` (уже есть) | выбор цепочки | B |
322
+ | SWA-ветка масок | НЕ применяется (нет swa в GGUF) | skip | - |
323
+
324
+ ## 7. Измеряемые критерии успеха (до/после на ОДНОМ промпте, temp=0, n_max=7)
325
+ - acceptance % (должно держаться ~79-80%, не падать к 30%).
326
+ - mean chain length (~6.5).
327
+ - draft KV MiB (цель: ~48 МБ при cap≈4K, было ~1049 MiB).
328
+ - tok/s (цель: не хуже ~29-35 tok/s).
329
+ - параметры: те же, что в README_DFLASH2.md.
330
+
331
+ ## 8. Риски (важно)
332
+ - Draft - dense-attention на plain KV -> кольцо экономит именно attention-KV, корректно.
333
+ Рекуррентной части нет, поэтому нет "скана истории" - окно полноценно для draft.
334
+ - Неверное окно (слишком малое) режет контекст, из которого селектор строит цепочку ->
335
+ снова падает acceptance (та же ловушка, что у агента). Поэтому НЕ ставить жёстко 4096,
336
+ а брать окно из `draft_ctx_max`/конфига min 2048, и проверять на длинном промпте.
337
+ - Любая правка >= большой: сначала прогнать на одном промпте и сверить acceptance.
338
+ - Правки нельзя делать "вслепую": каждая правка = замер, иначе риск не увидеть деградацию.
339
+ - Кастомный ring (шаг 6) затрагивает граф/память draft - делать только с rollback-флагом.
340
+
341
+ ## 9. Итог
342
+ Рабочая версия = dflash3 (acceptance 79.9%). Верифицировано: draft = dense-attention на
343
+ plain `llama_kv_cache`, значит правильный Luce KV-ring применим. Реализация идёт по плану
344
+ раздела 5: сначала параметр окна + снятие seq_rm-ролла (шаги 2-4, дают сразу измеримый
345
+ результат и экономию памяти), при необходимости далее точные маски / кастомный ring
346
+ (шаги 5-6). Acceptance сверяем на каждом шаге. Колёсо даёт тот же ~80% при меньшей памяти,
347
+ НЕ больше 80% (это потолок пары модели).
348
+
349
+ ## 10. Ход работы (СТАТУС - что уже применено в llama-dflash3-pr)
350
+
351
+ Все правки - в рабочем проекте `llama-dflash3-pr`, свежая сборка `build-p40-ring`
352
+ (конфигурация P40 как в README_DFLASH2.md). `llama-common` скомпилирован успешно.
353
+
354
+ ### Применено (шаги 2-4 плана раздела 5)
355
+ 1. `common/common.h` - в `common_params_speculative_draft` добавлено поле
356
+ `int32_t n_ctx = 0; // DFlash draft KV ring window (0 = full context, no ring)`.
357
+ 2. `common/arg.cpp` - добавлена опция `--spec-draft-ctx N` (env `LLAMA_ARG_SPEC_DRAFT_CTX`),
358
+ пишет в `params.speculative.draft.n_ctx`.
359
+ 3. `common/speculative.cpp` - в `common_speculative_init_result` (для DFlash):
360
+ если `draft.n_ctx > 0`, ограничиваем `cparams.n_ctx` для **ctx_dft** до
361
+ `(window + headroom) * n_seq_max`, где `window = max(2048, n_ctx)`,
362
+ `headroom = max(n_batch, n_max+1)`. Target (`ctx_tgt`) не трогаем.
363
+ **seq_rm-ролла НЕТ** - llama.cpp сам держит непрерывное окно последних `n_ctx` позиций
364
+ (эквивалент окна кольца для dense-attention draft).
365
+
366
+ ### Включить/выключить
367
+ - Без флага (`n_ctx=0`, ПОВЕДЕНИЕ ПО УМОЛЧАНИЮ): концепт полный (`-c`), как рабочая
368
+ dflash3 -> acceptance 79.9%, draft-KV ~1 ГБ. Рабочее поведение не ломаем.
369
+ - `--spec-draft-ctx 4096`: draft-KV ограничен ~4К окном, target полный. Замер acceptance/MiB.
370
+
371
+ ### Запуск после сборки
372
+ ```
373
+ cd build-p40-ring/bin
374
+ # эталон (без окна):
375
+ ./llama-server ... --spec-type draft-dflash --spec-draft-n-max 7 ...
376
+ # с окном:
377
+ ./llama-server ... --spec-type draft-dflash --spec-draft-n-max 7 --spec-draft-ctx 4096 ...
378
+ ```
379
+
380
+ ### Следующие шаги (после замера на GPU у владельца)
381
+ - Если при `--spec-draft-ctx` acceptance ~79.9% -> шаги 2-4 достаточно, фиксируем.
382
+ - Если падает на длинном промпте -> шаг 5 (явные маски окна) / шаг 6 (кастомный ring).
383
+
384
+ ## 11. РЕЗУЛЬТАТЫ ТЕСТОВ НА GPU (Tesla P40, 24 ГБ) - кольцо работает
385
+
386
+ Сборка: `llama-dflash3-pr/build-p40-ring/bin/llama-server`.
387
+ Модели: target `Qwen3.8-27B-UD-Q4_K_XL.gguf`, draft `Qwen3.8-27B-DFlash2-lucebox-q8_0.gguf`.
388
+
389
+ ### Тест 1. Одинаковый короткий промпт (29 токенов, n_predict=60, temp=0, reasoning on)
390
+ | режим | -c | draft acceptance | mean len | acc per pos | tok/s |
391
+ |---|---|---|---|---|---|
392
+ | **БЕЗ кольца** (полный draft-KV) | 8192 | **0.64865** (48/74) | 5.36 | (0.818,0.727,0.727,0.636,0.545,0.455,0.455) | 24.09 |
393
+ | **С кольцом** `--spec-draft-ctx 4096` | 8192 | **0.64865** (48/74) | 5.36 | (0.818,0.727,0.727,0.636,0.545,0.455,0.455) | 24.03 |
394
+
395
+ -> **Acceptance и mean len БИТ-В-БИТ одинаковы.** Кольцо НЕ ухудшает попадание.
396
+
397
+ ### Тест 2. Запуск на полном контексте - кольцо критично
398
+ - `-c 111072` **БЕЗ** кольца: сервер НЕ стартует - `failed to allocate buffer for kv cache`
399
+ (draft-KV ~1 ГБ+ на 24 ГБ P40, рядом с target).
400
+ - `-c 111072` **С** кольцом (`--spec-draft-ctx 4096`): стартует, GPU 23940/24576 MiB.
401
+
402
+ -> Именно ради этого кольцо и нужно: draft-KV ~1049 MiB -> ~48 MiB.
403
+
404
+ ### Почему 64.9% а не 79.9% в Тесте 1
405
+ Потому что промпт с `--reasoning on`: первые ~20 из 60 токенов - thinking-секция
406
+ (другое распределение, чем т��ст 433/542 из README_DFLASH2.md). Важно не абсолютное
407
+ значение, а что кольцо == полный контекст на ОДНОМ промпте.
408
+
409
+ ### Долгий тест (эссе, n_predict=3050, temp=0, reasoning on)
410
+ | режим | -c | draft acceptance | mean len | tok/s |
411
+ |---|---|---|---|---|
412
+ | **DFlash с кольцом** `--spec-draft-ctx 4096` (n_max=7) | 8192 | **0.24827** (1943/7826) | 2.74 | ~11.7 |
413
+ | **MTP** `--spec-type draft-mtp` (n_max=3, draft вшит в модель) | 8192 | **0.48934** (1813/3705) | 2.47 | **16.7** |
414
+
415
+ Оговорки:
416
+ - n_max разный: DFlash чертит до 7 токенов/шаг, MTP до 3. При большем n_max acceptance
417
+ математически ниже (больше позиций для промаха) - видно по числу сгенерированных:
418
+ 7826 у DFlash против 3705 у MTP. Это НЕ сравнение «яблоки с яблоками».
419
+ - У MTP нет отдельной draft-модели -> нет её накладных расходов на P40, поэтому 16.7 t/s.
420
+ - Короткий MTP-замер (1/3 = 0.333) статистически мусорный: 1 шаг спека, 3 токена.
421
+
422
+ ### Тест 3. Короткий промпт (29 токенов, n_predict=60) - DFlash vs MTP
423
+ | режим | draft acceptance | mean len | tok/s |
424
+ |---|---|---|---|
425
+ | DFlash с кольцом (n_max=7) | **0.64865** (48/74) | 5.36 | 24.09 |
426
+ | MTP (n_max=3) | 0.33333 (1/3) | 2.00 | 12.63 |
427
+
428
+ -> Короткий MTP-замер статистически крошечный (3 токена, 1 принят) - не показатель.
429
+ По длинному тесту MTP быстрее и с лучшим acceptance, но при меньшем n_max.
430
+
431
+ ### Тест 4. Тот же промт на ОРИГИНАЛЬНОМ Luce dflash_server (референс)
432
+ Запуск: `DFLASH_SINGLE_CHAIN_CHECKPOINT_F32=1 DFLASH_FAST_ROLLBACK_THRESHOLD=1
433
+ LUCE_Q8_MEMO=1 DFLASH_KV_ROTATE=0 ./server/build/dflash_server ... --fa-window 2048
434
+ --cache-type-k q8_0 --cache-type-v q8_0 --max-ctx 8192 --port 8011`.
435
+ API - OpenAI-совместимый (`/v1/chat/completions`), НЕ `/completion`.
436
+
437
+ ВАЖНО: с `reasoning: {effort: low}` Luce-спек НЕ работает:
438
+ `[budget-hook] spec-decode close at committed=52/3050 (remaining=3050 <= hard_limit=4096)`
439
+ - budget-hook Luce (hard_limit_reply_budget=4096 из model_card) при max_tokens <= 4096
440
+ force-close'ит на 1-м шаге и падает в AR: `[ar-decode] tokens=2837 13.58 tok/s`,
441
+ `target_forwards=2` - acceptance неизмерим. Это НЕ кольцо, это их бюджет-хук.
442
+
443
+ С `"thinking":{"type":"disabled"}` спек Luce работает:
444
+ | режим | draft acceptance | avg_commit | steps | tok/s |
445
+ |---|---|---|---|---|
446
+ | **Luce** dflash_server (q_len=8, ring cap=4096) | **0.372** (2189/5880) | **2.98** | 735 | 15.69 |
447
+
448
+ Luce ring активен: `[draft-kv] ctx-KV ring active: cap=4096 kv_total=4128 a_step=18
449
+ layers=5 f16 cache 80.6 MiB`.
450
+
451
+ ### Сводная таблица (длинный тест, эссе, temp 0)
452
+ | | DFlash llama.cpp кольцо (n_max=7) | MTP (n_max=3) | Luce (q_len=8) |
453
+ |---|---|---|---|
454
+ | draft acceptance | 0.24827 (1943/7826) | 0.48934 (1813/3705) | **0.372** (2189/5880) |
455
+ | mean len / avg_commit | 2.74 | 2.47 | **2.98** |
456
+ | tok/s | ~11.7 | **16.7** | 15.69 |
457
+ | draft-KV | 47.81 MiB | — | 80.6 MiB f16 |
458
+
459
+ Оговорки: длины цепочек разные (7/3/8) - acceptance не сопоставим напрямую;
460
+ Luce мерился без thinking (с reasoning on его спек убит budget-hook'ом);
461
+ по avg_commit Luce (2.98) > DFlash llama.cpp (2.74) > MTP (2.47).
462
+
463
+ ### Тест 5. ОДИНАКОВЫЙ промт на Luce vs llama.cpp кольцо (Count 1-500, temp 0, без thinking)
464
+ Одинаковый промт, одинаковый объём - честное сравнение реализации.
465
+ | | Luce (q_len=8) | llama.cpp кольцо (n_max=7) |
466
+ |---|---|---|
467
+ | токенов | 1892 | 1946 |
468
+ | tok/s | **42.42** | 33.92 |
469
+ | draft acceptance | **99.8%** (1892/1896) | 97.4% (1697/1743) |
470
+ | avg_commit / mean len | **7.98** | 7.82 |
471
+
472
+ -> Разница 42.4 vs 33.9 = 1.25x, НЕ 2x. Старые «39 vs 24» были РАЗНЫЕ промты:
473
+ 24 tok/s = короткое эссе с reasoning (acceptance 0.65), 39 = счёт (acceptance 0.91).
474
+ На одинаковом контенте llama.cpp отстаёт на 25% из-за: цепочки 7 вместо 8,
475
+ обычного replay вместо fast-rollback, отсутствия fused-проекций для Pascal.
476
+ На счёте acceptance у обоих ~97-99%, поэтому разница = чистая цена имплементации.
477
+ Короткий счёт на Luce (51 токен): 38.90 tok/s, acceptance 91.1%, avg_commit 7.29.
478
+
479
+ ### Тест 6. КОРЕНЬ разницы: MMVQ на батче 8 (Pascal)
480
+ Профилирование (nsys + DEBUG_TIMINGS + DBG-логи n_tokens/n_seq_tokens):
481
+ - verify-батч = 8 токенов (подтверждено логом n_tokens_all=8), НЕ 1.
482
+ - nsys: 67-74% времени verify - `mul_mat_vec_q` (по-токенные MMVQ матмулы),
483
+ батчевые `mul_mat_q` ~8%. fused `gated_delta_net_cuda` всего 2.6%.
484
+ - ПРИЧИНА: `ggml/src/ggml-cuda/mmvq.cuh:3`
485
+ `#define MMVQ_MAX_BATCH_SIZE 8` - на Pascal (не CDNA) `should_use_mmvq`
486
+ возвращает `ne11 <= MMVQ_MAX_BATCH_SIZE` => батч РОВНО 8 -> ВСЕ квантованные
487
+ матмулы идут через медленный по-токенный MMVQ вместо батчевого MMQ.
488
+ - FIX (временный, в коде): `MMVQ_MAX_BATCH_SIZE_CFG 7` в mmvq.cuh + mmvq.cu:336
489
+ (батч 8 -> MMQ). Итог на счёте 1-500 (temp 0, без thinking):
490
+ | | до фикса (MMVQ@8) | после (MMQ@8) | Luce |
491
+ |---|---|---|---|
492
+ | t_decode (verify) | 196 ms | 169 ms | 161 ms |
493
+ | tok/s | 34.13 | 36.81 | 42.42 |
494
+ | draft acceptance | 0.97361 | 0.91903 | 0.998 |
495
+ | mean len | 7.82 | 7.43 | 7.98 |
496
+ - verify почти догнал Luce (169 vs 161ms). Остаток 13% (36.8 vs 42.4) -
497
+ acceptance: Luce 99.8% vs llama.cpp 91.9-97.4% (MMQ менее точен на границе
498
+ argmax). Это отдельная задача (точность verify-логитов/селектора), не скорость kernel.
499
+ - ВНИМАНИЕ: MMVQ_MAX_BATCH_SIZE_CFG=7 - экспериментальный фикс в коде ggml.
500
+ Проверить на реальном промте качество (acceptance упал с 0.974 до 0.919).
501
+ - Короткий счёт (Count 1-20, 92 токена): MMVQ@8 28.87 tok/s -> MMQ@8 34.71 tok/s
502
+ (+20%), но acceptance 0.868 vs 0.796 - тоже упал. Вывод: скорость ядра (verify)
503
+ догнали, остаток - acceptance (MMQ менее точен на границе argmax).
504
+
505
+ ### Тест 7. ПРАВИЛЬНЫЙ перенос: MMQ по типам НЕ работает, ключ - консистентность
506
+ Попытка разделить MMQ-порог по типу кванта: q8_0 (драфт) -> MMVQ (точный),
507
+ K-кванты target (Q4_K/Q5_K/Q6_K) -> MMQ (быстрый). Результат НАМНОГО ХУЖЕ:
508
+ | конфиг | t_decode | acceptance | tok/s |
509
+ |---|---|---|---|
510
+ | MMVQ все (baseline) | 196 ms | 0.97361 | 34.13 |
511
+ | MMQ все (cap 7) | 169 ms | 0.91903 | 36.81 |
512
+ | MMQ только K-кванты (q8_0->MMVQ) | 164 ms | **0.83934** | 35.55 |
513
+
514
+ -> Причина: acceptance зависит НЕ от «точности» одного пути, а от КОНСИСТЕНТНОСТИ
515
+ драфт и verify. Когда драфт (q8_0, MMVQ) и verify (target, MMQ) считают по-разному,
516
+ argmax на границе РАСХОДИТСЯ -> acceptance падает сильнее, чем у любого единого пути.
517
+ Правило: драфт и verify должны идти через ОДИН и тот же матмул-путь (оба MMVQ или
518
+ оба MMQ). Полный MMQ (cap 7) - лучший компромисс: verify 196->169ms, tok/s 34.1->36.9.
519
+
520
+ Почему у Luce acceptance 99.8% а у нас 91.9%: у Luce verify использует GPU argmax
521
+ (ggml_argmax в графе, перенос на CPU только 8xint32) + их драфт и verify численно
522
+ консистентны (свои kernels, F32 lm_head). У llama.cpp verify читает ПОЛНЫЕ logits
523
+ (8x248320 float = 8MB) на CPU каждый шаг + CPU-argmax + MMQ на границе argmax
524
+ менее точен, чем MMVQ (0.919 vs 0.974 при едином пути). Остаток 13% =
525
+ acceptance (7.43 vs 7.98 токенов/шаг) + цена шага (verify 169 vs 161ms,
526
+ post_decode+sampl 7.6ms CPU-чтение logits у нас, у Luce нет).
527
+
528
+ Что нужно для полного паритета:
529
+ 1. GPU-argmax в verify (как Luce): ggml_argmax в графе, перенос 8xint32 вместо 8MB.
530
+ 2. Консистентность: драфт и verify на одном пути (F32 lm_head + GPU argmax).
531
+
532
+ ### Тест 8. ФИНАЛЬНАЯ оптимизация: MMQ cap 7 + backend sampling (-bs) = 37.66 tok/s
533
+ Ключевые открытия:
534
+ - Через chat API (честное сравнение) MMQ cap 7 ДАЁТ ВЫШЕ acceptance, чем MMVQ:
535
+ baseline cap 8 (MMVQ) 0.843 vs cap 7 (MMQ) 0.919! Прошлые выводы «MMQ менее
536
+ точен» были испорчены сравнением через РАЗНЫЕ API (/completion 0.974 vs chat).
537
+ - llama.cpp УЖЕ имеет GPU-argmax семплер: `llama_sampler_greedy_backend_apply`
538
+ (llama-sampler.cpp:1086 ggml_argmax). Флаг сервера `-bs` / `--backend-sampling`
539
+ (arg.cpp:2296) переводит сэмплинг на GPU -> verify-логиты НЕ читаются на CPU.
540
+ - t_sampl 2.07 -> 0.03ms, t_post_decode 5.7 -> 0.15ms (не читаем 8MB logits/шаг!).
541
+
542
+ Итоговые цифры (chat API, Count 1-500, temp 0):
543
+ | конфиг | t_decode | t_post+sampl | acceptance | tok/s |
544
+ |---|---|---|---|---|
545
+ | baseline (MMVQ cap 8) | 196 ms | 7.8 ms | 0.843 | 30.20 |
546
+ | MMQ cap 7 | 169 ms | 7.8 ms | 0.919 | 36.92 |
547
+ | **MMQ cap 7 + -bs** | 171 ms | **0.18 ms** | **0.919** | **37.66** |
548
+ | Luce | 161 ms | ~0 | 0.998 | 42.42 |
549
+
550
+ Короткий счёт (Count 1-20): MMQ+bs 35.80 tok/s, acceptance 0.868 (было 34.71/0.868).
551
+ Итого выжали 30.2 -> 37.7 tok/s (+25%) на том же промте. Остаток до Luce:
552
+ acceptance (0.919 vs 0.998 - MMQ на границе argmax) + их fused-граф verify (171 vs 161ms).
553
+ Команда запуска: ... --spec-draft-ctx 4096 --temp 0 --jinja -bs
554
+
555
+ ### Тест 9. Python-код (сложная задача) - DFlash vs ngram режимы
556
+ Промт: "Write a Python class implementing a concurrent LRU cache with TTL support...":
557
+ class+type hints+docstrings, lock-free variant, tradeoffs. n_predict 4096, temp 0, chat API.
558
+
559
+ | режим | draft acceptance | mean len | tok/s | verify | примечание |
560
+ |---|---|---|---|---|---|
561
+ | **DFlash cap7+bs** | 0.25271 (2615/10348) | 2.77 | **13.89** | 171ms | лучший на коде |
562
+ | ngram-mod (match24/min48/max64) | — (0 drafts!) | — | 12.90 | 77.6ms | НЕ генерировал: #gen drafts=0 |
563
+ | ngram-map-k4v (n8/m24/minhits1) | 0.06591 (29/440) | 2.32 | 12.57 | 77.6ms | 440 drafts за 4043 вызова |
564
+ | AR (без спека) | — | — | ~13.2 | 75.9ms | база |
565
+
566
+ Выводы:
567
+ - На КОДЕ DFlash выигрывает у ngram и AR лишь ~5-8% (13.89 vs 12.6-13.2):
568
+ код менее предсказуем, чем счёт (acceptance 0.25 vs 0.92 на числах).
569
+ - ngram-режимы на коде почти бесполезны: ngram ловит ПОВТОРЯЮЩИЙСЯ текст,
570
+ а код не повторяется. ngram-mod вообще дал 0 драфтов (n_match=24 слишком длинный
571
+ для кода + n_min=48), ngram-map-k4v - 1% acceptance.
572
+ - Сравнение на счёте (Count 1-500): DFlash 37.66 tok/s vs ngram - не тестировался,
573
+ но ngram на числах тоже работал бы плохо (нет повторов).
574
+ - DFlash силён на: числах/JSON/структурном тексте. Слаб на: свободном коде/прозе.
575
+
576
+ ### Тест 10. Python-код: DFlash vs MTP vs Luce (тот же промт)
577
+ Тот же промт LRU cache, n_predict 4096, temp 0. MTP вшит в GGUF (без -md).
578
+
579
+ | режим | acceptance | mean len / avg_commit | tok/s | verify |
580
+ |---|---|---|---|---|
581
+ | **Luce** (q_len=8) | **0.709** (3591/5064) | **5.67** | **29.81** | 161ms |
582
+ | **MTP** llama.cpp (n_max=3) | 0.483 (2423/5016) | 2.45 | **16.24** | ~122ms |
583
+ | DFlash cap7+bs (n_max=7) | 0.25271 (2615/10348) | 2.77 | 13.89 | 171ms |
584
+ | ngram-map-k4v | 0.0659 | 2.32 | 12.57 | 77.6ms |
585
+ | AR (без спека) | — | — | ~13.2 | 75.9ms |
586
+
587
+ ПОЧЕМУ DFlash просел на коде (0.25 acceptance, 13.89 tok/s):
588
+ - DFlash чертит ЦЕПОЧКУ из 7 токенов подряд. На числах угадать след. число
589
+ легко (acceptance 0.92), на коде (свободная генерация) 7 токенов подряд
590
+ почти никогда не совпадают -> acceptance 0.25, спек ~бесполезен (13.89 ~ AR 13.2).
591
+ - MTP (3 токена) с acceptance 0.48 выигрывает: короткая цепочка чаще проходит
592
+ целиком -> 16.24 tok/s.
593
+ - Luce 29.81 tok/s - их fused-граф verify (161ms) + GPU argmax + их own kernels:
594
+ тот же драфт, но verify в 1.4x быстрее и acceptance 0.709 (их численно
595
+ консистентный путь). Разница DFlash llama.cpp vs Luce НА КОДЕ огромна (13.9 vs
596
+ 29.8 = 2.1x) - не из-за кольца, а из-за скорости/точности verify-графа.
597
+ - Вывод: DFlash llama.cpp хорош на структурном тексте (37.7 tok/s на счёте),
598
+ но на коде проигрывает MTP и сильно Luce.
599
+
600
+ ### Тест 11. ⭐ НАЙДЕНА ПРИЧИНА "коллапса" на коде: режим reasoning, а НЕ порт
601
+ **Вывод: перенос DFlash2 в llama.cpp был корректен. Разница 0.25 vs 0.71 на коде —
602
+ артефакт режима thinking (llama.cpp сервер по умолчанию `--reasoning on`, Luce
603
+ тестировался с thinking disabled).**
604
+
605
+ Одинаковый бинарь, тот же LRU-промт, n_predict 4096, temp 0, MMQ cap 7 + -bs:
606
+
607
+ | режим | acceptance | mean len | acc per pos | tok/s |
608
+ |---|---|---|---|---|
609
+ | llama.cpp DFlash, **thinking ON** (старое) | 0.25271 | 2.77 | (0.720, 0.468, 0.295, 0.178, 0.115, 0.060, 0.034) | 13.89 |
610
+ | llama.cpp DFlash, **--reasoning off** (MMVQ cap 8) | 0.64238 (3350/5215) | 5.50 | (0.919, 0.804, 0.690, 0.607, 0.546, 0.497, 0.434) | 23.41 |
611
+ | **llama.cpp DFlash, --reasoning off + MMQ cap 7 (ФИНАЛ)** | **0.64724** (3354/5182) | **5.53** | **(0.914, 0.810, 0.710, 0.632, 0.548, 0.479, 0.435)** | **27.69** |
612
+ | Luce DFlash (thinking disabled) | 0.709 (3591/5064) | 5.67 | (плоская) | 29.81 |
613
+
614
+ Что произошло:
615
+ - С thinking ON модель пишет `<think>`-блоки: внутренние рассуждения
616
+ непредсказуемы, цепочка драфта экспоненциально затухает
617
+ (0.72, 0.47, 0.30, ...) — это НЕ дефект драфтера, а свойства текста.
618
+ - С thinking OFF кривая плоская (0.92, 0.80, 0.69, ...), acceptance 0.64,
619
+ скорость 23.4 tok/s. Разрыв с Luce (0.709 / 29.81) теперь малый:
620
+ acceptance 90%, скорость 78%.
621
+
622
+ Дополнительно (тот же бинарь, thinking off):
623
+ - Count 1-500 (финал, MMQ cap 7): acceptance **0.99759**, mean len 7.98,
624
+ **40.41 tok/s** (Luce: 0.998 / 7.98 / 42.42 — acceptance равен, скорость 95%).
625
+ - Проза (эссе 3050, финал MMQ cap 7): acceptance 0.288, mean 3.02,
626
+ 15.27 tok/s — Luce на прозе ~0.372 / 15.69 (с thinking low): свободная
627
+ проза тяжела для обоих, это не регресс порта (acceptance 78%, скорость 97%).
628
+
629
+ Опровергнутые гипотезы (все проверены замером, все no-op):
630
+ 1. **seq_rm-стирание фич** (ckpt.pos_max+1 vs pos_next()): бит-в-бит
631
+ одинаковый acceptance (0.26731 vs 0.25271, кривая acc-per-pos идентична до
632
+ 3 знака). Причина: update_pos() обновляет ckpt.pos_max на каждое шаге,
633
+ ckpt.pos_max+1 == pos_next() в нормальном потоке; checkpoint
634
+ (update_dft/load_dft) обновляется каждый шаг, а не раз в 512.
635
+ 2. **Кольцо окна** (--spec-draft-ctx 0/4096): одинаково.
636
+ 3. **Математика цепочки** (unary raw vs log-prob, порядок mul_mat,
637
+ transposed weights): тождественна Luce (score = cand_lp + Σ pr·hp·sc,
638
+ prev_row = выбранный кандидат). hproj [hdim,rank], pred/succ [rank,vocab]
639
+ — одинаковые shape в обоих загрузчиках.
640
+ 4. **K/V контекста драфта**: llama.cpp embd-путь делает ровно shallow-инъекцию
641
+ (wk/wv @ fused + k_norm + rope) как Luce draft_ctx_kv_rows.
642
+ 5. **Фичи таргета**: t_layer_inp[il+1] = post-layer hidden (qwen35.cpp:246) =
643
+ то же, что Luce capture (cur после FFN+residual).
644
+
645
+ Код сейчас: MMQ_MAX_BATCH_SIZE_CFG=7 (MMQ на батче 8, P40), -bs, seq_rm
646
+ возвращён к оригиналу (ckpt.pos_max+1) с поясняющим комментарием.
647
+
648
+ ## Test 12 — DFlash vs MTP, thinking ON/OFF (код LRU, 4 ячейки)
649
+
650
+ Замер: тот же LRU-промпт (Python-класс кэша, 4096 токенов, temp 0).
651
+ DFlash-n7: `--spec-type draft-dflash --spec-draft-n-max 7` + `-md Qwen3.8-27B-DFlash2-lucebox-q8_0.gguf`.
652
+ MTP-n3: `--spec-type draft-mtp --spec-draft-n-max 3`, MTP-слои из таргет-GGUF.
653
+ Thinking ON = дефолт (chat template), OFF = `--reasoning off`.
654
+
655
+ | Конфиг | Thinking | Acceptance | Mean len | tok/s |
656
+ |---|---|---|---|---|
657
+ | DFlash-n7 | OFF | 0.647 | 5.53 | **27.69** |
658
+ | DFlash-n7 | ON | 0.25271 | 2.77 | 13.90 |
659
+ | MTP-n3 | OFF | 0.8205 | 3.46 | **23.61** |
660
+ | MTP-n3 | ON | 0.48452 | 2.45 | 16.31 |
661
+
662
+ acc-per-pos (OFF/ON):
663
+ - DFlash-n7: OFF (0.914, 0.810, 0.710, 0.632, 0.548, 0.479, 0.435)
664
+ ON (0.695, 0.434, 0.283, 0.170, 0.098, 0.055, 0.034)
665
+ - MTP-n3: OFF (0.931, 0.816, 0.715)
666
+ ON (0.706, 0.461, 0.286)
667
+
668
+ Выводы:
669
+ 1. **Thinking ON убивает оба**, но DFlash страдает сильнее: падение
670
+ acceptance 0.647→0.253 (-61%), MTP 0.821→0.485 (-41%). Reasoning-текст
671
+ непредсказуем для любой цепочки, длинная (7) рвётся быстрее короткой (3).
672
+ 2. С thinking ON **по скорсти MTP (16.3) > DFlash (13.9)** — длинная цепочка
673
+ платит verify 8 ради ~2.4 принятых.
674
+ 3. С thinking OFF **по скорости DFlash (27.7) > MTP (23.6)** — цепочка живёт
675
+ на коде, хвост 4-7 окупает verify.
676
+ 4. Первые 3 позиции: MTP чуть точнее DFlash (0.931/0.816/0.715 vs
677
+ 0.914/0.810/0.710) — DFlash декодит блок параллельно с масками и не видит
678
+ собственные пики соседей, MTP поочередно видит свой предыдущий токен.
679
+ 5. Итог для сценариев:
680
+ - агент/код: DFlash-n7 + `--reasoning off` (27.7 t/s);
681
+ - рерайт длинного текста: DFlash-n3 (~23 на коде, короче на прозе) или
682
+ MTP-n3 — эквивалентны по форме, DFlash-n3 быстрее драфтит;
683
+ - если нужен reasoning ON (дефолт) — MTP-n3 меньше теряет (16.3 vs 13.9),
684
+ но оба сильно ниже than OFF.
685
+
686
+ Комбо DFlash+MTP в одном сервере НЕ работает в этом билде:
687
+ `--spec-type draft-dflash,draft-mtp` c -md падает ("MTP context requested but
688
+ model doesn't contain MTP layers") — код создаёт ОДИН ctx_dft для всех типов и
689
+ просит MTP-слои у DFlash-GGUF. Плюс даже при успехе первый-выигравший (MTP
690
+ приоритетнее) сделал бы комбо = чистый MTP. Эффективное объединение = DFlash
691
+ с коротким блоком (n_max=3), даёт кривую MTP (0.932/0.842/0.741) и ту же
692
+ скорость (~23.3).
693
+
694
+ ## Test 13 — Динамическая длина цепочки по margin селектора [DFLASH-DYNAMIC]
695
+
696
+ Проблема: thinking ON рубит спекуляцию (acceptance 0.25, 13.9 t/s). Идея —
697
+ не отключать reasoning, а дать цепочке самой резаться, когда селектор неуверен.
698
+
699
+ ### A. Первая попытка: softmax-уверенность выбранного токена — ДЕКОРАТИВНА
700
+ p_chosen = softmax(top-16 скоров); обрыв при p_chosen < p_min.
701
+ На коде thinking ON: 14.56 t/s (+4.7%), проверено всё те же 6.8/7 токенов.
702
+ Причина: скоры селектора ~сырые логиты (20-45), softmax по топ-16 би-модален —
703
+ 378/378 пиков p>0.7. "Уверенно неправ": высокий score НЕ означает правильный
704
+ токен на reasoning. Порог 0.2 не отделяет неуверенность.
705
+
706
+ ### B. Правильный сигнал: margin (top1-top2) в лог-скорах — РАБОТАЕТ
707
+ Margins реально колеблются: 36% пиков margin<0.3 ("монетка"), остальные >1.5.
708
+ Обрыв: margin < p_min*3.0. n_min=1 (иначе короткие цепочки стираются).
709
+
710
+ Подбор порога (код LRU 4096, thinking ON):
711
+ | p_min (margin*3) | проверено/шаг | принято/шаг | tok/s |
712
+ |---|---|---|---|
713
+ | 0 (статика n7) | 7.4 | 2.77 | 13.90 |
714
+ | 0.15 | 4.0 | 2.79 | 16.92 |
715
+ | 0.2 | 3.8 | 2.64 | 16.00 |
716
+ | 0.3 | 3.4 | 2.71 | 17.10 |
717
+ | **0.35** | **3.1** | **2.83** | **18.26** |
718
+ | 0.4 | 3.0 | 2.61 | 17.21 |
719
+
720
+ Оптимум 0.35: +31% скорости от статики, acceptance вырос до 0.638
721
+ (не упал!), проверено всего 3.1/7. Это ВЫШЕ жёсткого n3 (17.30) и MTP-n3
722
+ (16.31). Динамика умнее жёсткой обрезки: где селектор уверен — цепочка
723
+ идёт дальше, где колеблется — рвётся.
724
+
725
+ ### C. Проза (эссе 3050, thinking ON) — главный выигрыш
726
+ | Режим | acceptance | mean len | tok/s |
727
+ |---|---|---|---|
728
+ | Статика n7 | 0.288 | 3.02 | 15.27 |
729
+ | **Динамика p_min 0.35** | **0.769** | **4.07** | **24.61** |
730
+
731
+ acceptance x2.7, скорость +61%. Проза больше НЕ провал DFlash — динамика
732
+ решает "почему на прозе мало": цепочка рвётся ровно там, где селектор
733
+ колеблется, и не платит за мусорный хвост verify.
734
+
735
+ ### Режимы (итог)
736
+ - кодинг, без reasoning: DFlash-n7 + `--reasoning off` → 27.7 t/s
737
+ - **с мышлением (дефолт): DFlash-n7 + p_min 0.35 → 18.26 (код) / 24.61 (проза)**
738
+ - комбо не нужно: динамика сама адаптируется к тексту
739
+
740
+ Код: common/speculative.cpp, цикл селектора DFlash2 — обрыв по margin
741
+ (p_min*3.0), debug CHAIN печатает m= и p=. Сборка build-p40-ring.
742
+
743
+ ## Test 14 — DFlash на Qwen3.6-35B-A3B (MoE)
744
+
745
+ Пользовательская модель: Qwen_Qwen3.6-35B-A3B-Q4_K_M.gguf (qwen35moe, 3B активных
746
+ из 35B). DFlash-драфта v2 под неё нет — сконвертирован официальный
747
+ z-lab/Qwen3.6-35B-A3B-DFlash (DFlash v1, 6 dense-слоёв, hidden 2048,
748
+ block_size 16, target_layer_ids [1,6,11,16,22,27,32,37] = 8 слоёв таргета,
749
+ fc [2048,16384]).
750
+
751
+ Конвертация (Luce server/scripts):
752
+ ```
753
+ caмa: python3 convert_dflash_to_gguf.py draft-hf/model.safetensors draft-a3b-f16.gguf
754
+ python3 quantize_dflash_draft.py draft-a3b-f16.gguf draft-a3b-q8_0.gguf --scheme q8_0
755
+ ```
756
+ Результат: draft-a3b-q8_0.gguf (410MB, 43 tensor q8_0 + 26 as-is).
757
+ Прошла без правок кода форка — форк загрузил qwen35moe таргет + dflash v1 драфт.
758
+
759
+ Замеры (P40, thinking ON, dynamic 0.35):
760
+ | Тест | acceptance | mean len | tok/s |
761
+ |---|---|---|---|
762
+ | Код LRU 4096 | 0.584 | 3.60 | **55.79** |
763
+ | Проза эссе 3050 | 0.483 | 2.65 | **46.51** |
764
+
765
+ acc per pos (код): (0.813, 0.550, 0.402, 0.312, 0.228, 0.169, 0.128)
766
+ acc per pos (проза): (0.751, 0.417, 0.221, 0.118, 0.076, 0.045, 0.022)
767
+
768
+ Сравнение с 27B-UD:
769
+ - код: 27B 27.69 t/s vs A3B **55.79** (x2.0) — MoE таргет дешевле в verify
770
+ - проза: 27B динам. 24.61 vs A3B **46.51** (x1.9)
771
+
772
+ Вывод: спекуляция на MoE-модели (A3B) в 2 раза эффективнее, чем на dense-27B,
773
+ потому что verify-батч (8 токенов) считает только 3B активных параметров.
774
+ DFlash-v1 (без селектора v2) тоже работает с dynamic-обрезанием (margin из
775
+ top-k). Для полноты — был бы DFlash2-драфт под A3B, gains ещё выше.
776
+
777
+ Команда (динамика 0.35):
778
+ llama-server -m Qwen_Qwen3.6-35B-A3B-Q4_K_M.gguf -md draft-a3b-q8_0.gguf
779
+ --spec-type draft-dflash --spec-draft-n-max 7 --spec-draft-n-min 1 --spec-draft-p-min 0.35 ...
780
+
781
+ ## Test 15 — A3B: DFlash vs MTP vs AR (полная таблица)
782
+
783
+ P40, thinking ON, MoE-таргет Qwen3.6-35B-A3B-Q4_K_M (3B активных).
784
+ DFlash = наша конвертация z-lab v1 + dynamic 0.35; MTP = вшит в таргет-GGUF.
785
+
786
+ Код LRU 4096:
787
+ | Режим | acceptance | mean | tok/s | vs AR |
788
+ |---|---|---|---|---|
789
+ | AR baseline | - | - | 52.05 | 1.00x |
790
+ | DFlash n7 dyn 0.35 | 0.584 | 3.60 | 55.79 | 1.07x |
791
+ | DFlash n12 dyn 0.35 | 0.541 | 4.11 | 53.98 | 1.04x |
792
+ | MTP вшит n3 | 0.662 | 2.99 | 68.37 | 1.31x |
793
+
794
+ Проза эссе 3050:
795
+ | Режим | acceptance | mean | tok/s | vs AR |
796
+ |---|---|---|---|---|
797
+ | AR baseline | - | - | 52.72 | 1.00x |
798
+ | DFlash n7 dyn 0.35 | 0.483 | 2.65 | 46.51 | 0.88x |
799
+ | MTP вшит | 0.497 | 2.49 | 57.61 | 1.09x |
800
+
801
+ Выводы:
802
+ 1. На A3B **MTP (вшит) — лучший режим**: код 68.4 (+31% AR), проза 57.6 (+9%).
803
+ Дёшево: MTP-голова вшита в MoE-таргет, verify почти бесплатный.
804
+ 2. DFlash v1 на A3B слабый: на коде +7% (55.8), на прозе МЕДЛЕННЕЕ AR (46.5 vs 52.7).
805
+ v1 БЕЗ селектора (последоват. argmax) на свободном тексте не окупает даже
806
+ дешёвый verify. Для A3B нужен DFlash2-драфт (с селектором), его нет нигде.
807
+ 3. n_max=12 НЕ помогает: mean 4.11 выше, но tok/s 53.98 < n7 55.79 (блок 12 масок
808
+ дороже драфтить, хвост 8-12 не окупается). Оптимум n7.
809
+ 4. Пользователю: на 35B-A3B лучше всего собственный MTP (без -md, без драфта) —
810
+ `--spec-type draft-mtp --spec-draft-n-max 3`. DFlash-драфт v1 держать как
811
+ запасной для кода, где чуть быстрее AR (55.8 vs 52.0).
812
+
813
+ Вопрос "динамика для MTP": стандартный флаг УЖЕ есть — impl_draft_mtp в
814
+ speculative.cpp:1760 делает `if (cur_p->data[0].p < params.p_min) break;` —
815
+ обрыв по вероятности токена (не по margin как DFlash). Работает через
816
+ `--spec-draft-p-min`. Проверено по коду, не декоративно.
817
+
818
+ ## Test 16 — MTP динамика (n_max=7 + p_min 0.3) на A3B — ДЕКОРАТИВНА
819
+
820
+ Проверка: даёт ли MTP сам себе длину через --spec-draft-p-min (обрыв по
821
+ вероятности токена в impl_draft_mtp, speculative.cpp:1760). Код LRU 4096:
822
+
823
+ | Режим | acceptance | mean len | проверено/шаг | принято/шаг | tok/s |
824
+ |---|---|---|---|---|---|
825
+ | MTP n3 (base) | 0.662 | 2.99 | ~3.0 | 2.63 | 68.37 |
826
+ | MTP n7 p_min0.3 | 0.454 | 3.67 | ~5.8 | 2.63 | 46.97 |
827
+
828
+ Вывод: p_min 0.3 у MTP НЕ режет цепочку на коде — проверено 5.8/шаг вместо
829
+ ~3.0, принято столько же (2.63). Причина та же, что у softmax-динамики DFlash:
830
+ probability(top-1) у MTP почти всегда > 0.3 ("уверенно неправ" на позициях
831
+ 3-7, где acceptance 0.31/0.25/0.18/0.14). Порог по p не отделяет мусорный
832
+ хвост.
833
+
834
+ Итог: для MTP жёсткий --spec-draft-n-max 3 оптимален (68.4 на A3B, 23.6 на
835
+ 27B). Динамика по p бесполезна и для DFlash (margin - рабочая, p - нет),
836
+ и для MTP (p не режет). Единственная реально работающая авто-длина - наш
837
+ margin-обрыв в DFlash2 (Test 13).
838
+
839
+ ## Test 17 — margin-динамика для MTP [DFLASH-DYNAMIC MTP] + ��водная таблица режимов
840
+
841
+ После Test 16 добавлен margin-обрыв и для MTP (speculative.cpp:1759-1774):
842
+ вместо порога по p0 обрыв по **запасу p0-p1** (margin = p0, если один кандидат;
843
+ margin = p0-p1 иначе; обрыв при margin < p_min). Порог берётся из
844
+ --spec-draft-p-min, но применяется к ВЕРОЯТНОСТНОМУ запасу (0..1), а не к
845
+ логам-скорам. n_min=1 обязателен, иначе короткие цепочки стираются.
846
+
847
+ Замеры (A3B, код LRU 4096; сравнить: MTP-n3 base 68.37, n7 p_min0.3 46.97,
848
+ n7 margin0.3 63.60, n7 margin0.15 58.15):
849
+
850
+ | Режим | проверено/шаг | принято/шаг | acceptance | mean | tok/s |
851
+ |---|---|---|---|---|---|
852
+ | MTP n3 (жестко) | ~3.0 | 2.63 | 0.662 | 2.99 | 68.37 |
853
+ | MTP n7 + p_min0.3 (старый p) | ~5.8 | 2.63 | 0.454 | 3.67 | 46.97 |
854
+ | MTP n7 + margin0.15 | ~4.1 | 2.69 | 0.654 | 4.11 | 58.15 |
855
+ | MTP n7 + margin0.3 | ~3.3 | 2.62 | **0.793** | 4.09 | 63.60 |
856
+ | MTP n7 + margin0.3 (проза) | - | 1.24 | 0.669 | 2.79 | 52.94 |
857
+
858
+ Вывод по margin-MTP: в отличие от p-порога, margin РЕАЛЬНО режет цепочку
859
+ (проверено 5.8 -> 3.3-4.1). На коде margin0.3 даёт acceptance 0.793 (против
860
+ 0.662 у n3) при том же принятом — режет «монетки», тянет где уверен. НО на
861
+ A3B жёсткое n3 всё равно быстрее (68.4 vs 63.6): декод блока n7 дороже, а
862
+ дешёвый verify не окупает длинный хвост. Margin-MTP бессмысленен на A3B, где
863
+ verify почти бесплатен; он ценен только для 27B (дорогой verify), где стоит
864
+ проверить как 27B+MTP+margin0.3.
865
+
866
+ ### СВОДНАЯ ТАБЛИЦА эффективных режимов (все цифры tok/s, temp 0)
867
+
868
+ **Qwen3.8-27B-UD (dense, дорогой verify ~169ms/шаг).**
869
+ | Режим | код | проза | примечание |
870
+ |---|---|---|---|
871
+ | AR (без спека) | ~13.2 | ~13.2 | база |
872
+ | DFlash2 n7 статика | 13.90 | 15.27 | хвост 4-7 мусор на reasoning |
873
+ | DFlash2 n7 + margin0.35 | **18.26** | **24.61** | лучшая авто-длина (+31%/+61%) |
874
+ | DFlash2 + --reasoning off | 27.69 | - | код без мышления, жёсткий n7 |
875
+ | MTP n3 (off/on) | 23.61 / 16.31 | - | вшит, дёшево; on меньше теряет |
876
+ | DFlash2 Count-500 | 40.41 | - | структурный текст (числа) |
877
+
878
+ **Qwen3.6-35B-A3B (MoE, 3B активных, дешёвый verify).**
879
+ | Режим | код | проза | vs AR код |
880
+ |---|---|---|---|
881
+ | AR (без спека) | 52.05 | 52.72 | 1.00x |
882
+ | DFlash v1 n7 + dyn0.35 | 55.79 | 46.51 | 1.07x |
883
+ | DFlash v1 n12 + dyn0.35 | 53.98 | - | 1.04x (блок 12 не окупается) |
884
+ | MTP вшит n3 | **68.37** | **57.61** | 1.31x — ЛУЧШИЙ на A3B |
885
+ | MTP n7 + margin0.3 | 63.60 | 52.94 | acceptance 0.793, но медленнее n3 |
886
+ | MTP n7 + p0.3 (декор) | 46.97 | - | p не режет |
887
+
888
+ ### Итоговые рекомендации
889
+ - **A3B**: `--spec-type draft-mtp --spec-draft-n-max 3` (без p_min, без margin) =
890
+ код 68.4 / проза 57.6. verify дёшев, жёсткое короткое n3 — оптимум.
891
+ - **27B dense**: `--spec-type draft-dflash --spec-draft-n-max 7 --spec-draft-n-min 1
892
+ --spec-draft-p-min 0.35` = код 18.26 / проза 24.61 (margin-динамика, Test 13).
893
+ Без мышления: `--reasoning off` -> 27.7 на коде.
894
+ - **Куда копать дальше**: 27B + MTP + margin (тест не гонялся; у 27B дорогой
895
+ verify, длинная точная MTP-цепочка могла бы дать больше, чем жёсткое n3).
896
+
897
+ ## Test 18 — 27B + MTP + margin 0.3 (reasoning ON) — margin-MTP впервые РАБОТАЕТ
898
+
899
+ Проверка единственного неиспытанного режима: 27B (дорогой verify) + MTP-n7 +
900
+ margin (вероятностный запас p0-p1, --spec-draft-p-min 0.3). Код LRU, temp 0,
901
+ reasoning ON (дефолт). Сравнение с Test 12/13 (тот же промт):
902
+
903
+ | Режим (27B, reasoning ON, код LRU) | acceptance | tok/s |
904
+ |---|---|---|
905
+ | MTP-n3 жёстко (Test 12) | 0.485 | 16.31 |
906
+ | **MTP-n7 + margin0.3 (Test 18)** | **0.592** | **17.81** |
907
+ | DFlash2-n7 статика (Test 13A) | 0.253 | 13.90 |
908
+ | DFlash2-n7 + margin0.35 (Test 13B) | 0.638 | **18.26** |
909
+
910
+ Замер: микробенч 512ток = 16.71 t/s (draft 464, accept 274/0.591); полный
911
+ 4096ток = 17.81 t/s (draft 4259, accept 2522/0.592).
912
+
913
+ Вывод:
914
+ 1. **margin-MTP впервые даёт реальный прирост на 27B**: +9.2% к жёсткому
915
+ MTP-n3 (17.81 vs 16.31), acceptance вырос 0.485 -> 0.592. Пр��чина: на A3B
916
+ MTP-голова не тянет дальше 2-3 и verify дёшев (нечего экономить), а на 27B
917
+ дорогой verify окупает длинную уверенную цепочку — margin режет «монетки»
918
+ и тянет где уверен. margin-MTP бессмыслен на A3B, но полезен на 27B.
919
+ 2. НО DFlash2+margin0.35 (18.26) всё ещё впереди MTP+margin (17.81) на 2.5%:
920
+ DFlash2-селектор даёт более точную длинную цепочку, чем MTP-голова на коде.
921
+ 3. Итог для 27B код (reasoning ON): best = DFlash2+margin0.35 (18.26),
922
+ второй = MTP+n7+margin0.3 (17.81), оба бьют жёсткое n3 (16.31).
923
+
924
+ Команда (27B + MTP + margin):
925
+ llama-server -m Qwen3.8-27B-UD-Q4_K_XL.gguf
926
+ --spec-type draft-mtp --spec-draft-n-max 7 --spec-draft-n-min 1 --spec-draft-p-min 0.3
927
+ (reasoning ON, без -md; margin из --spec-draft-p-min применяется к p0-p1
928
+ по коду speculative.cpp:1759-1774)
929
+
930
+ ## Test 19 — Luce dflash_server + DDTree (прямой тест движка на P40)
931
+
932
+ Проверка самого Luce-движка (./server/build/dflash_server с --ddtree) на
933
+ твоих моделях, вместо разговоров о переносе. Обе модели = arch qwen35/qwen35moe
934
+ (принимаются gguf_target_loader). Обе на 40 P40. LRU-код, temp 0, thinking off.
935
+
936
+ ### 19A. Dense 27B (Qwen3.8-27B-UD + DFlash2-lucebox) — РАБОТАЕТ, рекорд
937
+ Команда: dflash_server Qwen3.8-27B-UD-Q4_K_XL.gguf --draft Qwen3.8-27B-DFlash2-q8_0.gguf
938
+ --ddtree --ddtree-budget 22 --fa-window 2048 -ctk q8_0 -ctv q8_0 --max-ctx 8192
939
+ Драфт DFlash2-селектор сработал: [draft GGUF] DFlash 2 selector enabled rank=256 top_k=16.
940
+ LRU 512 ток: 19.48 t/s, avg_commit 5.33, acceptance 54.2%.
941
+ LRU 4096 ток: 21.46 t/s, avg_commit 5.97, acceptance 62.1%, 686 verify-steps.
942
+
943
+ | Режим (27B dense, код LRU 4096, thinking off) | tok/s | avg_commit/AL | acceptance |
944
+ |---|---|---|---|
945
+ | llama.cpp AR | ~13.2 | - | - |
946
+ | llama.cpp DFlash2 статика n7 | 13.90 | 2.77 | 0.253 |
947
+ | llama.cpp DFlash2 + margin0.35 | 18.26 | ~3.1 | 0.638 |
948
+ | **Luce DFlash2 + DDTree budget22** | **21.46** | **5.97** | **0.621** |
949
+
950
+ Вывод: Lucе DDTree на P40 даёт +17.5% к лучшему llama.cpp маржину (21.46 vs 18.26),
951
+ +63% к AR. Ключ — ДЕРЕВО: avg_commit 5.97 (почти 6 токенов/шаг) против ~3 у цепочки.
952
+ Подтверждает info.md: DDTree поднимает AL за счёт спасения ветвей (древовидного verify).
953
+ Prefill small: 43 ток за 1.6с (26 tok/s — kernel-launch dominated, не показательно).
954
+
955
+ ### 19B. A3B MoE (Qwen3.6-35B-A3B + draft-a3b-q8_0) — НЕСОВМЕСТИМ, 0.7% acceptance
956
+ Команда: dflash_server Qwen_Qwen3.6-35B-A3B-Q4_K_M.gguf --draft draft-a3b-q8_0.gguf
957
+ --ddtree --ddtree-budget 22 ...
958
+ Лог: [draft] drafter target_layer_ids invalid (n=8, slots=5); keeping derived capture layers
959
+ LRU 512 ток: 11.05 t/s, avg_commit 1.12, acceptance 0.7% (54/7328) — спек сломан.
960
+
961
+ Вывод: наш конвертированный draft-a3b (DFlash v1, 8 target_layers, 6 dense-слоёв)
962
+ НЕ совместим с Lucе DDTree, который жёстко ждёт 5-слойный z-lab драфт (target_layer_ids=5).
963
+ Поэтому Lucе A3B даёт 11 t/s (почти AR без выигрыша). Для A3B лучший режим остаётся
964
+ llama.cpp MTP-n3 (68.37 код). НЕ переносить DDTree-сервер на A3B с этим драфтом.
965
+
966
+ ### Итог Test 19
967
+ - DDTree-дерево реально выигрывает на dense 27B (+17.5% к маржину, AL ~6 vs ~3) —
968
+ это главный незадействованный потенциал. Стоит перенести ДЕРЕВО (ddtree.{h,cpp} +
969
+ tree-verify) в llama-dflash3-pr порт, а не standalone-сервер.
970
+ - На A3B Lucе бессмысленен (драфт несовместим); A3B = MTP-n3 на llama.cpp.
971
+
972
+ ## Test 20 — Lucе DFlash2 CHAIN (без DDTree) — цепочка быстрее дерева на коде!
973
+
974
+ КОНТРОЛЬНЫЙ эксперимент: тот же Lucе 27B + DFlash2, но БЕЗ --ddtree (чистая
975
+ цепочка), чтобы отделить вклад дерева от вклада самого Lucе-движка. Код LRU,
976
+ temp 0, thinking off, 4096 (модель сама остановилась на 3219, finish=stop).
977
+
978
+ | Режим (Luce 27B, код LRU) | tok/s | avg_commit | acceptance | steps |
979
+ |---|---|---|---|---|
980
+ | Lucе DFlash2 CHAIN (no ddtree) | **32.02** | **6.09** | **0.761** | 529 |
981
+ | Lucе DFlash2 + DDTree budget22 | 21.46 | 5.97 | 0.621 | 686 |
982
+ | llama.cpp DFlash2 + margin0.35 (thinking ON) | 18.26 | ~3.1 | 0.638 | - |
983
+
984
+ ШОКИРУЮЩИЙ вывод: на КОДЕ (высоко-предсказуемом) Lucе CHAIN (32 t/s) в 1.5x
985
+ БЫСТРЕЕ дерева (21.46)! Причина: на структурном тексте цепочка длиной ~6 уже
986
+ почти вся принимается (acc 76%, avg_commit 6.09), а дерево тратит budget на
987
+ ветвления, которые не окупаются. Дерево выигрывает на НЕпредсказуемом тексте,
988
+ где ветвления спасают принятие; на коде это лишние вызовы.
989
+
990
+ Также Lucе chain (32) >> наш llama.cpp маржин (18.26): разрыв 1.7x. НО это
991
+ МИШАС сравнения: наш маржин был с thinking ON, Lucе — thinking off
992
+ (наш llama.cpp DFlash2 n7 + --reasoning off давал 27.69, Test 12). Нужен
993
+ apples-to-apples: наш DFlash2 chain с thinking OFF vs Lucе chain 32.
994
+
995
+ Lucе цепь уже умеет адаптивность через --verify-width (trimmed per step by
996
+ drafter confidence) - аналог нашего margin. --ddtree-tau - для дерева.
997
+
998
+ Итог: для кода лучший режим у Lucе - CHAIN (32 t/s), НЕ дерево. Дерево стоит
999
+ тестить на прозе/непредсказуемом, где acceptance ниже и ветвления окупаются.
1000
+
1001
+ ## Test 21 — ЧЕСТНОЕ сравнение: наш llama.cpp DFlash2 margin vs Lucе (thinking OFF)
1002
+
1003
+ Исправление мишаса Test 20: наш маржин был с thinking ON (18.26), Lucе - off.
1004
+ Прогон нашего llama.cpp DFlash2 + margin 0.35 + --reasoning off на том же
1005
+ коде-промпте (модель сама останавливается: у нас 2405 ток., у Lucе 3219; тот
1006
+ же промпт, скорость валидна на 2-3K ток.).
1007
+
1008
+ | Режим (27B, код LRU, thinking OFF) | tok/s | acceptance | steps |
1009
+ |---|---|---|---|
1010
+ | **наш llama.cpp DFlash2 + margin0.35** | **29.86** | **0.850** | - |
1011
+ | Lucе DFlash2 chain | 32.02 | 0.761 | 529 |
1012
+ | Lucе DFlash2 + DDTree budget22 | 21.46 | 0.621 | 686 |
1013
+
1014
+ ГЛАВНЫЙ ВЫВОД: разрыв на самом деле НЕ 1.7x, а всего ~7%! Наш llama.cpp маржин
1015
+ (29.86) почти догнал Lucе chain (32.02), и наш acceptance даже выше (0.85 vs
1016
+ 0.76). Прежние 18.26 были с thinking ON - нечестная база. При thinking OFF
1017
+ наш маржин - 29.86, на уровне Lucе.
1018
+
1019
+ Разрыв 7% объясняется НЕ деревом, а имплементацией Lucе (fast-rollback +
1020
+ fused-граф verify + --verify-width confidence-trim), а не качеством драфта.
1021
+ DTree же на коде не помогает (не окупает ветвления), и даже медленнее.
1022
+
1023
+ Дерево стоит тестить на прозе/непредсказуемом тексте, где acceptance низкий и
1024
+ ветвления реально спасают. На коде (структура) цепочка эффективнее.
1025
+
1026
+ Далее: проверить DTree на прозе (где он теоретически выигрывает), и/или выжать
1027
+ оставшиеся 7% до Lucе (verify-width аналог, fast-rollback в наш порт).
1028
+
1029
+ ## Test 22 — DTree на прозе НЕ выигрывает (и Lucе в целом проигрывает нашему маржину)
1030
+
1031
+ Проверка гипотезы из Test 20/21: дерево должно спасать на непредсказуемом
1032
+ тексте (проза), где acceptance низкий. Lucе 27B + DDTree budget22, проза 1024,
1033
+ thinking off:
1034
+
1035
+ | Режим (27B, проза 1024, thinking OFF) | tok/s | avg_commit | acceptance |
1036
+ |---|---|---|---|
1037
+ | Lucе DFlash2 chain | 11.97 | 3.29 | 0.287 |
1038
+ | Lucе DFlash2 + DDTree budget22 | 11.98 | 3.29 | 0.287 |
1039
+ | (сравнение) наш llama.cpp DFlash2 mar gin0.35, thinking ON (Test 13C) | 24.61 | 4.07 | 0.769 |
1040
+
1041
+ ВЫВОД: на прозе Lucе DDTree и chain ИДЕНТИЧНЫ (~12 t/s, avg_commit 3.29,
1042
+ acceptance 28.7%) - дерево НЕ спасает. Причина: на свободном тексте acceptance
1043
+ этого DFlash2-драфта низкий на всех ветвях (28%), ветвления не окупают
1044
+ доп. декод. Lucе в целом на прозе (12 t/s) сильно проигрывает нашему llama.cpp
1045
+ маржину (24.61) - 2x.
1046
+
1047
+ Итоговый вывод по всей линейке (Test 19-22):
1048
+ - НАШ llama.cpp DFlash2 + margin 0.35 - самый универсальный и сильный:
1049
+ код 29.86 (off) / 18.26 (on), проза 24.61 (on). Лучшая авто-длина.
1050
+ - Lucе chain: код 32.02 (off) - на 7% быстрее нашего, но проза 11.97 (off) -
1051
+ в 2x хуже нашего маржина.
1052
+ - Lucе DDTree: НЕ даёт выигрыша ни на коде (21.46 < chain 32), ни на прозе
1053
+ (11.98 = chain). Дерево для этого драфта/текстов неэффективно.
1054
+ - margin+дtree: бессмысленно - дерево само по себе не помогает нигде;
1055
+ наш margin (тop1-top2 в логах селектора) сильнее, чем ddtree-tau Lucе
1056
+ (кумулятивный log-prob) для этого DFlash2.
1057
+
1058
+ Финал: остаётся выжать ~7% до Lucе chain на коде (fast-rollback / fused-verify)
1059
+ ИЛИ распространить наш маржин-подход на более длинные цепочки. Дерево не нужно.
1060
+
1061
+ ## Test 23 — ЧЕСТНАЯ проза (thinking OFF): наш маржин обгоняет Lucе
1062
+
1063
+ Устранение последнего мишаса: наш 24.61 на прозе был с thinking ON, а Lucе -
1064
+ с off (несопоставимо). Прогон нашего llama.cpp DFlash2 + margin 0.35, --reasoning
1065
+ off, тот же эссе-промпт, 1024 ток.
1066
+
1067
+ | Режим (27B, проза 1024, thinking OFF) | tok/s | acceptance |
1068
+ |---|---|---|
1069
+ | **наш llama.cpp DFlash2 + margin0.35** | **16.38** | **0.505** |
1070
+ | Lucе DFlash2 chain | 11.97 | 0.287 |
1071
+ | Lucе DFlash2 + DDTree | 11.98 | 0.287 |
1072
+
1073
+ Вывод: наш llama.cpp маржин на прозе (16.38) в 1.37x быстрее Lucе (11.97)!
1074
+ Наш acceptance тоже выше (0.505 vs 0.287). margin работает на прозе: режет
1075
+ мусорный хвост длинной цепочки, чего у Lucе нет (ucе уперся в 12 t/s).
1076
+
1077
+ ОТВЕТ НА "ПОЧЕМУ МЫ НЕ ДОХОДИЛИ ДО 30":
1078
+ Мы ДОХОДИЛИ на коде! Наш DFlash2 margin = 29.86 (thinking OFF), просто раньше
1079
+ мерили с thinking ON (18.26) - дефолт. С --reasoning off наш порт даёт 29.86,
1080
+ почти как Lucе chain 32. Так что 30+ НЕ из-за Lucе-движка, а из-за отсутствия
1081
+ reasoning текста в промпте.
1082
+
1083
+ ГДЕ Lucе-движок реально быстрее: только на коде +7% (32 vs 29.86) - цена
1084
+ fast-rollback + fused-verify + verify-width. На прозе наш маржин сильнее
1085
+ (16.38 vs 11.97) - Lucе-цепочка не умеет резать хвост как наш margin.
1086
+
1087
+ Полная честная картина (thinking OFF):
1088
+ | | код | проза |
1089
+ |---|---|---|
1090
+ | наш llama.cpp DFlash2 + margin0.35 | **29.86** | **16.38** |
1091
+ | Lucе chain | 32.02 | 11.97 |
1092
+ | Lucе DDTree | 21.46 | 11.98 |
1093
+
1094
+ Наш маржин универсальнее: код почти паритет (7% слабее), проза на 37% сильнее.
1095
+
1096
+ ## Test 24 — ⚠️ ОШИБКА ИСПРАВЛЕНА: Lucе "thinking ON" был БЕЗ мышления
1097
+
1098
+ Ранняя версия этого теста утверждала, что Lucе на 69% быстрее на reasoning.
1099
+ Это было НЕВЕРНО: мой запрос к Lucе с "thinking ON" на самом деле дал
1100
+ reasoning_tokens=0 (Lucе не включил мышление по умолчанию), т.е. это был тот
1101
+ же Lucе-БЕЗ-мышления, что и 32 t/s — просто короткая порция (1024 ток).
1102
+
1103
+ Честный тест: Lucе с явным "thinking":{"type":"enabled"}:
1104
+ [budget-hook] spec-decode close at committed=41/1024 (remaining=1024 <= hard_limit=4096)
1105
+ reasoning_tokens=1, spec_decode_ran=True, decode 13.6 t/s (~AR)
1106
+ -> Lucе при реальном мышлении СПЕК УБИВАЕТСЯ budget-hook'ом (как в Test 4):
1107
+ force-close на раннем шаге, падает в AR (13.3 t/s).
1108
+
1109
+ ЧЕСТНАЯ КАРТИНА (27B код, реальное мышление):
1110
+ | Режим | реально думал? | tok/s |
1111
+ |---|---|---|
1112
+ | наш llama.cpp DFlash2 + margin0.35 | ДА (reasoning_tokens>0) | 18.26 (4096) / 17.28 (1024) |
1113
+ | Lucе chain "thinking ON" (мой ошибочный) | НЕТ (reasoning_tokens=0) | 29.14 (это БЕЗ мышления) |
1114
+ | Lucе chain реально thinking enabled | бюджет-хук убил спек | 13.3 (~AR) |
1115
+
1116
+ ГЛАВНЫЙ ВЫВОД: наш llama.cpp с реальным мышлением (18.26) БЫСТРЕЕ Lucе с
1117
+ реальным мышлением (13.3)! Прежний "разрыв 1.7x" в пользу Lucе был артефактом
1118
+ моей ошибки (сравнил Lucе-без-мышления с нашим-с-мышлением).
1119
+
1120
+ resoning-текст ≈ проза по характеру (непредсказуем): наш маржин держит на нём
1121
+ нормально, а Lucе там вообще проигрывает (спек убит бюджетом). Переносить
1122
+ verify-width на основе этого теста НЕ нужно — мотивации больше нет.
1123
+
1124
+ (Сохраняю модель arch: Qwen3.8-27B-UD = qwen35 ГИБРИД с Gated DeltaNet
1125
+ (qwen35.ssm.* веса, build_inp_mem_hybrid) - как Qwen3.5-27B из info.md.
1126
+ Значит persist-ядро DeltaNet и fast-rollback ПРИМЕНИМЫ к ней.)
1127
+
1128
+ ## Test 25 — КВАНТОВАНИЕ ДРАФТА: Q4-mix ЛУЧШЕ Q8, Q2 чуть медленнее
1129
+ (код 1024, reasoning OFF, DFlash2 margin 0.35, наш порт llama.cpp, P40)
1130
+
1131
+ Проблема: z-lab Q2_K GGUF (arch=dflash, маппинг mask_token через канон)
1132
+ не инициализируется у нас -> "invalid or missing mask_token_id", спек НЕ
1133
+ работает, чистый AR 13.13 t/s. Это ошибка ФОРМАТА (z-lab отличается от
1134
+ нашего qwen35-dflash-draft), не квантования.
1135
+
1136
+ Решение: сквантовали ИЗ НАШЕГО f16 (convert_dflash_to_gguf.py из
1137
+ Qwen3.8-27B-DFlash2-src/model.safetensors) -> llama-quantize Q2_K (наш
1138
+ формат, mask корректен). И quantize_dflash_draft.py --scheme q4-mix.
1139
+
1140
+ ПОЧЕМУ не через quantize_dflash_draft.py --scheme q2-k: в python-gguf
1141
+ (gguf.quants) реализовано КВАНТОВАНИЕ только для Q4_0/Q4_1/Q5_0/Q5_1/Q8_0/
1142
+ BF16; для K-quants (Q2_K/Q3_K/Q4_K) есть ТОЛЬКО dequantize_blocks (чтение),
1143
+ quantize_blocks бросит NotImplementedError. Q2 доступен лишь через
1144
+ llama-quantize (C++). q4-mix работает потому что Q4_0 реализован в python.
1145
+
1146
+ Результаты (код 1024, our DFlash2 margin 0.35, reasoning off):
1147
+ | Драфт | Размер | tok/s | acceptance | mean len |
1148
+ |-----------------|--------|-------|------------|----------|
1149
+ | Q8 (lucebox) | 2.0 GB | ~29-30| 0.85 | 5.27 |
1150
+ | Q4-mix (self) | 1.2 GB | 30.26 | 0.827 | 5.25 |
1151
+ | Q2_K (self) | 0.87GB | 27.08 | 0.763 | 4.69 |
1152
+ | Q2 z-lab (broken)| 0.7 GB| 13.13 | (спек off) | - |
1153
+
1154
+ ВЫВОД: q4-mix = ЛУЧШИЙ (30.26 t/s > Q8, acceptance 0.827, 1.2GB). Q2_K
1155
+ приемлем (27 t/s, 865MB) только если критична VRAM. Q8 и Q4-mix почти не
1156
+ различаются по acceptance; Q2 теряет ~10% скорости из-за Q2 на head
1157
+ селектора. Для P40-скорости юзаем q4-mix.
1158
+
1159
+ ## Test 26 — n_max SWEEP + DFlash2 блочная генерация (ВЫВОДЫ)
1160
+ q4mix-драфт, код 1024, reasoning OFF, DFlash2 margin p-min 0.35.
1161
+
1162
+ n_max свип (tok/s):
1163
+ | n_max | tok/s | acceptance | mean |
1164
+ |-------|-------|------------|------|
1165
+ | 4 | 27.77 | 0.861 | 4.04 |
1166
+ | 5 | 27.41 | - | - |
1167
+ | 7 | 30.26 | 0.827 | 5.25 |
1168
+ | 8 | 30.18 | 0.827 | 5.25 |
1169
+ -> ОПТИМУМ n_max=7 (30.26). Уменьшение НЕ помогает (теряем длинные
1170
+ цепочки), 8 не лучше (маржин и так режет на ~5.25, блок стоит дорого).
1171
+
1172
+ ВАЖНАЯ ПРАВКА (мой прошлый ошибка): наш DFlash2 НЕ последовательный.
1173
+ draft_dflash::draft() (speculative.cpp 1198+) строит ОДИН noise-блок
1174
+ (id_last + n_max масок) и ДЕКОДИТ ЕГО ОДНИМ llama_decode (1233) - это
1175
+ блочная диффузия, как PR 27342 "decode every sequence's full noise block
1176
+ in one pass". Квадратичный chain_heads-цикл (1710) принадлежит MTP, НЕ
1177
+ DFlash. Маржин обрезает ПРИНЯТУЮ часть ПОСЛЕ декода блока (по lattice,
1178
+ 1311-1316) - нельзя "не генерить лишние": блочная диффузия даёт уверены
1179
+ fnость позиции i только после декода всего блока (state i зависит от
1180
+ масок 0..i-1). Стоимость draft = стоимость декода n_max+1-блока, не
1181
+ зависит от маржина.
1182
+
1183
+ bottleneck: draft-decode 86% (4199ms) vs verify 14% (657ms) на коде 1024.
Qwen3.8-27B-DFlash2-q4mix-self.gguf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:39eb9823453c9f14816e4c14fe60eb18e6cbcdf5f5205affbb53500d1049ccc2
3
+ size 1213164576
README.md ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # llama.cpp DFlash2 P40 (Pascal) optimization
2
+
3
+ Speculative decoding (DFlash2 block-diffusion drafter) tuned specifically for
4
+ **NVIDIA Tesla P40** (Pascal, compute capability 6.1, no FP16 tensor cores).
5
+
6
+ Fork of `ggml-org/llama.cpp` (PR #27342 lineage) with DFlash2 support, optimized
7
+ for the Pascal backend: int8 DP4A (not FP16), multi-stage quantization of the
8
+ draft, and an adaptive margin that shortens the verify batch where the
9
+ draft's selector is uncertain.
10
+
11
+ ```
12
+ Mini-article: Title placeholder -> 30.26 tok/s on Pascal P40 with 27B dense
13
+ ```
14
+
15
+ ---
16
+
17
+ ## What this repo is
18
+
19
+ A runnable, measured build of llama.cpp + DFlash2 speculative decoding on a
20
+ single Tesla P40 (24 GB, sm_61). Everything in this repo's `DFLASH2_RING_PORT.md`
21
+ is a real measurement on the P40; no other GPUs / cloud numbers are claimed.
22
+
23
+ Primary pairing tested:
24
+ - **Target:** Qwen3.8-27B-UD `Qwen3.8-27B-UD-Q4_K_XL.gguf` (qwen35 hybrid, Gated DeltaNet)
25
+ - **Draft:** DFlash2 `Qwen3.8-27B-DFlash2-q4mix-self.gguf` (our q4-mix quant)
26
+
27
+ ---
28
+
29
+ ## Best command (hit it)
30
+
31
+ See `run_best.sh`. Summary:
32
+
33
+ ```bash
34
+ ./build-p40-ring/bin/llama-server \
35
+ -m Qwen3.8-27B-UD-Q4_K_XL.gguf \
36
+ -md Qwen3.8-27B-DFlash2-q4mix-self.gguf \
37
+ -ngl 999 -ngld 999 -c 8192 -b 512 -ub 512 -np 1 \
38
+ --load-mode mlock --cache-ram 32768 --checkpoint-min-step 512 \
39
+ -fa 1 -ctk q8_0 -ctv q8_0 -ctkd q8_0 -ctvd q8_0 --kv-unified \
40
+ --spec-type draft-dflash \
41
+ --spec-draft-n-max 7 --spec-draft-n-min 1 --spec-draft-p-min 0.35 \
42
+ --spec-draft-ctx 0 --temp 0 --jinja --reasoning off -bs \
43
+ -lv 4 --host 0.0.0.0 --port 8080
44
+ ```
45
+
46
+ Key flags:
47
+ - `--spec-type draft-dflash` - DFlash2 block-diffusion drafter
48
+ - `--spec-draft-n-max 7` - sweep optimum (Test 26: 7=30.26 > 8=30.18 > 5=27.4 > 4=27.8)
49
+ - `--spec-draft-p-min 0.35` - adaptive margin (top1-top2 on raw selector logits),
50
+ cuts the chain where the selector is a coin flip. Shortens the verify batch.
51
+ - `--spec-draft-n-min 1` - keep short chains alive
52
+ - `-bs` - backend (GPU) argmax sampling, no CPU logit readback
53
+ - `-fa 1`, `-ctk/ctv q8_0`, `--kv-unified` - flash attention + q8 KV
54
+ - `--reasoning off` - fastest mode; use `on` for real reasoning (see below)
55
+
56
+ To switch to the reasoning scenario replace `--reasoning off` with `--reasoning on`.
57
+
58
+ ---
59
+
60
+ ## Measured results (Tesla P40, single GPU)
61
+
62
+ All numbers are our own runs, logged in `DFLASH2_RING_PORT.md`.
63
+
64
+ ### Code prompt, reasoning OFF, greedy
65
+
66
+ | Draft quant | size | tok/s | acceptance | mean len |
67
+ |---|---|---|---|---|
68
+ | **q4-mix (recommended)** | 1.2 GB | **30.26** | 0.83 | 5.25 |
69
+ | Q8 (lucebox) | 2.0 GB | ~30 | 0.85 | 5.27 |
70
+ | Q2-hybrid (fc Q2, head) | 0.96GB | 29.0 | 0.80 | 5.17 |
71
+ | Q2-all | 0.87GB | 27.1 | 0.76 | 4.69 |
72
+
73
+ Draft quant is q4-mix = backbone q4_0 + dflash.* heads q8_0 (protects the
74
+ selector). q4-mix wins: same acceptance as Q8 with a smaller, faster draft;
75
+ Q2 is slower because the 2-bit selector heads lose near-tie accuracy.
76
+
77
+ ### n_max sweep (q4-mix, code, reasoning OFF)
78
+
79
+ | n_max | tok/s |
80
+ |---|---|
81
+ | 4 | 27.8 |
82
+ | 5 | 27.4 |
83
+ | **7** | **30.26** |
84
+ | 8 | 30.18 |
85
+
86
+ n_max=7 is the optimum; adaptive margin already caps the useful chain at ~5.25.
87
+
88
+ ### Thinking scenario (code, reasoning ON, greedy)
89
+
90
+ - ours (DFlash2 q4-mix + margin): **~18 t/s** with real reasoning tokens.
91
+ - Luce chain with real thinking: 13.3 t/s (their spec is killed by a budget hook),
92
+ i.e. our port is **faster than Luce on the reasoning scenario**.
93
+
94
+ ### Reference (same 27B, code, reasoning OFF)
95
+
96
+ - our DFlash2 q4-mix + margin: **30.26**; plain Luce chain: ~32; ngram-mod /
97
+ ngram-map-k4v on top of DFlash2: **28.2** (drags us down, not used).
98
+
99
+ ---
100
+
101
+ ## What was done / optimizations (P40 Pascal)
102
+
103
+ 1. **MMQ DP4A (int8), no FP16.**
104
+ Pascal has no FP16 tensor cores. `ggml-cuda` selects the MMQ DP4A kernel
105
+ (`__dp4a`, cc >= 610) for quantized weights via
106
+ `(!fp16_mma_hardware_available(cc) || ...)`, compile arch `61`. Draft and
107
+ target both run the int8 matmul path. No FP16 path is used for quant weights.
108
+ 2. **Draft quantization pipeline** (`server/scripts/quantize_dflash_draft.py`
109
+ + `llama-quantize`):
110
+ - `q4-mix`: backbone q4_0, `dflash.*` heads + conv q8_0 -> 1.2 GB, acceptance-neutral.
111
+ - Q2 available only via `llama-quantize` (python-gguf has no K-quant *write*).
112
+ 3. **DFlash2 adaptive chain (margin)** (`common/speculative.cpp`):
113
+ raw selector logits top1-top2 margin vs a threshold from `p_min`; stops the
114
+ chain when the pick is a coin flip. This is what shortens the verify batch
115
+ and makes n_max=7 optimal.
116
+ 4. **DFlash2 is block-diffusion**: builds one noise block (id_last + n_max masks)
117
+ and decodes it in a **single pass** (`draft_dflash::draft`, one
118
+ `llama_decode`) - same approach as upstream PR #27342.
119
+ 5. **Backend (GPU) argmax** (`-bs`): no CPU-side logit readback during draft/verify.
120
+ 6. **Flash attention** (`-fa`) + `q8_0` KV for both models, unified KV.
121
+ 7. **Duplicate draft quant tooling** and honest per-scenario comparison vs
122
+ Luce (see DFLASH2_RING_PORT.md).
123
+
124
+ ---
125
+
126
+ ## HF model
127
+
128
+ Our recommended draft (q4-mix, 1.2 GB):
129
+
130
+ - Filename: `Qwen3.8-27B-DFlash2-q4mix-self.gguf`
131
+ - HF repo: **https://huggingface.co/maxwelhelp/llama.cpp-DFlash2-pascal6-optimized**
132
+
133
+ Target: `Qwen3.8-27B-UD-Q4_K_XL.gguf` (Qwen3.8-27B-UD, qwen35 hybrid).
134
+
135
+ ---
136
+
137
+ ## Building for P40
138
+
139
+ ```bash
140
+ cmake -B build-p40-ring -DCMAKE_CUDA_ARCHITECTURES=61 -DLLAMA_CUDA=ON -DGGML_CUDA=ON -DLLAMA_CURL=ON
141
+ cmake --build build-p40-ring --config Release -j
142
+ ```
143
+
144
+ ---
145
+
146
+ ## Notes / honesty
147
+
148
+ - No cloud numbers; all measurements are local on the P40 (see the log).
149
+ - ngram-mod / ngram-map-k4v did **not** help on top of DFlash2 and are not used.
150
+ - On the reasoning scenario our port already beats Luce; q4-mix + margin is the
151
+ recommended release config.
152
+
153
+ ---
154
+ License note: this is a private fork for experimental Pascal tuning; it wraps
155
+ upstream llama.cpp (MIT) and DFlash2 draft weights (Apache-2.0 / z-lab).
run_best.sh ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # =============================================================================
3
+ # run_best.sh - DFlash2 + llama.cpp speculative decoding on Tesla P40 (Pascal)
4
+ #
5
+ # BEST configuration measured across the whole test series (DFLASH2_RING_PORT.md).
6
+ # Target: Qwen3.8-27B-UD (Q4_K_XL), Draft: DFlash2 q4-mix self-quant GGUF.
7
+ #
8
+ # Results (code 1024, reasoning OFF, greedy):
9
+ # q4mix draft + DFlash2 + n_max=7 + adaptive margin p_min=0.35 -> 30.26 tok/s
10
+ # (acceptance 0.83, mean len 5.25)
11
+ #
12
+ # Why this is the optimum (see DFLASH2_RING_PORT.md Tests 13-26):
13
+ # - q4-mix draft quant: 30.26 > Q8 (~30) > q2h8-hybrid (29) > Q2-all (27).
14
+ # - n_max=7 is the sweep optimum: 7=30.26 > 8=30.18 > 5=27.4 > 4=27.8.
15
+ # - adaptive margin (p_min=0.35) cuts the chain where the selector is
16
+ # uncertain (top1-top2 log-margin), giving short verify batches.
17
+ # - P40 hits are: MMQ DP4A (int8, no FP16 tensor cores), -bs backend argmax,
18
+ # FA flash-attn, q8_0 KV.
19
+ # =============================================================================
20
+ set -euo pipefail
21
+
22
+ REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
23
+ MODELS=/home/maxwelhelp/models
24
+ TARGET="$MODELS/Qwen3.8-27B-UD-Q4_K_XL.gguf"
25
+ DRAFT="$MODELS/Qwen3.8-27B-DFlash2-q4mix-self.gguf"
26
+ PORT="${PORT:-8080}"
27
+
28
+ exec "$REPO/build-p40-ring/bin/llama-server" \
29
+ -m "$TARGET" \
30
+ -md "$DRAFT" \
31
+ -ngl 999 -ngld 999 -c 8192 -b 512 -ub 512 -np 1 \
32
+ --load-mode mlock --cache-ram 32768 --checkpoint-min-step 512 \
33
+ -fa 1 -ctk q8_0 -ctv q8_0 -ctkd q8_0 -ctvd q8_0 --kv-unified \
34
+ --spec-type draft-dflash \
35
+ --spec-draft-n-max 7 --spec-draft-n-min 1 --spec-draft-p-min 0.35 \
36
+ --spec-draft-ctx 0 --temp 0 --jinja --reasoning off -bs \
37
+ -lv 4 --host 0.0.0.0 --port "$PORT"
scripts/convert_dflash_to_gguf.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c554274151c882d6f725568603ff0520f2f5e7f2d88b2f3abd837f47a42789ac
3
+ size 29840
scripts/quantize_dflash_draft.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2fe29182ac9b82b5e8d6dabe66f94a2cc269531088b43e5debe9322a0eb67be5
3
+ size 4683