Newer
Older
navi-1 / NGRAM_TASK.md

N-gram таблицы для Qwen3.6-35B-A3B — передача задачи в новую сессию

Дата подготовки: 2026-11-05. Этот файл — вся контекстная база для продолжения работы в новой сессии. Кода для самих n-gram таблиц ещё не написано: сделана только инфраструктура (экспорт корпуса из Navi) и разведка хоста.


1. Задача

Выяснить, насколько n-gram таблицы, построенные по нашему собственному диалоговому корпусу, ускоряют генерацию на модели Qwen3.6-35B-A3B на хосте .135.

Считаем, что ответ получен, когда есть:

  1. собранная из корпуса n-gram таблица в формате, который llama.cpp грузит штатно (см. §3 — он это умеет, вопреки первому впечатлению);
  2. замер tg t/s и acceptance rate на фиксированном наборе независимых промптов в четырёх конфигурациях: без спекуляции, только MTP, только n-gram-корпус, MTP + n-gram-корпус;
  3. вывод в процентах по каждой конфигурации и решение, стоит ли это внедрять в Navi.

Не делаем: не патчим llama.cpp без нужды (см. §3), не трогаем прод Navi, не меняем модель/квант.


2. Что уже готово

2.1 Экспорт корпуса из Navi (задеплоено на прод)

Реализовано и проверено end-to-end: периодический снапшот диалогов, age-шифрование в одну сторону, HTTP под машинным bearer-токеном. Документация — docs/export.md, клиент — scripts/navi_export.py.

Что Где
Манифест GET http://192.168.1.168:8099/export/v1/manifest
Шифртекст GET http://192.168.1.168:8099/export/v1/messages (ETag/304)
Токен (0600) ~/.config/navi-export/token на .135
Приватный ключ age (0600) ~/.config/navi-export/key.txt на .135
Клиент + venv ~/.local/share/navi-export/navi_export.py, ~/.local/share/navi-export/venv
Корпус ~/ngram/messages.jsonl.gz на .135
Состояние выгрузки ~/ngram/messages.jsonl.gz.state.json

Текущий снапшот: content_id 7aab3192bcca, 23 914 строк, 1 189 сессий, пользовательских 5 672, ассистентских 18 242; шифртекст 4 223 188 Б; после распаковки — 17,5 МБ JSONL, из них ~14 МБ собственно текста.

Формат строки (gzip-JSONL, одна запись на строку):

{"v":1,"session":"…","seq":42,"role":"assistant","content":"…","ts":"2026-11-04T10:22:31+00:00"}

Порядок: по created_at, затем session_id, sequence_number. Роли по умолчанию — user и assistant; tool, system, summary/compression исключены на уровне SQL.

Повторное выкачивание идемпотентно: клиент сначала читает манифест и выходит с кодом 3, если content_id совпал. Обновление раз в 4 часа на стороне Navi.

Не сделано: cron на .135 (предлагалось */30 * * * *), поэтому сейчас корпус обновляется только вручную.

2.2 Хост .135

  • Bazzite 7.2.11 (Fedora 44 base, immutable), kernel 7.2.11; Python 3.14.8.
  • Intel i5-3470, 4 ядра; 19 ГБ RAM (модель 14 ГБ живёт в RAM).
  • AMD RX 580 2048SP через RADV/Vulkan, без ROCm; 8 ГБ VRAM.
  • llama.cpp: prebuilt build b11960 (~/llama/llama-b11960), исходников и тулчейна нет (cmake, ninja, Vulkan dev-заголовков не установлено; система immutable → собирать только в toolbox/distrobox или через rpm-ostree layering).
  • Модели в ~/models:
    • Qwen3.6-35B-A3B-MTP-UD-IQ2_M.gguf (11,88 ГБ)
    • Qwen3.6-35B-A3B-MTP-UD-IQ3_XXS.gguf (14,07 ГБ) ← рабочая
    • Qwen3.6-35B-A3B-UD-IQ3_XXS.gguf (13,21 ГБ)
    • LFM3-Omni-2B-* (не по теме)
  • Скрипты: ~/bench.sh, ~/mtp_bench.sh, ~/pp16k.sh, ~/ncm_sweep.sh, логи ~/bench_results.txt, ~/bench_iq2m.txt, ~/mtp_combo.log, ~/server_iq2m_combo.log.
  • Утилиты из сборки, которые понадобятся: llama-server, llama-cli, llama-tokenize (умеет -f FILE --ids --no-bos → список id в Python-совместимом виде).

2.3 Уже измеренные числа

Базовый бенч (~/bench.sh: -ngl 99 --n-cpu-moe 999 -fa on -ctk q8_0 -ctv q8_0 -t 4):

Квант pp512, t/s tg128, t/s
IQ3_XXS 124,47 6,47
IQ2_M 134,31 6,11
IQ3_XXS, чистый CPU 105,48 5,00

MTP-спекуляция (IQ3_XXS, -c 4096, temp 0, n_predict 128):

Конфигурация tg, t/s Δ
без спекуляции 8,49 —
draft-mtp, n_max=1 11,00 +29 %
draft-mtp, n_max=2 10,72 +26 %
draft-mtp, n_max=3 9,99 +18 %
draft-mtp, n_max=5 8,78 +3 %

Важно: базы 6,47 и 8,49 — из разных прогонов с разным контекстом; сравнивать можно только внутри одного прогона, поэтому в §4 все четыре конфигурации меряются одним скриптом.

Отдельно: в ~/mtp_combo.log и ~/server_iq2m_combo.log лежат цифры 27–33 t/s с acceptance 1.0 и средней длиной драфта 18–21 — это артефакт self-echo (модель повторяла промпт), а не реальный выигрыш. На эти числа опираться нельзя.


3. Главное открытие: llama.cpp УМЕЕТ грузить внешнюю n-gram таблицу

В прошлой разведке было сказано «все n-граммы выводятся из контекста, внешней таблицы нет». Это неверно. В b11960 на .135 (проверено по llama-server --help) есть:

--spec-type ... ,ngram-cache      одиннадцатый тип спекуляции
-lcs, --lookup-cache-static FNAME  статическая таблица (не пополняется генерацией)
-lcd, --lookup-cache-dynamic FNAME динамическая таблица (пополняется и сохраняется)

Это старый механизм «lookup decoding», доживший до текущего master. Патчить C++ не нужно. Нужно только собрать файл таблицы — утилиту llama-lookup-create, которая когда-то это делала, из репозитория убрали, поэтому таблицу строим сами (§3.3).

3.1 Как это работает внутри

common/speculative.cpp, impl ngram-cache. Для каждой последовательности держатся три таблицы common_ngram_cache — отображение n-грамм (до 4 токенов) → распределение следующего токена (token → count):

Таблица Источник Живёт
context промпт + сгенерированное в этом запросе сбрасывается на begin()
dynamic файл -lcd, пополняется context-таблицей сохраняется при выходе процесса
static файл -lcs — наш корпус read-only

Драфт идёт цепочкой до 8 токенов (n_max для этого типа захардкожен = 8). На каждом шаге:

  1. из context/dynamic берутся n-граммы длиной 1..4; кандидат принимается, только если он встречается и в static-таблице по 2-грамме (она работает как фильтр правдоподобия) и проходит пороги draft_min_sample_size_strict = {4,3,2,2} / draft_min_percent_strict = {75,66,66,66};
  2. если ничего не нашлось — пробуется чисто static по 2-грамме с порогами sum ≥ 2 и top ≥ 50 %;
  3. если и там пусто — драфт закончился.

Практические следствия, важные для плана:

  • static участвует только 2-граммами (LLAMA_NGRAM_STATIC = 2). Ключ в файле — 4 слота, у 2-граммы два последних равны LLAMA_TOKEN_NULL (-1). Значит 3- и 4-граммы в файле — мёртвый вес: они загружаются в память, но при поиске по ним не попадают. Собирать только 2-граммы.
  • Корпус напрямую даёт лишь слабый (биграммный) драфт; основной вклад static — валидация драфтов из контекста. Поэтому ждать «+30 % от корпуса» наивно; измерять надо честно.
  • Цепочка типов: --spec-type a,b пробует a, и если он дал драфт, b для этой последовательности уже не вызывается. Порядок = приоритет.

3.2 Формат файла таблицы

Файл — это дамп std::unordered_map, без заголовка и без счётчика записей. Нативные little-endian int32:

повторять до EOF:
  int32 tokens[4]       # 16 байт; неиспользованные слоты = -1
  int32 n_pairs         # > 0
  n_pairs × { int32 token, int32 count }   # count > 0

Загрузчик (common_ngram_cache_load) читает до EOF и падает на GGML_ASSERT, если формат не сойдётся: лишний байт в конце — abort. Порядок записей произвольный.

3.3 Как собрать таблицу из корпуса

Рекомендуемый путь — свой билдер на Python (≈80 строк), без сборки C++:

  1. распаковать ~/ngram/messages.jsonl.gz;
  2. токенизировать тем же GGUF, с которым потом будем обслуживать (Qwen3.6-35B-A3B-MTP-UD-IQ3_XXS.gguf) — либо батчами через llama-tokenize -m MODEL -f chunk --ids --no-bos, либо HTTP POST /tokenize у запущенного llama-server (быстрее: модель грузится один раз). Через процесс на каждый из 24 тыс. сообщений — не вариант: загрузка 14 ГБ модели не бесплатна;
  3. считать 2-граммы внутри каждого сообщения отдельно (границы сообщений не должны образовывать пары);
  4. записать файл в формате §3.2; распределения можно обрезать (top-k, например k=8) — на качество драфта это почти не влияет, а размер и память режет заметно;
  5. опционально: собрать варианты по подмножествам (ассистентские реплики отдельно; только длинные сообщения; только «в стиле Navi»).

Альтернатива без своего кода — «прогреть» dynamic-таблицу: запустить llama-server --spec-type ngram-cache -lcd cache.bin и прогнать корпус промптами; context-таблица каждого запроса вливается в dynamic, при выходе файл сохраняется. Минус: каждое сообщение требует генерации хотя бы одного токена, суммарно это часы. Оставляем как резервный вариант для проверки, что наш билдер даёт тот же файл, что и родной код.

3.4 Что обязательно проверить до написания билдера

  • Смоук: llama-server --spec-type ngram-cache -lcs <файл из пары записей> действительно стартует и не падает на загрузке (проверяет формат).
  • Память: модель IQ3_XXS занимает 14 ГБ из 19 ГБ RAM. unordered_map при загрузке раздувается в разы относительно файла; до старта замерить RSS на маленькой таблице и экстраполировать. При риске OOM — брать подвыборку корпуса (2–4 МБ) и/или top-k обрезку.
  • Совместимость словаря: таблица валидна только для той же модели/кванта (id токенов). Кванты IQ2_M и IQ3_XXS должны иметь одинаковый словарь, но это надо подтвердить, прежде чем переиспользовать таблицу между ними.

4. Протокол измерения

Все четыре конфигурации — одним и тем же скриптом, одним контекстом, одним набором промптов, с перемешиванием порядка:

# --spec-type Доп. флаги
A none —
B draft-mtp --spec-draft-n-max 1
C ngram-cache -lcs ~/ngram/ngram-cache.bin
D draft-mtp,ngram-cache (и ngram-cache,draft-mtp) оба набора флагов

Что мерить на конфигурацию:

  • tg t/s — токенов/с на генерации (главная метрика);
  • acceptance rate и средняя принятая длина драфта — из логов спекуляции; статистика печатается на уровне TRC (-lv 4 / --log-verbosity 4, строка statistics ... #acc rate/pos = (...)), поэтому сервер запускается с подробным логом, а из него берётся grep statistics;
  • pp — чтобы убедиться, что спекуляция не просадила prompt processing;
  • RSS процесса — цена таблицы.

Требования к промптам:

  • не брать промпты из корпуса-источника: иначе получаем ровно тот self-echo, который уже один раз всех обманул (acceptance 1.0);
  • набор: ~20 независимых промптов, похожих на реальные запросы к Navi (короткий вопрос, просьба на русском, код-задача, длинный инструктирующий промпт), каждый — 3 повтора, temp 0 и temp 0.7;
  • одинаковый chat template и системный промпт во всех конфигурациях;
  • сравнивать только внутри одного прогона: базы 6,47 и 8,49 t/s из разных скриптов между собой несопоставимы.

Ожидания, которые надо подтвердить или опровергнуть: static-биграммы по чату дадут мало; больше даст комбинация «контекст + static-фильтр» на повторяющемся boilerplate (системный промпт Navi, шаблоны tool-call). Полезный контрольный прогон — ngram-mod и ngram-map-k4v (чисто контекстные, корпус не нужен): если они уже дают +X %, то вклад именно корпуса = разница с ngram-cache.


5. Открытые вопросы к владельцу

  1. Объём таблицы. Весь корпус (17,5 МБ, ~4,5 млн токенов) при 19 ГБ RAM и 14 ГБ модели — риск OOM. Начинать с подвыборки (2–4 МБ) или сразу пробовать весь и мерить RSS?
  2. Состав корпуса. Только ассистентские реплики (18 242 из 23 914) или оба голоса? Второй вариант ближе к тому, что модель видит и генерирует.
  3. Приоритет. MTP уже даёт +29 % без всякого корпуса. Цель — выжать максимум (тогда это стопка типов и подбор порогов) или именно измерить вклад собственного корпуса (тогда MTP — контроль, а не конкурент)?
  4. Обновление таблицы. Пересобирать при каждом новом экспорте (раз в 4 часа) или по требованию? Пересборка — минуты, но требует свободной RAM.

6. Полезные команды и пути (без секретов)

# корпус на .135
ssh gstation@192.168.1.135
zcat ~/ngram/messages.jsonl.gz | wc -l          # 23914
zcat ~/ngram/messages.jsonl.gz | head -1

# как выкачивался (ключи уже на месте, значения не печатать)
~/.local/share/navi-export/venv/bin/python ~/.local/share/navi-export/navi_export.py pull \
  --url http://192.168.1.168:8099 \
  --token-file ~/.config/navi-export/token \
  --identity ~/.config/navi-export/key.txt \
  --out ~/ngram/messages.jsonl.gz
# 0 = обновлено, 3 = без изменений, 1 = ошибка

# токенизация
cd ~/llama/llama-b11960
./llama-tokenize -m ~/models/Qwen3.6-35B-A3B-MTP-UD-IQ3_XXS.gguf -f prompt.txt --ids --no-bos

# проверка, что сборка умеет нужное
./llama-server --help 2>&1 | grep -E "spec-type|lookup-cache"

Правила, которые действуют и в новой сессии: ответы по-русски; подход обсуждать до реализации; коммиты можно делать самому, push/деплой — только по явной команде; .env не читать и не печатать; на прод-БД не писать без явного разрешения; не убивать perf-инстанс на портах 8097/8088.


7. Чек-лист «задача закрыта»

  • таблица собрана и грузится без abort (-lcs стартует);
  • замерены A/B/C/D (плюс контроли E/F/G и свип MTP) на одном наборе
    промптов; по решению владельца остановились на 1 повторе, а не на 3;
  • acceptance rate и средняя длина драфта вытащены из логов, а не оценены;
  • self-echo исключён (промпты не из корпуса, acceptance < 1,0);
  • записаны RSS и размер файла таблицы;
  • вывод в процентах + рекомендация: внедряем / не внедряем.

8. Результаты (2026-11-05): n-gram таблицы закрыты

8.1 Инструменты

Что Где
Билдер таблицы scripts/ngram_build.py — Python, токенизация через /tokenize, только 2-граммы внутри сообщения, top-k, фильтры уверенности
Харнесс замеров scripts/ngram_bench.py — поднимает llama-server на конфиг, /apply-template (со схемами инструментов) + /completion с cache_prompt=false, считает tg/pp/acceptance из timings
Промпты (общие) scripts/ngram_prompts.json — 20 штук
Промпты (агентские) scripts/ngram_prompts_agentic.json — 12 код/терминал/ssh/правка файлов
Схемы инструментов scripts/ngram_tools_schema.json — реальные filesystem, terminal, ssh_exec, code_exec, todo

Сервер на .135: ~/llama/llama-b11960/llama-server, модель Qwen3.6-35B-A3B-MTP-UD-IQ3_XXS.gguf, флаги -ngl 99 --n-cpu-moe 999 -fa on -ctk q8_0 -ctv q8_0 -t 4.

8.2 Формат и цена таблицы — подтверждено

  • --spec-type ngram-cache -lcs <файл> стартует без abort, драфты идут.
  • Таблица из 4,2 МБ assistant-текста (35 % корпуса): 234 659 ключей, 389 319 пар, 7,8 МБ.
  • Загрузка в память: RSS 12 832 → 13 180 МБ, то есть +348 МБ, ≈ ×45 к размеру файла. Отсюда потолок: файл больше ~20 МБ не влезет.

8.3 Замеры

Общие промпты (5 шт., temp 0, 1 повтор, 256 токенов):

cfg конфигурация tg t/s accept
A без спекуляции 8,36 —
B MTP n_max=1 10,30 85 %
C ngram-cache + таблица 4,54 14 %
D MTP + ngram-cache + таблица 5,46 33 %
E ngram-cache без таблицы 4,76 16 %
F ngram-mod 8,35 —
G MTP + ngram-cache без таблицы 6,29 40 %

C ≈ E — вклад корпуса в пределах шума. Строгая таблица (только биграммы с total ≥ 10 и долей топа ≥ 80 %; осталось 1 989 ключей, 0,1 МБ) дала C = 4,67, D = 5,97 — без изменений.

Агентские промпты (3 шт., temp 0, 256 токенов):

cfg конфигурация tg t/s accept
A без спекуляции 8,36 —
B MTP n_max=1 10,22 86 %
H MTP n_max=2 10,27 80 %
I MTP n_max=3 8,99 71 %
J MTP n_max=5 7,92 57 %
K MTP + ngram-mod 10,15 85 %
L MTP + ngram-map-k4v 9,52 70 %
M ngram-map-k4v + MTP 9,54 71 %

8.4 Вывод

Не внедряем. Причины:

  1. static-таблица используется только 2-граммами (LLAMA_NGRAM_STATIC = 2), а биграммы корпуса дают принятие 14–16 %;
  2. сам механизм lookup-декодинга на этой машине стоит −43 % даже без всякой таблицы (E = 4,76 против A = 8,36): узкое место — CPU на MoE-экспертах, а цепочки до 8 токенов добавляют работу почти без принятия;
  3. в стопке с MTP ngram-типы только портят (K/L/M ≤ B).

Оставляем MTP, n_max=1: +23 % на tg на агентских промптах.

8.5 Где запас на самом деле

Модель: RSS 13,2 ГБ, из них 12,8 ГБ — mmap-страницы GGUF (Private_Clean), free показывает их в buff/cache, а не в used. VRAM — 3,2 ГБ из 8: на карте только attention/dense, эксперты (~10,7 ГБ) считают 4 ядра через DDR3. Отсюда следующий шаг: --n-cpu-moe < 999, перенос части экспертных слоёв в VRAM (полоса ~224 ГБ/с против ~25 ГБ/с). Цель — 128k контекста (KV при q8_0 ≈ 1,33 ГиБ, n_ctx_train = 262144).

Поправка (2026-11-05, §9.2): гипотеза о полосе DDR3 как узком месте измерением опровергнута. Хост ограничен вычислениями, а не памятью; перенос экспертов в VRAM делает работу медленнее в любой раскладке.


9. Тюнинг хоста .135: что работает и что нет (2026-11-05)

Все числа ниже сняты харнессом из §8.1 на агентских промптах (ngram_prompts_agentic.json, temp 0, n_predict 256), кроме явно оговорённых. Сравнивать можно только строки внутри одной таблицы.

9.1 Узкое место — арифметика на CPU, а не память и не PCIe

scripts/ngram_bench.py + прямой замер (scripts/ngram_busy_probe.py: читает /proc/<pid>/stat и gpu_busy_percent во время генерации):

показатель значение
governor / частоты performance, 3392 МГц на всех 4 ядрах
CPU во время tg 351 % из 400 %
GPU busy во время tg 20 % (min 0, max 100)
VRAM во время tg 3114 МБ, не меняется
инструкции CPU avx f16c sse4_2 — без AVX2 и без FMA

CPU загружен почти в потолок, видеокарта простаивает четыре пятых времени. Сборка уже подгружает libggml-cpu-ivybridge.so, то есть лучшее ядро под этот процессор используется; пересборка llama.cpp не добавит инструкций, которых у Ivy Bridge нет. Это же объясняет, почему внешние референсы на GTX 1080 дают ~24 t/s: там сопоставимая карта, но процессор с AVX2.

9.2 Перенос экспертов в VRAM — закрыто, стало медленнее

Две независимые раскладки, обе проигрывают.

А. Только экспертные тензоры (--n-cpu-moe N, attention остаётся на GPU):

--n-cpu-moe слоёв экспертов на GPU tg t/s pp t/s VRAM МБ
999 (все в RAM) 0 10,36 / 10,23 89 / 97 3388
36 4 10,28 93,7 4509
32 8 7,75 / 7,66 61,4 / 61,4 5440
28 12 8,45 64,6 6538
24 16 9,94 68,3 7636
20 20 10,04 / 9,42 85,7 / 83,3 8147

Провал в середине диапазона воспроизведён дважды, pp на -n-cpu-moe 32 совпал до сотых (61,38 в двух прогонах). Раскладка действительно менялась (121 → 61 CPU-буфер), ошибки в логе отсутствуют.

Б. Целые слои (-ngl N --n-cpu-moe 0), чтобы убрать round-trip по PCIe внутри слоя:

-ngl что на GPU tg t/s pp t/s VRAM МБ
99 только attention (эталон) 10,10 88,4 3110
4 + 4 целых слоя 5,55 48,2 2075
8 + 8 целых слоёв 6,19 50,3 3362
12 + 12 целых слоёв 6,41 57,5 4538

Вывод: Vulkan/RADV на Polaris хорошо считает attention и плохо — MoE. GPU на этом хосте полезен ровно там, где уже используется. Дальнейшее «загрузить видеопамять» ухудшает результат.

9.3 Почему MTP не даёт обещанного

При принятии 0,83 теоретический потолок спекуляции 1 + α = 1,83×, фактически 1,21×. Причина в §9.1: на системе, ограниченной вычислениями, проверка батча из двух токенов стоит примерно вдвое дороже одного токена, и выигрыш остаётся только от дешёвого драфта. На системах, ограниченных полосой памяти, проверка почти бесплатна — там же и наблюдаются большие ускорения. Поэтому n_max=1 здесь не «недотюнинг», а потолок.

Проверено и закрыто: -t 3 хуже четырёх потоков на 11 % (9,04 против 10,10); конкуренция за общий L3 не подтвердилась.

9.4 Квант IQ2_M: +19 % скорости за +1,9 % perplexity

5 агентских промптов, одинаковые флаги, меняется только файл модели:

квант tg t/s pp t/s PPL (наш корпус)
IQ3_XXS (14,07 ГБ) 9,96 96,0 3,1846
IQ2_M (11,88 ГБ) 11,91 104,4 3,2441 (+1,9 %)

Быстрее на каждом из пяти промптов без исключения; на ag-code-fix оба прогона выдали ровно 90 токенов при одинаковой статистике драфта (44/46), то есть разница не в тексте.

Perplexity считалась llama-perplexity на 40 КБ ассистентских реплик из корпуса (~/ngram/ppl_corpus.txt), 5 окон по 2048; хуже у IQ2_M в каждом окне. Оговорка: ppl ловит потерю языка и почти не ловит потерю структуры — сломанный tool-call или испорченное рассуждение она не покажет. Для агентных задач 1,9 % — нижняя граница цены, а не полная.

Запись в §2.3 («IQ2_M 6,11 против IQ3_XXS 6,47») относится к другому скрипту и снята без MTP — для текущей конфигурации она неверна.

9.5 Батч префила: +71 % на приёме промпта

-ub/-b при -c 131072, IQ2_M, префил 13 283 токенов:

-ub / -b pp t/s время VRAM МБ
512 / 2048 (дефолт) 105,3 126 с 3799
1024 / 2048 151,5 88 с 4294
2048 / 4096 179,6 / 179,4 74 с 5073
4096 / 4096 129,2 / 130,9 102 с 5559
8192 / 8192 — vk::Queue::submit: ErrorDeviceLost 6837

Оптимум — 2048; 4096 воспроизводимо хуже, 8192 роняет Vulkan-устройство (после падения процесс завершается, VRAM возвращается к десктопным ~652 МБ). На генерацию флаг почти не влияет: 11,68 против 11,91 (−2 %, уровень шума), при pp 173,4 против 104,4.

От кванта флаг не зависит: на IQ3_XXS тот же -ub 2048 даёт 103,4 → 165,9 t/s (+60 %), VRAM 5358 МБ.

Кривая префила по длине (дефолтный батч, IQ2_M, -c 131072), снята scripts/ngram_pp_probe.py:

токенов pp t/s время
3 365 118,7 28 с
6 671 119,9 56 с
13 283 114,7 116 с
26 507 95,7 277 с

Квадратичный член (attention) растёт заметно уже на 26k. При -ub 2048 линейная часть падает с 8,15 до ~4,7 мс на токен, квадратичная остаётся; отсюда оценка полного префила 128k — ~22 минуты против ~30 при дефолте. На практике важен не этот случай, а прирост новых токенов при живом KV-кэше: там работает именно линейная часть.

9.6 Бюджет 128k и финальная конфигурация

-m Qwen3.6-35B-A3B-MTP-UD-IQ2_M.gguf
-c 131072 -ngl 99 --n-cpu-moe 999
-fa on -ctk q8_0 -ctv q8_0
-t 4 -np 1 -b 4096 -ub 2048
--spec-type draft-mtp --spec-draft-n-max 1
--no-webui

Модель гибридная: из 40 слоёв обычный KV держат ~10, остальные работают на recurrent-state. Отсюда бюджет:

статья размер
модель (attention/dense) на GPU ~2,2 ГБ
KV на 128k (q8_0), по 85 МиБ на 8k ~1,36 ГБ
RS-буфер, 4 слота ~0,50 ГБ
compute-буферы при -ub 2048 ~1,0 ГБ
десктоп ~0,6 ГБ
итого VRAM ~5,1 ГБ из 8,2

Эксперты (11,9 ГБ) остаются в RAM; при 19 ГБ это спокойно, Private_Clean mmap-страницы видны как buff/cache. Свободных ~3 ГБ VRAM не нужны ни под экспертов (§9.2), ни под KV — они остаются запасом.

9.7 Что осталось непроверенным

  • UD-Q2_K_XL того же размера, но с лучшим качеством на бит: скачать и померить ppl тем же способом;
  • A/B IQ2_M против IQ3_XXS на реальных агентных задачах Navi — ppl не видит сломанных tool-call;
  • промежуточные -ub (3072) и -b;
  • конфигурация каналов и частота RAM (sudo dmidecode, без root не снять) — чтобы окончательно закрыть вопрос полосы, хотя §9.1 уже говорит, что упираемся не в неё.