# DFlash2 draft KV-ring port to llama.cpp (рабочий проект llama-dflash3-pr) > Цель: сжать draft-KV с ~1 ГБ до ~48 МБ БЕЗ потери acceptance (держать 79-80%), > не повторив ошибку предыдущего агента (наивен seq_rm/урезание n_ctx -> 30% acceptance). > > Эталон для переноса: Luce `server/src/common/dflash_draft_kv.{h,cpp}` + > `server/src/common/dflash_feature_ring.h` + драйвер `qwen35_backend.cpp`. > База в llama.cpp: рабочий проект `llama-dflash3-pr` (acceptance 79.889%). > Сломанная dflash2 удалена пользователем; работаем только в dflash3-pr. ## TL;DR (для чтения перед правками) Проблема не в "окне 4096" как таковом, а в том, КАК его реализовали. Luce использует честный **mod-cap ring** (`slot = pos % cap`) + **явные attention-маски** (вне-окна = -inf, в-окна = 0) + **абсолютные RoPE-позиции** + **пересчёт K/V только для новых строк**. Это математически эквивалентно "полному кешу", поэтому acceptance не падает. Предыдущий агент вместо этого урезал `n_ctx` и резал кеш через `llama_memory_seq_rm`, что создаёт дырки/неконсистентные окна и ломает selector-lattice. Колесо НЕ даёт >80% (80% это потолок самой пары модели). Оно даёт тот же 80% при меньшей памяти. Если не нужно экономить ~1 ГБ - НЕ переносим ничего, просто собираем рабочую dflash3 и живём с полным контекстом. --- ## 0. Карта изменений (файлы, которые отличают рабочую dflash3 от сломанной dflash2) Единственное отличие между worktree dflash3 (рабочий) и dflash2 (сломанный) - это последняя правка агента "draft-only KV window". Оно затрагивает ровно 3 файла: | файл | что добавил сломанный агент | вердикт | |------|------------------------------|---------| | `common/arg.cpp` | флаг `--spec-draft-ctx N` (добавляет `n_ctx` в `params.speculative.draft`) | НЕ решил проблему; меняет конфиг без правильного ring | | `common/common.h` | поле `int32_t n_ctx = 4096` в `common_params_speculative_draft` | см. выше | | `common/speculative.cpp` | `draft_ctx_window=4096`, `llama_memory_seq_rm`-ролл в `process()`, урезание `cparams.n_ctx` в common_speculative_init_result | **корень бага** | Всё остальное (dflash.cpp, qwen35.cpp, llama-model-loader, server-context и т.д.) между worktree ИДЕНТИЧНО - это рабочая часть порта, её не трогаем. ### Вывод по эталону - Рабочая версия = **БЕЗ** `seq_rm`-ролла, БЕЗ урезания `n_ctx`, с полным контекстом. - Сломанная версия = добавила только вышеуказанное и сломала acceptance. - Правильный путь = перенести НАСТОЯЩИЙ ring (mod-cap + маски), а не наивный роллинг. --- ## 1. Что делает ring в Luce (механизм, код, математика) Файлы-эталоны: - `lucebox/server/src/common/dflash_draft_kv.h` (DraftKvState) - `lucebox/server/src/common/dflash_draft_kv.cpp` (init / begin_step / masks) - `lucebox/server/src/common/dflash_feature_ring.h` (DraftFeatureMirror - зеркало фич target) - `lucebox/server/src/qwen35/qwen35_backend.cpp` (драйвер decode: строки ~2690-2785) ### 1.1 Геометрия (draft_kv_init, dflash_draft_kv.cpp:16-130) ``` cap = ring-вместимость контекстных слотов (последние `cap` позиций) q_len = block_size (у нас из GGUF = 8) a_step = 2 * q_len + 2 (сколько новых строк можно доложить за шаг) trash_slot= cap + q_len (слот-приёмник для pad-строк) kv_total = align_up(cap + q_len + 1, 32) (всего строк кеша: ring + шумовой блок + pad) kv_row = head_dim * n_head_kv (128 * 8 = 1024 f16 на строку) ``` K/V - это отдельные F16-тензоры `[kv_row, kv_total]`, НЕ llama.cpp KV-кеш. Живут в `mem_buf` (persistent backend buffer, вне gallocr), обнулены один раз (пустые слоты читаются FA, masked -inf, обязаны быть конечными). ### 1.2 mod-cap отображение (критично!) В `draft_kv_begin_step` (dflash_draft_kv.cpp:226-318): ```cpp ap_rows[i] = p % st.cap; // СЛОТ = абсолютная позиция % cap st.slot_pos[p % st.cap] = p; // host-зеркало: слот -> его абсолютная позиция ``` - Абсолютные позиции сохраняются (RoPE по абсолюту). - Свободные слоты перезаписываются по кольцу, НЕ удаляются сдвигом. - `slot_pos[]` хранит абсолютную позицию каждого слота (для построения масок). ### 1.3 Окно и начало шага (begin_step) ``` win = min(committed, cap) // окно = последние `cap` закоммиченных lo = committed - win // нижняя граница окна start = max(next_pos, lo) // доложить всё, чего ещё нет в кеше n_new = committed - start // сколько новых строк добавляем ``` - Если `n_new > a_step` -> `draft_kv_bulk_append` (разово заливает по 1024 строки). - Иначе - fold-in append: реальные строки + pad-строки в `trash_slot`. Шумовой блок: ```cpp pq[i] = committed + i; // абсолютные позиции noise-блока (q_len штук) ``` ### 1.4 Attention-маски (критично! отличается от наивного ролла) `mask_full` (полное-внимание слои), dflash_draft_kv.cpp:280-294: ```cpp для каждого слота s: p = slot_pos[s] mask[s] = (p >= lo && p < committed) ? 0 : -inf // только окно видно для шумовых слотов (cap+j): 0 // noise виден (bidirectional) ``` - строки за пределами окна = -inf (полностью исключены). - шумовой блок виден всем шумовым строкам (не-causal внутри блока). `mask_swa` (SWA-слои), :295-317: ```cpp eff_win = min(swa_window, win); swa_lo = committed - eff_win; mask[s] = (p >= swa_lo && p < committed) ? 0 : -inf // общее якорение окна на `committed` для ВСЕХ шумовых строк (важно для trained drafter) // шумовые строки: causal внутри блока (j в <= q) ``` > ВАЖНО для нашего порта: у пользовательской draft GGUF **нет** sliding-window меты > (нет `attention.sliding_window`), поэтому `use_iswa=false`, слоёв SWA нет, > `causal_attn` выключен, и работает только `mask_full`-подобное поведение > (бидирекц. шумовой блок + полное окно). SWA-ветка для этой модели неактивна. ### 1.5 Один фикс-topology граф (CUDA graph) - Степовый граф строится ОДИН раз в init и реплеится бесконечно. - Все входные тензоры - в dedicated backend буферах (не gallocr-recycled). - `ggml_gallocr_alloc_graph` один раз; дальше только `backend_tensor_set` + `graph_compute`. ### 1.6 Драйвер (qwen35_backend.cpp:2689-2785) ```cpp ring_cap = feature_mirror_.cap; draft_ctx = min(committed, min(ring_cap, max(DRAFT_CTX_MAX_DEFAULT=2048, cfg_.draft_ctx_max))); draft_start = committed - draft_ctx; // ring path (DFLASH_DRAFT_KV, по умолчанию вкл): draft_kv_init(draft_kv_, dw_, backend, kv_cap = draft_ctx, lm_head=nullptr); // 1 раз draft_kv_begin_step(draft_kv_, dw_, backend, feature_mirror_, committed); backend_tensor_set(draft_kv_.inp_embed, noise_embed); // шумовой блок [q_len] f32 backend_graph_compute(draft_kv_.gf); // все слои draft get(draft_kv_.hidden_states) -> local_hidden // [hidden*q_len] ``` - `used_remote_draft` / `use_mirror_view` - опции отключения ring (fallback legacy, который пересчитывает ВСЁ окно каждый шаг). Ring = частный, но более быстрый случай. ### 1.7 Что даёт ring математически - Результат равен legacy-пересчёту полного окна (абсолютные RoPE, маски). - Память: `2 * n_layer * kv_row * kv_total * 2 / 1e6 МБ` на F16. Для cap≈4096 -> доли МБ, против ~1 ГБ при полном `-c 101072`. - Acceptance не меняется (математически то же окно). --- ## 2. Как это устроено в llama.cpp (dflash3 - рабочая) и почему наивный ролл ломает Файл: `common/speculative.cpp`, `struct common_speculative_impl_draft_dflash` (строки ~912-1358). ### 2.1 Поток в рабочей версии - `ctor`: читает block_size=8, mask_token_id=248070, selector_top_k=16 из GGUF; включ `llama_set_embeddings_layer_inp(ctx_tgt, target_layer_ids[k]+1)` (выходы слоёв); `llama_set_embeddings_nextn(ctx_dft, true, !is_dflash2)` (латтис селектора); `llama_set_causal_attn(ctx_dft, false)` (шумовой блок бидирекц.). - `process()` (prefill + каждый target-токен): берёт фичи target-слоёв, `llama_encode` -> `inp_g`, `llama_decode(ctx_dft, batch_inject)` ЗАПИСЫВАЕТ инжект в draft KV на абсолютных позициях. (В сломанной версии тут ещё и roll seq_rm - вот баг.) - `draft()`: один `llama_decode(ctx_dft, batch)` всего шумового блока `[last, mask...x7]` на позициях `n..n+7`, читает латтис из `llama_get_embeddings_nextn`, строит цепочку. ### 2.2 Почему acceptance упал до 30% - draft KV - это llama.cpp `llama_kv_cache` на полный `-c` (в рабочей) или урезанный `(4096+headroom)` (в сломанной). - Сломанная версия перед каждой инжекцией делала `llama_memory_seq_rm(mem_dft, seq, 0, pos-window)`: физически вырезала строки KV. Это НЕ mod-cap ring. Это: - удаляет позиции < границы (создаёт дырку в начале), - НО llama.cpp attention (plain `build_attn_inp_kv`) собирает ВСЕ присутствующие в кеше строки и attends по ним БЕЗ явной маски окна -inf. Наивный ролл полагался на наличие пустых/evict-ячеек, которые attention читает как мусор, - плюс `n_ctx=(4096+headroom)` слишком мал: noise-блок + инжект не вмещаются по round-robin -> вытеснение (eviction) режет нужные строки. - Итог: selector-lattice сходил с мусора -> цепочка мусорная -> target режет до 30%. ### 2.3 Ключевое отличие от Luce, которое надо воспроизвести Luce: - кеш фикс. размера `kv_total`, слоты = `pos % cap`, вне-окна строки ЯВНО маскируются -inf, in-окна плотно присутствуют. llama.cpp (plain attn, без ISWA): - кеш `n_ctx`, attention собирает все строки кеша, явной per-row маски окружения нет (masks строятся из `kq_mask` по `n_ctx`, а не по per-row виду). Из этого следует: **голый перенос идеи "урежь n_ctx + режь seq_rm" невозможен без нарушения математики окна**. Нужен ОДИН из двух правильных путей (см. раздел 3). --- ## 3. СТРУКТУРА DRAFT (ПЕРЕСМОТР - ВАЖНО): это обычный dense-attention трансформер, НЕ рекуррентный > ПРЕДЫДУЩАЯ версия этого раздела (гибрид с Gated DeltaNet) была ОШИБКОЙ и снята. > Проверено по llama-dflash3-pr. ### 3.1 Факты - Draft строится в `src/models/dflash.cpp` (arch `LLM_ARCH_DFLASH`). Его внимание - **обычные dense-проекции** `wq / wk / wv / wo` (`dflash.cpp:498-499, 588-590`). В dflash.cpp НЕТ `build_recurrent_attn` / `ssm_*` (рекуррентный код из qwen35.cpp относится к ОСНОВНОЙ целевой модели, не к draft). - Пер-layer есть **dynamic conv** (`dflash_attn_conv_proj/base`, `dflash_ffn_conv_*` `dflash.cpp:582-585, 235-236`) и **DFlash2 selector** (предшественник/следующий/скрытый -> латтис выбора цепочки; читается из `llama_get_embeddings_nextn`, `speculative.cpp:1245`). - `LLM_ARCH_DFLASH` НЕ входит в recurrent/hybrid списки (`llama-arch.cpp:998-1034`), значит `create_memory` даёт draft'у **обычный `llama_kv_cache`** (`llama-model.cpp:2265-2351`), а НЕ `llama_memory_recurrent`. Никакого рекуррентного скана истории нет. - У нашей GGUF нет sliding-window меты -> `use_iswa=false` -> plain `build_attn_inp_kv()`, `causal_attn=false` (шумовой блок бидирекц.), без SWA-масок. ### 3.2 Вывод для переноса Это ТОЧНО тот случай, для которого Luce ring и написан: **dense-attention draft на position-KV**. ~1 ГБ draft-KV при `-c 101072` - это и есть attention-KV, его и сжимаем кольцом. Рекуррентной части в draft нет -> нет аргумента "нельзя окно, нужен скан истории". Перенос кольца из Luce корректен и реализуем целиком. --- ## 4. КАК ПЕРЕНОСИТЬ (одобрено: переносим настоящий ring, а не наивный ролл) Так как draft = dense-attention на plain `llama_kv_cache`, у нас есть ДВА правильных пути (оба корректы математически; предпочтителен C, т.к. это буквально Luce). ### Вариант C (ОСНОВНОЙ): настоящий Luce KV-ring для draft (dense-attention) Заменить штатный бескрайний `ctx_dft` KV-кеш на кольцо с фикс. `cap` слотов, слот = `pos % cap`, и построить маски -inf для вне-окна. По шагам см. раздел 5 - там точный план по файлам/функциям. Свойства (равно Luce): - окно = последние `cap` закоммиченных позиций, - вне-окна строки физически НЕ присутствуют (или маскируются -inf) - attention их не видит, - шумовой блок (q_len=8) attend'ит окно бидирекц., - absolute RoPE -> позиции не сдвигаются -> кеш не "протухает" при перемотке/новых запросах. ### Вариант B (промежуточный, меньше кода): удержание окна через сам llama.cpp Не резать `seq_rm` вручную; просто задать draft'у умеренный `n_ctx` (= окно) и дать `llama_kv_cache` самому держать непрерывное окно (инвариант `[pos_min,pos_max]` из `apply_ubatch`, llama-kv-cache.cpp:1143-1160). НЕ вызывать `llama_memory_seq_rm` в `process()`. - Pиск, из-за которого это может не дать 79.9% на длинных промптах: llama.cpp evict выталкивает "старые" ячейки, и attention читает РЕЗИДЕНТНОЕ окно без -inf (window = "сколько ячеек влезло"), не строго `[lo, committed)`. Для чисто растyщего промпта это близко к кольцу, но менее точно при перемотках. - Использовать только как экспериментальную промежуточную точку/флаг для сравнения. ### Решение Реализуем Вариант C (настоящий ring) как основной, с Вариантом B как флагом отката для измерения. Начинаем с того, что возвращаем в dflash3 рабочий baseline (acceptance 79.9), затем накатываем ring так, чтобы acceptance НЕ менялся (это проверяется на одном промпте). --- ## 5. ПЛАН ПЕРЕНОСА (настоящий ring, dense-attention draft) - по шагам, файлам, функциям Baseline для замеров: текущий рабочий dflash3 (acceptance 79.9%). Каждый шаг собираем и сверяем acceptance на одном промпте (temp=0, n_max=7), иначе не двигаемся дальше. ### Шаг 1. Сохранить рабочий baseline (не обязательно коммитить) `git stash` или снимок `common/speculative.cpp` до начала правок - чтобы можно было одним `cp` вернуться к 79.9% и сравнивать. ### Шаг 2. Параметр окна (конфиг) - `common/common.h`, `struct common_params_speculative_draft`: добавить `int32_t n_ctx = 0; // 0 = полный контекст (без кольца); >0 = ring window` - `common/arg.cpp`: опция `--spec-draft-ctx N` (пишет в `draft.n_ctx`). > Здесь главное отличие от сломанной версии агента: `n_ctx` задаёт ОКНО кольца, а не > просто кромсает `cparams.n_ctx`. Код ниже явно строит кольцо и нигде не режет `seq_rm`. ### Шаг 3. Аллоцировать draft-KV как кольцо (не полный `-c`) В `common_speculative_init_result::common_speculative_init_result` (speculative.cpp:2452+): ```cpp if (spec_dflash && params.speculative.draft.n_ctx > 0) { // окно: min(n_ctx, ...) floor 2048, как Luce DRAFT_CTX_MAX_DEFAULT const uint32_t window = std::max(2048, params.speculative.draft.n_ctx); // draft контекст - только окно + 1 микробатч шума: cparams.n_ctx = (window + cparams.n_batch) * cparams.n_seq_max; // НЕ больше } ``` ВАЖНО: это НЕ трогает target (`ctx_tgt` остаётся на полном `-c 101072`). ### Шаг 4. Убрать seq_rm-ролл, дать Llama.cpp держать непрерывное окно (минимум для начала) В `process()` (speculative.cpp ~1101+): удалить блок `std::vector seq_end...` + `llama_memory_seq_rm`, вычислять/инжектить фичи как в рабочей dflash3. За счёт `n_ctx = окно` из Шага 3 кеш сам содержит только последние `window` позиций (непрерывно), attention читает их все - это эквивалент окна Luce. - Это даёт Вариант B сразу и является ПЕРВЫМ измеримым результатом (см. раздел 4). - Замер: acceptance должен остаться ~79.9%. Если падает - окно мало либо нужен Шаг 5. ### Шаг 5. Точные маски окна (до == Luce `mask_full`, если Шаг 4 падает по acceptance на длинных) Если на длинном промпте (когда окно начинает резать) acceptance падает, значит нужно явное -inf-маскирование вне-окна как в Luce `draft_kv_begin_step` (:280-294). Это требует вмешательства в `build_attn_inp_kv` / `set_input_kq_mask` (llama-graph.cpp, llama-kv-cache.cpp) либо отдельного модуля ring. В этом случае: - ввести кастомный KV-модуль для draft (по образу Luce `DraftKvState`, см. шаг 6), - либо параметр `n_ctx` с переносом позиций и kq-маской по окну. ### Шаг 6. (при необходимости) Кастомный ring-модуль = полный перенос Luce Это буквально перенос `dflash_draft_kv.{h,cpp}` + `draft_graph.cpp` логики в новый `common/draft-ring.{h,cpp}` под llama.cpp backend (CUDA/CPU), с драйвером в `common_speculative_impl_draft_dflash::draft()` вместо `llama_decode(ctx_dft, batch)`. Потребуется: - ручная сборка noise-эмбеддингов + `inp_embed` [q_len], - mod-cap append (слот = pos % cap, trash_slot для pad), - маски (full и, если появятся SWA-слои, swa — у нас их нет), - считывание hidden_states (дальше селектор строит цепочку как сейчас), - fixed-topology CUDA graph (как Luce `st.gf`, переиспользуемый). Это самый трудоёмкий, но полный и честный путь. ОСНОВНОЙ, если Шаг 4 не даст 79.9%. --- ## 6. Мапа корреляции Luce -> llama.cpp (одна строка = один "шаг" переноса) | Luce (эталон) | llama.cpp / dflash3 (куда) | что переносим | сложность | |---|---|---|---| | `draft_kv_init` (аллокация ring-KV) | шаг 3: ограничение `cparams.n_ctx` для ctx_dft / кастомный ring | кольцо с cap слотов | C | | `draft_kv_begin_step` (append+маски+noise) | шаг 4/5/6: `process()`+`draft()` в speculative.cpp | fold-in append, маски, шумовой блок | C | | `draft_feature_mirror` (зеркало фич) | `features_buf` + `llama_encode`+`llama_decode(batch_inject)` (уже есть в рабочей) | целевые фичи в ring | B | | `draft_graph.cpp` (dense draft граф) | dflash.cpp `graph` (уже есть) | НЕ нужно переносить заново | - | | `dflash2_select_chain` | `llama_get_embeddings_nextn` + selector в `draft()` (уже есть) | выбор цепочки | B | | SWA-ветка масок | НЕ применяется (нет swa в GGUF) | skip | - | ## 7. Измеряемые критерии успеха (до/после на ОДНОМ промпте, temp=0, n_max=7) - acceptance % (должно держаться ~79-80%, не падать к 30%). - mean chain length (~6.5). - draft KV MiB (цель: ~48 МБ при cap≈4K, было ~1049 MiB). - tok/s (цель: не хуже ~29-35 tok/s). - параметры: те же, что в README_DFLASH2.md. ## 8. Риски (важно) - Draft - dense-attention на plain KV -> кольцо экономит именно attention-KV, корректно. Рекуррентной части нет, поэтому нет "скана истории" - окно полноценно для draft. - Неверное окно (слишком малое) режет контекст, из которого селектор строит цепочку -> снова падает acceptance (та же ловушка, что у агента). Поэтому НЕ ставить жёстко 4096, а брать окно из `draft_ctx_max`/конфига min 2048, и проверять на длинном промпте. - Любая правка >= большой: сначала прогнать на одном промпте и сверить acceptance. - Правки нельзя делать "вслепую": каждая правка = замер, иначе риск не увидеть деградацию. - Кастомный ring (шаг 6) затрагивает граф/память draft - делать только с rollback-флагом. ## 9. Итог Рабочая версия = dflash3 (acceptance 79.9%). Верифицировано: draft = dense-attention на plain `llama_kv_cache`, значит правильный Luce KV-ring применим. Реализация идёт по плану раздела 5: сначала параметр окна + снятие seq_rm-ролла (шаги 2-4, дают сразу измеримый результат и экономию памяти), при необходимости далее точные маски / кастомный ring (шаги 5-6). Acceptance сверяем на каждом шаге. Колёсо даёт тот же ~80% при меньшей памяти, НЕ больше 80% (это потолок пары модели). ## 10. Ход работы (СТАТУС - что уже применено в llama-dflash3-pr) Все правки - в рабочем проекте `llama-dflash3-pr`, свежая сборка `build-p40-ring` (конфигурация P40 как в README_DFLASH2.md). `llama-common` скомпилирован успешно. ### Применено (шаги 2-4 плана раздела 5) 1. `common/common.h` - в `common_params_speculative_draft` добавлено поле `int32_t n_ctx = 0; // DFlash draft KV ring window (0 = full context, no ring)`. 2. `common/arg.cpp` - добавлена опция `--spec-draft-ctx N` (env `LLAMA_ARG_SPEC_DRAFT_CTX`), пишет в `params.speculative.draft.n_ctx`. 3. `common/speculative.cpp` - в `common_speculative_init_result` (для DFlash): если `draft.n_ctx > 0`, ограничиваем `cparams.n_ctx` для **ctx_dft** до `(window + headroom) * n_seq_max`, где `window = max(2048, n_ctx)`, `headroom = max(n_batch, n_max+1)`. Target (`ctx_tgt`) не трогаем. **seq_rm-ролла НЕТ** - llama.cpp сам держит непрерывное окно последних `n_ctx` позиций (эквивалент окна кольца для dense-attention draft). ### Включить/выключить - Без флага (`n_ctx=0`, ПОВЕДЕНИЕ ПО УМОЛЧАНИЮ): концепт полный (`-c`), как рабочая dflash3 -> acceptance 79.9%, draft-KV ~1 ГБ. Рабочее поведение не ломаем. - `--spec-draft-ctx 4096`: draft-KV ограничен ~4К окном, target полный. Замер acceptance/MiB. ### Запуск после сборки ``` cd build-p40-ring/bin # эталон (без окна): ./llama-server ... --spec-type draft-dflash --spec-draft-n-max 7 ... # с окном: ./llama-server ... --spec-type draft-dflash --spec-draft-n-max 7 --spec-draft-ctx 4096 ... ``` ### Следующие шаги (после замера на GPU у владельца) - Если при `--spec-draft-ctx` acceptance ~79.9% -> шаги 2-4 достаточно, фиксируем. - Если падает на длинном промпте -> шаг 5 (явные маски окна) / шаг 6 (кастомный ring). ## 11. РЕЗУЛЬТАТЫ ТЕСТОВ НА GPU (Tesla P40, 24 ГБ) - кольцо работает Сборка: `llama-dflash3-pr/build-p40-ring/bin/llama-server`. Модели: target `Qwen3.8-27B-UD-Q4_K_XL.gguf`, draft `Qwen3.8-27B-DFlash2-lucebox-q8_0.gguf`. ### Тест 1. Одинаковый короткий промпт (29 токенов, n_predict=60, temp=0, reasoning on) | режим | -c | draft acceptance | mean len | acc per pos | tok/s | |---|---|---|---|---|---| | **БЕЗ кольца** (полный 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 | | **С кольцом** `--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 | -> **Acceptance и mean len БИТ-В-БИТ одинаковы.** Кольцо НЕ ухудшает попадание. ### Тест 2. Запуск на полном контексте - кольцо критично - `-c 111072` **БЕЗ** кольца: сервер НЕ стартует - `failed to allocate buffer for kv cache` (draft-KV ~1 ГБ+ на 24 ГБ P40, рядом с target). - `-c 111072` **С** кольцом (`--spec-draft-ctx 4096`): стартует, GPU 23940/24576 MiB. -> Именно ради этого кольцо и нужно: draft-KV ~1049 MiB -> ~48 MiB. ### Почему 64.9% а не 79.9% в Тесте 1 Потому что промпт с `--reasoning on`: первые ~20 из 60 токенов - thinking-секция (другое распределение, чем тест 433/542 из README_DFLASH2.md). Важно не абсолютное значение, а что кольцо == полный контекст на ОДНОМ промпте. ### Долгий тест (эссе, n_predict=3050, temp=0, reasoning on) | режим | -c | draft acceptance | mean len | tok/s | |---|---|---|---|---| | **DFlash с кольцом** `--spec-draft-ctx 4096` (n_max=7) | 8192 | **0.24827** (1943/7826) | 2.74 | ~11.7 | | **MTP** `--spec-type draft-mtp` (n_max=3, draft вшит в модель) | 8192 | **0.48934** (1813/3705) | 2.47 | **16.7** | Оговорки: - n_max разный: DFlash чертит до 7 токенов/шаг, MTP до 3. При большем n_max acceptance математически ниже (больше позиций для промаха) - видно по числу сгенерированных: 7826 у DFlash против 3705 у MTP. Это НЕ сравнение «яблоки с яблоками». - У MTP нет отдельной draft-модели -> нет её накладных расходов на P40, поэтому 16.7 t/s. - Короткий MTP-замер (1/3 = 0.333) статистически мусорный: 1 шаг спека, 3 токена. ### Тест 3. Короткий промпт (29 токенов, n_predict=60) - DFlash vs MTP | режим | draft acceptance | mean len | tok/s | |---|---|---|---| | DFlash с кольцом (n_max=7) | **0.64865** (48/74) | 5.36 | 24.09 | | MTP (n_max=3) | 0.33333 (1/3) | 2.00 | 12.63 | -> Короткий MTP-замер статистически крошечный (3 токена, 1 принят) - не показатель. По длинному тесту MTP быстрее и с лучшим acceptance, но при меньшем n_max. ### Тест 4. Тот же промт на ОРИГИНАЛЬНОМ Luce dflash_server (референс) Запуск: `DFLASH_SINGLE_CHAIN_CHECKPOINT_F32=1 DFLASH_FAST_ROLLBACK_THRESHOLD=1 LUCE_Q8_MEMO=1 DFLASH_KV_ROTATE=0 ./server/build/dflash_server ... --fa-window 2048 --cache-type-k q8_0 --cache-type-v q8_0 --max-ctx 8192 --port 8011`. API - OpenAI-совместимый (`/v1/chat/completions`), НЕ `/completion`. ВАЖНО: с `reasoning: {effort: low}` Luce-спек НЕ работает: `[budget-hook] spec-decode close at committed=52/3050 (remaining=3050 <= hard_limit=4096)` - budget-hook Luce (hard_limit_reply_budget=4096 из model_card) при max_tokens <= 4096 force-close'ит на 1-м шаге и падает в AR: `[ar-decode] tokens=2837 13.58 tok/s`, `target_forwards=2` - acceptance неизмерим. Это НЕ кольцо, это их бюджет-хук. С `"thinking":{"type":"disabled"}` спек Luce работает: | режим | draft acceptance | avg_commit | steps | tok/s | |---|---|---|---|---| | **Luce** dflash_server (q_len=8, ring cap=4096) | **0.372** (2189/5880) | **2.98** | 735 | 15.69 | Luce ring активен: `[draft-kv] ctx-KV ring active: cap=4096 kv_total=4128 a_step=18 layers=5 f16 cache 80.6 MiB`. ### Сводная таблица (длинный тест, эссе, temp 0) | | DFlash llama.cpp кольцо (n_max=7) | MTP (n_max=3) | Luce (q_len=8) | |---|---|---|---| | draft acceptance | 0.24827 (1943/7826) | 0.48934 (1813/3705) | **0.372** (2189/5880) | | mean len / avg_commit | 2.74 | 2.47 | **2.98** | | tok/s | ~11.7 | **16.7** | 15.69 | | draft-KV | 47.81 MiB | — | 80.6 MiB f16 | Оговорки: длины цепочек разные (7/3/8) - acceptance не сопоставим напрямую; Luce мерился без thinking (с reasoning on его спек убит budget-hook'ом); по avg_commit Luce (2.98) > DFlash llama.cpp (2.74) > MTP (2.47). ### Тест 5. ОДИНАКОВЫЙ промт на Luce vs llama.cpp кольцо (Count 1-500, temp 0, без thinking) Одинаковый промт, одинаковый объём - честное сравнение реализации. | | Luce (q_len=8) | llama.cpp кольцо (n_max=7) | |---|---|---| | токенов | 1892 | 1946 | | tok/s | **42.42** | 33.92 | | draft acceptance | **99.8%** (1892/1896) | 97.4% (1697/1743) | | avg_commit / mean len | **7.98** | 7.82 | -> Разница 42.4 vs 33.9 = 1.25x, НЕ 2x. Старые «39 vs 24» были РАЗНЫЕ промты: 24 tok/s = короткое эссе с reasoning (acceptance 0.65), 39 = счёт (acceptance 0.91). На одинаковом контенте llama.cpp отстаёт на 25% из-за: цепочки 7 вместо 8, обычного replay вместо fast-rollback, отсутствия fused-проекций для Pascal. На счёте acceptance у обоих ~97-99%, поэтому разница = чистая цена имплементации. Короткий счёт на Luce (51 токен): 38.90 tok/s, acceptance 91.1%, avg_commit 7.29. ### Тест 6. КОРЕНЬ разницы: MMVQ на батче 8 (Pascal) Профилирование (nsys + DEBUG_TIMINGS + DBG-логи n_tokens/n_seq_tokens): - verify-батч = 8 токенов (подтверждено логом n_tokens_all=8), НЕ 1. - nsys: 67-74% времени verify - `mul_mat_vec_q` (по-токенные MMVQ матмулы), батчевые `mul_mat_q` ~8%. fused `gated_delta_net_cuda` всего 2.6%. - ПРИЧИНА: `ggml/src/ggml-cuda/mmvq.cuh:3` `#define MMVQ_MAX_BATCH_SIZE 8` - на Pascal (не CDNA) `should_use_mmvq` возвращает `ne11 <= MMVQ_MAX_BATCH_SIZE` => батч РОВНО 8 -> ВСЕ квантованные матмулы идут через медленный по-токенный MMVQ вместо батчевого MMQ. - FIX (временный, в коде): `MMVQ_MAX_BATCH_SIZE_CFG 7` в mmvq.cuh + mmvq.cu:336 (батч 8 -> MMQ). Итог на счёте 1-500 (temp 0, без thinking): | | до фикса (MMVQ@8) | после (MMQ@8) | Luce | |---|---|---|---| | t_decode (verify) | 196 ms | 169 ms | 161 ms | | tok/s | 34.13 | 36.81 | 42.42 | | draft acceptance | 0.97361 | 0.91903 | 0.998 | | mean len | 7.82 | 7.43 | 7.98 | - verify почти догнал Luce (169 vs 161ms). Остаток 13% (36.8 vs 42.4) - acceptance: Luce 99.8% vs llama.cpp 91.9-97.4% (MMQ менее точен на границе argmax). Это отдельная задача (точность verify-логитов/селектора), не скорость kernel. - ВНИМАНИЕ: MMVQ_MAX_BATCH_SIZE_CFG=7 - экспериментальный фикс в коде ggml. Проверить на реальном промте качество (acceptance упал с 0.974 до 0.919). - Короткий счёт (Count 1-20, 92 токена): MMVQ@8 28.87 tok/s -> MMQ@8 34.71 tok/s (+20%), но acceptance 0.868 vs 0.796 - тоже упал. Вывод: скорость ядра (verify) догнали, остаток - acceptance (MMQ менее точен на границе argmax). ### Тест 7. ПРАВИЛЬНЫЙ перенос: MMQ по типам НЕ работает, ключ - консистентность Попытка разделить MMQ-порог по типу кванта: q8_0 (драфт) -> MMVQ (точный), K-кванты target (Q4_K/Q5_K/Q6_K) -> MMQ (быстрый). Результат НАМНОГО ХУЖЕ: | конфиг | t_decode | acceptance | tok/s | |---|---|---|---| | MMVQ все (baseline) | 196 ms | 0.97361 | 34.13 | | MMQ все (cap 7) | 169 ms | 0.91903 | 36.81 | | MMQ только K-кванты (q8_0->MMVQ) | 164 ms | **0.83934** | 35.55 | -> Причина: acceptance зависит НЕ от «точности» одного пути, а от КОНСИСТЕНТНОСТИ драфт и verify. Когда драфт (q8_0, MMVQ) и verify (target, MMQ) считают по-разному, argmax на границе РАСХОДИТСЯ -> acceptance падает сильнее, чем у любого единого пути. Правило: драфт и verify должны идти через ОДИН и тот же матмул-путь (оба MMVQ или оба MMQ). Полный MMQ (cap 7) - лучший компромисс: verify 196->169ms, tok/s 34.1->36.9. Почему у Luce acceptance 99.8% а у нас 91.9%: у Luce verify использует GPU argmax (ggml_argmax в графе, перенос на CPU только 8xint32) + их драфт и verify численно консистентны (свои kernels, F32 lm_head). У llama.cpp verify читает ПОЛНЫЕ logits (8x248320 float = 8MB) на CPU каждый шаг + CPU-argmax + MMQ на границе argmax менее точен, чем MMVQ (0.919 vs 0.974 при едином пути). Остаток 13% = acceptance (7.43 vs 7.98 токенов/шаг) + цена шага (verify 169 vs 161ms, post_decode+sampl 7.6ms CPU-чтение logits у нас, у Luce нет). Что нужно для полного паритета: 1. GPU-argmax в verify (как Luce): ggml_argmax в графе, перенос 8xint32 вместо 8MB. 2. Консистентность: драфт и verify на одном пути (F32 lm_head + GPU argmax). ### Тест 8. ФИНАЛЬНАЯ оптимизация: MMQ cap 7 + backend sampling (-bs) = 37.66 tok/s Ключевые открытия: - Через chat API (честное сравнение) MMQ cap 7 ДАЁТ ВЫШЕ acceptance, чем MMVQ: baseline cap 8 (MMVQ) 0.843 vs cap 7 (MMQ) 0.919! Прошлые выводы «MMQ менее точен» были испорчены сравнением через РАЗНЫЕ API (/completion 0.974 vs chat). - llama.cpp УЖЕ имеет GPU-argmax семплер: `llama_sampler_greedy_backend_apply` (llama-sampler.cpp:1086 ggml_argmax). Флаг сервера `-bs` / `--backend-sampling` (arg.cpp:2296) переводит сэмплинг на GPU -> verify-логиты НЕ читаются на CPU. - t_sampl 2.07 -> 0.03ms, t_post_decode 5.7 -> 0.15ms (не читаем 8MB logits/шаг!). Итоговые цифры (chat API, Count 1-500, temp 0): | конфиг | t_decode | t_post+sampl | acceptance | tok/s | |---|---|---|---|---| | baseline (MMVQ cap 8) | 196 ms | 7.8 ms | 0.843 | 30.20 | | MMQ cap 7 | 169 ms | 7.8 ms | 0.919 | 36.92 | | **MMQ cap 7 + -bs** | 171 ms | **0.18 ms** | **0.919** | **37.66** | | Luce | 161 ms | ~0 | 0.998 | 42.42 | Короткий счёт (Count 1-20): MMQ+bs 35.80 tok/s, acceptance 0.868 (было 34.71/0.868). Итого выжали 30.2 -> 37.7 tok/s (+25%) на том же промте. Остаток до Luce: acceptance (0.919 vs 0.998 - MMQ на границе argmax) + их fused-граф verify (171 vs 161ms). Команда запуска: ... --spec-draft-ctx 4096 --temp 0 --jinja -bs ### Тест 9. Python-код (сложная задача) - DFlash vs ngram режимы Промт: "Write a Python class implementing a concurrent LRU cache with TTL support...": class+type hints+docstrings, lock-free variant, tradeoffs. n_predict 4096, temp 0, chat API. | режим | draft acceptance | mean len | tok/s | verify | примечание | |---|---|---|---|---|---| | **DFlash cap7+bs** | 0.25271 (2615/10348) | 2.77 | **13.89** | 171ms | лучший на коде | | ngram-mod (match24/min48/max64) | — (0 drafts!) | — | 12.90 | 77.6ms | НЕ генерировал: #gen drafts=0 | | ngram-map-k4v (n8/m24/minhits1) | 0.06591 (29/440) | 2.32 | 12.57 | 77.6ms | 440 drafts за 4043 вызова | | AR (без спека) | — | — | ~13.2 | 75.9ms | база | Выводы: - На КОДЕ DFlash выигрывает у ngram и AR лишь ~5-8% (13.89 vs 12.6-13.2): код менее предсказуем, чем счёт (acceptance 0.25 vs 0.92 на числах). - ngram-режимы на коде почти бесполезны: ngram ловит ПОВТОРЯЮЩИЙСЯ текст, а код не повторяется. ngram-mod вообще дал 0 драфтов (n_match=24 слишком длинный для кода + n_min=48), ngram-map-k4v - 1% acceptance. - Сравнение на счёте (Count 1-500): DFlash 37.66 tok/s vs ngram - не тестировался, но ngram на числах тоже работал бы плохо (нет повторов). - DFlash силён на: числах/JSON/структурном тексте. Слаб на: свободном коде/прозе. ### Тест 10. Python-код: DFlash vs MTP vs Luce (тот же промт) Тот же промт LRU cache, n_predict 4096, temp 0. MTP вшит в GGUF (без -md). | режим | acceptance | mean len / avg_commit | tok/s | verify | |---|---|---|---|---| | **Luce** (q_len=8) | **0.709** (3591/5064) | **5.67** | **29.81** | 161ms | | **MTP** llama.cpp (n_max=3) | 0.483 (2423/5016) | 2.45 | **16.24** | ~122ms | | DFlash cap7+bs (n_max=7) | 0.25271 (2615/10348) | 2.77 | 13.89 | 171ms | | ngram-map-k4v | 0.0659 | 2.32 | 12.57 | 77.6ms | | AR (без спека) | — | — | ~13.2 | 75.9ms | ПОЧЕМУ DFlash просел на коде (0.25 acceptance, 13.89 tok/s): - DFlash чертит ЦЕПОЧКУ из 7 токенов подряд. На числах угадать след. число легко (acceptance 0.92), на коде (свободная генерация) 7 токенов подряд почти никогда не совпадают -> acceptance 0.25, спек ~бесполезен (13.89 ~ AR 13.2). - MTP (3 токена) с acceptance 0.48 выигрывает: короткая цепочка чаще проходит целиком -> 16.24 tok/s. - Luce 29.81 tok/s - их fused-граф verify (161ms) + GPU argmax + их own kernels: тот же драфт, но verify в 1.4x быстрее и acceptance 0.709 (их численно консистентный путь). Разница DFlash llama.cpp vs Luce НА КОДЕ огромна (13.9 vs 29.8 = 2.1x) - не из-за кольца, а из-за скорости/точности verify-графа. - Вывод: DFlash llama.cpp хорош на структурном тексте (37.7 tok/s на счёте), но на коде проигрывает MTP и сильно Luce. ### Тест 11. ⭐ НАЙДЕНА ПРИЧИНА "коллапса" на коде: режим reasoning, а НЕ порт **Вывод: перенос DFlash2 в llama.cpp был корректен. Разница 0.25 vs 0.71 на коде — артефакт режима thinking (llama.cpp сервер по умолчанию `--reasoning on`, Luce тестировался с thinking disabled).** Одинаковый бинарь, тот же LRU-промт, n_predict 4096, temp 0, MMQ cap 7 + -bs: | режим | acceptance | mean len | acc per pos | tok/s | |---|---|---|---|---| | 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 | | 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 | | **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** | | Luce DFlash (thinking disabled) | 0.709 (3591/5064) | 5.67 | (плоская) | 29.81 | Что произошло: - С thinking ON модель пишет ``-блоки: внутренние рассуждения непредсказуемы, цепочка драфта экспоненциально затухает (0.72, 0.47, 0.30, ...) — это НЕ дефект драфтера, а свойства текста. - С thinking OFF кривая плоская (0.92, 0.80, 0.69, ...), acceptance 0.64, скорость 23.4 tok/s. Разрыв с Luce (0.709 / 29.81) теперь малый: acceptance 90%, скорость 78%. Дополнительно (тот же бинарь, thinking off): - Count 1-500 (финал, MMQ cap 7): acceptance **0.99759**, mean len 7.98, **40.41 tok/s** (Luce: 0.998 / 7.98 / 42.42 — acceptance равен, скорость 95%). - Проза (эссе 3050, финал MMQ cap 7): acceptance 0.288, mean 3.02, 15.27 tok/s — Luce на прозе ~0.372 / 15.69 (с thinking low): свободная проза тяжела для обоих, это не регресс порта (acceptance 78%, скорость 97%). Опровергнутые гипотезы (все проверены замером, все no-op): 1. **seq_rm-стирание фич** (ckpt.pos_max+1 vs pos_next()): бит-в-бит одинаковый acceptance (0.26731 vs 0.25271, кривая acc-per-pos идентична до 3 знака). Причина: update_pos() обновляет ckpt.pos_max на каждое шаге, ckpt.pos_max+1 == pos_next() в нормальном потоке; checkpoint (update_dft/load_dft) обновляется каждый шаг, а не раз в 512. 2. **Кольцо окна** (--spec-draft-ctx 0/4096): одинаково. 3. **Математика цепочки** (unary raw vs log-prob, порядок mul_mat, transposed weights): тождественна Luce (score = cand_lp + Σ pr·hp·sc, prev_row = выбранный кандидат). hproj [hdim,rank], pred/succ [rank,vocab] — одинаковые shape в обоих загрузчиках. 4. **K/V контекста драфта**: llama.cpp embd-путь делает ровно shallow-инъекцию (wk/wv @ fused + k_norm + rope) как Luce draft_ctx_kv_rows. 5. **Фичи таргета**: t_layer_inp[il+1] = post-layer hidden (qwen35.cpp:246) = то же, что Luce capture (cur после FFN+residual). Код сейчас: MMQ_MAX_BATCH_SIZE_CFG=7 (MMQ на батче 8, P40), -bs, seq_rm возвращён к оригиналу (ckpt.pos_max+1) с поясняющим комментарием. ## Test 12 — DFlash vs MTP, thinking ON/OFF (код LRU, 4 ячейки) Замер: тот же LRU-промпт (Python-класс кэша, 4096 токенов, temp 0). DFlash-n7: `--spec-type draft-dflash --spec-draft-n-max 7` + `-md Qwen3.8-27B-DFlash2-lucebox-q8_0.gguf`. MTP-n3: `--spec-type draft-mtp --spec-draft-n-max 3`, MTP-слои из таргет-GGUF. Thinking ON = дефолт (chat template), OFF = `--reasoning off`. | Конфиг | Thinking | Acceptance | Mean len | tok/s | |---|---|---|---|---| | DFlash-n7 | OFF | 0.647 | 5.53 | **27.69** | | DFlash-n7 | ON | 0.25271 | 2.77 | 13.90 | | MTP-n3 | OFF | 0.8205 | 3.46 | **23.61** | | MTP-n3 | ON | 0.48452 | 2.45 | 16.31 | acc-per-pos (OFF/ON): - DFlash-n7: OFF (0.914, 0.810, 0.710, 0.632, 0.548, 0.479, 0.435) ON (0.695, 0.434, 0.283, 0.170, 0.098, 0.055, 0.034) - MTP-n3: OFF (0.931, 0.816, 0.715) ON (0.706, 0.461, 0.286) Выводы: 1. **Thinking ON убивает оба**, но DFlash страдает сильнее: падение acceptance 0.647→0.253 (-61%), MTP 0.821→0.485 (-41%). Reasoning-текст непредсказуем для любой цепочки, длинная (7) рвётся быстрее короткой (3). 2. С thinking ON **по скорсти MTP (16.3) > DFlash (13.9)** — длинная цепочка платит verify 8 ради ~2.4 принятых. 3. С thinking OFF **по скорости DFlash (27.7) > MTP (23.6)** — цепочка живёт на коде, хвост 4-7 окупает verify. 4. Первые 3 позиции: MTP чуть точнее DFlash (0.931/0.816/0.715 vs 0.914/0.810/0.710) — DFlash декодит блок параллельно с масками и не видит собственные пики соседей, MTP поочередно видит свой предыдущий токен. 5. Итог для сценариев: - агент/код: DFlash-n7 + `--reasoning off` (27.7 t/s); - рерайт длинного текста: DFlash-n3 (~23 на коде, короче на прозе) или MTP-n3 — эквивалентны по форме, DFlash-n3 быстрее драфтит; - если нужен reasoning ON (дефолт) — MTP-n3 меньше теряет (16.3 vs 13.9), но оба сильно ниже than OFF. Комбо DFlash+MTP в одном сервере НЕ работает в этом билде: `--spec-type draft-dflash,draft-mtp` c -md падает ("MTP context requested but model doesn't contain MTP layers") — код создаёт ОДИН ctx_dft для всех типов и просит MTP-слои у DFlash-GGUF. Плюс даже при успехе первый-выигравший (MTP приоритетнее) сделал бы комбо = чистый MTP. Эффективное объединение = DFlash с коротким блоком (n_max=3), даёт кривую MTP (0.932/0.842/0.741) и ту же скорость (~23.3). ## Test 13 — Динамическая длина цепочки по margin селектора [DFLASH-DYNAMIC] Проблема: thinking ON рубит спекуляцию (acceptance 0.25, 13.9 t/s). Идея — не отключать reasoning, а дать цепочке самой резаться, когда селектор неуверен. ### A. Первая попытка: softmax-уверенность выбранного токена — ДЕКОРАТИВНА p_chosen = softmax(top-16 скоров); обрыв при p_chosen < p_min. На коде thinking ON: 14.56 t/s (+4.7%), проверено всё те же 6.8/7 токенов. Причина: скоры селектора ~сырые логиты (20-45), softmax по топ-16 би-модален — 378/378 пиков p>0.7. "Уверенно неправ": высокий score НЕ означает правильный токен на reasoning. Порог 0.2 не отделяет неуверенность. ### B. Правильный сигнал: margin (top1-top2) в лог-скорах — РАБОТАЕТ Margins реально колеблются: 36% пиков margin<0.3 ("монетка"), остальные >1.5. Обрыв: margin < p_min*3.0. n_min=1 (иначе короткие цепочки стираются). Подбор порога (код LRU 4096, thinking ON): | p_min (margin*3) | проверено/шаг | принято/шаг | tok/s | |---|---|---|---| | 0 (статика n7) | 7.4 | 2.77 | 13.90 | | 0.15 | 4.0 | 2.79 | 16.92 | | 0.2 | 3.8 | 2.64 | 16.00 | | 0.3 | 3.4 | 2.71 | 17.10 | | **0.35** | **3.1** | **2.83** | **18.26** | | 0.4 | 3.0 | 2.61 | 17.21 | Оптимум 0.35: +31% скорости от статики, acceptance вырос до 0.638 (не упал!), проверено всего 3.1/7. Это ВЫШЕ жёсткого n3 (17.30) и MTP-n3 (16.31). Динамика умнее жёсткой обрезки: где селектор уверен — цепочка идёт дальше, где колеблется — рвётся. ### C. Проза (эссе 3050, thinking ON) — главный выигрыш | Режим | acceptance | mean len | tok/s | |---|---|---|---| | Статика n7 | 0.288 | 3.02 | 15.27 | | **Динамика p_min 0.35** | **0.769** | **4.07** | **24.61** | acceptance x2.7, скорость +61%. Проза больше НЕ провал DFlash — динамика решает "почему на прозе мало": цепочка рвётся ровно там, где селектор колеблется, и не платит за мусорный хвост verify. ### Режимы (итог) - кодинг, без reasoning: DFlash-n7 + `--reasoning off` → 27.7 t/s - **с мышлением (дефолт): DFlash-n7 + p_min 0.35 → 18.26 (код) / 24.61 (проза)** - комбо не нужно: динамика сама адаптируется к тексту Код: common/speculative.cpp, цикл селектора DFlash2 — обрыв по margin (p_min*3.0), debug CHAIN печатает m= и p=. Сборка build-p40-ring. ## Test 14 — DFlash на Qwen3.6-35B-A3B (MoE) Пользовательская модель: Qwen_Qwen3.6-35B-A3B-Q4_K_M.gguf (qwen35moe, 3B активных из 35B). DFlash-драфта v2 под неё нет — сконвертирован официальный z-lab/Qwen3.6-35B-A3B-DFlash (DFlash v1, 6 dense-слоёв, hidden 2048, block_size 16, target_layer_ids [1,6,11,16,22,27,32,37] = 8 слоёв таргета, fc [2048,16384]). Конвертация (Luce server/scripts): ``` caмa: python3 convert_dflash_to_gguf.py draft-hf/model.safetensors draft-a3b-f16.gguf python3 quantize_dflash_draft.py draft-a3b-f16.gguf draft-a3b-q8_0.gguf --scheme q8_0 ``` Результат: draft-a3b-q8_0.gguf (410MB, 43 tensor q8_0 + 26 as-is). Прошла без правок кода форка — форк загрузил qwen35moe таргет + dflash v1 драфт. Замеры (P40, thinking ON, dynamic 0.35): | Тест | acceptance | mean len | tok/s | |---|---|---|---| | Код LRU 4096 | 0.584 | 3.60 | **55.79** | | Проза эссе 3050 | 0.483 | 2.65 | **46.51** | acc per pos (код): (0.813, 0.550, 0.402, 0.312, 0.228, 0.169, 0.128) acc per pos (проза): (0.751, 0.417, 0.221, 0.118, 0.076, 0.045, 0.022) Сравнение с 27B-UD: - код: 27B 27.69 t/s vs A3B **55.79** (x2.0) — MoE таргет дешевле в verify - проза: 27B динам. 24.61 vs A3B **46.51** (x1.9) Вывод: спекуляция на MoE-модели (A3B) в 2 раза эффективнее, чем на dense-27B, потому что verify-батч (8 токенов) считает только 3B активных параметров. DFlash-v1 (без селектора v2) тоже работает с dynamic-обрезанием (margin из top-k). Для полноты — был бы DFlash2-драфт под A3B, gains ещё выше. Команда (динамика 0.35): llama-server -m Qwen_Qwen3.6-35B-A3B-Q4_K_M.gguf -md draft-a3b-q8_0.gguf --spec-type draft-dflash --spec-draft-n-max 7 --spec-draft-n-min 1 --spec-draft-p-min 0.35 ... ## Test 15 — A3B: DFlash vs MTP vs AR (полная таблица) P40, thinking ON, MoE-таргет Qwen3.6-35B-A3B-Q4_K_M (3B активных). DFlash = наша конвертация z-lab v1 + dynamic 0.35; MTP = вшит в таргет-GGUF. Код LRU 4096: | Режим | acceptance | mean | tok/s | vs AR | |---|---|---|---|---| | AR baseline | - | - | 52.05 | 1.00x | | DFlash n7 dyn 0.35 | 0.584 | 3.60 | 55.79 | 1.07x | | DFlash n12 dyn 0.35 | 0.541 | 4.11 | 53.98 | 1.04x | | MTP вшит n3 | 0.662 | 2.99 | 68.37 | 1.31x | Проза эссе 3050: | Режим | acceptance | mean | tok/s | vs AR | |---|---|---|---|---| | AR baseline | - | - | 52.72 | 1.00x | | DFlash n7 dyn 0.35 | 0.483 | 2.65 | 46.51 | 0.88x | | MTP вшит | 0.497 | 2.49 | 57.61 | 1.09x | Выводы: 1. На A3B **MTP (вшит) — лучший режим**: код 68.4 (+31% AR), проза 57.6 (+9%). Дёшево: MTP-голова вшита в MoE-таргет, verify почти бесплатный. 2. DFlash v1 на A3B слабый: на коде +7% (55.8), на прозе МЕДЛЕННЕЕ AR (46.5 vs 52.7). v1 БЕЗ селектора (последоват. argmax) на свободном тексте не окупает даже дешёвый verify. Для A3B нужен DFlash2-драфт (с селектором), его нет нигде. 3. n_max=12 НЕ помогает: mean 4.11 выше, но tok/s 53.98 < n7 55.79 (блок 12 масок дороже драфтить, хвост 8-12 не окупается). Оптимум n7. 4. Пользователю: на 35B-A3B лучше всего собственный MTP (без -md, без драфта) — `--spec-type draft-mtp --spec-draft-n-max 3`. DFlash-драфт v1 держать как запасной для кода, где чуть быстрее AR (55.8 vs 52.0). Вопрос "динамика для MTP": стандартный флаг УЖЕ есть — impl_draft_mtp в speculative.cpp:1760 делает `if (cur_p->data[0].p < params.p_min) break;` — обрыв по вероятности токена (не по margin как DFlash). Работает через `--spec-draft-p-min`. Проверено по коду, не декоративно. ## Test 16 — MTP динамика (n_max=7 + p_min 0.3) на A3B — ДЕКОРАТИВНА Проверка: даёт ли MTP сам себе длину через --spec-draft-p-min (обрыв по вероятности токена в impl_draft_mtp, speculative.cpp:1760). Код LRU 4096: | Режим | acceptance | mean len | проверено/шаг | принято/шаг | tok/s | |---|---|---|---|---|---| | MTP n3 (base) | 0.662 | 2.99 | ~3.0 | 2.63 | 68.37 | | MTP n7 p_min0.3 | 0.454 | 3.67 | ~5.8 | 2.63 | 46.97 | Вывод: p_min 0.3 у MTP НЕ режет цепочку на коде — проверено 5.8/шаг вместо ~3.0, принято столько же (2.63). Причина та же, что у softmax-динамики DFlash: probability(top-1) у MTP почти всегда > 0.3 ("уверенно неправ" на позициях 3-7, где acceptance 0.31/0.25/0.18/0.14). Порог по p не отделяет мусорный хвост. Итог: для MTP жёсткий --spec-draft-n-max 3 оптимален (68.4 на A3B, 23.6 на 27B). Динамика по p бесполезна и для DFlash (margin - рабочая, p - нет), и для MTP (p не режет). Единственная реально работающая авто-длина - наш margin-обрыв в DFlash2 (Test 13). ## Test 17 — margin-динамика для MTP [DFLASH-DYNAMIC MTP] + сводная таблица режимов После Test 16 добавлен margin-обрыв и для MTP (speculative.cpp:1759-1774): вместо порога по p0 обрыв по **запасу p0-p1** (margin = p0, если один кандидат; margin = p0-p1 иначе; обрыв при margin < p_min). Порог берётся из --spec-draft-p-min, но применяется к ВЕРОЯТНОСТНОМУ запасу (0..1), а не к логам-скорам. n_min=1 обязателен, иначе короткие цепочки стираются. Замеры (A3B, код LRU 4096; сравнить: MTP-n3 base 68.37, n7 p_min0.3 46.97, n7 margin0.3 63.60, n7 margin0.15 58.15): | Режим | проверено/шаг | принято/шаг | acceptance | mean | tok/s | |---|---|---|---|---|---| | MTP n3 (жестко) | ~3.0 | 2.63 | 0.662 | 2.99 | 68.37 | | MTP n7 + p_min0.3 (старый p) | ~5.8 | 2.63 | 0.454 | 3.67 | 46.97 | | MTP n7 + margin0.15 | ~4.1 | 2.69 | 0.654 | 4.11 | 58.15 | | MTP n7 + margin0.3 | ~3.3 | 2.62 | **0.793** | 4.09 | 63.60 | | MTP n7 + margin0.3 (проза) | - | 1.24 | 0.669 | 2.79 | 52.94 | Вывод по margin-MTP: в отличие от p-порога, margin РЕАЛЬНО режет цепочку (проверено 5.8 -> 3.3-4.1). На коде margin0.3 даёт acceptance 0.793 (против 0.662 у n3) при том же принятом — режет «монетки», тянет где уверен. НО на A3B жёсткое n3 всё равно быстрее (68.4 vs 63.6): декод блока n7 дороже, а дешёвый verify не окупает длинный хвост. Margin-MTP бессмысленен на A3B, где verify почти бесплатен; он ценен только для 27B (дорогой verify), где стоит проверить как 27B+MTP+margin0.3. ### СВОДНАЯ ТАБЛИЦА эффективных режимов (все цифры tok/s, temp 0) **Qwen3.8-27B-UD (dense, дорогой verify ~169ms/шаг).** | Режим | код | проза | примечание | |---|---|---|---| | AR (без спека) | ~13.2 | ~13.2 | база | | DFlash2 n7 статика | 13.90 | 15.27 | хвост 4-7 мусор на reasoning | | DFlash2 n7 + margin0.35 | **18.26** | **24.61** | лучшая авто-длина (+31%/+61%) | | DFlash2 + --reasoning off | 27.69 | - | код без мышления, жёсткий n7 | | MTP n3 (off/on) | 23.61 / 16.31 | - | вшит, дёшево; on меньше теряет | | DFlash2 Count-500 | 40.41 | - | структурный текст (числа) | **Qwen3.6-35B-A3B (MoE, 3B активных, дешёвый verify).** | Режим | код | проза | vs AR код | |---|---|---|---| | AR (без спека) | 52.05 | 52.72 | 1.00x | | DFlash v1 n7 + dyn0.35 | 55.79 | 46.51 | 1.07x | | DFlash v1 n12 + dyn0.35 | 53.98 | - | 1.04x (блок 12 не окупается) | | MTP вшит n3 | **68.37** | **57.61** | 1.31x — ЛУЧШИЙ на A3B | | MTP n7 + margin0.3 | 63.60 | 52.94 | acceptance 0.793, но медленнее n3 | | MTP n7 + p0.3 (декор) | 46.97 | - | p не режет | ### Итоговые рекомендации - **A3B**: `--spec-type draft-mtp --spec-draft-n-max 3` (без p_min, без margin) = код 68.4 / проза 57.6. verify дёшев, жёсткое короткое n3 — оптимум. - **27B dense**: `--spec-type draft-dflash --spec-draft-n-max 7 --spec-draft-n-min 1 --spec-draft-p-min 0.35` = код 18.26 / проза 24.61 (margin-динамика, Test 13). Без мышления: `--reasoning off` -> 27.7 на коде. - **Куда копать дальше**: 27B + MTP + margin (тест не гонялся; у 27B дорогой verify, длинная точная MTP-цепочка могла бы дать больше, чем жёсткое n3). ## Test 18 — 27B + MTP + margin 0.3 (reasoning ON) — margin-MTP впервые РАБОТАЕТ Проверка единственного неиспытанного режима: 27B (дорогой verify) + MTP-n7 + margin (вероятностный запас p0-p1, --spec-draft-p-min 0.3). Код LRU, temp 0, reasoning ON (дефолт). Сравнение с Test 12/13 (тот же промт): | Режим (27B, reasoning ON, код LRU) | acceptance | tok/s | |---|---|---| | MTP-n3 жёстко (Test 12) | 0.485 | 16.31 | | **MTP-n7 + margin0.3 (Test 18)** | **0.592** | **17.81** | | DFlash2-n7 статика (Test 13A) | 0.253 | 13.90 | | DFlash2-n7 + margin0.35 (Test 13B) | 0.638 | **18.26** | Замер: микробенч 512ток = 16.71 t/s (draft 464, accept 274/0.591); полный 4096ток = 17.81 t/s (draft 4259, accept 2522/0.592). Вывод: 1. **margin-MTP впервые даёт реальный прирост на 27B**: +9.2% к жёсткому MTP-n3 (17.81 vs 16.31), acceptance вырос 0.485 -> 0.592. Причина: на A3B MTP-голова не тянет дальше 2-3 и verify дёшев (нечего экономить), а на 27B дорогой verify окупает длинную уверенную цепочку — margin режет «монетки» и тянет где уверен. margin-MTP бессмыслен на A3B, но полезен на 27B. 2. НО DFlash2+margin0.35 (18.26) всё ещё впереди MTP+margin (17.81) на 2.5%: DFlash2-селектор даёт более точную длинную цепочку, чем MTP-голова на коде. 3. Итог для 27B код (reasoning ON): best = DFlash2+margin0.35 (18.26), второй = MTP+n7+margin0.3 (17.81), оба бьют жёсткое n3 (16.31). Команда (27B + MTP + margin): llama-server -m Qwen3.8-27B-UD-Q4_K_XL.gguf --spec-type draft-mtp --spec-draft-n-max 7 --spec-draft-n-min 1 --spec-draft-p-min 0.3 (reasoning ON, без -md; margin из --spec-draft-p-min применяется к p0-p1 по коду speculative.cpp:1759-1774) ## Test 19 — Luce dflash_server + DDTree (прямой тест движка на P40) Проверка самого Luce-движка (./server/build/dflash_server с --ddtree) на твоих моделях, вместо разговоров о переносе. Обе модели = arch qwen35/qwen35moe (принимаются gguf_target_loader). Обе на 40 P40. LRU-код, temp 0, thinking off. ### 19A. Dense 27B (Qwen3.8-27B-UD + DFlash2-lucebox) — РАБОТАЕТ, рекорд Команда: dflash_server Qwen3.8-27B-UD-Q4_K_XL.gguf --draft Qwen3.8-27B-DFlash2-q8_0.gguf --ddtree --ddtree-budget 22 --fa-window 2048 -ctk q8_0 -ctv q8_0 --max-ctx 8192 Драфт DFlash2-селектор сработал: [draft GGUF] DFlash 2 selector enabled rank=256 top_k=16. LRU 512 ток: 19.48 t/s, avg_commit 5.33, acceptance 54.2%. LRU 4096 ток: 21.46 t/s, avg_commit 5.97, acceptance 62.1%, 686 verify-steps. | Режим (27B dense, код LRU 4096, thinking off) | tok/s | avg_commit/AL | acceptance | |---|---|---|---| | llama.cpp AR | ~13.2 | - | - | | llama.cpp DFlash2 статика n7 | 13.90 | 2.77 | 0.253 | | llama.cpp DFlash2 + margin0.35 | 18.26 | ~3.1 | 0.638 | | **Luce DFlash2 + DDTree budget22** | **21.46** | **5.97** | **0.621** | Вывод: Lucе DDTree на P40 даёт +17.5% к лучшему llama.cpp маржину (21.46 vs 18.26), +63% к AR. Ключ — ДЕРЕВО: avg_commit 5.97 (почти 6 токенов/шаг) против ~3 у цепочки. Подтверждает info.md: DDTree поднимает AL за счёт спасения ветвей (древовидного verify). Prefill small: 43 ток за 1.6с (26 tok/s — kernel-launch dominated, не показательно). ### 19B. A3B MoE (Qwen3.6-35B-A3B + draft-a3b-q8_0) — НЕСОВМЕСТИМ, 0.7% acceptance Команда: dflash_server Qwen_Qwen3.6-35B-A3B-Q4_K_M.gguf --draft draft-a3b-q8_0.gguf --ddtree --ddtree-budget 22 ... Лог: [draft] drafter target_layer_ids invalid (n=8, slots=5); keeping derived capture layers LRU 512 ток: 11.05 t/s, avg_commit 1.12, acceptance 0.7% (54/7328) — спек сломан. Вывод: наш конвертированный draft-a3b (DFlash v1, 8 target_layers, 6 dense-слоёв) НЕ совместим с Lucе DDTree, который жёстко ждёт 5-слойный z-lab драфт (target_layer_ids=5). Поэтому Lucе A3B даёт 11 t/s (почти AR без выигрыша). Для A3B лучший режим остаётся llama.cpp MTP-n3 (68.37 код). НЕ переносить DDTree-сервер на A3B с этим драфтом. ### Итог Test 19 - DDTree-дерево реально выигрывает на dense 27B (+17.5% к маржину, AL ~6 vs ~3) — это главный незадействованный потенциал. Стоит перенести ДЕРЕВО (ddtree.{h,cpp} + tree-verify) в llama-dflash3-pr порт, а не standalone-сервер. - На A3B Lucе бессмысленен (драфт несовместим); A3B = MTP-n3 на llama.cpp. ## Test 20 — Lucе DFlash2 CHAIN (без DDTree) — цепочка быстрее дерева на коде! КОНТРОЛЬНЫЙ эксперимент: тот же Lucе 27B + DFlash2, но БЕЗ --ddtree (чистая цепочка), чтобы отделить вклад дерева от вклада самого Lucе-движка. Код LRU, temp 0, thinking off, 4096 (модель сама остановилась на 3219, finish=stop). | Режим (Luce 27B, код LRU) | tok/s | avg_commit | acceptance | steps | |---|---|---|---|---| | Lucе DFlash2 CHAIN (no ddtree) | **32.02** | **6.09** | **0.761** | 529 | | Lucе DFlash2 + DDTree budget22 | 21.46 | 5.97 | 0.621 | 686 | | llama.cpp DFlash2 + margin0.35 (thinking ON) | 18.26 | ~3.1 | 0.638 | - | ШОКИРУЮЩИЙ вывод: на КОДЕ (высоко-предсказуемом) Lucе CHAIN (32 t/s) в 1.5x БЫСТРЕЕ дерева (21.46)! Причина: на структурном тексте цепочка длиной ~6 уже почти вся принимается (acc 76%, avg_commit 6.09), а дерево тратит budget на ветвления, которые не окупаются. Дерево выигрывает на НЕпредсказуемом тексте, где ветвления спасают принятие; на коде это лишние вызовы. Также Lucе chain (32) >> наш llama.cpp маржин (18.26): разрыв 1.7x. НО это МИШАС сравнения: наш маржин был с thinking ON, Lucе — thinking off (наш llama.cpp DFlash2 n7 + --reasoning off давал 27.69, Test 12). Нужен apples-to-apples: наш DFlash2 chain с thinking OFF vs Lucе chain 32. Lucе цепь уже умеет адаптивность через --verify-width (trimmed per step by drafter confidence) - аналог нашего margin. --ddtree-tau - для дерева. Итог: для кода лучший режим у Lucе - CHAIN (32 t/s), НЕ дерево. Дерево стоит тестить на прозе/непредсказуемом, где acceptance ниже и ветвления окупаются. ## Test 21 — ЧЕСТНОЕ сравнение: наш llama.cpp DFlash2 margin vs Lucе (thinking OFF) Исправление мишаса Test 20: наш маржин был с thinking ON (18.26), Lucе - off. Прогон нашего llama.cpp DFlash2 + margin 0.35 + --reasoning off на том же коде-промпте (модель сама останавливается: у нас 2405 ток., у Lucе 3219; тот же промпт, скорость валидна на 2-3K ток.). | Режим (27B, код LRU, thinking OFF) | tok/s | acceptance | steps | |---|---|---|---| | **наш llama.cpp DFlash2 + margin0.35** | **29.86** | **0.850** | - | | Lucе DFlash2 chain | 32.02 | 0.761 | 529 | | Lucе DFlash2 + DDTree budget22 | 21.46 | 0.621 | 686 | ГЛАВНЫЙ ВЫВОД: разрыв на самом деле НЕ 1.7x, а всего ~7%! Наш llama.cpp маржин (29.86) почти догнал Lucе chain (32.02), и наш acceptance даже выше (0.85 vs 0.76). Прежние 18.26 были с thinking ON - нечестная база. При thinking OFF наш маржин - 29.86, на уровне Lucе. Разрыв 7% объясняется НЕ деревом, а имплементацией Lucе (fast-rollback + fused-граф verify + --verify-width confidence-trim), а не качеством драфта. DTree же на коде не помогает (не окупает ветвления), и даже медленнее. Дерево стоит тестить на прозе/непредсказуемом тексте, где acceptance низкий и ветвления реально спасают. На коде (структура) цепочка эффективнее. Далее: проверить DTree на прозе (где он теоретически выигрывает), и/или выжать оставшиеся 7% до Lucе (verify-width аналог, fast-rollback в наш порт). ## Test 22 — DTree на прозе НЕ выигрывает (и Lucе в целом проигрывает нашему маржину) Проверка гипотезы из Test 20/21: дерево должно спасать на непредсказуемом тексте (проза), где acceptance низкий. Lucе 27B + DDTree budget22, проза 1024, thinking off: | Режим (27B, проза 1024, thinking OFF) | tok/s | avg_commit | acceptance | |---|---|---|---| | Lucе DFlash2 chain | 11.97 | 3.29 | 0.287 | | Lucе DFlash2 + DDTree budget22 | 11.98 | 3.29 | 0.287 | | (сравнение) наш llama.cpp DFlash2 mar gin0.35, thinking ON (Test 13C) | 24.61 | 4.07 | 0.769 | ВЫВОД: на прозе Lucе DDTree и chain ИДЕНТИЧНЫ (~12 t/s, avg_commit 3.29, acceptance 28.7%) - дерево НЕ спасает. Причина: на свободном тексте acceptance этого DFlash2-драфта низкий на всех ветвях (28%), ветвления не окупают доп. декод. Lucе в целом на прозе (12 t/s) сильно проигрывает нашему llama.cpp маржину (24.61) - 2x. Итоговый вывод по всей линейке (Test 19-22): - НАШ llama.cpp DFlash2 + margin 0.35 - самый универсальный и сильный: код 29.86 (off) / 18.26 (on), проза 24.61 (on). Лучшая авто-длина. - Lucе chain: код 32.02 (off) - на 7% быстрее нашего, но проза 11.97 (off) - в 2x хуже нашего маржина. - Lucе DDTree: НЕ даёт выигрыша ни на коде (21.46 < chain 32), ни на прозе (11.98 = chain). Дерево для этого драфта/текстов неэффективно. - margin+дtree: бессмысленно - дерево само по себе не помогает нигде; наш margin (тop1-top2 в логах селектора) сильнее, чем ddtree-tau Lucе (кумулятивный log-prob) для этого DFlash2. Финал: остаётся выжать ~7% до Lucе chain на коде (fast-rollback / fused-verify) ИЛИ распространить наш маржин-подход на более длинные цепочки. Дерево не нужно. ## Test 23 — ЧЕСТНАЯ проза (thinking OFF): наш маржин обгоняет Lucе Устранение последнего мишаса: наш 24.61 на прозе был с thinking ON, а Lucе - с off (несопоставимо). Прогон нашего llama.cpp DFlash2 + margin 0.35, --reasoning off, тот же эссе-промпт, 1024 ток. | Режим (27B, проза 1024, thinking OFF) | tok/s | acceptance | |---|---|---| | **наш llama.cpp DFlash2 + margin0.35** | **16.38** | **0.505** | | Lucе DFlash2 chain | 11.97 | 0.287 | | Lucе DFlash2 + DDTree | 11.98 | 0.287 | Вывод: наш llama.cpp маржин на прозе (16.38) в 1.37x быстрее Lucе (11.97)! Наш acceptance тоже выше (0.505 vs 0.287). margin работает на прозе: режет мусорный хвост длинной цепочки, чего у Lucе нет (ucе уперся в 12 t/s). ОТВЕТ НА "ПОЧЕМУ МЫ НЕ ДОХОДИЛИ ДО 30": Мы ДОХОДИЛИ на коде! Наш DFlash2 margin = 29.86 (thinking OFF), просто раньше мерили с thinking ON (18.26) - дефолт. С --reasoning off наш порт даёт 29.86, почти как Lucе chain 32. Так что 30+ НЕ из-за Lucе-движка, а из-за отсутствия reasoning текста в промпте. ГДЕ Lucе-движок реально быстрее: только на коде +7% (32 vs 29.86) - цена fast-rollback + fused-verify + verify-width. На прозе наш маржин сильнее (16.38 vs 11.97) - Lucе-цепочка не умеет резать хвост как наш margin. Полная честная картина (thinking OFF): | | код | проза | |---|---|---| | наш llama.cpp DFlash2 + margin0.35 | **29.86** | **16.38** | | Lucе chain | 32.02 | 11.97 | | Lucе DDTree | 21.46 | 11.98 | Наш маржин универсальнее: код почти паритет (7% слабее), проза на 37% сильнее. ## Test 24 — ⚠️ ОШИБКА ИСПРАВЛЕНА: Lucе "thinking ON" был БЕЗ мышления Ранняя версия этого теста утверждала, что Lucе на 69% быстрее на reasoning. Это было НЕВЕРНО: мой запрос к Lucе с "thinking ON" на самом деле дал reasoning_tokens=0 (Lucе не включил мышление по умолчанию), т.е. это был тот же Lucе-БЕЗ-мышления, что и 32 t/s — просто короткая порция (1024 ток). Честный тест: Lucе с явным "thinking":{"type":"enabled"}: [budget-hook] spec-decode close at committed=41/1024 (remaining=1024 <= hard_limit=4096) reasoning_tokens=1, spec_decode_ran=True, decode 13.6 t/s (~AR) -> Lucе при реальном мышлении СПЕК УБИВАЕТСЯ budget-hook'ом (как в Test 4): force-close на раннем шаге, падает в AR (13.3 t/s). ЧЕСТНАЯ КАРТИНА (27B код, реальное мышление): | Режим | реально думал? | tok/s | |---|---|---| | наш llama.cpp DFlash2 + margin0.35 | ДА (reasoning_tokens>0) | 18.26 (4096) / 17.28 (1024) | | Lucе chain "thinking ON" (мой ошибочный) | НЕТ (reasoning_tokens=0) | 29.14 (это БЕЗ мышления) | | Lucе chain реально thinking enabled | бюджет-хук убил спек | 13.3 (~AR) | ГЛАВНЫЙ ВЫВОД: наш llama.cpp с реальным мышлением (18.26) БЫСТРЕЕ Lucе с реальным мышлением (13.3)! Прежний "разрыв 1.7x" в пользу Lucе был артефактом моей ошибки (сравнил Lucе-без-мышления с нашим-с-мышлением). resoning-текст ≈ проза по характеру (непредсказуем): наш маржин держит на нём нормально, а Lucе там вообще проигрывает (спек убит бюджетом). Переносить verify-width на основе этого теста НЕ нужно — мотивации больше нет. (Сохраняю модель arch: Qwen3.8-27B-UD = qwen35 ГИБРИД с Gated DeltaNet (qwen35.ssm.* веса, build_inp_mem_hybrid) - как Qwen3.5-27B из info.md. Значит persist-ядро DeltaNet и fast-rollback ПРИМЕНИМЫ к ней.) ## Test 25 — КВАНТОВАНИЕ ДРАФТА: Q4-mix ЛУЧШЕ Q8, Q2 чуть медленнее (код 1024, reasoning OFF, DFlash2 margin 0.35, наш порт llama.cpp, P40) Проблема: z-lab Q2_K GGUF (arch=dflash, маппинг mask_token через канон) не инициализируется у нас -> "invalid or missing mask_token_id", спек НЕ работает, чистый AR 13.13 t/s. Это ошибка ФОРМАТА (z-lab отличается от нашего qwen35-dflash-draft), не квантования. Решение: сквантовали ИЗ НАШЕГО f16 (convert_dflash_to_gguf.py из Qwen3.8-27B-DFlash2-src/model.safetensors) -> llama-quantize Q2_K (наш формат, mask корректен). И quantize_dflash_draft.py --scheme q4-mix. ПОЧЕМУ не через quantize_dflash_draft.py --scheme q2-k: в python-gguf (gguf.quants) реализовано КВАНТОВАНИЕ только для Q4_0/Q4_1/Q5_0/Q5_1/Q8_0/ BF16; для K-quants (Q2_K/Q3_K/Q4_K) есть ТОЛЬКО dequantize_blocks (чтение), quantize_blocks бросит NotImplementedError. Q2 доступен лишь через llama-quantize (C++). q4-mix работает потому что Q4_0 реализован в python. Результаты (код 1024, our DFlash2 margin 0.35, reasoning off): | Драфт | Размер | tok/s | acceptance | mean len | |-----------------|--------|-------|------------|----------| | Q8 (lucebox) | 2.0 GB | ~29-30| 0.85 | 5.27 | | Q4-mix (self) | 1.2 GB | 30.26 | 0.827 | 5.25 | | Q2_K (self) | 0.87GB | 27.08 | 0.763 | 4.69 | | Q2 z-lab (broken)| 0.7 GB| 13.13 | (спек off) | - | ВЫВОД: q4-mix = ЛУЧШИЙ (30.26 t/s > Q8, acceptance 0.827, 1.2GB). Q2_K приемлем (27 t/s, 865MB) только если критична VRAM. Q8 и Q4-mix почти не различаются по acceptance; Q2 теряет ~10% скорости из-за Q2 на head селектора. Для P40-скорости юзаем q4-mix. ## Test 26 — n_max SWEEP + DFlash2 блочная генерация (ВЫВОДЫ) q4mix-драфт, код 1024, reasoning OFF, DFlash2 margin p-min 0.35. n_max свип (tok/s): | n_max | tok/s | acceptance | mean | |-------|-------|------------|------| | 4 | 27.77 | 0.861 | 4.04 | | 5 | 27.41 | - | - | | 7 | 30.26 | 0.827 | 5.25 | | 8 | 30.18 | 0.827 | 5.25 | -> ОПТИМУМ n_max=7 (30.26). Уменьшение НЕ помогает (теряем длинные цепочки), 8 не лучше (маржин и так режет на ~5.25, блок стоит дорого). ВАЖНАЯ ПРАВКА (мой прошлый ошибка): наш DFlash2 НЕ последовательный. draft_dflash::draft() (speculative.cpp 1198+) строит ОДИН noise-блок (id_last + n_max масок) и ДЕКОДИТ ЕГО ОДНИМ llama_decode (1233) - это блочная диффузия, как PR 27342 "decode every sequence's full noise block in one pass". Квадратичный chain_heads-цикл (1710) принадлежит MTP, НЕ DFlash. Маржин обрезает ПРИНЯТУЮ часть ПОСЛЕ декода блока (по lattice, 1311-1316) - нельзя "не генерить лишние": блочная диффузия даёт уверены fnость позиции i только после декода всего блока (state i зависит от масок 0..i-1). Стоимость draft = стоимость декода n_max+1-блока, не зависит от маржина. bottleneck: draft-decode 86% (4199ms) vs verify 14% (657ms) на коде 1024.