Главная / Статьи / Локальная станция для LLM: от 20 до 60 токенов в секунду на двух GPU

Локальная станция для LLM: от 20 до 60 токенов в секунду на двух GPU

Б/у машина на Xeon с двумя RTX 5060 Ti, Qwen 27B, тензорный параллелизм и NCCL: правила выбора железа, реальный прирост скорости и расчёт окупаемости по сравнению с ценами API.

2038 слов

Год экспериментов с локальным инференсом многому учит, и в основном разочарованию. Даже при изрядном оптимизме, как только нужно сделать реальную работу, побеждает облачная подписка. Но в последнее время есть давление в обратную сторону. Подписки с каждым месяцем покупают всё меньше работы. Времена, когда GitHub Copilot или Cursor можно было использовать, не следя за бюджетом, кажутся далёкими, а деньги, которых раньше хватало на месяц, могут улететь за день. Этого достаточно, чтобы отнестись к локальной установке всерьёз. Дальше рассказ о том, как один глючный стек на 20 токенах в секунду превратился в надёжную рабочую станцию, которая в среднем выдаёт 60 токенов в секунду, а на пиках заметно больше.

Железо, которое имеет смысл

Интернет полон людей, которые ставят друг на друга несколько Mac Pro с сотнями гигабайт объединённой памяти, запускают модели на Raspberry Pi (у этого есть своё место, но это место — edge AI) или хвастаются одним токеном в минуту, выжатым из SSD. Это цирк. Настоящие правила проще:

  • Объём VRAM на первом месте. Модель и контекст должны помещаться в VRAM, а 32 ГБ или 48 ГБ сегодня — доступные оптимальные варианты.
  • Дополнительные GPU нужны в основном, чтобы поместилось больше, а не ради скорости. Вторая карта добавляет VRAM для больших моделей и длинного контекста, но разделение работы добавляет накладные расходы на связь, так что ждите небольшого замедления, а не ускорения.
  • Когда карт несколько, важна топология. Ищите связи между GPU с высокой пропускной способностью и низкой задержкой, в идеале за одним PCIe-коммутатором или хотя бы через линии, подключённые к CPU. Маршрут через чипсет гораздо хуже, особенно при тензорном параллелизме.
  • Питание и охлаждение — часть конструкции. Два-три GPU потребляют много и столько же тепла выбрасывают в корпус, поэтому важны мощность блока питания, обдув, расстояние между слотами и TDP видеокарт.

Второй пункт на потребительских платах выполнить трудно. Два слота PCIe на бумаге не гарантируют хорошего пути между картами, а платы, которые его дают, недёшевы.

Альтернатива — б/у бизнес-станции. Многие идут с процессорами Xeon, кучей линий PCIe и штатными блоками питания на 1 кВт и больше, потому что их строили под несколько профессиональных GPU. Поставить туда две-три карты — обычное дело, и часто это намного дешевле сопоставимой современной потребительской платформы. Есть компромиссы: старая DDR4, PCIe Gen4 или даже Gen3 и процессоры на несколько поколений позади.

Инференс сильно отличается от игр, но опубликованные бенчмарки масштабирования PCIe для RTX 5090 показывают, что даже очень быстрые GPU теряют удивительно мало скорости при урезанной пропускной способности, и этого хватило, чтобы решиться попробовать. В конце концов, почти всё решает экономика.

Выбор пал на две NVIDIA RTX 5060 Ti на 16 ГБ, самый дешёвый путь к 32 ГБ VRAM с отличной энергоэффективностью, в станции на Xeon по причинам выше. Одна карта физически не влезла, хотя слоты и раскладка линий подходили, поэтому её закрепили на корпусе через PCIe-удлинитель и стяжку. Назовём это польской инженерией на стяжках.

Модели, которые помещаются

Это железо тянет короткий список моделей, у каждой контекстное окно от 160k до 256k при квантованном KV-кэше. Две от Google: gemma-4-26b-a4b и её QAT-вариант. Две от Qwen: qwen3.6-35b-a3b и qwen3.6-27b в q4_k_s.

Ежедневной рабочей лошадкой стала Qwen на 27B, и не просто из личных предпочтений. Рейтинги и бенчмарки могут вводить в заблуждение, но если пробежаться по нескольким, бросаются в глаза две вещи. Мало какие модели работают на таком скромном железе, и Qwen лидирует в этой категории с явным отрывом. Кроме того, это одна из немногих, что могут потягаться с небольшими фронтир-моделями, из тех, что уже доказали умение надёжно выполнять маленькие чётко поставленные задачи.

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

Софт и почему первая попытка была медленной

Сначала был LM Studio, потому что он простой. Он отлично подходит, чтобы экспериментировать и разбираться в разных опциях llama.cpp и моделей. Он ещё и помогает отлаживать слой интеграции между инструментами, которые используют выставленные модели. Не всегда очевидно, доходят ли до запросов настройки, переданные через что-то вроде OpenCode, а LM Studio позволяет их посмотреть, найти пробелы и поправить.

Но с Qwen3.6-27B он в среднем выдавал около 20 токенов в секунду даже с включённым flash attention, а стабильность хромала: то и дело случались странные таймауты.

Два изменения, которые сработали

Тензорный параллелизм

С тех пор CUDA-тензорный параллелизм появился в LM Studio, судя по журналу изменений 0.4.15, но так было не всегда, и протестированный здесь готовый рантайм всё равно оставлял производительность на столе. Сначала — что это такое?

Традиционный способ работы с несколькими GPU кладёт часть слоёв на GPU 0, часть на GPU 1 и передаёт активации от одной к другой:

Model
                   │
          ┌────────┴────────┐
          │                 │
       GPU 0             GPU 1
      layers 0–N        layers N–end
          │                 │
          └───────→─────────┘

Это позволяет запустить модель, которая не влезает в одну карту, и всё. Для одного потока две GPU никогда не работают вместе над одним слоем. Работа движется по цепочке: GPU 0 обрабатывает часть слоёв, активации переезжают, GPU 1 доделывает остальное.

Тензорный параллелизм вместо этого делит вычисление каждого слоя между GPU:

One layer
                     │
             ┌───────┴───────┐
             │               │
          GPU 0           GPU 1
       part of tensor   part of tensor
             │               │
             └───────┬───────┘
                     │
                 next layer

Разница описана в руководстве по нескольким GPU в репозитории llama.cpp. Конвейерный параллелизм, режим layer, размещает разные слои на разных GPU и пропускает токены последовательно. Он минимизирует трафик между картами, но для масштабирования ему нужно много токенов в работе одновременно. Тензорный параллелизм, режим tensor, режет каждый слой между GPU и выполняет несколько межкарточных редукций на слой. Он может распараллелить любую нагрузку, но сильно зависит от скорости межсоединения. Конвейерный режим максимизирует пропускную способность батча, а тензорный минимизирует задержку.

Когда обе GPU работали над одним слоем, Qwen3.6-27B вышла примерно на 50 токенов в секунду. Но это создаёт новую проблему. Если две GPU совместно обрабатывают каждый слой, им приходится постоянно общаться.

NCCL

NCCL от NVIDIA, Collective Communications Library, создана именно для такого обмена между GPU. Операции вроде AllReduce лежат в основе распределённой работы на GPU, и без эффективного коллективного слоя тензорный параллелизм может тратить слишком много времени на перекладывание данных вместо вычислений.

Сюрпризом стало то, что в готовом рантайме llama.cpp в протестированной версии LM Studio поддержка NCCL была выключена. Тензорному параллелизму NCCL не обязателен, потому что llama.cpp умеет выполнять нужные редукции собственным кодом, но NCCL даёт для этого трафика оптимизированный бэкенд.

Вот что происходит. При тензорном параллелизме GPU обмениваются промежуточными результатами, например после части умножения матриц:

Matrix operation
                       │
             ┌─────────┴─────────┐
             │                   │
          GPU 0               GPU 1
       computes part        computes part
             │                   │
             └─────────┬─────────┘
                       │
                   AllReduce
                       │
                 combined result

AllReduce объединяет частичные результаты всех GPU и отдаёт общий ответ каждой из них. У llama.cpp есть своя реализация, так что встроенный вариант работает. Здесь AllReduce из NCCL оказался быстрее.

Несколько часов спустя

Болезненную часть (недостающие библиотеки на Fedora, компиляция, жонглирование параметрами llama и модели) не стоит расписывать подробно, отчасти потому, что этот уголок софта очень быстро меняется. Результат в средних цифрах:

  • Разбиение по слоям: около 20 токенов в секунду
  • Тензорный параллелизм со встроенным AllReduce: около 50
  • Тензорный параллелизм с NCCL: около 60

Пики выше. Лог llama.cpp одного прогона показывает обработку промпта около 950–970 токенов в секунду и декодирование примерно 78–84 токена в секунду:

113.07.960.883 I slot print_timing: id  0 | task 972 | prompt processing, n_tokens =   4810, progress = 0.79, t =   4.96 s / 969.87 tokens per second
113.08.772.504 I slot print_timing: id  0 | task 972 | prompt processing, n_tokens =   5580, progress = 0.92, t =   5.77 s / 966.90 tokens per second
113.09.409.053 I slot print_timing: id  0 | task 972 | prompt processing, n_tokens =   6092, progress = 1.00, t =   6.41 s / 950.75 tokens per second
...
114.19.200.672 I slot print_timing: id  0 | task 972 | n_decoded =   5384, tg =  77.34 t/s, tg_3s =  83.57 t/s
114.22.229.460 I slot print_timing: id  0 | task 972 | n_decoded =   5639, tg =  77.63 t/s, tg_3s =  84.19 t/s
114.25.259.925 I slot print_timing: id  0 | task 972 | n_decoded =   5894, tg =  77.89 t/s, tg_3s =  84.15 t/s
114.28.274.967 I slot print_timing: id  0 | task 972 | n_decoded =   6149, tg =  78.15 t/s, tg_3s =  84.58 t/s
114.31.308.233 I slot print_timing: id  0 | task 972 | n_decoded =   6405, tg =  78.38 t/s, tg_3s =  84.40 t/s
114.34.343.320 I slot print_timing: id  0 | task 972 | n_decoded =   6661, tg =  78.59 t/s, tg_3s =  84.35 t/s
114.37.376.671 I slot print_timing: id  0 | task 972 | n_decoded =   6913, tg =  78.75 t/s, tg_3s =  83.08 t/s
114.40.405.502 I slot print_timing: id  0 | task 972 | n_decoded =   7169, tg =  78.94 t/s, tg_3s =  84.52 t/s
114.43.413.731 I slot print_timing: id  0 | task 972 | n_decoded =   7421, tg =  79.09 t/s, tg_3s =  83.77 t/s
114.46.451.228 I slot print_timing: id  0 | task 972 | n_decoded =   7673, tg =  79.21 t/s, tg_3s =  82.96 t/s

Всё это на Qwen3.6-27B. Gemma-4-26B-QAT может работать вдвое быстрее, и хотя она, возможно, менее точна, чем Qwen, как ассистент она ощущается почти мгновенной.

Абсолютные цифры важны меньше, чем соотношение. Это старое подержанное железо, которое обошлось примерно в $1,100, и эти изменения примерно утроили исходную производительность.

Окупается ли это?

Возьмём такие допущения:

  • Стоимость железа: $1,100.
  • Цены API: $0.36 за вход и $2.80 за выход за миллион токенов для хостированной Qwen 27B на OpenRouter. Это не совсем честное сравнение с квантованной локальной моделью, но какое-то допущение нужно.
  • Нагрузка кодинга: около 15 входных токенов на один выходной, потому что при кодинге читают больше, чем пишут.
  • Обработка промпта со скоростью 700 токенов в секунду.
  • Генерация вывода со скоростью 60 токенов в секунду.

Обработка промпта и генерация идут друг за другом. Для 15 входных токенов плюс 1 выходного время равно 15 / 700 + 1 / 60, около 0.0381 секунды. Это примерно 393.75 входных и 26.25 выходных токенов в секунду, или около 1.42M входных и 94.5K выходных токенов в час.

Прогнать ту же нагрузку через API стоило бы около $0.51 за вход (1.4175 умножить на $0.36) и $0.26 за выход (0.0945 умножить на $2.80). Значит, машина заменяет примерно $0.775 расходов на API за час активного инференса, а точка окупаемости — $1,100, делённые на $0.775, примерно 1 420 часов.

Заметьте, речь о часах реального инференса. Никто не гоняет модель на полную восемь часов подряд, так что календарный срок окупаемости гораздо больше. А электричество, обслуживание, поломки и запчасти вообще не учтены.

Так окупается ли? Этот расчёт каждому придётся сделать самому. Для простого ИИ-ассистента экономика неубедительна, а срок окупаемости настолько длинный, что API может оказаться просто практичнее. Но добавьте оркестрацию агентов, задачи без присмотра и конвейеры, и картина переворачивается: локальный инференс вдруг выглядит куда разумнее. Здесь полуавтономные конвейеры с редкими точками принятия решений держат машину занятой около 12 часов в день, и эта цифра продолжает расти.