Плотная 27B-модель на двух RTX 3090 в llama.cpp из коробки даёт 32 токена в секунду. Интуитивно понятно, что это не предел: две карты по 24 ГБ и 936 ГБ/с каждая не должны выдавать столько, сколько выдаёт одна. Значит, где-то узкое горлышко, и надо разобраться, что и где подкрутить. Оказалось, что горлышек несколько, и все они снимаются флагами запуска: те же веса — 75 токенов в секунду. Ниже каждый флаг описан одинаково: зачем, что писать, что даёт, где ломается. В конце — готовый docker run.

Стенд: 2× RTX 3090 (24 ГБ, PCIe 4.0 x16, без NVLink), Threadripper 3960X, 121 ГБ RAM, Ubuntu 26.04, драйвер 595.71.05. llama.cpp — образ ghcr.io/ggml-org/llama.cpp:full-cuda, build 10666. Модель — Qwen3.8-27B (плотная, мультимодальная, с MTP-головой внутри GGUF).

Результат: 32.4 → 74.8 ток/с на генерации, два запроса по 262 144 токена одновременно, качество на моих тестах не изменилось.

Почему 32 — это не «упёрлись в железо»

Плотная модель на каждый токен читает все веса. Файл 24.1 ГБ, память 3090 — ~936 ГБ/с. При послойном разбиении карты работают по очереди: 12 ГБ / 936 ГБ/с ≈ 12.9 мс на карту, 25.7 мс на токен, потолок ≈ 39 ток/с. Из коробки — 32.4, то есть 83% от потолка. Сам потолок низкий, и его поднимают ровно две вещи: читать веса на обеих картах одновременно и получать больше одного токена за проход. Это приёмы 2 и 1.

Приём 1. Включить MTP-спекулятивное декодирование

Зачем. В GGUF Qwen3.8 лежит слой Multi-Token Prediction (blk.64.nextn.*) — встроенная draft-модель. По умолчанию llama.cpp его не использует и пишет в лог unused tensor.

Флаги:

--spec-type draft-mtp --spec-draft-n-max 3
-ctkd q4_0 -ctvd q4_0

Эффект: 32.4 → 58.5 ток/с (послойный режим), ×1.8.

Где ломается:

  • KV-кэш черновика — отдельный. -ctk/-ctv на него не действуют; без -ctkd/-ctvd он хранится в f16 и молча ест VRAM.

  • Глубина черновика больше 4 вредит. Замеры: 2 → 52.8, 3 → 57.3, 4 → 57.6, 6 → 54.3 ток/с. Ставьте 3 или 4.

  • Выигрыш зависит от промпта: на коде и структурированном тексте черновик угадывает чаще, на свободном рассуждении — реже. Все цифры здесь на одном фиксированном промпте.

Приём 2. -sm tensor вместо -sm layer

Зачем. С build 10666 у --split-mode есть значение tensor — тензорный параллелизм через NCCL: обе карты считают каждый слой одновременно, вместо того чтобы работать по очереди.

Флаг: -sm tensor. Плюс на контейнере обязательно --ipc=host (или --shm-size).

Эффект (обе колонки с MTP, три прогона на точку, разброс ±1 ток/с):

-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-кэша, пока вторая ждёт; при тензорном обе читают параллельно, а по шине ходит только all-reduce активаций, который от длины контекста не зависит.

Цена: обработка промпта −20% на 40K, −11% на 130K, −3% на 240K. Для интерактивной работы выгодно; для batch-прогонов коротких промптов — не факт.

Где ломается:

  • Без --ipc=host первый же decode падает с CUDA error: unhandled system error в ggml_backend_cuda_comm_allreduce_nccl. Сообщение на docker не намекает.

  • -ts не работает: llama_params_fit is not implemented for SPLIT_MODE_TENSOR. Автоподбора нет, размер контекста подбирайте руками и проверяйте по nvidia-smi.

  • Сэмплирование уезжает на CPU, и отдельно на каждый слот. В логе это выглядит так:

    spec common_specu: backend offload failed for seq_id=0; using CPU sampler
    spec common_specu: backend offload failed for seq_id=1; using CPU sampler
    

    Строк ровно столько, сколько слотов. Под -sm layer их нет вообще — я специально сверил оба контейнера: 0 вхождений против одного на слот. На моём стенде это ничего не стоит, но 24 ядра и обе карты на x16 — щадящие условия. Если у вас слабый CPU, много слотов или карты на райзерах x1/x4, черновиковый сэмплер на CPU плюс перекачка логитов туда-обратно вполне могут стать узким местом. Проверьте, что эти строки не превращаются в потерю скорости именно у вас.

  • -sm row на этой конфигурации не работает вообще: error loading model: device CUDA0 does not support split buffers.

  • У меня обе карты на PCIe 4.0 x16. На x8/x4 all-reduce дороже, результат может быть другим — не проверял.

Приёмы 1 и 2 вместе (одни веса, один промпт):

без MTP

с MTP

-sm layer

32.4

58.5

-sm tensor

48.2

74.8

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

Зачем. Два файла с одной меткой «Q6» могут быть устроены по-разному, и меньший не обязательно быстрее. Эффект здесь единицы процентов, раздел скорее про метод.

Что я сравнил. Четыре сборки одной модели: unsloth Q8_0, bartowski Q8_0, bartowski Q6_K_L, orcarouter Uncensored (абляция). Качество: все четыре — 13/16 на моём наборе, провалили одни и те же три задачи. Набор: 3 задачи на код с исполнением, 2 вызова функций с проверкой JSON, 3 на формат, 5 логических ловушек, тест на второе system-сообщение, needle-in-a-haystack на 69K и 240K (3/3 у всех). Это дымовой тест, не бенчмарк; perplexity не мерил.

Скорость в один день, одни аргументы: unsloth Q8_0 — 57.6, bartowski Q6_K_L — 60.2 ток/с. Двумя неделями раньше плоский Q6_K от unsloth против того же Q8_0: 56.0 против 56.7 — ничего. Прямого замера Q6 против Q6 в один день нет (unsloth к тому моменту удалил плоские K-кванты), но через общую базу Q8_0 картина такая: Q6 от bartowski быстрее Q8, Q6 от unsloth — нет.

Почему (гипотеза, согласуется с замерами, вручную не проверял). Таблицы тензоров двух файлов:

bartowski Q6_K_L

unsloth UD-Q6_K

Типов квантования

3

7

token_embd / output

Q8_0 / Q8_0

Q6_K / Q8_0

Блок blk.64 (голова MTP)

все 8 тензоров Q4_0

eh_proj — Q6_K

Размер файла

24.1 ГБ

~22.9 ГБ

При --spec-draft-n-max 3 голова MTP выполняется три раза за шаг — это самый горячий блок в модели, и её формат важнее формата тела. bartowski положил её в Q4_0, unsloth — в Q6_K. Файл bartowski больше, но быстрее, так что дело не в пропускной способности памяти.

Как применить:

  • Перед бенчмарком сдампите таблицу тензоров. Заголовок GGUF читается последовательно; единственная подлость — массивы в метаданных нужно вычитывать до конца, иначе поедет позиция. Дампер — полсотни строк на Python.

  • Заголовок файла с HuggingFace берётся range-запросом, качать 24 ГБ не нужно:

curl -sSL -r 0-16000000 -o header.gguf \
  https://huggingface.co/<repo>/resolve/main/<file>.gguf
  • Не сравнивайте сборки по draft acceptance из лога: у меня он гулял от 56% до 100% в зависимости от промпта. Сравнивайте конечную скорость на одинаковом входе.

  • На модели без MTP-головы вывод «bartowski быстрее» может не повториться.

Приём 4. Брать чат-шаблон отдельно от весов

Зачем. С --jinja llama.cpp форматирует запросы шаблоном из метаданных GGUF. Шаблон bartowski — стоковый от Qwen, и на трёх проверках он:

  • молча выбрасывает все system-сообщения после первого;

  • отвергает reasoning_effort=high ошибкой;

  • не валидирует аргументы вызова функций.

В сборке unsloth все три места исправлены.

Флаг:

--chat-template-file /models/Qwen3.8-27B/chat-template-unsloth.jinja

Шаблон — строковое поле tokenizer.chat_template в метаданных, вынимается из чужого GGUF тем же range-запросом.

Эффект: после подмены второе system-сообщение доходит, вызов функции приходит с правильными аргументами, reasoning_effort=high принимается. Скорость не меняется.

Где ломается: квант-репозитории живут своей жизнью. unsloth перезалил файлы через несколько часов после моей первой загрузки (с починенным шаблоном), а через неделю заменил плоские K-кванты семейством UD-*. Проверяйте по коммитам квант-репозитория и sha256, а не по дате релиза модели.

Приём 5. q4_0 для KV-кэша

Зачем. KV-кэш в f16/q8_0 на 262K токенов и двух слотах требует ~52 ГБ и не влезает в 48. На q4_0 тот же пул занимает вдвое меньше.

Флаги:

-ctk q4_0 -ctv q4_0 -c 524288 --parallel 2 -ub 256

Эффект: два слота по 262 144 токена одновременно на тех же 48 ГБ.

Про -c стоит сказать отдельно, потому что читается он не так, как выглядит. -c задаёт суммарный пул KV на весь сервер, а не длину одного запроса — llama.cpp делит его на слоты: 524 288 / 2 = 262 144, ровно родной контекст модели. Напишете -c 262144 --parallel 2 — получите два слота по 131 072, и длинные промпты начнёт обрезать. Результат дележа сервер печатает при старте — эта строка и есть проверка:

srv load_model: initializing, n_slots = 2, n_ctx_slot = 262144

Два слота нужны, чтобы второй запрос не ждал первого в очереди: агент и редактор ходят в один эндпоинт одновременно. Платите за это скоростью каждого — 47.4 + 50.3 ток/с при двух занятых слотах против 74.8 в одиночку.

Что проверил: multi-needle retrieval на 240K (92% слота), 20 отвлекающих записей с кодами, отличающимися на один символ, — 10/10. Тест говорит только, что точное извлечение не сломалось; о качестве рассуждения на длинном контексте он не говорит ничего. На некоторых моделях q4_0 длинный контекст портит заметно — проверьте на своей нагрузке.

Где ломается (при измерении, обе ловушки дали мне ложные провалы):

  • маленький n_predict обрезает ответ — модель успевает только начать рассуждать;

  • нельзя грепать весь вывод: в рассуждении модель цитирует отвлекающие коды, правильно их отвергая. Оценивайте только текст после </think>.


Итоговый конфиг

docker run -d --name qwen38 --restart unless-stopped --gpus all \
  --ipc=host --shm-size=2g \
  -p 8080:8080 -v /models:/models:ro \
  --entrypoint /app/llama-server ghcr.io/ggml-org/llama.cpp:full-cuda \
  --model /models/Qwen3.8-27B-bartowski/Qwen3.8-27B-Q6_K_L.gguf \
  --mmproj /models/Qwen3.8-27B-bartowski/mmproj-Qwen3.8-27B-f16.gguf \
  --chat-template-file /models/Qwen3.8-27B/chat-template-unsloth.jinja \
  --flash-attn on -sm tensor -ngl 99 --no-mmap \
  -ctk q4_0 -ctv q4_0 -ctkd q4_0 -ctvd q4_0 \
  -c 524288 --parallel 2 -ub 256 \
  --spec-type draft-mtp --spec-draft-n-max 3 \
  --cache-ram 16384 --jinja --reasoning off \
  --host 0.0.0.0 --port 8080

74.8 ток/с генерация, 1121 ток/с обработка промпта при 40K, два слота по 262 144, 43.6 ГБ VRAM из 48. Мультимодальность через --mmproj работает.

Как проверить, что стало лучше, а не хуже

llama-server возвращает timings в ответе /completion: predicted_per_second и prompt_per_second. Отдельный бенчмарк не нужен.

После каждой смены флага или сборки:

  1. Скорость. Три прогона, "cache_prompt": false, temperature: 0, короткий промпт и длинный (40K, 130K, 240K). Если разброс больше пары процентов — сравнивать рано.

  2. Качество. Задачи с машинной проверкой: код исполняется и проверяется ассертами, вызов функции сверяется по JSON, формат — регуляркой. Разницу Q6/Q8 такой набор не поймает, но поймает сломанный шаблон или провал вызова функций — именно это ломается чаще всего.

  3. Длинный контекст. Needle-in-a-haystack на глубинах 10/50/90%. Считайте промпт в токенах, а не в символах: у меня первый прогон улетел на 540K вместо 120K и «провалил» тест на всех четырёх сборках сразу.

Что не сработало, чтобы не тратить время: -sm row (не поддерживается), плоский Q6 от unsloth (быстрее Q8 не стал), абляция Uncensored (52.7 ток/с — правка весов сбивает MTP-голову), -ts в тензорном режиме (игнорируется).

Если отдаёте это агенту

Текст устроен как инструкция: у каждого приёма есть флаг, измеренный эффект и строки ошибок, по которым узнаются ловушки. Его можно отдать агенту с доступом к серверу целиком и попросить применить к своей конфигурации, а проверять — по разделу «Как проверить». Самая ценная часть при этом — список «что не сработало»: ровно в эти тупики полезет любой, кто настраивает с нуля.

Комментарии (23)


  1. ampir-nn
    29.08.2026 20:42

    Четыре P102-100 Qwen3.8-27B-Q5 без -sm tensor было 15-20 ток. нагрузка на GPU была 50%. С -sm tensor стало 25-33 ток. и все GPU загружаются на 100%. Отлично - теперь llama.cpp работает с картами как и vllm. Спасибо.


    1. Livaur
      29.08.2026 20:42

      Можно поинтересоваться какое pci-e подключение карт и какой процессор?


  1. tachyon-tomsk
    29.08.2026 20:42

    У меня на 4x3060 12гб, при включении MTP сразу просадка до 6т/c на двух потоках. С выключенным 40-50т/c на двух потоках (суммарно).
    А на одном потоке включенный MTP подымает примерно с 30 до 40т/c.


    1. kotafey Автор
      29.08.2026 20:42

      У меня на 2×3090 та же связка ведёт себя иначе: -sm layer + MTP + --parallel 2 даёт 36.3 + 36.3 ток/с, без всякого обвала. Так что MTP с несколькими слотами сам по себе не сломан — у вас что-то конкретное, и, судя по цифрам, это не деградация, а падение с обрыва. 8-кратный провал — почерк не «стало неэффективно», а «работа уехала туда, где её быть не должно». Если не жалко, покажите вывод nvidia-smi --query-gpu=pcie.link.width.current --format=csv.


  1. h0ldfast69
    29.08.2026 20:42

    Большое спасибо, Добрый человек. Половину из этого уже накопал, стала стабильно работать на трёх mi50 32g, вторую половину сейчас проверю. Особенно надежда на квантование кэша черновика и sm tensor


  1. Mintavrus
    29.08.2026 20:42

    Не понял, вот это -c 524288 --parallel 2зачем?


    1. DimanODG
      29.08.2026 20:42

      Для обработки (генерации ответов) от 2-х параллельных запросов.


    1. kotafey Автор
      29.08.2026 20:42

      Дам подробный ответ. -c — это суммарный пул KV на весь сервер, а не длина одного запроса. llama.cpp делит его на слоты, поэтому -c 524288 --parallel 2 = два независимых слота по 262 144 токена, ровно родной контекст модели. Обратная сторона: если написать -c 262144 --parallel 2, каждый слот получит по 131 072, и длинные промпты начнёт обрезать — при том что в аргументах вроде бы стоит полный контекст модели. Проверяется по строке при старте:

      srv load_model: initializing, n_slots = 2, n_ctx_slot = 262144, kv_unified = 'false'

      Два слота нужны, чтобы второй запрос не ждал в очереди, пока идёт первый: агент и редактор ходят в один эндпоинт одновременно. Цена — скорость каждого: 47.4 + 50.3 ток/с при двух занятых слотах против 74.8 в одиночку, то есть суммарная пропускная способность растёт, а отдельный ответ идёт медленнее.

      Я в работе использую связку из скилов и субагентов - и мне это важно. Обычно я ставлю 4 слота, т.е. контекст на 1М в 4 параллели - 4 слота по 256к. С одной стороны это мой кейс, и мой конфиг - для всех читателей это карта куда идти за результатом. С другой - конкретно эти параметры можно было бы более подробно описать. В этом комментарии и исправляюсь.


      1. punzik
        29.08.2026 20:42

        Можно использовать unified kv cache (--kv-unified), чтобы был общий пул кэша для всех запросов.


        1. kotafey Автор
          29.08.2026 20:42

          Проверил на стенде — спасибо.

          Флаг работает как обещано: --kv-unified -c 262144 --parallel 2 освобождает 8.8 ГиБ (42.6 → 33.8), needle на 240K те же 3/3, скорость просела на 2%. Но есть цена, которую видно только отдельным тестом.

          Общий пул вытесняет кэши слотов. Даю слоту 0 контекст на 143K, слоту 1 другой на 143K, возвращаюсь к слоту 0:

          unified : возврат 143 562 ток, 163.6 с статика : возврат 261 ток, 1.8 с

          Два контекста по 143K — это 286K, пул 262 144, вместе не помещаются, первый вытесняется. --cache-idle-slots не спасает, это честный пересчёт с нуля. Разница в 91 раз.

          Мне слоты нужны как независимые контексты со своими живущими кэшами, поэтому остаюсь на статической нарезке. Правило: unified выигрывает, когда слотов много, а сумма их активных контекстов заведомо меньше пула. Как только сумма превышает пул, начинается вытеснение.

          Два побочных наблюдения. С unified n_ctx_slot упирается в родной контекст модели, а не в -c — поэтому unified с -c 524288 бессмыслен, памяти больше, скорость ниже, возможности те же. И unified включён по умолчанию, если --parallel не задан явно, так что часть читателей уже на нём.


      1. Mintavrus
        29.08.2026 20:42

        Вы используете 2 параллельных запроса в однопользовательском режиме правильно понимаю? То есть модель подключает субагента и тот работает параллельно основной задаче?


        1. kotafey Автор
          29.08.2026 20:42

          Смысл в слотах - это кэши. Если агент+субагент, то это 2 независимых контекста со своими кэшами. Они могут работать и параллельно и последовательно, главное - у них свои кэши.


  1. denis_service36
    29.08.2026 20:42

    На CMP 50HX 20GB Qwen3.8-27B Heretic IQ4_XS + MTP n=2 + Vision у меня завелась на 2×64K контекста. Prefill: ~558 tok/s на 5K, ~531 на 20K, ~427 на 60K. TG в коротких тестах: ~45/32/28 tok/s соответственно. MTP acceptance ~67%


    1. Anton_Timofeev
      29.08.2026 20:42

      Кажется, что acceptance слишком низкий. Может поднять –spec-draft-p-min повыше?


  1. Weron2
    29.08.2026 20:42

    Хочу тоже поделиться наблюдениями. Очень похоже что качество действительно зависит от сборки. Unsloth зарекомендовал себя,но похоже что иногда они переигрывают. Недавно заиорочился тоже с проверкой скорости, но было интереснее именно математически рассчитать - получился своебразный калькулятор +- показывающий то что у меня на выходе. Но для чистоты эксперимента и прогонов я взял модель бартовски обратил внимание в метаданных что там 33 слоя у квена, а у анслоф 32...

    В общем погонял модели и удивидся что мой промпт на 4b модель смогла сделать играбельную игру, в то время как на сборке unsloth такое выходило раз в 10-20 прогонов. Надо бы еще раз погонять...

    Пожалуй напишу небольшую статью про то как я это делал, и может вы сделаете что-то похожее... Я сделал букмарклет который на странице модели посвечивает теоретическую скорость на моем железе. Кстати обратил внимание что если залогиниться на hf можно добавить свое железо и он тоже может выдать какие-то цифры, но пока не проверял


  1. moooV
    29.08.2026 20:42

    У меня Q6_K_M на 200к контексте отлично влезли с MTP головой, 100т/с на одной 5090. 256к влезло, но скорость падает до 1т/с после достижения 210к, так что 200 поставил.

    Если кому надо, вот команда которую использую:

    llama-server.exe -m "d:\llamacpp_models\Qwen3.8-27B-UD-Q6_K_M.gguf" --port 11434 --host 0.0.0.0 --ctx-size 200000 --temp 1.0 --top-p 0.95 --top-k 20 --min-p 0.0 --jinja --threads 24 -ngl 99 --parallel 1 --timeout 60000 -cram -1 -fa on -ctk turbo4 -ctv turbo3 -ctkd q8_0 -ctvd q8_0 --spec-type draft-mtp --spec-draft-n-max 3 --kv-unified --chat-template-kwargs "{\"reasoning_effort\":\"xhigh\"}" --reasoning-preserve -ngld all -md d:\llamacpp_models\mtp-Qwen3.8-27B-Q4_0.gguf

    Там собран под железо (5090 - sm120) форк ламы с турбоквантом свежайший из гита.

    Кванты от unsloth.

    На этом конфиге через него за неделю около 100м токенов прогнал.


    1. kotafey Автор
      29.08.2026 20:42

      а мне как раз интересно было сколько можно выжать на 5090.


    1. An_Sm_ru
      29.08.2026 20:42

      а откуда взялась модель -md d:\llamacpp_models\mtp-Qwen3.8-27B-Q4_0.gguf , она отдельно скачивается?
      Это ведь модель только с MTP слоем?



  1. sgolomedov
    29.08.2026 20:42

    5090 + 3090 через oculink (PCIe x4 4.0) все с MTP

    -sm layer

    Prompt processing speed @42K: 2161.54 tokens/s

    Generation speed: 53.07 t/s

    -sm tensor

    Prompt processing speed @42K: 1120.00 tokens/s

    Generation speed: 70.46 t/s

    Как запускаю:
    llama-server -m “\unsloth\Qwen3.8-27B-GGUF\Qwen3.8-27B-UD-Q8_K_XL.gguf” --mmproj “\unsloth\Qwen3.8-27B-GGUF\mmproj-F16.gguf” -ngl 99 -fa on -np 2 -ts 36,24 -dev CUDA0,CUDA1 --main-gpu 0 -c 262141 -b 2048 -ub 512 -ctk f16 -ctv f16 --spec-type draft-mtp --reasoning-budget -1 --reasoning-preserve --host 0.0.0.0 --temp 1.0 --top-p 0.95 --top-k 20 --min-p 0 --kv-unified -sm tensor

    Учитывая что qwen 3.8 очень много думает, то пожертвовать скоростью обработки промпта можно ради прибавке в скорости генерации


    1. kotafey Автор
      29.08.2026 20:42

      Даже как-то обидно такие цифры видеть. 3090 просто режет 5090. Скорость обработки у меня тоже просела, но не сильно −20% на 40K, −11% на 130K, −3% на 240K - это я уже писал в статье.


      1. sgolomedov
        29.08.2026 20:42

        А что делать, я покупал 5090 за 220, сейчас можно купить от 300 на авито, 3090 тоже покупал за 65, сейчас около 90 живая.

        Но тут думаю еще влияет узкий канал PCIe x4 4.0, так как если запускать deepseek v4 flash, то там падение скорости обработки промпта в 3 раза и прироста по скорости генерации почти нет.


  1. ExModE
    29.08.2026 20:42

    /Нейросети между собой переписываются./

    Оставлю тут https://github.com/noonghunna/club-3090 27b int4 + dflash2 200 токенов в секунду на паре 3090