Запуск Qwen3.6-35B-A3B Q6 на RTX 3090 с 24 ГБ VRAM даёт всего 564 tok/s на prefill при батче 512. Это узкое место: генерация простаивает, пока GPU перемалывает длинный промпт. Решение - перенести восемь слоёв MoE-экспертов на CPU и увеличить батч до 1024, а микро-батч - до 512. Результат: 1330 tok/s на том же железе, прирост в 2.36 раза, скорость генерации не изменилась.
Метод воспроизводим, не требует апгрейда GPU и автоматизируется через эволюционный поиск LEVI. Разбираем механику, цифры и границы применимости.
Проблема: prefill упирается в VRAM на 24GB
Prefill - этап, на котором модель обрабатывает входной промпт и заполняет KV-кэш. В отличие от генерации, где токены идут последовательно, prefill перемалывает весь промпт сразу. Это матричное умножение высокой плотности, и его пропускная способность прямо зависит от размера батча. Больше батч - выше утилизация тензорных ядер, выше tok/s.
На RTX 3090 с 24 ГБ VRAM запуск Qwen3.6-35B-A3B Q6 оставляет под KV-кэш около 6-8 ГБ после загрузки весов. При батче 512 этого хватает впритык. Prefill выдаёт 564 tok/s. Генерация идёт на своих 30-40 tok/s и большую часть времени ждёт, пока prefill закончит обработку следующих запросов. Увеличить батч нельзя - OOM. Проблема не в вычислительной мощности GPU, а в объёме доступной памяти под KV-кэш. Именно это ограничение снимает гибридный offload.
Гибридный CPU offload: освобождаем память под батч
Qwen3.6-35B-A3B - это Mixture-of-Experts модель: 35 миллиардов общих параметров, но на каждый токен активируется только 3 миллиарда (A3B). Остальные эксперты лежат в памяти и ждут своей очереди. Архитектура избыточна по числу параметров и разрежена по активациям. Перенос части экспертов на CPU снижает пиковое потребление VRAM, освобождая место для KV-кэша большего батча.
В эксперименте на CPU выгрузили восемь слоёв экспертов. Потребление VRAM упало настолько, что батч вырос с 512 до 1024, а микро-батч - с 128 до 512. Prefill получил вдвое больше параллельной работы, утилизация GPU выросла, tok/s подскочили с 564 до 1330. Генерация осталась на прежнем уровне: 30-40 tok/s. Почему - разберём ниже.
Почему не страдает генерация: специфика MoE
На этапе генерации каждый новый токен активирует лишь малую долю экспертов. Запрос к эксперту на CPU добавляет задержку на передачу данных через PCIe, но таких запросов немного, и они перекрываются с вычислениями на GPU. Пока GPU обсчитывает dense-слои и attention, CPU подтягивает нужного эксперта. В prefill ситуация иная: промпт обрабатывается целиком, эксперты вызываются массово, и оверхед на CPU-запросы был бы критичным. Но prefill выигрывает от большого батча сильнее, чем проигрывает от offload. Рост батча с 512 до 1024 даёт прирост утилизации тензорных ядер, который перекрывает накладные расходы на PCIe-передачу для тех экспертов, что остались на GPU. Баланс сходится в плюс: 2.36× на prefill, генерация не задета.
Этот же принцип работает в других MoE-моделях. В запуске MoE-модели весом 204 ГБ на одной видеокарте комбинация unified memory и regex offload экспертов дала до 340 tok/s на префилле и 9.6 t/s при генерации. Механика та же: освободить VRAM под KV-кэш, пожертвовав минимальной задержкой на редких экспертных вызовах.
Батч-тюнинг: находим оптимальный размер
Offload восьми слоёв - необходимое, но не достаточное условие. После освобождения памяти нужно подобрать размер батча и микро-батча, при которых prefill работает быстрее всего. Зависимость нелинейная. Маленький батч недогружает GPU. Слишком большой - упирается в latency CPU-экспертов или снова вызывает OOM. Оптимум ищется экспериментально.
В эксперименте перебрали комбинации: батч от 256 до 1536 с шагом 128, микро-батч от 64 до 768. Для каждой конфигурации замеряли tok/s на prefill и стабильность генерации. Пик пришёлся на батч 1024 и микро-батч 512. При батче 1280 начинались провалы: очередь к CPU-экспертам росла, prefill замедлялся. При микро-батче 768 росло пиковое потребление памяти, риск OOM увеличивался. Найденная точка - 1330 tok/s - на 136% быстрее исходных 564 tok/s.
Инструмент для автоматизации: эволюционный поиск LEVI
Ручной перебор - дело небыстрое. LEVI (Latency Evolution via Inference) автоматизирует процесс. Алгоритм создаёт популяцию конфигураций: число offload-слоёв, размер батча, размер микро-батча. Каждая конфигурация прогоняется через короткий бенчмарк-промпт. Результат - tok/s на prefill. Лучшие конфигурации скрещиваются и мутируют: добавляется/убирается слой offload, меняется батч. Через 20-30 поколений популяция сходится к оптимуму.
В нашем случае LEVI за 15 минут нашёл ту же связку - 8 слоёв на CPU, батч 1024, микро-батч 512 - что и ручной перебор за два часа. Код поиска опирается на стандартные биндинги llama.cpp и не требует модификации рантайма. Это снижает порог входа: не нужно вручную гонять десятки конфигураций, алгоритм делает это сам.
Автоматический подбор параметров особенно полезен при смене модели или железа. Спекулятивный декодинг на частично выгруженных MoE показывает, как пошаговый тюнинг p_min/n_max возвращает скорость с 23 до 64 tok/s. LEVI делает этот тюнинг автоматическим.
Воспроизводимость: что нужно для повторения эксперимента
Минимальные требования: GPU с 24 ГБ VRAM (RTX 3090, RTX 4090, A5000), CPU с 8+ ядрами и поддержкой AVX2, 64 ГБ системной RAM, llama.cpp сборки не ниже b4530 с флагом --n-cpu-moe. Модель: Qwen3.6-35B-A3B в квантовании Q6_K_M (размер файла GGUF около 28 ГБ).
Пример команды запуска:
./llama-cli \
-m qwen3.6-35b-a3b-q6_k_m.gguf \
-ngl 99 \
--n-cpu-moe 8 \
-c 32768 \
-b 1024 \
-ub 512 \
--mlock \
--no-mmapФлаг -ngl 99 загружает все dense-слои на GPU. --n-cpu-moe 8 выгружает восемь слоёв экспертов на CPU. -b 1024 и -ub 512 задают батч и микро-батч. --mlock и --no-mmap предотвращают свопинг и ускоряют доступ к весам в RAM.
Важные нюансы. Частота системной памяти влияет на задержку CPU-экспертов: DDR5-5600 даёт на 15-20% меньше latency, чем DDR4-3200. Версия llama.cpp должна быть собрана с поддержкой CUDA 12.x и флагом LLAMA_CUDA_MMQ=ON для быстрых матричных умножений на GPU. Разные версии библиотек могут давать разброс в 5-10% по tok/s - это нормально, главное, чтобы offload работал стабильно без падений.
Ограничения и когда метод не сработает
Offload экспертов на CPU не даст выигрыша в трёх случаях. Первый: модель с плотными слоями (dense), без MoE-архитектуры. Выгружать нечего, каждый слой нужен на каждом токене, перенос на CPU убьёт и prefill, и генерацию. Второй: очень медленный CPU, например, мобильный чип или серверный Xeon 10-летней давности без AVX2. PCIe-передача и вычисления на CPU будут добавлять столько задержки, что рост батча не компенсирует. Третий: короткие промпты до 500 токенов. Prefill на маленьком объёме данных не успевает насытить GPU даже при большом батче, оверхед на offload становится заметен, итоговый выигрыш - в пределах погрешности.
Накладные расходы на передачу данных между CPU и GPU через PCIe 4.0 x16 составляют около 25-30 ГБ/с. При батче 1024 и микро-батче 512 суммарный объём передаваемых экспертных весов за один prefill - порядка 200-300 МБ. Передача занимает 8-10 мс. Это менее 5% от общего времени prefill на промпте в 4000 токенов. Пока эти 5% не превращаются в 20% из-за медленного CPU или перегруженной шины, метод работает.
Потенциал обобщения: другие модели и GPU
Подход применим к любой MoE-модели, где эксперты можно выгружать независимо от dense-слоёв. Mixtral 8x7B и 8x22B, DeepSeek-V2 и V3, Qwen2.5-MoE, DBRX - все они выигрывают от offload при нехватке VRAM. Для DeepSeek-V3 с 671B общих параметров offload экспертов - единственный способ запустить модель на потребительском GPU. Оптимизация инференса DeepSeek-V4-Flash на одном B300 показала, что даже на серверном GPU отказ от expert parallel и ручная настройка offload критичны для производительности.
На GPU с 12 ГБ VRAM (RTX 3060, RTX 4070) метод тоже работает, но с оговорками. Модель придётся квантовать агрессивнее - до Q4_K_M или IQ4_NL. Число offload-слоёв вырастет до 12-16. Prefill-ускорение будет скромнее: 1.3-1.5× вместо 2.36×, потому что батч ограничен не только VRAM, но и пропускной способностью PCIe. На GPU с 48 ГБ (RTX 6000 Ada, A6000) offload может не понадобиться вовсе - всё помещается в VRAM. Но если цель - максимизировать батч для throughput, offload 2-4 слоёв даст дополнительные 10-15% к prefill за счёт освобождения памяти под KV-кэш.
Метод комбинируется с другими оптимизациями. Квантизация весов (Q4, Q5, Q6) снижает потребление VRAM и оставляет больше места под батч. FlashAttention сокращает размер KV-кэша в 2-4 раза. Предсказание загрузки экспертов MoE с использованием MTP-головки скрывает латентность PCIe и потенциально удваивает скорость генерации при offload. Комбинация offload + предсказание экспертов + FlashAttention способна выжать максимум из ограниченного железа.
Выводы: когда оффлоад окупается
Гибридный CPU offload с батч-тюнингом даёт кратный прирост prefill при трёх условиях: модель - MoE, prefill упирается в VRAM, батч меньше желаемого. На RTX 3090 с Qwen3.6-35B-A3B Q6 это 2.36× ускорение: с 564 до 1330 tok/s. Генерация не страдает. Метод не требует нового железа, воспроизводится стандартными средствами llama.cpp и автоматизируется через LEVI.
Правило принятия решения простое. Запускаете модель, смотрите на утилизацию GPU во время prefill. Если она ниже 80%, а VRAM заполнена под завязку - offload поможет. Если утилизация и так под 100% - offload не нужен, у вас другой bottleneck. Если модель не MoE - метод неприменим.
Ближайший практический шаг: скопируйте команду запуска из раздела «Воспроизводимость», подставьте путь к своей модели и проверьте на типичном для вас промпте. Разница между 564 и 1330 tok/s на prefill - это минуты ожидания, превращённые в секунды.