IT-инфраструктура • инструкции • практикаПоиск по сайту

Серверы и системное администрирование

Найти материал →

Qwen3.8 в llama.cpp: как достичь 75 токенов/с на двух Rtx 3090

Qwen3.8: MTP и тензорный параллелизм в llama.cpp - до 75 токенов/с на двух RTX 3090

Плотная модель Qwen3.8-27B на двух RTX 3090 при стандартном запуске llama.cpp показывает около 32 токенов в секунду. На первый взгляд кажется, что система почти упёрлась в возможности оборудования: у каждой видеокарты 24 ГБ памяти и пропускная способность примерно 936 ГБ/с. Однако такой результат оказался далеко не пределом. После корректной настройки скорость генерации выросла до 74,8 токена в секунду - без замены весов и заметного ухудшения качества.

Практический опыт показывает, что llama.cpp настройка производительности упирается не в один параметр, а сразу в несколько независимых механизмов. В данном случае решающими стали MTP-спекулятивное декодирование, тензорное распределение вычислений и правильный выбор GGUF-сборки. Подробный разбор конфигурации и поведения модели можно найти в материале о [настройке инференса Qwen3.8 в llama.cpp](https://habr.com/ru/articles/1076080/?utm_campaign=1076080&utm_source=habrahabr&utm_medium=rss).

Тестовая конфигурация

Использовался следующий стенд:

- две RTX 3090 по 24 ГБ;
- PCIe 4.0 x16, без NVLink;
- AMD Threadripper 3960X;
- 121 ГБ оперативной памяти;
- Ubuntu 26.04;
- драйвер NVIDIA 595.71.05;
- контейнер `ghcr.io/ggml-org/llama.cpp:full-cuda`;
- сборка llama.cpp 10666;
- Qwen3.8-27B в формате GGUF.

Модель плотная, мультимодальная, а MTP-голова уже включена в файл GGUF. В результате скорость генерации выросла с 32,4 до 74,8 токена в секунду. Параллельно удавалось обслуживать два запроса с контекстом до 262 144 токенов, а в проведённых тестах качество ответов не изменилось.

Почему базовые 32 токена в секунду - не предел

Плотная модель при генерации каждого нового токена последовательно обращается ко всем своим весам. Размер используемого файла составляет около 24,1 ГБ. При послойном распределении одна видеокарта обрабатывает свою часть слоёв, затем в работу вступает вторая. Условная оценка выглядит так: 12 ГБ данных при пропускной способности 936 ГБ/с требуют примерно 12,9 мс на карту, или около 25,7 мс на токен для обеих карт. Теоретический предел такого режима - приблизительно 39 токенов в секунду.

Показатель 32,4 токена в секунду действительно близок к этому пределу. Значит, проблема не в неисправности GPU, а в самой схеме работы: видеокарты читают веса не одновременно, а последовательно. Дополнительный резерв даёт возможность за один проход предсказывать несколько токенов, а не только следующий.

Приём 1. Включение MTP

В GGUF Qwen3.8 присутствует встроенный блок Multi-Token Prediction - набор тензоров `blk.64.nextn.*`. По сути, это draft-модель, которая предлагает несколько вероятных продолжений, после чего основная модель проверяет их. По умолчанию llama.cpp может не использовать этот блок и выводить в журнал сообщение о неиспользуемых тензорах.

Для включения MTP применяются параметры:

```bash
--n-predict 4096
--draft-max 4
--draft-min 1
```

Конкретный набор флагов зависит от версии сборки, поэтому при обновлении llama.cpp стоит сверяться с выводом `--help`. На тестовом стенде MTP поднял скорость с 32,4 до 58,5 токена в секунду в послойном режиме - примерно в 1,8 раза.

Оптимальной оказалась глубина чернового предсказания 3-4. При двух шагах получилось 52,8 токена в секунду, при трёх - 57,3, при четырёх - 57,6, а при шести скорость снизилась до 54,3. Слишком длинная цепочка чаще приводит к ошибкам черновика и увеличивает накладные расходы.

Важно учитывать отдельный KV-кэш MTP. Параметры `-ctk` и `-ctv` на него не распространяются: без `-ctkd` и `-ctvd` он хранится в формате F16 и незаметно расходует видеопамять. Эффект также зависит от характера запроса. Код и структурированный текст обычно лучше подходят для MTP, чем свободное рассуждение.

Приём 2. Тензорный параллелизм

Начиная с соответствующих сборок llama.cpp, параметр `--split-mode tensor` позволяет распределить вычисления каждого слоя между видеокартами через NCCL. В отличие от режима `layer`, GPU работают одновременно, а не передают управление друг другу после обработки собственных слоёв.

Используется следующая настройка:

```bash
-sm tensor
```

При запуске контейнера обязательно требуется общий IPC-пространство:

```bash
--ipc=host
```

Альтернативой может быть увеличение общей памяти через `--shm-size`. Без этого первый этап декодирования завершается ошибкой `CUDA error: unhandled system error` внутри `ggml_backend_cuda_comm_allreduce_nccl`.

Измерения с включённым MTP дали такие результаты:

| Сценарий | `-sm layer` | `-sm tensor` |
|---|---:|---:|
| Генерация, короткий контекст | 58,5 | 74,7 |
| Генерация при 130K | 41,4 | 58,9 |
| Генерация при 240K | 34,4 | 55,4 |
| Два параллельных слота | 36,3 + 36,3 | 47,4 + 50,3 |
| Обработка промпта при 40K | 1410 | 1121 |
| Обработка промпта при 240K | 764 | 743 |
| Используемая VRAM | 42,4 ГБ | 43,6 ГБ |

Чем длиннее контекст, тем заметнее преимущества тензорного режима. Вероятное объяснение состоит в том, что при послойной схеме каждая карта обращается к своей части KV-кэша, пока другая ожидает. При тензорном параллелизме чтение выполняется одновременно, а между устройствами передаются в основном результаты коллективных операций над активациями.

Обратная сторона - более медленная обработка коротких промптов: примерно на 20% при 40K и на 11% при 130K. Поэтому для интерактивного диалога и длинного контекста режим `tensor` выглядит особенно выгодно, а в коротких batch-задачах выигрыш может оказаться неочевидным.

Параметр `-ts` в этой конфигурации не помогает: автоматическая подгонка для `SPLIT_MODE_TENSOR` не реализована. Размер контекста приходится выбирать вручную, контролируя загрузку через `nvidia-smi`.

Приём 3. Выбор GGUF по таблице тензоров

Файлы GGUF с одинаковым названием модели могут заметно отличаться внутренней раскладкой тензоров. Это влияет на то, насколько эффективно llama.cpp размещает веса на нескольких GPU. Поэтому при выборе квантования недостаточно смотреть только на размер файла и обозначение вроде Q4 или Q5.

Нужно проверять таблицу тензоров, объём KV-кэша, возможность загрузки всех критичных слоёв в VRAM и совместимость с выбранным режимом распределения. В некоторых случаях чуть более крупная сборка работает быстрее компактной, поскольку лучше соответствует архитектуре видеопамяти и схеме split.

Для KV-кэша в тесте использовался `q4_0`. Такой вариант заметно снижает расход VRAM и позволяет сохранить большой контекст. Однако слишком агрессивное сжатие может негативно повлиять на стабильность длинных диалогов, поэтому настройки стоит проверять не только по скорости, но и по качеству ответов.

Приём 4. Чат-шаблон нужно проверять отдельно

Корректный chat template не является косметической деталью. Если шаблон не соответствует формату, ожидаемому Qwen3.8, модель может хуже понимать роли сообщений, неверно завершать ответы и демонстрировать нестабильное поведение при длинном контексте.

Весам и шаблону не обязательно доверять одинаково. Перед тестами нужно проверить специальные токены, формат системного сообщения, маркеры инструментов и правила завершения генерации. Даже идеальная оптимизация llama.cpp для видеокарт не компенсирует неправильную разметку диалога.

Дополнительные ограничения

В режиме `tensor` сэмплирование переносится на CPU и выполняется отдельно для каждого слота. В журнале появляется столько соответствующих строк, сколько одновременно активных запросов. На мощном процессоре это почти незаметно, но при слабом CPU, большом числе слотов или подключении GPU через PCIe x1/x4 обмен логитами может стать новым узким местом.

Режим `-sm row` на описанной конфигурации не запустился: llama.cpp вернула ошибку `device CUDA0 does not support split buffers`. Результаты могут отличаться на системах с PCIe x8 или x4, поскольку стоимость all-reduce там выше. В данном тесте обе RTX 3090 работали через PCIe 4.0 x16.

Итоговая связка - MTP с глубиной 3-4, `-sm tensor`, общий IPC контейнера, подходящая GGUF-квантизация и сжатый KV-кэш - дала рост с 32,4 до 74,8 токена в секунду. Это хороший пример того, как ускорение инференса LLM на GPU достигается не одним универсальным флагом, а согласованной настройкой всей цепочки.

При этом каждую конфигурацию нужно измерять на собственных запросах. Скорость зависит от длины контекста, количества параллельных слотов, типа текста, пропускной способности PCIe и нагрузки на CPU. Полезно сравнивать не только tokens/s при генерации, но и скорость обработки промпта, расход VRAM, стабильность на длинных сессиях и фактическое качество ответов. Именно такой комплексный подход позволяет превратить Qwen3.8 запуск на двух RTX 3090 из формального эксперимента в практичную рабочую конфигурацию для локального инференса.

Прокрутить вверх