llama.cpp-DFlash2-pascal6-optimized / DFLASH2_RING_PORT.md
maxwelhelp
DFlash2 q4-mix draft for llama.cpp on Tesla P40 (Pascal)
7c97475
|
Raw
History Blame Contribute Delete
84.7 kB

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):

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.

Шумовой блок:

pq[i] = committed + i;   // абсолютные позиции noise-блока (q_len штук)

1.4 Attention-маски (критично! отличается от наивного ролла)

mask_full (полное-внимание слои), dflash_draft_kv.cpp:280-294:

для каждого слота 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:

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)

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+):

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<uint32_t>(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<llama_pos> 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 модель пишет <think>-блоки: внутренние рассуждения непредсказуемы, цепочка драфта экспоненциально затухает (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.