Когда я взялся за этот небольшой эксперимент, мной двигало исключительно любопытство и желание ответить на вопрос — а что, если взять три моих старых ноутбука, объединить их вычислительные ресурсы и использовать их под инференс LLM? Идея запуска на них чего-то более-менее тяжелого выглядела заманчиво: три процессора, 52 ГБ оперативной памяти — в теории должно получиться быстрее, чем на любом из них по отдельности.

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

Сразу же оговорюсь — любой GPU средней ценовой категории сделает ту же самую работу в десятки раз быстрее. Но моя задача была увидеть узкие места в «естественной среде обитания». Приятного чтения.

Тестовый стенд

Делать упор на бренды ноутбуков не стану — это в целом не так важно. Укажу лишь общие характеристики:

Роль

CPU

Поколение

RAM

Тип

Coordinator

Core i7-4700MQ 

Haswell (2013) 

16 ГБ

DDR3

Worker

Core i5-8265U 

Whiskey Lake (2018) 

20 ГБ

DDR4

Worker

Core i3-1115G4 

Tiger Lake (2020) 

16 ГБ

DDR4

Итого — три поколения процессоров Intel с разбросом в семь лет. Тут разнородность не баг, а фича. Так можно увидеть эффект самого слабого узла в чистом виде. Отличие в наборах инструкций минимально — у всей троицы есть AVX2, FMA и F16C. Расширение AVX-512 присутствует только у Tiger Lake (i3), но в разнородном кластере это не станет преимуществом — llama.cpp работает по общему знаменателю.

Что до количества ядер — у i7 и i5 по 4 ядра / 8 потоков, а у i3 всего лишь 2 ядра / 4 потока. Можно подумать, что слабейший найден, но, как выяснится ниже, он вовсе не станет плестись в хвосте.

На всех узлах был развернут следующий софт:

  • Ubuntu 24.04 LTS 

  • llama.cpp, собранная из одного коммита 178a6c449 (build b10069). RPC-протоколу важно, чтобы на всех машинах версия была одинаковая.

  • governor выставил в performance (все ноутбуки от розетки)

Что же до сетевого подключения — любопытства ради присоединил все узлы к единому Wi-Fi 2.4 ГГц (увы, старенький i7 столько так умеет) и померял пинги:

rtt min/avg/max/mdev = 20.872/67.360/116.416/28.175 ms

Средний RTT ~67 мс, джиттер ~30 мс. Сразу же звучит как приговор: узлам надо обмениваться данными на каждый токен синхронно, а подобный разброс убьет всю производительность. Далее все узлы я подключил к обычной гигабитной сети:

rtt min/avg/max/mdev = 0.732/0.875/1.316/0.113 ms

Джиттер упал в 300 раз, средний RTT — в 85. Все следующие замеры стану делать, используя проводную сеть, а Wi-Fi оставлю как сюжет на будущее — надо будет выполнить апгрейд i7 до современных стандартов.

Методика измерений

В качестве инструмента взял llama-cli из llama-cpp. Каждую конфигурацию прогонял по три раза, бралась медиана. Для модели с 32b параметров снижал количество токенов, так как совсем медленно. Seed фиксированный, промпт одинаковый — всё, как доктор прописал.

Запуск распределенного инференса реализуется флагом --rpc со списком адресов воркеров. На последних при этом крутится ggml-rpc-server. Это, кстати, оказалось первыми граблями: я сначала по памяти безуспешно пытался запустить rpc-server. Выяснилось, что в свежих сборках его переименовали.

На воркерах:

~/llama.cpp/build/bin/ggml-rpc-server -H 0.0.0.0 -p 50052 -t "$(nproc)"

На координаторе:

~/llama.cpp/build/bin/llama-cli -m model.gguf \
--rpc 192.168.88.18:50052,192.168.88.77:50052 \
-ngl 99 -n 128 -p "..."

В таком режиме локальная копия модели на воркерах не требуется — координатор сам загружает GGUF и раздает слои по сети. В качестве метрик выбрал обработку входного промпта (prompt eval) и генерацию ответа (token generation). Это важно, так как ведут они себя по-разному.

Результаты

Модель 1B (Llama-3.2-1B-Instruct, Q4_K_M)

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

Таблица

Узел

Prompt eval t/s

Token generation t/s

i3-1115G4 

127.6

29.2 

i7-4700MQ 

103.9 

26.2 

i5-8265U 

108.5 

22.8 

Занятно, но двухъядерный i3 оказался быстрее всех. Тут, очевидно, играет роль не количество ядер, а тактовая частота и скорость ОЗУ. Tiger Lake способен держать до 4,1 ГГц (в boost-режиме), что значительно быстрее старого i7 (базовая 2,4 ГГц, буст 3,4 ГГц) и энергоэффективного i5 (базовая 1,6 ГГц, буст 3,9 ГГц), который в режиме полной нагрузки упирается в теплопакет. 

Перед тестом я полагал, что именно i5 будет самым шустрым, но гипотеза не подтвердилась. Как обычно — ожидания vs реальность. Но теперь самое интересное — распределенный инференс. Для сравнения туда же приложил данные лучшего solo теста (А):

Таблица

Конфигурация

Узлы

Prompt eval t/s

Token generation t/s

A

i3

127.6

29.2 

B1

i3 + i5

70.3

22.5

B2

i3 + i5 + i7

67.0

22.6

B3

i5 через RPC

74.5

19.6

Хорошо видно, что на такой небольшой модели распределенка (22.5) подчистую проигрывает одиночному узлу (29.2) — отставание на 23%. Наиболее интересен B3 — это, по факту, тот же самый i5, который давал в solo 22.8, а обернутый в RPC теперь дает 19.6. Получается, что 14% скорости генерации теряется на то, чтобы поговорить с самим собой по TCP. Это значение — чистый налог RPC-протокола.

Модель 8B (Llama-3.1-8B-Instruct, Q4_K_M)

Теперь попробую модель у которой в восемь раз больше параметров и при этом влезает в каждый узел. В качестве референса запущу тесты по-отдельности в solo, а затем по RPC:

Таблица

Конфигурация

Узлы

Prompt eval t/s

Token generation t/s

A1 (solo)

i3

1.5

4.9

A2 (solo)

i5

3.9

3.9

A3 (solo)

i7

2.9

4.4

B1

i3+i5

10.9

4.6

B2

i3+i5+i7

10.7

4.5

B3

i5 через RPC

11.3

3.9

Общая картина начала меняться. Теперь по генерации кластер (4.6) почти догнал лучший solo (4.9), а отставание сократилось с 23% до 6%. Опять результат B3 удивляет — RPC налог на генерации исчез. Объяснение, на мой взгляд, простое: модель стала достаточно тяжелой и поэтому сетевые накладные расходы перестали доминировать над вычислениями.

Еще одна любопытная деталь — обработка промпта. Если в одиночку i5 разгрызает его со скоростью 3.9 t/s, то кластер делает это почти втрое быстрее — 10.9 t/s. Причина в характере нагрузки: промпт может быть обработан батчем и параллельно (на разных узлах), а вот генерация — строго последовательный процесс (по токену за раз).

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

Модель 32B (Qwen2.5-32B-Instruct, Q4_K_M)

Выбор был сделан неслучайно — эта модель уже не влезет ни в один узел. Даже i5 с его 20 ГБ ОЗУ пасует — веса с KV-кешем и накладными расходами уведет ОС в своп (конфигурация A):

Таблица

Конфигурация

Узлы

Prompt eval t/s

Token generation t/s

A (solo)

i5

0.7

0.3

B1

i3 + i5

2.4

1.1

B2

i3 + i5 + i7

1.9

1.1

Тот самый момент, когда распределенный инференс перестает быть «налогом» и становится единственно возможным способом запустить большую модель с «адекватной» скоростью. Разумеется 1.1 t/s — это медленно. Но все же в 3,7 раза быстрее, чем на i5, падающим в своп (бутылочным горлышком становится уже дисковый накопитель, а не ОЗУ).

Итоги dense-моделей

Если подвести общие результаты производительности:

Таблица

Модель

Лучший в solo

Кластер (i3 + i5)

Победитель

1B

29.2

22.5

solo (+30%)

8B

4.9

4.6

паритет (solo + 6%)

32B

0.3 (своп)

1.1

кластер (+268%)

Хорошо видно, что перелом происходит между 8B и 32B — там, где модель перестает помещаться в один узел. До этого момента — сетевой оверхед есть, а ускорения нет.

Вывод: распределенный инференс оправдан, только когда модель не влезает на один узел. Во всех остальных случаях ее имеет смысл запускать на самом быстром одиночном узле.

MoE (gpt-oss-20b)

«А что там с MoE, позвольте поинтересоваться?» — спросите вы и будете абсолютно правы. Предыдущие примеры — это dense-архитектура, где при обработке каждого токена задействуются все параметры. 

Eсли взять модель класса Mixture-of-Experts (MoE), вроде gpt-oss-20b, то на каждый токен будет активироваться лишь примерно 3,6B параметров (32 эксперта, топ-4 на токен). Таким образом, в теории MoE должна работать на скорости маленькой модели, но со знаниями большой. Предлагаю взглянуть на реальность пока что на одном узле (i5) в solo:

Таблица

Модель

Активных параметров

Token generation t/s

Llama-3.1-8B-Instruct

8B

~4–5

gpt-oss-20b

3,6B

~7–8

Первое наблюдение: в генерации MoE действительно быстрее dense-модели той же «весовой категории». Играет роль то, что на каждый токен приходится лишь 3,6B параметров. Эта магия прекрасно работает на CPU. Однако есть и ложка дегтя — MoE ускоряет вычисления, но не влияет на потребление памяти. Несмотря на то что на каждом шаге используется часть весов, все они должны лежать в RAM или VRAM.

На практике это означает, что gpt-oss-20b банально не влезла без свопа ни в один узел. Даже 20 Гб в i5 ей оказалось мало, а поэтому все данные solo-замеров получились «грязными». Взгляните на скорость обработки промпта:

prompt eval t/s по повторам на i5: 5.5 ; 17.2 ; 3.3

Один и тот же тест — от 3 до 17 токенов в секунду — характерный признак того, что модель то подгружается с диска, то берется из оперативной памяти. В кластере же все работает стабильно, без какого-либо разброса:

Таблица

Конфигурация

Узлы

Prompt eval t/s

Token generation t/s

Разброс генерации

A (i5, solo)

i5

5.5

7.3

нестабильно

B1

i3 + i5

15.4

8.1

стабильно

B2

i3 + i5 + i7

15.9

8.0

стабильно

Ни один из узлов кластера не пользуется свопом — каждый берет данные из ОЗУ. Обработка промпта вновь выросла почти втрое (15.4 против 5.5) — сказывается параллельная работа узлов.

Подводя итог, можно сделать вывод, что на CPU заявленные преимущества MoE работают лишь наполовину. Да, скорость выше dense-модели, однако памяти требуется как для полной 20B. Выходит, что на таком железе получить адекватную производительность можно лишь в кластере, распределяющем веса по узлам.

Побочные наблюдения

Перед началом эксперимента мне казалось, что каждый новый узел будет добавлять полезную мощность, но в реальности все они становятся участниками синхронного обмена. Любое «слабое звено» станет просаживать показатели всего кластера. В моем случае старый Haswell с DDR3 не добавлял полезной мощности — дополнительная сетевая координация нивелировала его усилия. А в случае с 32B лишний узел увеличил обмен данными, но не скорость.

Обработка промпта и генерация токенов — совершенно разные истории. Распределенный инференс ускоряет первый процесс, но не влияет на второй. Получается, что если вам нужно обрабатывать длинные промпты с короткими ответами (например, суммаризация или RAG), то построение кластера действительно даст заметный прирост скорости. В случае же чат-бота с длинной генерацией — почти никак не повлияет.

В будущем хочу попробовать проделать те же тесты на Wi-Fi 7 (6 ГГц), а также на 2.5GBASE-T. У меня есть гипотеза, что с более быстрой сетью переломный момент сдвинется в сторону меньших моделей. 

Ну а если захотите повторить этот тест — буду рад, если поделитесь цифрами и наблюдениями в комментариях.

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