Есть у меня отдельная машина: материнка под Xeon E5-2696 v4 (22 ядра, 44 потока), 128 ГБ DDR3-1600 и видеокарта GTX 1070 на 8 ГБ. Плата с процессором и памятью обошлась примерно в 35 тысяч рублей, карта — ещё 8 тысяч с рук. Итого — тысяч 43 за всё, железо уровня “давно списано из майнинг-фермы и дата-центра”.

На этой штуке я запустил Qwen3.8-Flash-Next — MoE-модель на 125 миллиардов параметров (~6 млрд активных на токен) — и она реально работает. Не “теоретически запускается с квантованием до состояния овоща”, а выдаёт 14-15 токенов в секунду генерации на контексте 24,5К. Честно, не ожидал.

Под катом — как это возможно, какие движки я сравнивал, и что из тюнинга реально дало прирост.

Почему 125B параметров — это не то же самое, что 125B параметров

Если бы это была плотная (dense) модель на 125 миллиардов параметров, разговор был бы коротким: даже в 4-битном кванте это ~65 ГБ только под веса, и при каждом токене нужно пройти через все эти веса. На GTX 1070 с её 8 ГБ VRAM и ~180 ГБ/с пропускной способности это были бы единицы токенов в минуту, не в секунду.

Qwen3.8-Flash-Next — Mixture-of-Experts: 48 слоёв, 512 маршрутизируемых экспертов на слой, но на каждый токен реально активируется лишь малая часть — порядка 6 млрд параметров-эквивалент вычислений. Это меняет всё: основную массу весов (экспертов, которые сейчас не нужны) можно держать в обычной оперативной памяти или вовсе на диске, и подгружать/считать на CPU только то, что реально потребовалось. GPU в этой схеме обслуживает “горячую” часть — attention, несколько всегда-активных слоёв, и кэш самых часто используемых экспертов.

Поэтому ключевой ресурс здесь — не VRAM, а суммарная память (VRAM + RAM) и количество ядер CPU, которые считают офлоаженных экспертов. 128 ГБ DDR3 — медленной, 1600 МГц, без изысков — но её много, и 22 физических ядра есть кому эту арифметику считать параллельно.

Железо и файлы модели

  • GPU: NVIDIA GeForce GTX 1070, 8 ГБ VRAM — ~8 000 ₽

  • CPU: Intel Xeon E5-2696 v4, 22 ядра / 44 потока, 2.2 ГГц

  • RAM: 128 ГБ DDR3-1600 (материнка + CPU + память — ~35 000 ₽ одним комплектом)

  • Накопители: модель переехала с 4ТБ HDD на системный NVMe — об этом ниже, разница ощутимая

Квантованные файлы модели — порядка 95-98 ГБ на диске (три шарда GGUF в IQ4_XS/UD-IQ4_XS). В 8 ГБ VRAM из этого влезает меньше десятой части — всё остальное читается с диска при старте и живёт в RAM.

Два движка: llama.cpp и Strata

Гонял одну и ту же задачу (MoE-офлоад экспертов на CPU) на двух инференс-движках.

llama.cpp — классика, --n-cpu-moe держит часть слоёв экспертов на CPU, --override-tensor позволяет точечно докинуть на GPU конкретные тензоры сверх этого.

Strata — движок с явным упором на MoE: адаптивный кэш экспертов в VRAM (--expert-cache auto, сам решает, какие эксперты держать на карте по частоте обращения), и спекулятивное декодирование через MTP-драфтер (Multi-Token Prediction) — лёгкий слой предсказывает несколько токенов вперёд, основная модель их пачкой проверяет, из-за чего при удачном совпадении генерация идёт быстрее, чем токен-за-токеном.

Методика

Для каждого размера запроса (1К / 4К / 16К / 24,5К токенов “уже в контексте”) — 3 прогона со случайным текстом-заполнителем и случайным ID в начале (чтобы не словить кэш промпта), генерация 128 токенов, температура 0. Результат — медиана трёх повторов. Контекст — 32768 токенов (упирается в VRAM на этой карте даже с квантованным int8 KV-кэшем). Скрипты и сырые данные — в репозитории, см. ссылку в конце.

Результаты: llama.cpp, подбор офлоада

Сначала — что получилось выжать из llama.cpp. Три шага: все эксперты на CPU → добавить один слой экспертов целиком на GPU (-ncmoe) → докинуть ещё одну матрицу тензора точечно (-ot).

Генерация, ток/с (медиана):

Запрос

все эксперты на CPU

+1 слой на GPU

+ точечный тензор

1K

5,1

5,6

6,2

4K

5,4

6,1

6,1

16K

4,6

5,1

5,5

24,5K

4,3

5,0

5,4

Итог — +20-26% к генерации от простейшей раскладки до финальной, бесплатно, просто перераспределением слоёв между GPU и CPU. VRAM в финале — 7666 из 8192 МиБ, то есть упёрлись в карту почти впритык: следующая матрица экспертов (ещё ~425-450 МиБ) уже не влезает без урезания контекста.

Любопытная деталь: лог llama.cpp сам советовал добавить --load-mode none при использовании --override-tensor (типа “так быстрее”). Проверил — это оказалось явной регрессией: prefill упал с 82-99 до 10-29 ток/с. Без mmap операционная система не держит страницы CPU-резидентных весов в файловом кэше, и они перечитываются с диска на каждый токен. Совет из лога, судя по всему, относится к другому сценарию — не доверяйте слепо.

Результаты: Strata — и тут начинается интересное

Та же модель (вариант unsloth UD-IQ4_XS), движок Strata с адаптивным кэшем экспертов и MTP-спекуляцией:

Запрос

llama.cpp, ген. ток/с

Strata, ген. ток/с

Strata, prefill ток/с

1K

6,2

15,3

237,6

4K

6,1

15,3

224,9

16K

5,5

15,2

236,5

24,5K

5,4

15,1

237,6

Эта первая таблица сравнивает разные файлы модели (Uncensored-вариант на llama.cpp, официальный unsloth-квант на Strata) — честно предупреждаю сразу, чтобы не выглядело как жульничество. Дальше разобрался, насколько чистая эта разница на самом деле.

Пересобрал llama.cpp и прогнал на том же файле — и тут выяснилось интересное

Чтобы сравнить движки по-честному, нужен был llama-server, который грузит ровно тот же GGUF, что и Strata. Апстрим llama.cpp эту архитектуру (Qwen3.8 Flash Next) не понимает — но в исходниках Strata есть собственный патченный форк llama.cpp именно под такие модели. Собрал из него llama-server под CUDA 12.6 (ушло на пересборку минут 15; по пути всплыла мелочь — в этом чекауте не хватало каталога веб-морды чата, слепил пустую заглушку, раз нужен был только API) и прогнал на тех же файлах, что грузила Strata.

Тактика с Uncensored-кванта не перенеслась. На нём точечный перенос матрицы blk.46.ffn_down_exps весил ~450 МБ и укладывался в свободные 424-613 МиБ VRAM. На официальном unsloth UD-IQ4_XS та же матрица весит 891 МБ — почти вдвое больше: Unsloth Dynamic Quant держит часть тензоров в повышенной точности ради качества, жертвуя компактностью. Свободных 348 МиБ на такое уже не хватает.

Хуже: даже без точечного переноса, один -ncmoe 47 на этом файле оказался на самой грани VRAM и деградировал с ростом запроса — без перегрева (53°C), без видимого троттлинга, но стабильно медленнее на каждом следующем прогоне:

Запрос

prefill, ток/с

генерация, ток/с

1K

64,0

4,9

4K

79,1

4,6

16K

18,1

1,6

24,5K

13,0

1,5

На 24,5К один повтор занимал около 33 минут (на Uncensored-кванте такой же запрос обрабатывался за секунды) — бенчмарк на этом размере пришлось остановить после 2 повторов вместо 3, дальше деградация уже ничего нового не показывала.

Вот теперь сравнение действительно чистое — один и тот же файл модели на обоих движках:

Запрос

llama.cpp ген., ток/с

Strata ген., ток/с

ускорение

llama.cpp prefill

Strata prefill

ускорение

1K

4,9

15,3

×3,1

64,0

237,6

×3,7

4K

4,6

15,3

×3,3

79,1

224,9

×2,8

16K

1,6

15,2

×9,5

18,1

236,5

×13,1

24,5K

1,5

15,1

×10,1

13,0

237,6

×18,3

Разрыв оказался даже больше, чем в первой таблице — но смысл у него другой. Это не “Strata как движок в 3-18 раз быстрее llama.cpp”. Это конкретная находка: выбор кванта решает, останется ли у 8-гигабайтной карты запас VRAM для ручной подгонки llama.cpp вообще. На Uncensored-кванте запас был, и тюнинг дал честные 5,4-6,2 ток/с стабильно на всех размерах. На официальном unsloth-кванте запаса не осталось — и без переподбора конфигурации под конкретный файл llama.cpp на этой карте просто расползается на длинном контексте.

Strata в этом смысле выигрывает не только спекулятивным декодированием (о нём — ниже), а тем, что --expert-cache auto сам считает, сколько экспертов влезет в оставшуюся VRAM, под любой файл, без пересчёта тензоров руками под каждый новый квант. Для человека, который не хочет гонять gguf.GGUFReader каждый раз, когда меняет модель, — это не просто цифры в таблице, а реальная экономия вечера.

Часть разрыва по генерации всё ещё приходится на спекулятивное декодирование: accept rate у MTP-драфтера Strata гулял от 0% до 100% по разным прогонам (медиана 50-65%), llama.cpp в этом тесте спекуляцию не использовал вообще. На малопредсказуемом тексте разрыв по генерации будет меньше, чем в таблице.

Тюнинг на бесплатных флагах

Полез в strata --help в поисках того, что можно включить без риска регрессии — и нашёл два флага чисто про расстановку потоков и памяти, не трогающих арифметику:

  • --host-core sibling — по умолчанию поток, спинящийся в ожидании GPU-прерываний, занимает отдельное физическое ядро. На HT-процессоре его можно пересадить на второй логический поток той же пары — и отдать освободившееся физическое ядро CPU-пулу экспертов. На 22-ядерном Xeon это 22 воркера вместо 21.

  • --expert-cache-per-layer — по умолчанию слоты VRAM-кэша экспертов раздаются из общего счётчика в порядке появления, из-за чего часть слоёв получает все слоты, часть — почти ничего. Флаг даёт каждому слою свой персональный бюджет.

Результат — стабильный прирост без каких-либо компромиссов:

Запрос

генерация: база → тюнинг

prefill: база → тюнинг

1K

14,0 → 15,3 (+9%)

151,4 → 237,6 (+57%)

4K

14,1 → 15,3 (+9%)

214,2 → 224,9 (+5%)

16K

14,3 → 15,2 (+6%)

225,8 → 236,5 (+5%)

24,5K

13,9 → 15,1 (+9%)

228,0 → 237,6 (+4%)

На коротких запросах эффект на prefill особенно заметный (+57% на 1К) — похоже, там выше доля времени, которое съедает “холодный” проход через CPU-пул, и лишнее ядро там решает больше.

NVMe вместо HDD — не опция, а необходимость

Отдельно стоит сказать про хранение файлов модели. Изначально 95 ГБ лежали на старом 4ТБ HDD — холодная загрузка модели зависла больше чем на 10 минут, пришлось перезагружать машину, не дождавшись. Перенос тех же файлов на системный NVMe — и загрузка укладывается в ~150 секунд (из которых 55 ГБ весов экспертов читаются в RAM со скоростью ~1,2 ГБ/с). Для MoE-моделей с офлоадом в 90+ ГБ это не “было бы неплохо”, а разница между “работает” и “не дождался”.

Что в итоге

Бюджетная связка видеокарта-за-8к + списанный серверный Xeon с кучей медленной DDR3 — рабочий вариант для MoE-моделей таких размеров, если понимать принцип: платите не за то, чтобы весь 125-миллиардный монстр влез в VRAM, а за то, чтобы было, где держать офлоаженных экспертов (много RAM) и кому их считать (много ядер). GPU в этой схеме — ускоритель горячей части, а не обязательное условие “влезет/не влезет”.

Понятно, что 14-15 ток/с — это не скорость топового железа, но для локальной модели на 125B параметров на столе за 43 тысячи рублей — более чем рабочий результат.

Полные данные, сырые JSON/CSV замеров и скрипты, которыми всё это измерялось — в репозитории: github.com/DmNep/llm-bench-1070