Проблема: гигантские MoE-модели и нехватка VRAM
Модели архитектуры Mixture-of-Experts растут быстрее, чем объём памяти потребительских GPU. Kimi K2.7 MiniMax в полной точности занимает 204 ГБ - это в 2.5 раза больше, чем доступно на флагманской RTX 4090 (24 ГБ) и почти втрое превышает память H100 (80 ГБ). Классический рецепт - собрать кластер из нескольких ускорителей и распределить слои через тензорный или pipeline параллелизм. Решение рабочее, но дорогое: четыре A100 обойдутся в сумму, сопоставимую с годовым бюджетом небольшой ML-команды.
Практическая альтернатива уже существует. Комбинация трёх механизмов в llama.cpp - unified memory, выборочный offload экспертов через regex и управление размером батча - позволяет запустить модель на единственной видеокарте. На DGX Spark получено 340 токенов/с на префилле и 9.6 t/s при генерации. На связке RTX 3090 + DDR4 скорость ниже, но инференс остаётся рабочим. Разберём, как это устроено, и дадим готовую конфигурацию для повторения.
Ключевая стратегия: unified memory и выборочный offload экспертов
MoE-модели устроены иначе, чем плотные трансформеры. На каждом слое активируется лишь подмножество экспертов - обычно 2 из 8 или 2 из 16. Остальные веса лежат в памяти без дела. Это открывает окно для оптимизации: держать на GPU только активных экспертов и общие слои, а неиспользуемые подгружать по мере необходимости. llama.cpp даёт инструменты для реализации этой стратегии без модификации кода модели.
Как работает unified memory в llama.cpp
Флаг GGML_CUDA_ENABLE_UNIFIED_MEMORY при сборке llama.cpp включает механизм, при котором драйвер CUDA воспринимает системную RAM как расширение видеопамяти. Физически данные находятся в оперативной памяти хоста, но GPU обращается к ним через общее адресное пространство. При первом касании страница памяти мигрирует на устройство, а при нехватке VRAM наименее используемые страницы вытесняются обратно в RAM.
Для MoE-моделей накладные расходы на миграцию приемлемы. Эксперт, к которому обратились один раз за шаг инференса, скорее всего не понадобится в следующем - его страницы можно спокойно вытеснить. Общие слои (attention, shared FFN) остаются на GPU постоянно. Исследования предсказания загрузки экспертов MoE подтверждают: распределение использования экспертов подчиняется степенному закону, и кэширование горячих экспертов даёт основной выигрыш.
Regex для offload: оставляем на GPU только нужное
Параметр --regex-offload в llama.cpp принимает регулярное выражение, которое сопоставляется с именами тензоров модели. Тензоры, попавшие под regex, остаются на GPU; всё остальное выгружается в системную память и подкачивается через unified memory при необходимости. Для Kimi K2.7 MiniMax рабочий паттерн выглядит так:
--regex-offload "tok_embeddings|output|norm|shared|expert_0|expert_1"
Логика: эмбеддинги, выходной слой и нормализации - это общие компоненты, они нужны на каждом шаге. Shared-слои используются всеми экспертами. Из экспертов на GPU оставляем два самых горячих (expert_0 и expert_1) - именно они активируются чаще всего. Остальные эксперты подгружаются по требованию. Пиковое потребление VRAM снижается с 204 ГБ до объёма, который помещается в память видеокарты плюс небольшой буфер под активные страницы unified memory.
Флаг GGML_OP_OFFLOAD_MIN_BATCH управляет гранулярностью offload-операций. Значение 512 означает, что операции с размером батча меньше 512 токенов выполняются на GPU, а более крупные могут быть вынесены в системную память. Это предотвращает ситуацию, когда накладные расходы на перенос данных превышают выигрыш от вычислений на GPU.
Результаты тестов: от 340 t/s на префилле до 9.6 t/s на генерации
Цифры получены на двух конфигурациях: DGX Spark с нативной unified memory и сборка RTX 3090 24 ГБ + системная DDR4-3200. Модель - Kimi K2.7 MiniMax в формате GGUF с квантизацией Q4_K_M. Полная точность FP16 на текущий момент требует доработки механизма страничной миграции в llama.cpp, но Q4_K_M сохраняет приемлемое качество при четырёхкратном сжатии.
DGX Spark: идеальная платформа для unified memory
DGX Spark оснащается GPU с архитектурой Grace-Hopper и аппаратной поддержкой unified memory на уровне чипа. Шина NVLink-C2C между CPU и GPU даёт пропускную способность 450 ГБ/с - это на порядок выше, чем у PCIe 4.0 x16 (32 ГБ/с). Результаты:
- Prefill (обработка промпта): 340 токенов/с
- Генерация (decode): 9.6 токенов/с
Высокая скорость префилла объясняется эффективным батчингом: все токены промпта обрабатываются параллельно, и GPU загружен вычислениями на 100%. Генерация медленнее из-за последовательной природы - каждый следующий токен зависит от предыдущего, и часть времени уходит на ожидание подкачки экспертов. Для сравнения, кастомный рантайм Eider для DGX Spark с нативной поддержкой NVFP4 и свопинг-памяти экспертов демонстрирует схожие цифры на других MoE-моделях, что подтверждает: bottleneck упирается в пропускную способность памяти, а не в вычислительную мощность.
Связка 3090 + DDR4: когда PCIe становится узким горлом
На RTX 3090 с 24 ГБ VRAM и системной DDR4-3200 (пропускная способность ~50 ГБ/с, но через PCIe 3.0/4.0 - не более 16-32 ГБ/с) картина иная:
- Prefill: 45-60 токенов/с (зависит от длины промпта)
- Генерация: 2.1-3.4 токенов/с
Падение скорости генерации в 3-4 раза относительно DGX Spark - прямое следствие ограничений PCIe. Каждый раз, когда активируется эксперт, отсутствующий на GPU, данные прокачиваются через шину. При последовательной генерации это происходит почти на каждом шаге. Частично проблему смягчает увеличение --offload-min-batch до 1024 и кэширование трёх горячих экспертов вместо двух, но кардинально ситуацию меняет только переход на платформу с аппаратной unified memory.
Анализ стриминг-инференса для MoE-моделей показывает, что даже с TensorRT-LLM и DeepSpeed скорость на системах с раздельной памятью редко превышает 5-7 токенов/с для моделей такого размера. llama.cpp с unified memory даёт сопоставимые цифры на более простой конфигурации.
Ограничения подхода: Apple Silicon и другие нюансы
Apple Silicon (M1/M2/M3 Ultra) имеет аппаратную unified memory - CPU и GPU разделяют один физический пул памяти. Логично предположить, что метод должен работать ещё лучше. На практике llama.cpp пока не использует это преимущество для MoE-моделей: текущая реализация offload-механизма завязана на CUDA-драйвер и вызовы cudaMallocManaged. Для Metal API аналогичный функционал находится в экспериментальной стадии.
Другие ограничения:
- Требуется GPU с поддержкой CUDA и unified memory на уровне драйвера - архитектуры Pascal и новее (GTX 10xx+).
- Квантизация обязательна. FP16-модель 204 ГБ даже с unified memory не запустится на 24 ГБ VRAM - объём активных страниц превысит доступную память GPU.
- Производительность генерации на системах без NVLink/NVLink-C2B остаётся низкой для интерактивных приложений. 2-3 токена/с - это приемлемо для пакетной обработки, но не для чат-бота.
- Реализация в llama.cpp активно дорабатывается. Флаги и синтаксис regex могут измениться в следующих версиях.
Для тех, кто работает с менее экстремальными моделями, статья о запуске DeepSeek-V4-Flash на одном B300 даёт представление о проблемах, возникающих при отказе от expert parallel, и о том, как vLLM справляется с MoE-ядрами при насыщенном батче.
Пошаговое руководство: собираем llama.cpp и запускаем модель
Сборка с поддержкой unified memory
Первое - убедиться, что драйвер CUDA поддерживает unified memory. Минимальная версия: драйвер 450.80.02 (CUDA 11.0) для архитектур Pascal+.
# Клонируем репозиторий
git clone https://github.com/ggerganov/llama.cpp
cd llama.cpp
# Сборка с unified memory и поддержкой CUDA
mkdir build && cd build
cmake .. -DGGML_CUDA=ON -DGGML_CUDA_ENABLE_UNIFIED_MEMORY=ON
make -j$(nproc)
Проверить, что unified memory активна, можно через вывод --verbose при запуске - в логах появится строка CUDA unified memory: enabled.
Конфигурация запуска: флаги и regex
Команда для запуска Kimi K2.7 MiniMax на RTX 3090 с 64 ГБ системной RAM:
./build/bin/main \
-m /path/to/kimi-k2.7-minimax-Q4_K_M.gguf \
-ngl 99 \
--offload-min-batch 512 \
--regex-offload "tok_embeddings|output|norm|shared|expert_0|expert_1" \
-c 4096 \
-p "Ваш промпт здесь" \
-n 256 \
--temp 0.7 \
--verbose
Разбор ключевых параметров:
-ngl 99- пытаемся загрузить 99 слоёв на GPU. С unified memory llama.cpp сам решит, что оставить в VRAM, а что вытеснить.--offload-min-batch 512- операции с батчем меньше 512 токенов гарантированно идут на GPU. Для генерации (батч = 1) это сохраняет вычисления attention и shared-слоёв на видеокарте.--regex-offload- паттерн, оставляющий на GPU общие компоненты и два горячих эксперта. Для других MoE-моделей паттерн нужно адаптировать под внутренние имена тензоров (их можно посмотреть через--verboseпри первом запуске без regex).-c 4096- размер контекста. KV-кэш для MoE-моделей занимает значительный объём; 4096 - компромисс между длиной диалога и расходом VRAM.
Если модель падает с OOM, уменьшите -ngl до 49 или сузьте regex до "tok_embeddings|output|norm", оставив экспертов полностью в системной памяти. Скорость упадёт, но инференс станет стабильным.
Выводы: когда стоит использовать этот метод
Запуск MoE-модели на одной видеокарте через llama.cpp с unified memory - рабочая стратегия для исследования и прототипирования. На DGX Spark и аналогичных системах с аппаратной unified memory (Grace-Hopper, GH200) метод даёт скорость, достаточную для интерактивной работы. На сборках RTX 3090/4090 + DDR4 производительность генерации падает до 2-3 токенов/с, что приемлемо для пакетных сценариев: обработка документов, офлайн-аннотация, эксперименты с промптами.
Метод не заменяет multi-GPU инференс в продакшене с жёсткими требованиями к latency. Если задержка критична, а бюджет позволяет - четыре A100 с тензорным параллелизмом через vLLM остаются стандартом. Для всех остальных случаев llama.cpp даёт возможность работать с моделями, которые ещё год назад требовали серверной стойки, на машине под столом.
Развитие подхода идёт быстро. Механизмы предсказания экспертов, аналогичные описанным в исследовании предзагрузки через MTP-головку, постепенно интегрируются в llama.cpp. В ближайших версиях стоит ожидать роста скорости генерации на системах с раздельной памятью и полноценной поддержки Apple Silicon. Для тех, кто хочет оценить производительность MoE на более мощных конфигурациях, разбор GLM-5.2 на 8× GB10 даёт ориентиры по скорости prefill/decode на кластерных системах.