Скорость 109 токенов/с на RTX 5080 и RTX 3060 с 35B-моделью через PCIe Gen2 x4

Прокомментировать Просмотры: 5

«Как архитектура MoE и послойное распределение сводят на нет тезис „одна дискретная карта эффективнее двух“.»


В сообществе SE7ENа этой проблематике посвящена отдельная публикация: «Собираем сервер под свой локальный ИИ: сколько нужно VRAM и почему одна видеокарта лучше двух». Там прямо утверждается: «32-гигабайтная одиночная видеокарта предпочтительнее двух по 16 ГБ», а ключевым узким горлышком выступает пропускная способность шины. Второй разъём на потребительских материнских платах функционирует в урезанном режиме x4 или x8, из-за чего межкартаня коммуникация упирается в аппаратные ограничения. Аналогичные холивары возникают вокруг сравнений вроде «две RTX 5060 Ti 16GB против одной RTX 3090 24GB», а также в ветках про мультиграфен-сборки. Впрочем, в комментариях тут же парируют: 24 ГБ вполне успешно вывозят верифицированные 27B- и 35B-модели с экспертной маршрутизацией (MoE) при контекстном окне в 256K.

Мой сетап полностью отражает опасения той статьи: на системной плате я сэкономил, установив ASUS Prime B450-Plus, где дополнительный слот x16 физически разведён как PCIe 2.0 x4. Это худший сценарий из возможных, а не компромиссное «чуть медленнее, чем x8».

Инференс модели Qwen3.6–35B‑A3B у меня функционирует на связке RTX 5080 и RTX 3060. При этом тридцатитысячная серия висит на шине PCIe Gen2 x4. Это обеспечивает пропускную способность всего в 2 ГБ/с, что почти в восемь раз медленнее возможностей 5080. Скорость генерации текста держится на уровне 109 токенов в секунду для коротких промптов и 77 токенов для контекста в 53 тысячи единиц.

Ниже я подробно разберу физику процессов и объясню, в каких кейсах скепсис относительно PCIe всё-таки обоснован.

Аппаратный стенд

  • ЦП: Ryzen 7 5800X (8 ядер / 16 потоков)

  • ОЗУ: 64 ГБ DDR4

  • Материнская плата: ASUS Prime B450-Plus (дополнительный x16 слот функционирует как PCIe 2.0 x4)

  • GPU 0: RTX 3060 12 ГБ (GDDR6, шина 192-bit, 15 Гбит/с → 360 ГБ/с)

  • GPU 1: RTX 5080 16 ГБ (GDDR7, шина 256-bit, 30 Гбит/с → 960 ГБ/с)

  • Интерфейс NVLink: отсутствует

  • ОС: Arch Linux, кастомное ядро 7.1.11 (i use arch btw)

  • Видеодрайвер: 610.57.04, CUDA 13.3

  • Билд llama.cpp: 10 738

Совокупный объем видеопамяти составляет 28 ГБ. Суммировать пропускную способность подсистемы памяти бессмысленно: ускорители задействованы последовательно, а не параллельно. К этому тезису мы еще вернемся.

Особенности конфигурации PCIe заслуживают отдельного упоминания, поскольку это наиболее уязвимый узел системы:

GPU 0 (RTX 3060):  Gen2 x4   ≈ 2 ГБ/с    (потенциал карты: Gen4 x16)
GPU 1 (RTX 5080):  Gen3 x16  ≈ 16 ГБ/с   (потенциал карты: Gen5 x16)

Старенькая 3060 задействует второй слот, который, согласно официальной спецификации B450-Plus, реализован как PCIe 2.0 x4 — разработчики заложили лишь четыре линии вместо шестнадцати намеренно. Главный слот функционирует в стандарте PCIe 3.0 x16, поэтому передовая 5080 со своей поддержкой Gen5 также упирается в системные ограничения платформы B450. Оба графических адаптера делят один хост-контроллер PCIe. Никакого моста NVLink и симметричного режима x16/x16 здесь нет и в помине.

Почему интерфейс PCIe в данном случае не является узким местом

Традиционные контраргументы против использования мульти-GPU сетапов базируются на концепции тензорного параллелизма. В этой парадигме каждый ускоритель обсчитывает собственный фрагмент любого слоя сети, требуя синхронизации через операцию all-reduce по завершении каждого шага. Увеличение длины контекста экспоненциально нагружает шину данными, и вот тут низкая пропускная способность вроде Gen2 x4 способна напрочь убить производительность.

В моем сценарии активирован послойный сплит (layer-wise splitting, штатный режим для llama.cpp). Слои нейросети просто распределены между устройствами: первое держит начальные пласты, второе — завершающие. По шине курсируют вовсе не массивные веса, а исключительно промежуточные активации — ровно один раз за генерацию одного токена.

Произведем расчеты. Скрытая размерность выбранной нейросети составляет 2048. Вектор активации в формате fp16 требует 2048 × 2 байта, что эквивалентно 4 КБ. Один пересыл между картами за токен.

При скорости генерации в 109 токенов в секунду итоговый трафик составит скромные 437 КБ/с. Пропускная способность линии Gen2 x4 равна 2 ГБ/с. Таким образом, нагрузка на шину исчисляется сотыми долями процента.

Даже на этапе префилла (предварительной обработки промпта) ситуация кардинально не меняется. Пакет из 2048 токенов занимает около 8 МБ и равномерно распределяется во время инференса. На скорости префилла в 1500 токенов/с мы получаем около 6 МБ/с.

Именно поэтому урезанная шина Gen2 x4 не создает фризов: она исключена из критического пути выполнения. Веса надежно закреплены за локальной памятью карт, а по межсистемному интерфейсу передаются лишь легковесные тензоры активаций.

Почему архитектура MoE меняет правила игры

Будь Qwen3.6–35B‑A3B плотной (dense) нейросетью, картина выглядела бы совершенно иначе. Размер весов в квантованном файле достигает 21,3 ГиБ, и классическая модель вычитывала бы весь этот массив данных для генерации абсолютно каждого токена. На 5080 с ее пропускной способностью 960 ГБ/с это дало бы потолок примерно в 42 токена в секунду, причем в одиночном режиме. С послойным разделением устройства функционируют последовательно, поэтому полосы пропускания не суммируются, а вот задержки — складываются.

Однако рассматриваемая модель построена на базе Mixture of Experts (MoE). Из общих 35 миллиардов параметров на обработку каждого отдельного токена привлекается лишь около 3 миллиардов: 8 маршрутизируемых экспертов из 256 возможных плюс один базовый общий. Оставшиеся 32 миллиарда параметров пассивно покоятся в видеопамяти и не требуют постоянного чтения.

Отсюда проистекает итоговый перформанс. По требованиям к объему VRAM это тяжелая 35-миллиардная модель, но по вычислительным затратам на генерацию одного токена она сопоставима с компактными 3B-моделями. Именно это технологическое несоответствие позволяет комфортно запускать ее на гетерогенном парке потребительских видеокарт.

Архитектурные особенности согласно карточке репозитория на HuggingFace:

  • 40 слоев архитектуры, скрытое измерение скрытого пространства — 2048

  • 256 экспертных модулей, из которых 8 активируются динамически + 1 глобальный на токен

  • Комбинирование слоев Gated DeltaNet (линейный механизм внимания) и классического Gated Attention

  • Нативная поддержка контекстного окна до 262 144 токенов

  • Дополнительная MTP-голова (Multi-Token Prediction) для спекулятивного декодирования

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

Практические замеры

Все тесты производились через эндпоинт /completion с флагом cache_prompt: false, что исключает кэширование и заставляет сервер каждый раз заново обрабатывать входной промпт. Предустановки сервера и веса не менялись, перезагрузки не выполнялись, бенчмарки запускались циклично. Никакой искусственной магии — исключительно «живой» прогон серверного окружения.

Базовая скорость генерации

Короткий запрос, генерация 256 токенов, серия из пяти непрерывных запусков:

Номер прогона

Генерация, токен/с

1

109,2

2

109,7

3

106,0

4

109,6

5

108,4

Диапазон отклонений укладывается в 3,5%, среднее значение — 108,6. Это ничтожно мало на фоне разницы между конфигурациями, так что базовую производительность можно считать эталонной.

Зависимость от доменной специфики контента

Здесь вариативность носит уже не статистический, а содержательный характер:

Тип задачи

Генерация, токен/с

Валидированные MTP-черновики

Программирование (Python)

114,6

85,7%

Структурированный JSON

113,3

85,1%

Код на русском языке

100,4

68,8%

Англоязычная беллетристика

96,0

62,9%

Русскоязычная проза

95,4

62,0%

Скорость инференса линейно коррелирует с процентом принятых предсказаний спекулятивного декодирования. На строго структурированном коде MTP угадывает более восьми токенов из десяти, тогда как на художественном тексте точность падает до шести.

Эффективность спекулятивного прохода

Аналогичный замер с фиксацией метрик предсказаний (сгенерировано минус подтверждено):

Тип задачи

Токенов за итерацию

Успешные черновики

Код на Python

2,70

85,7%

Структурированный JSON

2,68

85,1%

Русскоязычная проза

2,22

62,0%

При ограничении --spec-draft-n-max 2 предельный теоретический порог составляет три токена за проход. На формализованных данных модель вплотную приближается к этому максимуму.

Важная ремарка: количество токенов за итерацию не тождественно конечному ускорению в разах. Проверяющая фаза обрабатывает пачки токенов параллельно, требуя больших затрат, чем базовый шаг. Создатели Unsloth заявляют буст в 1,5–2 раза; я не стал изолировать этот фактор, поскольку для чистого A/B тестирования потребовался бы перезапуск демона без активации --spec-type draft-mtp, а конфигурация пресета оставалась неизменной.

Поведение на длинном контексте

Пожалуй, наиболее показательная таблица тестов:

Контекстная база, токенов

Промпт-процессинг, токен/с

Генерация, токен/с

13 865

1820

90,8

27 703

1708

87,9

42 528

1586

80,6

53 401

1504

76,7

Падение скорости генерации при четырехкратном раздувании контекста (с 14K до 53K токенов) составляет всего 15%: с 90,8 до 76,7. Это заметно, но далеко от катастрофического падения. Секрет кроется в линейном внимании: емкость скрытого слоя Gated DeltaNet не разрастается пропорционально контексту, а классический механизм внимания развернут лишь в десяти слоях из сорока. При этом KV-кэш предельно компактен — всего 2 головы по 256 измерений, что дает примерно 10 КБ на один токен для всей группы слоев. Плотная языковая модель с полным вниманием во всех сорока пластах просела бы по перформансу гораздо сильнее.

Скорость обработки промпта снижается несколько динамичнее (с 1820 до 1504, то есть на 17%), упираясь в чисто вычислительные мощности ядер.

Параллельная обработка двух слотов

Параметр parallel = 2 делит максимальное окно в 131 072 токена на два независимых слота по 65 536. Запуск двух параллельных потоков запросов дает следующие цифры:

Генерация, токен/с

Слот 1 (программирование)

69,3

Слот 2 (беллетристика)

60,5

Суммарная пропускная способность

≈ 130

Совокупный воркпут вырос со 109 до 130 токенов в секунду, однако каждый отдельный поток просел почти вдвое. Для автономных AI-агентов, отправляющих запросы в фоне, это скорее минус: отдельные задачи ожидают завершения дольше, чем при последовательной обработке.

Конфигурационный файл

Рабочий пресет в директории ~/.config/llama.cpp/models.ini:

[unsloth/Qwen3.6-35B-A3B-MTP-GGUF-64K]
hf = unsloth/Qwen3.6-35B-A3B-MTP-GGUF:UD-Q4_K_XL
ctx-size = 131072
cache-type-k = q8_0
cache-type-v = q8_0
spec-type = draft-mtp
spec-draft-n-max = 2
temperature = 1.0
top-p = 0.95
top-k = 20
min-p = 0.0
presence-penalty = 1.5
repeat-penalty = 1.0

Инференс оркестрируется через утилиту llama-server --models-preset, автоматически разворачивающую модель в изолированном процессе. Итоговый синтаксис команды:

llama-server --host 127.0.0.1 --port <dynamic> \
  --alias unsloth/Qwen3.6-35B-A3B-MTP-GGUF-64K \
  --hf-repo unsloth/Qwen3.6-35B-A3B-MTP-GGUF:UD-Q4_K_XL \
  --ctx-size 131072 --parallel 2 \
  --cache-type-k q8_0 --cache-type-v q8_0 \
  --n-gpu-layers -1 --flash-attn on --load-mode mmap \
  --spec-type draft-mtp --spec-draft-n-max 2 \
  --temperature 1.0 --top-p 0.95 --top-k 20 --min-p 0.0 \
  --presence-penalty 1.5 --repeat-penalty 1.0

Критически важные детали конфигурации:

Отсутствие флага --split-mode. Задействован дефолтный послойный метод распределения. Именно он нивелирует узкие места убогой шины Gen2 x4.

Квантование KV-кэша до пресета q8_0. У данной архитектуры кэш памяти внимания компактен (всего 2 головы), и на коротких дистанциях профит едва заметен. Тем не менее, это высвобождает около гигабайта драгоценной VRAM, а в условиях дефицита памяти на разнородных картах каждый мегабайт определяет, сколько слоев удастся скинуть на реактивную RTX 5080. При масштабировании контекста экономия памяти становится кратной.

--load-mode mmap вместо mlock. На этом аспекте стоит остановиться подробнее, так как параметр mlock часто работает не так, как ожидается. На моей машине лимит ядра RLIMIT_MEMLOCK жестко зафиксирован на отметке 8 МБ, в то время как веса модели занимают 22,85 ГБ. Бэкенд llama.cpp честно фиксирует в логах ошибку failed to mlock ... Cannot allocate memory, а метрика VmLck в выводе /proc/<pid>/status остается нулевой. Иными словами, модель не закрепляется в оперативной памяти в обход свопа, создавая лишь ложное чувство безопасности. Стратегия mmap в данном контексте честнее: файл удерживается в страничном кэше ядра и эффективно переиспользуется при последующих стартах.

Параметры сэмплирования заимствованы из официальной карточки модели для режима рассуждений (reasoning): temperature=1.0, top_p=0.95, top_k=20, min_p=0.0, presence_penalty=1.5.

parallel = 2 при активированном модуле MTP. Команда Unsloth в описании репозитория прямо заявляла о несовместимости параметров -np > 1 и --mmproj с технологией MTP. Тем не менее, с конфигурацией parallel = 2 (что аналогично -np 2) вся связка работает стабильно: токены генерируются корректно, а все приведенные бенчмарки сняты именно с этой конфигурации. Мультимодальный флаг --mmproj в конфиге отсутствует, так что вторая часть ограничения меня не затрагивает.

Что на самом деле выступает бутылочным горлышком?

Результаты тестов полностью сняли подозрения с интерфейса PCIe. Но что именно ограничивает производительность в таком случае?

Я мониторил показатели nvidia-smi с дискретностью в одну секунду во время затяжного инференса — 2200 токенов за 24 секунды, что дало 23 валидных семпла на каждый ускоритель. В расчет принимались исключительно замеры с ненулевой нагрузкой, дабы исключить периоды простоя и не искажать картину.

Графический чип

Средняя загрузка

Диапазон

Энергопотребление

Диапазон ватт

Паверлимит

RTX 3060

62%

58–66%

110,7 Вт

102–113 Вт

170 Вт

RTX 5080

15%

14–18%

67,6 Вт

65–68 Вт

360 Вт

Вот и разгадка. Старенькая RTX 3060 загружена на 62% и утилизирует две трети своего лимита энергопотребления. В то же время флагманская RTX 5080 простаивает на 15%, потребляя менее пятой части своего потенциала. Ускорители не сбалансированы по нагрузке: быстрая карта вынуждена ждать медленную напарницу.

Уточнение по методике: усреднение метрик раз в секунду сглаживает пики на масштабе сотен токенов, поэтому по ним нельзя судить о микропроцессах внутри генерации одного токена. Однако для оценки глобальной утилизации во время прогона этой точности более чем достаточно.

Распределение памяти под модель: 10 462 МиБ на RTX 3060 и 14 986 МиБ на RTX 5080. Мощная карта забрала на себя заметно большую долю слоев, но три четверти рабочего времени она все равно ожидает соседа.

Резюме: лимитирующим фактором выступает не шина и не пропускная способность видеопамяти, а последовательная цепь зависимостей. Ускоритель 5080 физически не может начать расчеты своих слоев, пока 3060 не завершит обработку своей порции, и наоборот. Сорок слоев — это сорок передач управления, на каждой из которых устройство простаивает в ожидании партнера. Мы расплачиваемся за асимметричность вычислений высокой латентностью, а не дефицитом пропускной способности каналов.

Архитектура MoE делает выполнение каждого отдельного шага столь легковесным, что даже при столь неоптимальной схеме распределения удается выжимать 109 токенов в секунду.

Чему не было уделено внимание

Три аспекта, о которых честнее заявить прямо, чем умалчивать:

Тензорный параллелизм на этой паре карт. Теоретически он должен устранить простои, поскольку оба чипа обрабатывают слои синхронно. Однако этот метод предъявляет жесткие требования к ширине канала, а у 3060 задействован убогий разъем Gen2 x4. Будет ли профит от такого подхода на столь несбалансированной связке — вопрос открытый.

Инференс без задействования модуля MTP. Для чистоты такого замера потребовался бы повторный запуск сервера, а конфигурацию я принципиально не трогал.

Качество генерации на экстремальном контексте. Все замеры производительности снимались на фиксированном наборе промптов. Сохраняет ли модель одинаково высокое качество ответов на длине в 53K и 1K токенов, я не исследовал — в фокусе была исключительно скорость работы.

Выводы

Тезис «одиночная видеокарта эффективнее связки из двух» безупречно работает для монолитных плотных моделей и тензорного параллелизма. В подобных сценариях требования к полосе пропускания формируют непреодолимое бутылочное горлышко, и диспропорция в характеристиках графических адаптеров больно бьет по перформансу.

Для архитектур MoE в сочетании с послойным распределением это правило теряет актуальность по двум ключевым причинам:

  1. Количество активных параметров кратно меньше общего объема. Задействуется лишь 3B из 35B, тогда как полновесная модель аналогичного класса считывала бы на порядок больше терабайт за один токен.

  2. По межсистемному интерфейсу передаются лишь активации, а не веса. 4 КБ данных на один переход при пропускной способности шины в 2 ГБ/с — это статистическая погрешность. Урезанная линия PCIe 3060 попросту выведена за пределы критического пути.

Чего действительно стоит опасаться в подобной конфигурации, так это последовательного характера вычислений: карты работают по очереди, из-за чего задержки суммируются, а пропускная способность не складывается. Если финансовые возможности позволяют приобрести одну мощную современную карту взамен пары устаревших, плотная модель на ней запустится комфортнее. Если же у вас в корпусе уже пылятся два разных графических ускорителя, а выбор пал на MoE-архитектуру с компактным KV-кэшем — подобная гибридная связка способна выдавать 109 токенов/с на коротких запросах и 77 токенов/с при длине контекста в 53 тысячи единиц.

Это по-прежнему не промышленный серверный кластер. Но эта система тихо гудит под рабочим столом и не требует ничего, кроме свободных слотов PCIe.

Как воспроизвести сетап у себя

Базовый пресет, использованный мной в качестве отправной точки, выложен в публичный доступ: репозиторий github.com/aigen31/llama‑cpp‑preset‑base распространяется под лицензией MIT. Это минималистичный инсталлятор, разворачивающий три компонента: управляющий лаунчер llama-qwen, INI-конфиг ~/.config/llama.cpp/models.ini и проверку наличия бинарника llama-server в системной переменной PATH.

Скрипт не пытается самостоятельно компилировать из исходников llama.cpp. Если исполняемый файл отсутствует в системе, утилита выведет прямую ссылку на официальный репозиторий и справочную таблицу бэкендов под конкретное железо: CUDA, ROCm, Vulkan, SYCL, Metal или чистое исполнение на CPU. Сборка остается за пользователем, поскольку корректные флаги компиляции напрямую зависят от конфигурации вашей рабочей станции.

Нейросети описываются единственным параметром hf, содержащим полный путь к репозиторию и префикс квантования, например hf = unsloth/Qwen3.6-35B-A3B-MTP-GGUF:UD-Q4_K_XL. При первом запросе сервер автоматически выкачивает GGUF-модели с хранилища Hugging Face. API-ключ генерируется в формате sk- с добавлением 48 случайных символов, установщик не интерактивен и полностью готов к запуску через конвейер curl | bash. Документация README дублируется на русском, английском и китайском языках.

Мой персональный рабочий конфиг незначительно отличается от шаблонного репозитория: я дополнительно прописал квантование KV-кэша и принудительно заменил параметр mlock на mmap после того, как экспериментально выяснил, что системный лимит на аллокацию заблокированной памяти на моей машине жестко ограничен на отметке 8 МБ.

 

Источник

Похожие статьи

Поиск по играм, новостям и статьям…

Введите не менее двух символов

Введите не менее двух символов