Всем привет, с вами Низамов Илья. Qwen 3.8 Flash Next — это MoE-модель на 512 экспертов, которую Unsloth выложил в GGUF: unsloth/Qwen3.8-Flash-Next-GGUF. Даже в трёхбитном кванте UD-Q3_K_XL она весит 83.80 GiB, а на моём стенде всего 55.8 GiB видеопамяти на трёх картах. То есть модель на 28 GiB больше, чем вся VRAM машины, и вопрос был простой: запустится ли она вообще, во сколько слоёв упрётся и что из этого получится по скорости.
Запустилась. Рабочая команда в конце статьи — она одна, потому что перебор раскладок ничего не дал: -ngl auto оказался оптимальным «из коробки». Дальше по порядку: что это за архитектура, обо что спотыкаешься до первого запуска, куда физически уходит видеопамять, и главное — замеры на той же методике и том же железе, что и у Qwen 3.8 27B, так что цифры сравнимы напрямую.
Стенд: RTX 3090 на 24 GB плюс две RTX 5070 Ti по 16 GB, Ryzen 5 3600, 76 GB оперативной памяти, Ubuntu 26.04, драйвер 595.71.05. Сборка — форк llama.cpp-qwen4exp, коммит 6c5afc86a (build 10669).
Что за модель и почему её тяжело запустить
Заголовок GGUF рассказывает о ней больше, чем карточка на Hugging Face:
general.architecture = qwen4exp
qwen4exp.block_count = 48
qwen4exp.context_length = 262144
qwen4exp.expert_count = 512
qwen4exp.expert_used_count = 10
qwen4exp.attention.head_count = 24 head_count_kv = 2
qwen4exp.full_attention_interval = 4
qwen4exp.attention.indexer.top_k = 2048
qwen4exp.ssm.inner_size = 6144 state_size = 128
qwen4exp.hyper_connection.count = 4
qwen4exp.ple.head_vocab_sizes = [20000003, 20000023, ...] x 16
Три вещи отсюда определяют всё дальнейшее.
Это гибрид, а не трансформер. Из 48 слоёв полноценное внимание стоит только на каждом четвёртом — двенадцать слоёв. Остальные 36 — SSM, рекуррентные слои с состоянием. Практическое следствие приятное: KV-кэш платится всего за двенадцать слоёв из сорока восьми, поэтому контекст в 262144 токена стоит смешные 3.4 GiB. Следствие неприятное — ниже, в разделе про то, что не работает.
Экспертов 512, а на токен работают 10. Это классический MoE-размен: считается мало, а лежать в памяти должно всё. Веса экспертов — 51.99 GiB из 83.80, то есть 62 % модели. Именно они и не влезают.
Есть PLE-таблица. per_layer_token_embd — 16 голов примерно по 20 миллионов строк, вместе 26.82 GiB, ещё 32 % модели. Работает она через GGML_OP_GET_ROWS: на каждый токен читаются единицы строк. Держать такое в видеопамяти бессмысленно — оно одно съест целую карту ради нескольких килобайт полезного чтения за шаг. Но в llama.cpp эта таблица относится к LLM_TENSOR_LAYER_INPUT и по умолчанию просится на GPU, так что при ручной раскладке нужен явный -ot per_layer_token_embd=CPU.
Итого раскладка весов:
| Группа | Размер | Доля |
|---|---|---|
Эксперты (ffn_{gate,up,down}_exps), 48 слоёв | 51.99 GiB | 62 % |
Таблица per_layer_token_embd | 26.82 GiB | 32 % |
| Остальное: внимание, SSM, hyper-connections, выход | 4.98 GiB | 6 % |
| Итого | 83.80 GiB |
На один слой экспертов приходится в среднем 1.083 GiB: нулевой слой тяжелее (1.660), остальные укладываются в 1.038–1.08. Эта цифра дальше будет главной единицей счёта.
Три вещи, о которые спотыкаешься до первого запуска
Мастер-ветка llama.cpp модель не грузит. В src/llama-arch.cpp архитектуры QWEN4EXP просто нет. Нужен форк llama.cpp-qwen4exp, и он по умолчанию собирает только llama-cli, llama-server и llama-bench. Планировщик раскладки придётся добрать отдельно тем же кэшем:
cd /home/ilya/ai/llama.cpp-qwen4exp/build && make -j12 llama-fit-params
llama-cli здесь — обёртка над сервером. Он поднимает llama-server под капотом, и в логах это видно по строке llama_server exited with code 1. Отдельного пути исполнения нет, так что воспроизводить баг через cli в надежде обойти сервер бесполезно. Там же поменялся флаг: -no-cnv больше нет, вместо него -st/--single-turn. И при неизвестном аргументе процесс падает на разборе командной строки, не доходя до загрузки модели — если грепать логи только по GGML_ASSERT, такой запуск легко принять за успешный.
Сервер советует --reasoning-preserve, потому что встроенный jinja-шаблон умеет сохранять рассуждения. Кстати, шаблон здесь рабочий: в нём есть и tools, и thinking. Тот исправленный qwen38-fixed.jinja, который понадобился для Qwen 3.8 27B, сюда не подходит и не нужен.
Модель на жёстком диске убивает всё
Первый заход был неудачный по глупой причине: /home на этой машине — это sda1, семитысячник Seagate на ~76 MB/с, и модель лежала там. Разница с NVMe оказалась не в проценты.
| Q4_K_XL с HDD | Q3_K_XL с NVMe | |
|---|---|---|
| Загрузка модели | 25 мин | 58 с |
| Чтение запроса (префилл) | 0.22 ток/с | 269–287 ток/с |
| Генерация | 1.77 ток/с | 26–27 ток/с |
Обратите внимание на префилл: 0.22 токена в секунду — это не «медленно», это неработоспособно. Причина видна по счётчикам ввода-вывода: за одну загрузку с HDD было прочитано 135 GB при размере модели 103.7 GB. Страницы вытеснялись из страничного кэша и перечитывались с диска по кругу, потому что половина модели живёт не в видеопамяти, а в маппинге файла.
Отсюда правило, которое дороже всех остальных советов в этой статье: если часть модели остаётся на процессоре, файл модели обязан лежать на NVMe. Для целиком влезающей в VRAM модели диск влияет только на время старта, здесь — на каждый токен.
Какой квант брать под такую раскладку?
В репозитории Unsloth лежит семь вариантов:
| Квант | Размер |
|---|---|
| UD-Q4_K_XL | 103.7 GiB |
| UD-IQ4_XS | 87.2 GiB |
| UD-Q3_K_XL | 83.8 GiB |
| UD-IQ3_XXS | 76.3 GiB |
| UD-Q2_K_XL | 73.5 GiB |
| UD-IQ1_M | 69.4 GiB |
| UD-IQ1_S | 67.6 GiB |
Я взял UD-Q3_K_XL: примерно 41 GiB ложится в видеопамять, ещё около 43 GiB в оперативную, всё резидентно с запасом на 76 GB системной памяти. IQ4_XS точнее и всего на 3.4 GiB больше, но IQ-кванты заметно медленнее считаются на процессоре, а на процессоре здесь ровно половина модели. То есть выбор кванта в такой раскладке — это не только «сколько влезет», но и «чем это будет считаться».
Ещё одна мелочь, которая стоила мне разъехавшейся на порядок арифметики: если писать свой парсер GGUF-заголовков, не забудьте, что блок IQ3_XXS — это 256 элементов в 98 байтах. Без него в таблице размеров счёт по тензорам врёт в разы.
Где на самом деле лежит модель
Проверять раскладку по логам запуска — самообман. Я снял её на живом сервере через /proc/<pid>/smaps: llama.cpp мапит из GGUF только те диапазоны, которые остаются на процессоре, а GPU-тензоры читает и отпускает маппинг.
шард 2: замаплено 27.5 GiB, резидентно 11.3 GiB
шард 3: замаплено 15.2 GiB, резидентно 15.2 GiB
итого на CPU 42.7 GiB
VRAM занято 51.1 GiB (веса ~41 + KV и буферы ~10)
На процессоре остаётся PLE-таблица целиком (26.8 GiB) плюс эксперты примерно тринадцати-пятнадцати хвостовых слоёв. Резидентно из 42.7 GiB только 26.5 — остальное живёт в страничном кэше, и на NVMe это в пределах шума.
Теперь бюджет видеопамяти, ради которого всё и считалось:
всего VRAM 55.8 GiB
├─ compute-буфер 2.13 GiB x 3 карты = 6.4
├─ KV-кэш (12 attn-слоёв, q8_0) ~3.4 GiB при 262144
├─ dense-веса 4.98 GiB
└─ остаётся на экспертов ~39 GiB → 36 слоёв из 48
Вот главный вывод про это железо: на полном контексте минимум двенадцать слоёв экспертов обязаны считаться на процессоре, и урезание контекста спасает от силы от трёх из них. Авто-раскладка держит на CPU около 14.7 слоя, то есть весь возможный выигрыш от ручного тюнинга — два-три слоя, и дальше идти некуда.
Отдельно стоит проговорить, что контекст здесь почти не рычаг. KV-кэш стоит 3.4 GiB на всю четверть миллиона токенов — спасибо гибридной архитектуре, где полное внимание всего на двенадцати слоях. Урезав контекст с 262144 до 8192, вы освободите около 3.3 GiB, то есть три слоя экспертов. Compute-буфер при этом не уменьшится: он зависит от размера батча, а не от длины контекста.
Ручная раскладка не выигрывает у автоматической
Я прогнал шесть конфигураций. Методика одинаковая: полный --ctx-size 262144, --parallel 1, -fa on, KV q8_0/q8_0, -b 2048 -ub 512. Каждая конфигурация — отдельный запуск сервера с холодным кэшем, прогрев коротким запросом, затем чтение 6878 токенов и генерация 256 при temperature=0.
| Конфигурация | Чтение запроса, ток/с | Генерация, ток/с | VRAM, MiB | Загрузка, с |
|---|---|---|---|---|
-ngl auto -sm layer | 280.6 | 27.36 | 52086 | 40 |
| То же, повтор | 277.5 | 26.12 | 52086 | 55 |
-ncmoe 14 -ts 28,10,10 | 287.4 | 24.98 | 51054 | 50 |
-ncmoe 15 -ts 29,10,9 | 269.2 | 24.04 | 49990 | 56 |
-ot последние 13 слоёв, -ts 16,10,22 | 271.1 | 26.13 | 51480 | 45 |
-ncmoe 13/16/20 с авто--ts | OOM |
Разброс между двумя прогонами одной и той же конфигурации — 4.7 %. То есть всё, кроме -ncmoe, укладывается в шум, а -ncmoe устойчиво хуже примерно на 9 %. Вывод скучный и приятный: берите -ngl auto -sm layer и не тратьте вечер.
Три грабли, если всё-таки тюнить руками
Но если тратить всё-таки хочется, вот три вещи, которые сэкономят вам этот вечер.
Ключ называется наоборот, чем думаешь. -ncmoe, --n-cpu-moe N — это «MoE-веса первых N слоёв на CPU». Есть ещё -cmoe/--cpu-moe — все. Обратного ключа, «сколько MoE оставить на GPU», не существует.
-ncmoe с автоматическим -ts падает всегда. Логика простая: -ncmoe делает первые N слоёв дешёвыми (только внимание и SSM, около 0.1 GiB), остальные остаются дорогими (плюс 1.083 GiB экспертов). А автоматический -ts делит слои пропорционально объёму видеопамяти, а не их реальному весу. 3090 получает 43 % слоёв — то есть дешёвый хвост, — а тяжёлая середина достаётся шестнадцатигиговым картам:
ggml_backend_cuda_buffer_type_alloc_buffer: allocating 16416.00 MiB on device 1:
cudaMalloc failed: out of memory
16.4 GiB на 16-гиговую карту. Лечится только ручным -ts.
Любой ручной оверрайд отключает авто-подбор. Тронули -ncmoe или -ot — считайте -ngl и -ts сами:
common_fit_params: failed to fit params to free device memory:
model_params::tensor_buft_overrides already set by user, abort
Как считать -ts руками
Считать так. Дешёвый слой на GPU — примерно 0.104 GiB, дорогой — 0.104 + 1.083 = 1.187 GiB. Резерв на карту: 2.13 GiB compute-буфера, плюс доля KV-кэша (3.4 GiB, разделённые пропорционально числу слоёв), плюс полгигабайта на фрагментацию. На моём стенде под веса остаётся примерно 18.9 GiB на 3090 и по 12.3 GiB на каждой 5070 Ti. Если про compute-буфер забыть, веса загрузятся нормально, а упадёт контекст, и ошибка будет выглядеть непонятно:
graph_reserve: failed to allocate compute buffers
llama_init_from_model: failed to initialize the context: failed to allocate compute pp buffers
Рабочие ручные комбинации, если всё же понадобятся: -ncmoe 14 -ts 28,10,10 и -ncmoe 15 -ts 29,10,9.
И ещё наблюдение про позицию выгруженных слоёв. При одинаковом их количестве (тринадцать) хвостовой вариант через -ot даёт 26.13 ток/с против 24.98 у -ncmoe 14, который выгружает первые слои. То есть авто-раскладка отправляет на процессор именно хвост не случайно. Правда, разница на грани разброса, а меньшее число слоёв на CPU (13 против 14.7) прироста не дало вовсе.
Что не работает
--split-mode tensor для этой архитектуры сломан.
ggml/src/ggml-backend-meta.cpp:847: GGML_ASSERT(ne_sum == tensor->ne[ret.axis]) failed
Бэктрейс уходит в llama_memory_recurrent — то есть падает разбиение кэша SSM-состояний, а не весов. Воспроизводится при любых -ngl, любых -ts, любой длине контекста, с -cmoe и вообще без единого -ot. К раскладке отношения не имеет: тензорный параллелизм для qwen4exp в форке просто не готов, в ветке лежат незавершённые коммиты ровно про это. Обидно, потому что на 27B именно -sm tensor давал прирост скорости печати в llama.cpp. Заодно при падении он оставляет core dump вместо чистой ошибки, а авто-подбор параметров его не поддерживает и молча вырождается в «всё на GPU» с последующим OOM.
--spec-type draft-mtp неприменим. В GGUF просто нет MTP-тензоров — ни nextn, ни mtp в списке тензоров не встречается:
llama_init_from_model: context type MTP requested but model doesn't contain MTP layers
На Qwen 3.8 27B спекулятивное декодирование через MTP давало плюс 82 % к генерации. Здесь этого рычага нет вообще, и это одна из причин, почему цифры ниже такие скромные.
Замеры: чем платишь за то, что модель не влезла
Замеры я снимал не руками, а своим инструментом с единой базой прогонов — той же методикой версии 4, что и все остальные модели в разделе бенчмарков. Батч 1, фиксированное зерно, анти-кэш-маркер в начале каждого промпта. Два независимых прогона разошлись на 0.5 % по генерации (28.52 и 28.66 ток/с), так что цифрам можно верить. Всё, что ниже, — из карточки этого прогона: там лежат все одиннадцать тестов целиком, паспорт железа и команда запуска, ничего не сокращено.
| Метрика | Значение |
|---|---|
| Генерация (то, что чувствует человек) | 28.52 ток/с |
| Чистая скорость печати | 30.17 ток/с |
| Чтение запроса (префилл) | 293.7 ток/с, R² 0.9998 |
| Постоянные потери на запрос | 0.822 с |
| Пауза до первого слова, короткий вопрос | 0.459 с |
| Пауза до первого слова, промпт ~500 токенов | 2.116 с |
| Экономия на повторах (кэш префикса) | 40.4x начиная с 656 токенов |
| Падение скорости на каждое удвоение длины | чтение −2.5 %, печать −1.7 % |
| Исправность сборки | 95 % |
| Точность выбора инструмента агентом | 96.2 % |
| Медианный ход агента | 11.28 с |
Разберу то, что здесь важно, а не просто написано.
Узкое место — чтение запроса, а не генерация. 293.7 токена в секунду. Для сравнения: та же машина на Qwen 3.8 27B в vLLM читает 3140 ток/с, а в llama.cpp — 2118. Разница в десять раз, и берётся она ровно оттуда, где лежат веса: чтение промпта упирается в вычисления, и половина слоёв считает его на шести ядрах Ryzen 5 3600 вместо тензорных блоков. Генерация просела скромнее (28.5 против 100.7 у 27B с MTP), потому что она упирается в чтение весов из памяти, а не в счёт.
Можно ли доехать до заявленных 262144 токенов?
Деградации по длине почти нет. Каждое удвоение длины промпта отнимает 2.5 % скорости чтения и 1.7 % скорости печати. В абсолютных числах: с 3871 до 15370 токенов префилл просел с 248.7 до 236.7 ток/с, а печать на том же росте от 998 до 15370 токенов — с 28.7 до 26.9 ток/с. Это лучший результат из всего, что я мерил на этом стенде: у 27B на четырёхбитном KV-кэше печать теряла 16.6 % на каждое удвоение и падала с 55.4 до 13.1 ток/с. Гибридная архитектура своё обещание держит, длинный контекст ей действительно почти ничего не стоит. Беда в том, что вы до этого длинного контекста не доедете по времени.
Подтверждённый контекст — 15370 токенов, и это не предел модели. Лестница длин остановилась сама: на точку 65536 токенов инструменту нужно было около 19 минут при бюджете теста в 20, из которых 6 уже потрачено. Иголку в стоге сена модель нашла на всех трёх глубинах на 998, 3871 и 15370 токенах. То есть заявленные 262144 никто не опроверг — просто проверить их дороже, чем хочется. Само по себе это и есть самый честный ответ про длинный контекст на такой раскладке: прочитать 65536 токенов здесь стоит почти четыре минуты.
Почему сервер отвечает 500 на системное сообщение?
Исправность 95 %, и потерянные пять процентов — знакомая история. 50 задач с проверяемым ответом из 50, языковая целостность 5 из 5, побайтовая повторяемость 5 из 5. Провалились две попытки из десяти в блоке строгого формата — обе на кейсе system_not_first, то есть когда системное сообщение приходит не первым в диалоге:
HTTPError: 500 Server Error: Internal Server Error for url:
http://127.0.0.1:8000/v1/chat/completions
Это ровно тот же дефект шаблона чата, из-за которого не заводился Claude Code на локальной модели — я разбирал его в статье «Claude Code на локальной модели». Только там его лечил подменённый jinja-шаблон, а здесь встроенный шаблон в остальном рабочий, и трогать его ради одного кейса я не стал. Если вы будете подключать эту модель к агенту, который вставляет системные сообщения по ходу диалога, — почините шаблон, иначе получите пятисотку в середине сессии.
Кэш префикса и агент: почему диалог всё-таки живёт
Кэш префикса вытягивает диалог. Повторный запрос быстрее холодного в 40.4 раза, и работает это уже с 656 токенов. Конкретно: промпт на 5256 токенов холодным читается 17.7 секунды, повторно — 0.21. При такой скорости чтения кэш префикса перестаёт быть оптимизацией и становится условием работоспособности: диалог живёт только потому, что начало переписки не перечитывается.
Агент выбирает инструменты хорошо, а ходит медленно. 25 верных вызовов из 26 — это 96.2 %, столько же, сколько у лучшей конфигурации 27B. Единственная осечка — не перепутанный инструмент, а обрезанный ответ: модель писала длинный файл и упёрлась в потолок в 640 токенов, аргументы не разобрались как JSON. Живёт этот агент ровно на кэше префикса: доля переиспользованного контекста по всем ходам — 93.9 %, без неё каждый ход перечитывал бы историю заново. А вот медианный ход — 11.28 секунды против 1.52 у 27B в vLLM. От 3.77 до 30.18 секунды на ход, при истории, доросшей к концу до 28720 токенов. Двадцать шесть ходов такого агента — это пять минут ожидания.
Кому эта модель годится на таком железе?
Скажу прямо: под интерактивного агента — не годится. Одиннадцать секунд на ход умножаются на десятки ходов, и работать в таком темпе невозможно, каким бы умным ни был выбор инструментов. Если задача — кодовый агент или ассистент в редакторе, на этом же железе лучше поднять 27B в vLLM: она в три с половиной раза быстрее печатает и в десять раз быстрее читает.
Где Flash Next на такой раскладке имеет смысл:
- Одиночные вопросы, где важна голова, а не темп. 28 ток/с — это примерно скорость беглого чтения человеком. Для «подумай и ответь развёрнуто» приемлемо.
- Пакетная обработка без человека у экрана. Ночной прогон по стопке документов, разметка, генерация описаний. Здесь важна пропускная способность за ночь, а не пауза до первого слова.
- Длинные диалоги на одном и том же префиксе. Сорокакратный кэш префикса вытягивает сценарий, где системный промпт и приложенные документы не меняются, а меняется только последняя реплика.
И вывод, который выходит за рамки этой конкретной модели. Когда веса не влезли в видеопамять, теряется не «немного скорости». Теряется префилл — на порядок, — а вместе с ним и весь класс сценариев с длинным входом. Генерация же проседает втрое, и это ещё терпимо. Так что если выбираете между «модель побольше, но половина на процессоре» и «модель поменьше, но целиком в VRAM» — для интерактивной работы вторая выиграет почти всегда. Как эти же движки ведут себя, когда модель влезает целиком, я разбирал в статье vLLM vs llama.cpp, а выбор карт — в RTX 5070 Ti vs RTX 3090.
Итоговая команда запуска
Она одна, и она же лежит у меня в llama-server-qwen38-next.sh:
cd /home/ilya/ai/llama.cpp-qwen4exp && CUDA_VISIBLE_DEVICES=0,1,2 ./build/bin/llama-server \
-m /opt/hf-cache/gguf/Qwen3.8-Flash-Next-GGUF/UD-Q3_K_XL/Qwen3.8-Flash-Next-UD-Q3_K_XL-00001-of-00003.gguf \
--host 0.0.0.0 --port 8000 \
--ctx-size 262144 \
--parallel 1 \
--cache-type-k q8_0 --cache-type-v q8_0 \
--flash-attn on \
--n-gpu-layers auto \
--split-mode layer \
--batch-size 2048 --ubatch-size 512 \
--jinja
Указывать нужно первый шард, остальные два подхватятся сами. Мультимодальность включается отдельным ключом: -mm /opt/hf-cache/gguf/Qwen3.8-Flash-Next-GGUF/mmproj-F16.gguf. Сэмплинг я намеренно не задаю — берутся зашитые в GGUF temp=1.0, top_p=0.95, top_k=20. Готовность проверяется как обычно: curl -s localhost:8000/health должен вернуть {"status":"ok"}, поднимается примерно за минуту.
Сама сборка форка — три строчки:
cd /home/ilya/ai/llama.cpp-qwen4exp
cmake -B build -DGGML_CUDA=ON -DCMAKE_CUDA_ARCHITECTURES="86;120" -DLLAMA_CURL=ON
cmake --build build -j12
Три момента из реальных флагов компиляции, которые стоит знать. Первое: 86;120 — это ровно под это железо, sm_86 для 3090 и sm_120a для 5070 Ti, причём 120a — арх-специфичный вариант, и PTX-фолбэка под другие поколения в сборке нет. На чужой карте такой бинарник не запустится, как и на машине с другим процессором: -march=native включён по умолчанию. Второе: CUDA-графы (GGML_CUDA_USE_GRAPHS) включены, и на малом батче это заметно для скорости генерации. Третье: GGML_CPU_REPACK=ON, то есть рантайм-переупаковка весов на процессоре — для нас существенно, раз половина модели считается именно там.
Куда двигаться дальше
Всё упирается не в раскладку, а в объём видеопамяти. Рычагов остаётся три, и все с оговорками.
- Квант поменьше.
UD-IQ1_Mна 69.4 GiB сократит процессорную часть примерно вдвое, а вместе с ней и главную беду — префилл. Но однобитные кванты заметно теряют в качестве, а IQ медленнее считаются на CPU. Выигрыш не гарантирован, надо мерить. - Урезать контекст. Освободит около 3.3 GiB, то есть три слоя экспертов. Честно говоря, немного — гибридная архитектура и так экономит на KV-кэше.
- Дождаться тензорного параллелизма в форке. При рабочем
-sm tensorтри карты считали бы каждый слой параллельно, а не по очереди конвейером. Именно этот режим на 27B давал в llama.cpp самую высокую скорость печати.
Прогон этой модели выложен в общий раздел бенчмарков — там же лежат все конфигурации Qwen 3.8 27B на этом же стенде, так что сравнивать можно построчно.
Локальная модель — это фундамент, но польза появляется, когда она встроена в рабочий процесс: читает документы, отвечает клиентам, заполняет данные в учётной системе. Как довести дело до работающей интеграции — в курсе «Применение искусственного интеллекта ChatGPT для 1С»: от локального движка до агента внутри вашей 1С.