← ИнструментыКейс · opencode

Две RTX 3060 и Qwen на 27B: что реально даёт прирост от 9 до 40 токенов/с

Разбор домашнего стенда под кодинг-агента: какие настройки llama.cpp сработали, какие нет и где упирается локальный инференс.

· 4 мин чтения

Автор кейса на Habr собрал домашний сервер под локального кодинг-агента. В стенде Core i7-4790, 32 ГБ DDR3 и две RTX 3060 по 12 ГБ без NVLink и без CUDA P2P. Суммарно 24 ГБ VRAM, платформа старая, PCIe 8x, карты связаны через PHB. Цель — запускать Qwen3.8-27B с длинным контекстом в OpenCode и не платить за облачные тарифы по 100–200 долларов в месяц.

С чего всё начиналось

Базовый конфиг: Qwen3.8-27B Q4_K_M, контекст 131 072, Flash Attention, KV-cache в Q4, распределение модели по слоям между двумя GPU. На коротком запросе получалось 16.6 токенов/с. При заполненном контексте скорость падала: ~11.35 на 65K и ~8.89 на 120K. В агентоподобном тесте с заполненным ~100K модель выдавала 9.63 токенов/с. Эти «~9 токенов/с» из заголовка — реальная характеристика длинного рабочего контекста, а не синтетический бенчмарк.

Самый дешёвый прирост: layer → tensor

Замена распределения по слоям на тензорное распараллеливание дала почти удвоение скорости без смены модели и квантизации. На коротком тесте получилось 29.5 токенов/с. На длинном контексте: 20.27 на 65K и 16.09 на 120K. Логика простая: при autoregressive decode объём пересылаемых между GPU активаций мал относительно объёма весов, которые читаются каждый токен. Поэтому две подсистемы VRAM лучше нагружать параллельно, чем гнать данные последовательно через слои. Автор подчёркивает: вывод относится к его Z97/PHB-топологии, для другой платформы результат может отличаться.

MTP и борьба за память

Следующий шаг — speculative decoding через MTP (Multi Token Prediction). С отдельным MTP-черновиком на коротком контексте получалось до 66 токенов/с при глубине n=4. Но в режиме tensor split + полные 128K память стала узким местом: приходилось искать компромисс по размеру контекста, 96K помещались. На 90K вышло ~21.5 токенов/с, однако принятие токенов при n=4 просело до 25%. Цена была неприемлемой: ради MTP пришлось отдать четверть контекста, который для агента был одной из главных целей.

NCCL и shared buffers: что не сработало

NCCL на этом железе дал 61.6 токенов/с против 66.3 у встроенного механизма, то есть медленнее примерно на 7%. Причина та же: между картами нет прямого CUDA P2P, библиотека всё равно идёт по менее выгодному пути через PHB. Попытка разделить буферы основной модели и внешнего MTP через экспериментальную ветку QVAC показала same_buft=0 и same_dev=0. Объекты живут в разных backend-ах, и дальше это уже не настройка llama.cpp, а переписывание управления памятью ggml.

Поворот: Unsloth со встроенным MTP

Решением стал Qwen3.8-27B-UD-Q4_K_M от Unsloth, где MTP-тензоры лежат в том же файле модели. llama.cpp создаёт MTP-контекст против основной модели без второго объекта и его накладных расходов. Конфигурация: tensor split, полные 128K, KV-cache Q8, встроенный self-speculative decoding. В простое сервер занимает 11.3–11.4 ГБ на каждой карте, запас минимальный, но достаточный. В контролируемом тесте с ~120K входных токенов получилось 28.16 токенов/с. В реальной сессии OpenCode среднее по серверным логам составило 39.6 токенов/с.

Главный урок про VRAM

«Сервер запустился с -c 131072» и «модель реально пережила запрос на 120K» — разные утверждения. При старте KV-cache выделяется, но во время настоящего prefill нужны дополнительные рабочие буферы. Конфигурация, которая выглядела рабочей в nvidia-smi, падает с CUDA OOM. Автор перестал считать максимальный -c доказательством работоспособности и проверяет каждую настройку реально заполненным контекстом.

Как повторить

  1. Начните с Qwen3.8-27B Q4_K_M, контекста 131 072, Flash Attention, KV-cache Q4 и распределения по слоям: -sm layer, -fa on, -ctk q4_0, -ctv q4_0. Это отправная точка, и сразу видно, как длинный контекст роняет скорость.
  2. Замените -sm layer на -sm tensor. На платформе без CUDA P2P это всё равно даёт почти x2 на 120K, потому что параллельная нагрузка на две подсистемы VRAM выгоднее последовательного прохода по слоям.
  3. Проверьте стартовый -c реальным запросом на 65K и 120K, а не только nvidia-smi. Конфигурация, которая запускается, часто падает на prefill с CUDA OOM.
  4. Если используете внешний MTP, закладывайте потерю контекста: 128K в tensor split + внешний MTP не помещаются, рабочий диапазон около 96K. Следите за процентом принятых токенов: при просадке качество ускорения от большей глубины черновика снижается.
  5. Перейдите на Unsloth Qwen3.8-27B-UD-Q4_K_M со встроенным MTP. Это убирает второй объект модели и его накладные расходов и позволяет вернуть полные 128K вместе с KV-cache Q8.
  6. Не включайте NCCL вслепую: на платформе без CUDA P2P он может оказаться медленнее встроенного механизма llama.cpp. Сначала сравните цифры на вашем железе.