Qwen 3.8 Flash Next локально: 84 GiB на RTX 3090 и двух RTX 5070 Ti

Всем привет, с вами Низамов Илья. 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 GiB62 %
Таблица per_layer_token_embd26.82 GiB32 %
Остальное: внимание, SSM, hyper-connections, выход4.98 GiB6 %
Итого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 с HDDQ3_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_XL103.7 GiB
UD-IQ4_XS87.2 GiB
UD-Q3_K_XL83.8 GiB
UD-IQ3_XXS76.3 GiB
UD-Q2_K_XL73.5 GiB
UD-IQ1_M69.4 GiB
UD-IQ1_S67.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 layer280.627.365208640
То же, повтор277.526.125208655
-ncmoe 14 -ts 28,10,10287.424.985105450
-ncmoe 15 -ts 29,10,9269.224.044999056
-ot последние 13 слоёв, -ts 16,10,22271.126.135148045
-ncmoe 13/16/20 с авто--tsOOM

Разброс между двумя прогонами одной и той же конфигурации — 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, то есть рантайм-переупаковка весов на процессоре — для нас существенно, раз половина модели считается именно там.

Куда двигаться дальше

Всё упирается не в раскладку, а в объём видеопамяти. Рычагов остаётся три, и все с оговорками.

  1. Квант поменьше. UD-IQ1_M на 69.4 GiB сократит процессорную часть примерно вдвое, а вместе с ней и главную беду — префилл. Но однобитные кванты заметно теряют в качестве, а IQ медленнее считаются на CPU. Выигрыш не гарантирован, надо мерить.
  2. Урезать контекст. Освободит около 3.3 GiB, то есть три слоя экспертов. Честно говоря, немного — гибридная архитектура и так экономит на KV-кэше.
  3. Дождаться тензорного параллелизма в форке. При рабочем -sm tensor три карты считали бы каждый слой параллельно, а не по очереди конвейером. Именно этот режим на 27B давал в llama.cpp самую высокую скорость печати.

Прогон этой модели выложен в общий раздел бенчмарков — там же лежат все конфигурации Qwen 3.8 27B на этом же стенде, так что сравнивать можно построчно.

Локальная модель — это фундамент, но польза появляется, когда она встроена в рабочий процесс: читает документы, отвечает клиентам, заполняет данные в учётной системе. Как довести дело до работающей интеграции — в курсе «Применение искусственного интеллекта ChatGPT для 1С»: от локального движка до агента внутри вашей 1С.

Частые вопросы