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

Это первая статья из небольшого цикла, посвящённого сетям для AI/ML-кластеров и HPC.

В этой серии мы коснёмся принципов работы и обучения моделей, параллелизации, технологий DMA и RDMA, сетевых топологий, InfiniBand и RoCE, а ещё пофилософствуем на тему общих и специальных решений.

Конкретно в этой статье мы разберёмся, что представляет из себя нейросеть, как она работает, как происходит её обучение, а самое главное, почему для неё нужны сотни дорогущих GPU-карточек и какая-то особенная сеть.

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

Ныряем!


Содержание


Как читать эту статью?

Дело в том, что я не удержался — и вместо поверхностного обзора нейросетей, я закопался глубже, чем нужно для серии. Поэтому всё чрезмерное скрыто под катом и это можно пропустить.
А ещё в этой статье я всё описываю по несколько раз со всё новой степенью погружения. Поэтому если вам при первом знакомстве что-то непонятно, или вызывает возмущение, не торопитесь — читайте дальше.
Однако
В рамках этой статьи я не пытаюсь дать глубокое всестороннее описание работы нейросетей и построения кластеров для них.
Всё, что вы прочтёте ниже, дано с одной лишь целью — обрисовать доменную область и акцентировать внимание на том, что важно при выборе архитектуры сети (ну, компьютерной) и оборудования.
Мы с вами будем разбирать работу нейросетей преимущественно на примере LLM. Хотя разные типы нейросетей устроены очень по-разному, в контексте построения инфраструктуры для их обучения и инференса, их особенностями можно пренебречь.


Словарь

Что вам может понадобиться..

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

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

Блок — это «вычислительный модуль», состоит из нескольких слоёв, функций и операций. тыц

Веса — ��то сила связей между нейронами разных слоёв. Математически: значения в матрицах весов на каждом слое.
Веса — это только один из видов параметров.
тыц

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

MLPMulti-Layer Perceptron — архитектура нейронных сетей, содержащая несколько слоёв и нелинейные функции между ними. тыц

Трансформер или Transformer — архитектура современных нейросетей (не только LLM), где входные данные обрабатываются параллельно, а не последовательно, как раньше. тыц

Inference или инференс — процесс использования уже обученной модели. тыц

Обучение или Training — процесс обучения модели, когда подбираются параметры для наилучшего результата. тыц

Гиперпараметры — это внешние настройки модели: число слоёв, размер эмбеддинга, какая функция активации используется, шаг градиента, число экспертов в MoE. тыц

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

Эмбеддинги — это числа, описывающие входные данные (текст, изображения, звук итд). тыц

Активация — результат работы слоя или блока: вектор, который активируют нейроны следующего слоя. тыц

Вектор — набор чисел, вида [0, 1, 2, 3, 4].
Примеры: векторы входа, активации, смещения, внимания и другие.
А ещё это тензор ранга 1.
Размерность вектора — число элементов. Пример выше — вектор размерности 5.

Матрица — таблица чисел

[00, 01, 02, 03, 04]
[10, 11, 12, 13, 14]
[20, 21, 22, 23, 24]
[30, 31, 32, 33, 34]
[40, 41, 42, 43, 44]
[50, 51, 52, 53, 54]

Примеры: матрицы весов модели, градиентов.
А ещё это тензор ранга 2.
Размерность матрицы — число элементов в строках и столбцах. Пример выше — матрица размерности 6х5 (6 строк, 5 столбцов).

Тензоры — массивы чисел произвольного ранга.
Тензор ранга 0 — просто число (скаляр). Аналогия в геометрии — точка.
Тензор ранга 1 — вектор. Аналогия — отрезок.
Тензор ранга 2 — матрица. Аналогия — прямоугольник.
Тензор ранга 3 — уже нет названия, но можно говорить 3D-тензор. Аналогия — параллелепипед.
Итд.
Размерность тензора — это число элементов по каждой из осей.

Backpropagation — механизм обучения нейронных сетей через изменение параметров

Градиенты — это тензоры, позволяющие подстраивать значение параметров модели во время обучения, чтобы получать более точные ответы. тыц

Батч — порция данных для одного обновления весов.

Чекпоинт — сохранённое состояние модели в определённый момент времени во время процесса обучения.

KV-кэш — кэш ранее рассчитанных значений Key и Value, который переиспользуется для оптимизации. тыц

Контекст — вся история взаимодействия с нейросетью в рамках текущей сессии. То есть когда нейросеть «помнит», о чём вы «говорили». тыц

PyTorch — самый распространённый фреймворк для машинного обучения, реализующий API к NCCL и другим библиотекам. тыц


Из чего состоит кластер для AI/ML/HPC?

Я хотел бы коснуться реального мира, прежде, чем мы с головой нырнём в устройство нейронных сетей.

Всё, что нам нужно для использования и обучения нейронок — это много серверов, в которых стоит очень много GPU, и объединить это всё сетью.

  • Графические процессоры, они же GPU (Graphics Processing Unit) или вычислительные ускорители. Они базовый элемент кластера. Если максимально упрощать, то обучение моделей — это операции с матрицами — огромными массивами чисел, которые нужно складывать и перемножать. А лучше всех с этим справляются GPU. Хотя существуют и специализированные ML-акселераторы типа Google TPU.

  • Серверы, в которые эти GPU устанавливаются. Функция у них обычная: обеспечить р��боту компонентов, хранение данных, быструю сеть между карточками.

  • Сеть, связывающая всё воедино: коммутаторы, кабели.

  • Инфраструктура управления кластером: планировщики, библиотеки коллективных операций, хранилища данных и системы управления и мониторинга.

GPUClusterDiagram
GPUClusterDiagram

Зачем нужны GPU вопросов не вызывает, да? Но зачем какая-то специальная сеть?


Зачем нужны специальные сети для ML?

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

Нужно много видеокарт. И ещё нужно уметь распараллеливать вычисления между ними.

Соответственно, собираем их в сеть. Существуют десятки разных топологий с разными оптимизациями: что-то направлено на уменьшение диаметра, а следовательно задержек в сети, что-то на увеличение полосы, что-то на снижение цены. На это мы ещё взглянем.
Но что делает сети для ML и HPC настолько особенными, что о них нужно думать и писать статьи?

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

Например, обучение GPT-3 на кластере из нескольких тысяч GPU заняло в своё время около двух месяцев. Два месяца непрерывных вычислений на тысячах узлов, соединённых через сотни коммутаторов тысячами кабелей.
Это всё ломается. И то, как мы строим сети и какие используем схемы параллелизации, имеет решающее значение.

Поэтому для ML нужна Low Latency LossLess сеть. Почему — смотрим сегодня. Как её такой сделать — в следующих статьях.


Что такое нейросеть

По сути:

Нейроны

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

Естественный Нерон
Естественный Нерон
Искусственный Нерон
Искусственный Нерон

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

Каждый кружочек в этом простом примере ниже — это отдельный нейрон в искусственной нейронной сети:

Слои Неронов
Слои Неронов

Но делать аппаратную реализацию, в которой электрические сигналы проходят через миллионы нейронов, мы пока не очень умеем (если не считать мутные схемы на мемристорах), поэтому почти любая искусственная нейросеть — это сотни матриц и векторов, в каждой из которых несколько миллионов чисел, хранящихся в памяти и обрабатываемых на процессоре.

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

А на выходном слое нейроны магически создают для нас то, что мы просили.

Одни сети на вход принимают текст и создают на выходе текст — это LLM, вроде BERT или GPT.
Другие на вход принимают картинку, а выдать должны текст, например свёрточные сети CNN, распознающие изображения, в частности текст на них.
Третьи на вход принимают текст и должны сгенерировать картинку (диффузионнные сети).
Четвёртые на вход принимают звук и должны распознать в нём текст.

Принципиально все они работают одинаково — сложные математические модели с миллиардами и триллионами параметров, которые умеют самостоятельно находить закономерности в данных и «понимать» их.
И все они не программируются, а обучаются на основе огромного количества примеров.

Слои и веса

Нейросети состоят из слоёв:

  • входные

  • скрытые

  • выходные

Количество скрытых слоёв может варьироваться от единиц до сотен.
Слои соединяются друг с другом связями, имеющими веса (w).

Нероны и веса
Нероны и веса
Слои Неронов
Слои Неронов

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

Веса связей (они же значения матриц), значения векторов — всё это параметры модели. Они вычисляются при обучении и далее уже не меняются. То есть все эти миллиарды чисел — и есть сама модель.
Когда говорят, что GPT-3 содержит 175 миллиардов параметров, имеют в виду, что в её матрицах и векторах в общей сложности 175 миллиардов обученных значений

Активации

Если бы в каждом слое были просто матричные умножения, то все слои можно было бы схлопнуть в один, просто единожды их перемножив. И не было бы никакой магии.
Свобода, гибкость и креативность нейросетей обусловлены тем, что в каждом слое есть ещё нелинейная функция активации, которая усиливает хорошие связи и ослабляет паразитные.
Для этого могут использоваться функции sigmoid, ReLU или GELU.

Это очень похоже на настоящий нейрон — он возбуждается и передаёт сигнал дальше только в том случае, если входящий сигнал преодолел порог возбуждения.

Давайте рассмотрим, что происходит внутри, на конкретном примере MLP.


Конкретный пример - Multi-Layer Perceptron (MLP)

За основу взято видео But what is a neural network?, в котором рассматривается MLP — Multi-Layer Perceptron — простейшая нейросеть, распознающая рукописные цифры.

В рассматриваемой модели один входной слой, один выходной и два скрытых слоя. Между каждой парой слоёв каждый нейрон связан с каждым.

MLP вход
MLP вход

На вход по��аётся изображение размером 28х28 пикселей, то есть всего 784 — это размерность входного слоя — 784 нейрона. Каждый нейрон получает значение 0-255, в зависимости от яркости пикселя, за который он отвечает.
На выходе — десять нейронов, которые соответствуют десяти цифрам.
Два скрытых слоя посередине — по 16 нейронов. Это число выбрано в целом произвольно, как и количество скрытых слоёв.
У каждой связи есть свой вес.

MLP вход. Каждая линия — это параметр, число, означающее силу связи
MLP вход. Каждая линия — это параметр, число, означающее силу связи

И таким образом получается, что у нашей модели 784 * 16 + 16 * 16 + 16 * 10 = 12 960 параметров — по числу связей между слоями.
И у MLP есть ещё вектор смещения (bias) — по размеру каждого слоя, то есть это ещё 16 + 16 + 10 = 42 параметра.
В общей сложности получается 12 960 + 42 = 13 002 параметра, которые нужно обучить таким образом, чтобы они смогли определять с высокой точностью (>90%) рукописные цифры.

И на этом же примере давайте сделаем первый шаг к тому, чтобы понять как нейросети работают.

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

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

MLP скрытые слои
MLP скрытые слои

А выходной слой уже «понимает», какая цифра, скорее всего, из них складывается:

MLP скрытые слои
MLP скрытые слои
MLP выход
MLP выход

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

Так в примере с MLP:

  • Во входном слое зажигаются те нейроны, под которыми есть "белые" пиксели. От каждого из них ведут связи во второй слой. У каждой связи есть свой вес.

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

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

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

В качест��е отдалённой аналогии можно привести поход к окулисту, когда вас просят смотреть на таблицу ШБМНК, а буквы все плывут. Вы пробуете новые линзы, со всё большими диоптриями — и с каждой новой линзой изображение становится всё чётче и чётче.

Ещё другими словами.

Если от нейронов входного слоя с 53-го по 84-й в нейрон 5 следующего слоя ведут сильные связи с высокими весами, то этот 5-й нейрон «понимает», что обнаружена горизонтальная палочка, и он активируется.

В свою очередь во втором слое активировались 5-9 и 11 нейроны и на 7-й нейрон третьего слоя подали достаточно мощный сигнал, чтобы он определил, что получился кружочек, за распознавание которого он отвечает.

В этом слое активировались 1, 5, 7 и 8 нейроны, и они зажгли на выходе нейрон, отвечающий за цифру 9.

Все нейронные сети работают по схожему принципу.
Мы рассмотрели сравнительно простой пример с MLP, распознающим рукописные символы на фотографиях.

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

Вход → [Полносвязный слой → Нелинейность] × N → Выход
Где N — это количество скрытых слоёв

MLP используется в том числе и в трансформерах, на которых строятся все современные LLM: GPT, Gemini, DeepSeek, Claude, QWEN.
Вот за них мы сейчас и возьмёмся — чтобы поглубже разобраться с принципами работы нейросетей.


Принципы работы нейросетей

Для начала давайте точно разделим процессы обучения модели и её использования — Training и Inference.

Использование (inference) — это когда вы просите модель рассказать вам, в какой части Москвы лещи побольше, или сгенерировать вам картинку с анимешной девочкой.
Или она пишет для вас код телеграм-ботика. Или составляет план силовых тренировок.
То есть просто пользуетесь ею.

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

Обучение (Training) — это длительный процесс подбора параметров, когда через модель прогоняют миллионы примеров, с каждым циклом улучшая результат её работы.
То есть инференс становится возможным после обучения.

Нужны выделенные кластеры на тысячи карточек, соединённые специальной сетью.
Обучение может длиться непрерывно несколько месяцев.

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

Давайте начнём с инференса как более простой и общей для обоих процессов операции.

Инференс

Слои и блоки

Все современные модели многослойные. От единиц слоёв (перцептроны) до сотен (LLM). Каждый слой выполняет определённую функцию.
Например, при распознавании букв на изображениях первый слой находит углы, линии, изгибы. Второй объединяет их в палочки, кружочки, петли. Третий определяет на что похожи получившиеся символы («вероятность, что это цифра 8, равна 0.998472»)

Общая схема неровной сети
Общая схема неровной сети

В LLM сейчас используются блоки, каждый из которых — это комбинация слоёв.
В GPT-3 , например, 96 блоков. Одни блоки понимают синтаксис, части речи, род, время, имена. Другие — семантику и контекст, отслеживают отношения субъект-объект. Третьи распознают стиль, находят исторический контекст.

Чем дальше блок от входного слоя, тем более сложные и отдалённые взаимосвязи он находит и тем более общий смысл и контекст текста он определяет.

Каждый блок внутри состоит в свою очередь из слоёв. Слои функциональные: один умножает входную матрицу на матрицу весов, другой делает смещение, третий активацию итд.

А вот иллюстрация из классической работы Attention Is All You Need. Она о том, как Attention добавляет частичку магии (всё-таки) в трансформеры.

Схематическая и верхнеуровневая архитекутра трансформеров от входных эмбеддингов до выходных вероятностей.
Схематическая и верхнеуровневая архитекутра трансформеров от входных эмбеддингов до выходных вероятностей.

С этой жуткой картинкой мы и будем разбираться дальше.


Что важно понимать про LLM — они НЕ идут по конкретному плану: разобрать смысл вопроса ➞ подготовить план ответа ➞ сформу��ировать ответ.
Они всего лишь находят следующее вероятное слово.
...
Они всего лишь находят следующее вероятное слово.

Как?

Попросим нейронку: «Продолжи фразу «Ох, у ямы холм с кулями».

  1. Нейронка разбирает её на составляющие, «понимает» точный смысл как отдельных слов, так и всей фразы целиком.
    Она понимает, что это скороговорка.
    А так же «понимает», что от неё ожидается — продолжить фразу.

  2. После первого прогона через все блоки она создаёт слово «Вот».
    Это просто самое вероятное первое слово, учитывая текущий запрос и все примеры, которые она видела.
    Да-да! После всех миллиардов умножений, она просто выдала «Вот».

  3. Теперь на вход в нейронку передаётся фраза «Продолжи фразу «Ох, у ямы холм с кулями». Вот». Она прогоняется полностью ещё раз через все блоки и на выходе добавляет ещё одно слово: «Продолжи фразу «Ох, у ямы холм с кулями». Вот несколько»
    Это просто самое вероятное следующее слово, учитывая все примеры, которые она видела, запрос и последнее сгенерированное слово.

  4. Эта новая фраза снова передаётся целиком на вход и ещё раз полностью прогоняется через все блоки, и в конце выбирается следующее слово «Продолжи фразу «Ох, у ямы холм с кулями». Вот несколько вариантов».
    Это просто самое вероятное следующее слово, учитывая все примеры, которые она видела, запрос, ответ и последнее сгенерированное слово.

  5. Этот процесс продолжается до тех пор, пока нейросеть не решит, что она закончила:

    Вот несколько вариантов продолжения фразы в духе скороговорки:

    Ох, у ямы холм с кулями, Выйду на холм, куль поправлю
    Ох, у ямы холм с кулями, Куль поправлю — дальше пойду.

Как она понимает, что она закончила? Просто следующим наиболее вероятным словом будет символ окончания генерации. Как так? Просто так было в примерах, на которых она училась. Магия? Да, но нет.


Три фундаментальных вещи в LLM:

  1. Они не понимают в человеческом смысле. Они в математическом смысле видят близость разных векторов и тем самым находят связи между словами и фразами.

  2. Они не придумывают и не готовят ответ. Они просто в результате всех вычислений получают вероятности каждого слова (токена) в доступном словаре

  3. Текст прогоняется через все блоки модели много раз: после каждого нового сгенерированного слова весь текст подаётся на вход модели.


Последовательность событий при инференсе

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

Возьмём GPT-3 и фразу, выбранную выше.

На самом деле не создано ещё лучшей визуализации работы LLM, чем это: https://bbycroft.net/llm.
Горячо рекомендую до, во время, и после прочтения статьи тыкаться в неё, чтобы стабилизировать хаос в голове. А я буду дёргать оттуда иллюстрации для своих параграфов.

https://bbycroft.net/llm
https://bbycroft.net/llm

1. Токенизация

Первое, что делает модель — разбивает фразу на части, равные одному токену. Это могут быть целые слова или отдельные буквы, но обычно не то и не другое, а части равного или примерно равного размера.
По буквам получается слишком много токенов и слишком сложно связывать всё в единый смысл (попробуйте сами:
["О", "х", ",", "у", "я", "м", "ы", "х", "о", "л", "м", "с", "к", "у", "л", "я", "м", "и"]).
По словам мы растим словарь — придётся под все формы слова заводить отдельную сущность. Например, «кальянная», «кальянный», «кальянщик», «калик» и прочие.

Например, наша фраза может быть разбита так:
["Ох", ",", "у", "ямы", "холм", "с", "куля", "ми"].

Рзабиение входной последовательности на токены
Рзабиение входной последовательности на токены

Так мы разбили текст на отдельные токены.


2. Embedding или встраивание

Дальше происходит первый этап магии (да-да, магии): нейросеть определяет координаты полученных слов в общем смысловом пространстве.
Ни много ни мало.

Но это всё слова. А нам нужны действия числа!
Числа нам нужны, потому что мы не можем применять мат. аппарат матричных операций к словам.

Поэтому каждому токену сопоставляется вектор чисел. В случае GPT-3 длина этого вектора — 12 288. Это называется размерностью модели. Грубо ��оворя, размерность означает, насколько точно модель может определять все возможные смыслы и вариации слова во всех возможных контекстах.
При этом, чем ближе слова по смыслу, тем более похожи их векторы. Так векторы «у» и «рядом» будут очень похожи, а «у» и «далеко» — нет, хотя всё равно более похожи, чем «у» и «динозавр».
При этом у слов «замóк» и «зáмок», несмотря на разное значение, будут одинаковые векторы. Которые поменяются потом в процессе прохода через слои.

Встраивание токенов в пространство модели
Встраивание токенов в пространство модели

Как модель это делает? У неё есть Embedding-словарь — полный словарь всех возможных слов. Для GPT-3 — это 50 257 слов (или токенов). Каждый представлен вектором из 12 288 чисел. И это первое слагаемое всех параметров модели: 50257 * 12288 = 617 558 016 = ~617M.
Собственно в этой матрице модель находит строку, совпадающую с переданным на вход словом.

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

В результате для нашей фразы мы получим матрицу размерности 8×12 288.

Это был этап подготовки данных — мы определили координаты текста.


Далее эта матрица прогоняется последовательно через десятки однообразных блоков-тренсформеров. В случае GPT-3 — это 96 блоков, каждый из которых состоит из нескольких функциональных слоёв.

На иллюстрации это блок transformer i

Схема устройства LLM
Схема устройства LLM

Как я уже упоминал выше, каждый блок выявляет новые знания об этой фразе.
Например, выделяет подлежащее («холм»), сказуемое (его нет, но сеть поймёт, что это что-то вроде «находится»), дополнение («кулями»), их род, число, как соотносятся между собой эти слова, в каком предположительно контексте они были использованы.

Клод, будь ласка
Клод, будь ласка

3. Механизм внимания (self-attention)

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

Про механизм внимания и то, как он преображает обычный MLP, написана статья, ставшая хрестоматийной: Attention Is All You Need.

В нашем случае головы внимания определяют, что главное слово «холм», что оно мужского рода единственного числа, что холм находится возле ямы, что холм состоит из кулей итд.
В каждом блоке несколько голов внимания (Attention Heads — в GPT-3 96 в каждом блоке), каждая из которых «смотрит» на слово под разными углами:

  • синтаксические отношения (подлежащее — сказуемое);

  • семантические связи (синонимы, антонимы);

  • близкие и дальние зависимости;

  • тематические и стилистические паттерны,

  • грамматика

  • итд.

Как в примере выше про MLP каждый нейрон распознавал свой признак в изображении, так в трансформерах каждая голова распознаёт свой тип связи. В каждом блоке.

В деталях self-attention

Для каждого токена вычисляется три вектора:

  • Q (Query) — Что я хочу узнать

  • K (Key) — О чём я знаю

  • V (Value) — Что я в итоге рассказывают

Давайте сначала разберём на интуитивном уровне, что это такое.

Возьмём слово «ямы» из фразы «Ох, у ямы холм с кулями».
Возможные: Query слова «ямы» (Q_ямы). То есть что этот токен хочет узнать у других

  • «Ищу синтаксические связи с другими словами»

  • «Кто управляет мной грамматически?»

  • «Есть ли рядом предлоги?»

Токен «ямы» настроен искать: предлоги, глаголы, связанные существительные, объекты рельефа.

Возможные: Keys слова «ямы» (K_ямы). То есть что этот токен знает о себе и как его могут найти другие.

  • «Я существительное в род.падеже»

  • «Я могу быть дополнением»

  • «Я объект ландшафта»

Сообщает другим: «Я объект места, могу отвечать на вопрос «где?» через предлог»

Возможные: Values слова «ямы» (V_ямы). То есть что токен расскажет другим.

  • «Углубление в земле»

  • «Контекст: рельеф, география»

  • «Грамматика: род.падеж, ед.число, жен.род»

  • «Может сочетаться с: холм (противоположность), предлоги места»

Содержит обогащённую информацию для передачи в контекст

А теперь возьмём слово «у».
Возможные: Query слова «у» (Q_у):

  • «Ищу существительное справа от меня»

  • «Кто мой объект в род.падеже?»

  • «Есть ли главное слово конструкции?»

Токен «у» настроен искать: существительные в род.падеже, особенно сразу после себя.
Предлог «у» знает: «Я неполноценен без объекта! Мне нужно существительное!»

Возможные: Keys слова «у» (K_у).

  • «Я предлог места»

  • «Я соединяю слова синтаксически»

  • «Я индикатор локативной конструкции»

  • «Обрати на меня внимание, если ты существительное после меня!»

Сигнализирует другим: «Я грамматический клей, важен для существительных рядом»
Для «ямы»: K_у кричит «Я твой управляющий предлог!»
Для «холм»: K_у говорит «Я не так важен, между нами есть «ямы»

Возможные: Values слова «у» (V_у):

  • «Локативное значение: «рядом с», «около»

  • «Синтаксическая роль: управляю род.падежом»

  • «Грамматическая связь: предлог + существительное»

  • «Добавляю смысл: пространственная близость»

Содержит «Я превращаю существительное в обстоятельство места»

Эти вектора Q, K, V рассчитываются для всех токенов.

И после голов внимания вектор, содержавший только эмбеддинг, содержит теперь: «яма + она стоит после предлога «у» + рядом есть контрастный объект «холм» + общий контекст местности»
А вектор «у»: «предлог «у» + управляет словом «ямы» + конкретная конструкция «у ямы» + контекст: местность, холм рядом»

И так для всех токенов.

Что это такое с математической точки зрения? Перемножение матриц, конечно. А вы чего ожидали?

Вектор каждого слова умножается на специальные (заранее обученные) матрицы весов Wq, Wk, Wv и получаются те самые векторы Q, K, V.
Далее для каждого слова его Query сравнивается с чужими Keys.

Например Query слова «ямы» и Keys других слов (включае самоё себя):

Q_ямы · K_Ох    = 0.3  (низкая связь)
Q_ямы · K_,     = 0.1  (почти нет связи)
Q_ямы · K_у     = 0.8  (высокая связь — предлог!)
Q_ямы · K_ямы   = 1.0  (сам с собой)
Q_ямы · K_холм  = 0.6  (средняя — связанные объекты)
Q_ямы · K_с     = 0.2  (низкая связь)
Q_ямы · K_кулями= 0.4  (низкая связь)

Q — вектор, K — векторы. Числа в примерах выше — это скалярное произведение Q_ямы и K других слов. И это число показывает, насколько токен j «отвечает» на запрос токена «ямы», то есть как бы сила связи.

Вычисляются скалярные произведения всех пар Qᵢ и Kⱼ где i, j — это все токены фразы.
Далее применяется функция softmax и эти силы связей превращаются в вероятности, сумма которых равна 1.
То есть Q относится к текущему слову, про которое мы хотим узнать, K, V — ко всем другим словам текста, включая само это слово.

Всё это складывается в матрица весов, то есть сил связи всех пар слов.
И дальше считается взвешенная сумма по всем словам: с какой силой вектор Vⱼ влияет на вектор i.

Иными словами: входной вектор слова изменяется на V с силой Q * K — и так в каждом блоке.

Механизм внимания
Механизм внимания

Я даже не буду пытаться сделать вид, что мы здесь с вами должны что-то понять:

Механизм внимания
Механизм внимания

Но попробуйте семь раз посмотреть это в динамике, прочитать ещё пару статей, перечитать эту статью, и ещё раз посмотреть в динамике — и начнёт становиться понятно.

Ну и.. это пример только для одной головы внимания. В блоке их несколько десятков или сотен, каждый из которых делает всё то же самое, но отслеживает другие взаимосвязи.

Параметрами в механизме внимания являются матрицы Wq, Wk, Wv и определяются в процессе обучения. Они неизменны для модели при инференсе.
Вектора Q, K, V специфичны для каждого конкретного примера и меняются от примера к примеру. Не являются параметрами модели.

То есть Q, K, V — это инструменты для вычисления «кто на кого и как влияет» в тексте.
И это ультимативный механизм «понимания» в трансформерах.

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


4. FFN и нелинейные преобразования

Язык — жёстко нелинейная штука.
Поэтому для обработки языка нужно добавить нелинейность. Ровно этим и занимается FFNFeed Forward Network — микроскопическая двухслойная нейросеть.
Она применяет к вектору нелинейную функцию. И это позволяет нейросети быть гибкой, распознавать сложные паттерны и не быть просто серией линейных преобразований,рые на самом деле могли бы просто схлопнуться в одно линейное преобразование.

Если говорить грубо, то FFN сохраняет сильные связи и зануляет слабые.

Это на самом деле MLP — Multi-Layer Perceptron, с которым мы имели дело выше. То есть MLP является встроенным компонентом блока трансформера, который имеет здесь своё название — FFN.

Это два матричных умножения, между которым работает нелинейная функция.

Feed Forward Network, он же Multi-Layer Perceptron
Feed Forward Network, он же Multi-Layer Perceptron
  1. Первое умножение увеличивает размерность вектора токена в 4 раза (в GPT-3 с 12288 до 49152). Далее добавляется вектор смещения Bias.

  2. Нелинейная функция модифицирует значения вектора: усиливает сильные связи, ослабляет слабые.

    Нелинейные функции

    Существует множество нелинейных функций, вот самые основные из них, используемые в ML:

    1. Сигмоида преобразует все числа в значения от 0 до 1. получается плавная линия, похожая на букву S

    Сигмоида
    Сигмоида
    1. ReLU: обнуляет отрицательные. Получается горизонтальная линия, переходящая в наклонную

    ReLU
    ReLU
    1. GELU: плавно снижает малые значения, сохраняя информацию. Используется в GPT.

      GELU
      GELU

    Подробнее о нелинейных функциях ниже

  3. Второе умножение возвращает вектору прежнюю размерность.

Более подробная иллюстрация:

https://bbycroft.net/llm
https://bbycroft.net/llm

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

В деталях нелинейная функция активации

В видео Why Deep Learning Works Unreasonably Well объясняется, почему модели полезнее иметь больше скрытых слоёв, чем нейронов, то есть препочтительно быть более глубокой, чем широкой (ну как и в жизни).

Преимущества глубоких моделей перед широкими
Преимущества глубоких моделей перед широкими

Каждый скрытый слой содержит в себе MLP с нелинейной функцией активации. Но ЗАЧЕМ?

А собственно они только вместе и работают: нелинейная функция и многослойность.

Давайте начнём с понимания почему вообще линейность — это .. плохо. Иллюстрации буду дёргать из хорошего видео, объясняющего принцип затухающих градиентов: Activation Functions - EXPLAINED!

Все мы знаем, что такое линейная функция.. Ну.. Это линия. И она хорошо работает лишь в том случае, когда у нас чёткая и линейная граница между объектами, которые нужно разделить:

Линейная функция работает
Линейная функция работает

Но вот в этой ситуации она уже не работает:

Линейная функция не работает
Линейная функция не работает

Но!
Если мы добавим нелинейность в модель, она может проводить уже не прямые границы:

Нелинейная функция работает
Нелинейная функция работает

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

Придётся добавить немного формул.

Всё схлопнулось. Халк крушить!
Всё схлопнулось. Халк крушить!
  • X — это входные данные

  • W — веса связей

  • b — вектор смещения

  • h — вектор активации на выходе слоя.

  • y — результат

Вектор активации первого слоя будет считаться так:

h₁ = W₁x + b₁

Умножили вход на веса, прибавили вектор смещения.
Аналогично считается вектор активации второго и третьего слоёв:

h₂ = W₂h₁ + b₂
y = W₃h₂ + b₃

Теперь подставим одно в другое:

y = W₃(W₂(W₁x + b₁) + b₂) + b₃
y = W₃W₂W₁x + W₃W₂b₁ + W₃b₂ + b₃

W₃W₂W₁ — это просто константная матрица — перемножение статических матриц весов — можем поменять на W. А W₃W₂b₁ + W₃b₂ + b₃ — это константный вектор — можем поменять на b:

y = Wx + b

И трёхслойная сеть с 46 параметрами превращается в элегантную однослойную с восемью параметрами.

Свёрнувшаяся (но не свёрточная) нейронная сеть. Она как свернувшийся белок, только нет.
Свёрнувшаяся (но не свёрточная) нейронная сеть. Она как свернувшийся белок, только нет.

Всё меняется, как только мы добавляем нелинейную функцию. Например ReLU:

ReLU не позволяет раскрыть скобки
ReLU не позволяет раскрыть скобки
h₁ = ReLU(W₁x + b₁)
h₂ = ReLU(W₂h₁ + b₂)
y = W₃h₂ + b₃

y = W₃ReLU(W₂ReLU(W₁x + b₁) + b₂) + b₃

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

Нелинейная функция работает
Нелинейная функция работает

Здесь мы получаем вектор, очищенный от шумов и с усиленным контекстом.


5. Layer Norm и Residual connection

Можно сказать, что LLM при любой возможности нормализует вектор, и добавляет к нему входной вектор. Зачем? Потому что эмпирически так модель стабильнее и даёт лучший результат.

Нормализация или Layer Norm

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

Для этого нужны два вектора размерности модели (12 288 для GPT-3 ): γ (scale) и β (shift). Выходной вектор получается по формуле output=γ × normalized + β.

Остаточные связи или Residual connection

Оказывается, если к выходному вектору добавить входной, который поступил в блок, то модель как бы лучше запоминает, что было раньше (некие исторические корни) и тоже работает стабильнее.

Почему? Это интересный вопрос. Сначала к этом пришли эмпирически. Но впоследствии нашли объяснение: Residual connection борется с затухающими градиентами и значительно улучшает обучение модели. О градиентах и обучении поговорим ниже.

На выходе из последнего слоя мы получаем вектор-представление последнего токена, обогащённый смыслом.


6. Output level

Для задач генерации текста, чего мы больше всего и ждём от LLM, нужен вектор только ПОСЛЕДНЕГО токена (слова).
Именно на основе него модель ищет следующий токен.
Выходные векторы всех предыдущих токенов для этого не используются, но они были критически важны для работы механизма внимания.
То есть вектор последнего токена «накопил» всю массу смысла всех входных данных.

В модели для выходного слоя есть словарь, такой же, как и в начале (где слова превращались в числовые векторы) — это тоже матрица 50 257 на 12 288 (для GPT-3 ). Обычно это та же матрица эмбеддинга.

Выходной вектор токена смысловой. Теперь его надо преобразить в вероятности следующего токена.

Для этого сначала вычисляется логит: вектор токена умножается на специальную весовую матрицу.
В результате получается вектор размерности словаря (50 257 для нашего примера).
Логит (термин из теории вероятностей) — это, скажем, оценка уверенности модели в следующем слове. Ещё не вероятность, потому что значения могут быть отрицательными и не обязательно в диапазоне от 0 до 1.

В нашем примере для фразы «Продолжи фразу «Ох, у ямы холм с кулями» Вот несколько вариантов» может быть так:

«продолжения» +4.3
«скороговорки» +2.1
«Крота» -12

Далее к вектору логита применяется функция softmax, и он превращается в вектор вероятностей, сумма которых равна 1.
Было 50 257 вещественных величин, а теперь 50 257 вероятностей, соответствующих словам в словаре.

Тогда может быть так:

«продолжения» 0.541
«скороговорки» 0.311
«Крота» 0.00001

Логично было бы взять слово с наибольшей вероятностью. Но на практике делают сэмплинг — случайным образом выбирают слово, в соответствии с его вероятностями.
В нашем примере в 54.1% случаев будет выбираться «продолжения», в 31.1% — «скороговорки», а в 0.001% — «Крота».

И так далее, каждый раз используя вектор последнего токена, модель итеративно подбирает наиболее вероятное следующее слово.

Выходной слой. У нас каникулы
Выходной слой. У нас каникулы

Путь вектора через модель

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

Именно так два одинаковых изначально вектора для слов «замóк» и «зáмок» становятся совершенно разными к выходному слою, потому что они обладают разными значениями.

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


Если для генерации следующего слова используется только вектор последнего слова, зачем считать все остальные?

Ну во-первых, механизм внимания — необход��мы те самые векторы K, V, чтобы понимать смысл — и для этого нужны все слова, как изначальные, так и сгенерированные.
Во-вторых, вектор последнего токена уже обогащён всеми предыдущими. Без них не было бы контекста, в котором используется это слово.
В-третьих, у LLM бывают и другие задачи, когда действительно имеют значение все векторы.
И, в-четвёртых, есть KV-кэш, оптимизрующий эти вычисления.


Температура модели

Есть также термин «температура модели» — это то, насколько она «смелая» в выборе следующего слова или насколько она креативна.
Чем более она горячая, тем выше вероятность взять не самое популярное слово.
Например, при низкой температуре модель продолжит нашу фразу «Ох, у ямы холм с кулями» так:

выйду на холм — куль поправлю

При повышенной температуре:

куль упал — и нету ямы

Если модель разогрета:

там мой WiFi ловит лучше

Термин пришёл из термодинамики, где температура по сути определяет степень хаотичности системы. Чем выше температура, тем более активно частицы движутся, и система менее предсказуема.


KV-кэш

Что может быть поразительным при первом глубоком понимании того, как работают, нейросети, так это то, что все слова текста многократно полностью перемалываются через все слои.
Представьте вы просите сеть пересказать вам «Войну и мир» не менее чем в 20 страницах? Пока лимита токенов хватает, для каждого нового слова нейросети будет запускать расчёты заново для всех уже сгенерированных слов.

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

Лааадно. Не так уж это и нужно, потому что есть неплохая такая оптимизация — избежать для новых пересчёта embeddings и FFN для всех предыдущих токенов

Каждый блок сохраняет KV-кэш (то есть кэш векторов K и V) всех предыдущих токенов, поэтому по сути только новый токен прогоняется через все блоки, а значения для старого берутся из вот этого сохранённого KV-кэша. Для нового токена вычисляются все вектора: Q, K, V. Но Query от нового токена нужно пересчитывать ещё и к старым, что логично.

Это классический размен вычисления на память — места этот кэш занимает более чем неприлично.

В деталях цифры для хранения KV-кэша

Для одного слоя: KV_cache_size = 2 × n_layers × batch_size × seq_len × n_heads × head_dim × bytes_per_param

Для GPT-3 (175B):

  • 2 — для K и V (два кэша)

  • n_layers = 96 — количество слоёв трансформера

  • batch_size =1 — сколько запросов обрабатывается параллельно

  • seq_len = 2048 — длина последовательности (контекста)

  • n_heads = 96 — количество голов внимания

  • head_dim = 12,288 / 96 = 128 — размерность каждой головы (d_model / n_heads)

  • bytes_per_param = 2 — размер одного числа (2 для fp16)

KV_cache = 2 × 96 × 1 × 2048 × 96 × 128 × 2
         = 9,663,676,416 bytes
         ≈ 9.0 GB

9 ГБ для всех слоёв на один промпт!
И учитываем линейный рост с длиной контекста (напомню, на сегодня до 1млн токенов) — так для Claude с 200к токенов контекста размер кэша будет уже 781 ГБ.

И реальная стоимость inference часто определяется именно размером KV-кэша, а не весами модели!

Почему KV-кэш, а не QKV? Ну потому что Query не нужен для новых генерируемых токенов, а старые токены уже «задали свои вопросы» и получили ответы, когда они сами были новыми токенами.

Важный момент: KV-кэш существует в рамках одного запроса (если быть точнее, в рамках одного forward pass через модель).
Вот вы написали вопрос в чат с нейросетью. Пока она его разбирает, генерирует первый токен, второй — она наполняет KV-кэш.
И так до того момента, пока ответ не будет готов и модель не закончит обработку этого запроса.
После этого кэш очищается.

Когда вы задаёте следующий вопрос, всё начинается заново, с нуля.
Если после обработки запроса «Ох, у ямы холм с кулями» вы напишете «в недрах тундры выдры», то модель начнёт работать с фразой «в недрах тундры выдры», ничего не зная про предыдущую.
Модель не помнит ничего, о чём вы с ней говорили. Поэтому и говорят, что у LLM нет долговременной памяти — только кратковременная в пределах одного запроса.

Если вы хотите вести с ней диалог, вам нужен контекст.


Контекст

Для диалога нужна история. Поэтому интерфейсы к моделям (например, чаты) сами намеренно дополняют запрос пользователя историей переписки: и вопросы пользователя и ответы LLM. Поэтому после двух скороговорок вы можете спросить «Что их объединяет?» — и она ответит.

Иллюстрацию разницы между KV-кэшем и контекстом вы можете видеть, когда наблюдаете за созданием ответа.
Сначала модель сравнительно долго думает, а потом она довольно быстро печатает ответ слово за словом.
Сначала идёт фаза Prefill: она проворачивает через себя весь контекст, наполняет KV-кэш, что занимает какое-то время. А для каждого нового слова ответа, модель использует кэш и просчитывает новое слово — это фаза Decode.

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

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

  • Первый запрос и ответ: 200 токенов

  • Второй: 150. Итого 150 + 200 = 350

  • Третий: 400. Итого 400 + 350 = 750 итд.

Это означает, что запросы будут «дороже» — в смысле денег, вычислений и времени.

А как на самом деле?

На самом деле оптимизируют всё, что можно, в том числе контекст. Многие провайдеры ИИ-интерфейсов реализуют Prefix Caching (или Prompt Caching). Контекст в этом случае хранится в условном Redis/Memcached несколько минут после запроса.
Когда вы шлёте запрос в модель, она сначала проверяет, нет ли кэша, который нужно вгрузить в GPU.

Чтобы избежать чрезмерного разрастания контекста, у всех моделей есть лимит: от нескольких тысяч до нескольких сотен тысяч токенов. Так у GPT-3 он был 2048, а Claude-3: 200 000. У современных на момент написания статьи (Gemini-3 Pro) контекст уже 1М токенов.
Если диалог длиннее лимита, то либо отрезается самая старая часть, либо история оптимизируется.

В чём проблема с длиной контекста? Как только мы его переполнили, модель начинает плющить — она забывает, что делала, она не может «в голове» удержать все данные, начинает ломать логику ответов. Например, вы пишете какую-то программу вместе с нейронкой, и вдруг она начала вставлять левые символы или ломать архитектуру приложения или предлагать очень плохие решения — скорее всего, дело в превышении контекста.


Суммируем инференс

  • Головы внимания (Attention Heads) находят взаимосвязи и паттерны.

  • FFN сначала расширяет размерность матрицы, то есть как бы её разрешение. Затем после нелинейной функции сужает её обратно.

  • Нелинейная функция, позволяет «срезать» шумы в данных, тем самым усиливая полезные веса.

  • Вектор смещения (bias) указывает, по какой именно линии срезать шумы.

  • Нормализация (Layer Norm) уменьшает слишком большое.

  • Остаточная связь (Residual Connection) прибавляет к полученному в блоке вектору изначальный входной, чтобы укрепить связь с ним.

  • KV-кэш позволяет значительно снизить время ответа при инференсе, засчёт временного сохранения вычислений для предыдущих токенов.


Так что же такое нейросеть?

Это миллиарды-триллионы чисел в матрицах и векторах, расположенных в правильном, эмпирически выверенном порядке.

Давайте рассмотрим на примере GPT-3 .

  • Матрица эмбеддинга 50 257 (размер словаря) × 12 288 (размерность модели) = ~617M

  • Позиционные эмбеддинги 2048 × 12 288 = ~25M

  • 96 последовательных блоков трансформеров

  • Головы внимания: Wq, Wk, Wv — 3 × (12 288 × 12 288) = ~453М. И Output проекция обратно в вектор токена: 12 288 × 12 288 = ~151М. Итого ~604M

  • FFN: сначала расширяем размерность в 4 раза: 12 288 × 49 152 = ~604M. Потом снижаем обратно: 49 152 × 12 288 = ~604M. Итого ~1208M

  • Два вектора нормализации с двумя параметрами: 2 (γ) × 2 (β) × 12 288 = 49152

Считаем: 642М(эмбеддинги) + 96 (слоёв) × (604M (внимание) + 1208М (FFN) + 0.049M (нормализация)) = ~175 млрд параметров.

Все эти 175 миллиардов параметров описывают весь доступный нейросети мир (не так уж он и велик, правда? Хотя у современных моделей это уже триллионы параметров).
И все они должны быть вычислены в процессе обучения.


Обучение

Процесс, описанный выше — это так называемый Inference, или использование уже обученной нейросети. Мы ей на вход данные — на выходе очень быстро (за секунды и даже миллисекунды) получаем ответ.

«Обученная» означает, что в матрицах и векторах уже стоят такие числа, которые дают хороший результат.

Задача этапа обучения — подобрать такие числа. Каждое из миллиардов, а то и триллионов чисел.
Процесс обучения современных сетей, вроде GPT, занимает месяцы вычислений на сотнях и тысячах GPU.

Ключевые этапы обучения:

  1. Прямой проход. Берём фразу, предсказываем следующее слово (по сути то, что происходит при инференсе).

  2. Вычисление функции потерь. Выясняем насколько плохо на данный момент подобраны параметры.

  3. Обратный проход. Слой за слоем вычисляются корректировочные матрицы — то есть как нужно изменить каждый параметр модели, чтобы уменьшить ош��бку.

  4. Обновление параметров. Отдельным шагом все изменения применяются.

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

Самое главное, что в результате процесса мы получаем сотни правильных матриц и векторов весов.

Подавляющее большинство современных нейросетей существует только благодаря механизму обратного распространения градиентаbackpropagation. Без него обучение практически невозможно.

Вот как обучение выглядит на осцилографе:

Запустили обучение на осцилографе
Запустили обучение на осцилографе

Гиперпараметры

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

Они задаются один раз перед началом обучения и являются далее уже неизменными её законами.
То есть не обучаются, как обычные параметры, а задают правила игры — на то они и гиперпараметры.


Как выглядит процесс обучения

На шаге 0 мы имеем несколько сотен огромных матриц, заполненных случайными числами.
Модель сейчас не сможет предсказать даже завершение фразы «я люблю». Точнее сможет, но будет белиберда, вроде «я люблю Саяно-Шушенская ультрафиолет жидкий вошкаться в у миллион»

Шаг 1: чтобы научить её составлять текст, нужно показать ей пример. Много примеров. Очень.
Это веб-сайты, книги, периодика, архивы, википедия.
В общей сложности у GPT-3 было порядка 300 млрд обучающих токенов.

Для каждого примера модель должна предсказать следующее слово. И в зависимости от того, насколько хорошо она угадала, подправляются все её параметры.
Повторяем до тех пор, пока предсказания не станут достаточно точными.

Давайте конкретнее.
Например, мы выбрали фразу «Кошка сидела на подоконнике и смотрела на птиц».
Она нарезается на тренировочные и контрольные данные.
Мы заставим модель закончить фразу (вход → цель)

Кошка → сидела
Кошка сидела → на
Кошка сидела на → подоконнике
Кошка сидела на подоконнике → и
Кошка сидела на подоконнике и → смотрела
Кошка сидела на подоконнике и смотрела → на
Кошка сидела на подоконнике и смотрела на → птиц

И таких примеров миллионы.

Вторым шагом модель делает ровно всё то же самое, что и при инференсе: self-attention, normalize, FFN, residual connection, и так через все блоки.
Точно так же модель пытается «понять» текст, найти связи между словами. Но пока это очень плохо у неё получается.
Однако всё же алгоритм есть алгоритм и следующее слово она в любом случае выбирает «Кошка сидела на подоконнике и смотрела фиолетовый»

Шаг три. На выходе новое слово сравнивается с тем, что должно быть на самом деле.
Вычисляется функция потерь (ошибки) — то есть насколько она промахнулась и как сильно нужно изменять модель.

«Фиолетовый» совсем не похоже на «на», поэтому веса нужно поменять.. Все.. 175 млрд.

Четвёртый шаг: все веса модели меняются. Этот процесс последовательно слой за слоем идёт в обратном направлении: из конца в начало, поэтому называется обратным проходом или Backpropagation.
Можно представить себе волну, которая на своём пути сметает всё и меняет ландшафт. Так и корректирующие матрицы на своём пути меняют все встречаемые числа.

И потом начинаем сначала: пробуем с новыми числами другие примеры. Ошибка должна стать чуть поменьше.
И ещё раз.
И ещё.
И так миллионы раз.

Схема обратного распространения
Схема обратного распространения

Это всё слова. А нам нужны числа — корректирующие матрицы для каждого слоя считаются с помощью градиентного спуска.

В деталях градиентный спуск и обратное распространение ошибки

Это два разных механизма:

  • С помощью градиентного спуска вычисляется, как нужно поправить веса на каждом слое

  • С помощью обратного прохода информация об ошибке для вычисления градиентов передаётся из слоя в слой

Давайте сначала поймём на очень простом примере, а что это вообще такое. За основу я взял видео The Most Important Algorithm in Machine Learning.

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

Точки на плоскости
Точки на плоскости

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

y(x) = k₀ + k₁x + k₂x² + k₃x³ + k₄x⁴ + k₅x⁵

Почему пятой? Да просто так, потому что считаем, что нам такой точности будет достаточно. При этом коэффициентов (параметров) довольно мало — и их легко подобрать.

Полином, описывающий распределение точек
Полином, описывающий распределение точек

Так вот на этапе обучения нам нужно как раз подобрать вот эти самые коэффициенты kᵢ так, чтобы итоговая функция прошла как можно ближе к точкам.

Ниже на левом графике мы подобрали их весьма неплохо, а на правом ужасно — ошибка очень высока (даже вызывающе красные линии говорят, что всё плохо) — а значит функция будет очень плохо предсказывать значение y в определённой точке x.

Функции с ошибками — большими и малыми
Функции с ошибками — большими и малыми

Обратное распространение ошибки

Вот эта самая ошибка вычисляется: для каждой известной точки x мы можем подсчитать отклонение предсказанного значения y от реально известного. Все их сложить и получить одно число, характеризующее нашу модель (выбранные коэффициенты kᵢ) — плохо получилось или пойдёт.
Это, кстати, единственное место, гда мы можем использовать термин «скаляр»: ошибка — это скаляр.

Мы можем случайным образом менять каждый из kᵢ и смотреть, какая получается итоговая ошибка.

Подбираем коэффициенты
Подбираем коэффициенты

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

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

Но, слава троице учёных, которые в 86-м показали практическую применимость механизма backpropagationобратного распространения ошибки. Они нашли способ, как на основе ошибки корректировать все-все-все коэффициенты, причём каждый по отдельности, проходя от конца к началу конвейера обработки входных данных — слой за слоем.

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

Градиентный спуск

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

чиво.jpg?
чиво.jpg?

Ладно, давайте сначала на интуитивном уровне.

Представьте себе, что вы человек.. А ну да. Представьте себе, что вы — ошибка. Не.. Тоже плохо.

В общем, стоите вы на склоне какой-то горки. Вам холодно. А хотите спуститься в долину, где потеплее.
Вы видите, в каком направлении вам нужно сделать шаг, чтобы спуститься. Делаете его. И оказываетесь в новой точке, где снова думаете, куда сделать шаг.
И так вот топ-топ в самый низ.

Для наглядности возьмём функцию линейной регрессии y(x) = k₀ + k₁x.

Вот пример градиентного спуска для неё:

С гор спустился
С гор спустился

Ну и вот каждый такой шаг вы делаете в направлении градиента — то есть направлении, где спуск круче всего. И каждый ваш шаг — это изменение координат k₀ и k₁ и на плоскости земли — вы их меняете. Вы находите такое их сочетание, которое уменьшает высоту точки, на которой вы стоите — подбираете параметры, получается.

В нашем примере с горкой два параметра — k₀ и k₁. И высота, зависящая от них. То, что мы можем представить в виде 3d-карты. Если параметра 3, то это уже будет четырёхмерное пространство. С его представлением есть проблемки, хотя некоторые утверждают, что по ночам визуализируют тессеракты.

А если мы вернёмся к изначальному примеру с многочленом 5-й степени, то там 6 параметров, а значит функция потерь будет уже в 7-имерном пространстве. Суть та же самая — мы материальная точка, которая движется вниз в многомерном пространстве.

Градиент, как мы помним из курса мат.анализа вычисляется как производная функции. В случае многих переменных это будет вектор — набор частных производных по каждой переменной. То есть по каждому параметру. В нашем 3D-примере градиент — это вектор из двух чисел — грубо говоря, наклон по двум осям. А когда осей (параметров) 6, то и компонентов в векторе будет 6.

Попробуем это немного «оцифровать»

Вот мы выбрали случайным образом 6 параметров (коэффициентов). Например:

y(x) = 3 + 2x + 8.3x² + 4.1x³ + x⁴ + 0.23x⁵

Мы их попробовали на определённом значении x, например 6.1. И получается, что y = 4581.82, а должен быть -0.22 (судя по графику). Посчитаем ошибку по простой формуле Mean Squared Error: (4581.82 - (-0.22))²/34 = (4582.04)²/34 = 617 502.66.
А при x=0 должно быть -1.2, а получается 3. Ошибка: 0.52.
А при x=8 должно быть -0.8, а получается 14282.04. Ошибка: 5 999 985.83.

Проходя так по всем известным значениям, считаем сумму: 617 502.66 + 0.52 + 5 999 985.83 + ....
И вот мы оказались где-то очень высоко на графике функции ошибки (то есть дали числовую оценку качеству текущей модели). И теперь нам нужно эту ошибку уменьшить, выбрав новые значения параметров. И вот именно тут от получившейся функции и берутся частные производные по каждому из параметров.

Тут важно понимать, что есть функция y от переменной x (y = f(x, k₁, k₂, k₃, k₄, k₅)), которую мы пытаемся получить, подбирая kᵢ — это наша нейросеть. А есть заранее известная функция ошибки от переменных kᵢ (Loss function, например MSE = (1/n)×Σ(y_true - y_pred)², которую мы и использовали выше). Вот именно от неё мы и считаем производные по каждому из kᵢ.

И вот мы производные нашли, допустим — по каждому параметру — получился какой-то вектор. И именно по этому вектору мы и меняем параметры.

Важный здесь ещё момент — шаг спуска. Чем, он больше, тем быстрее мы спускаемся в долину, то есть быстрее находим параметры и обучаем модель. Но при слишком большом шаге можно проскочить минимум — как бы перепрыгнуть ну другую сторону холма.

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


Так, а что это всё значит применительно к нашей реальной модели типа GPT-3 ?
Вот всё то же самое на самом деле, только функция ошибки определяется в 175млрд-мерном пространстве, то есть где 175 млрд осей, а не 3. То есть вся сложность здесь только в том, как такие гигантские числа считать, но сами операции такие же простые, как и когда у нас было два коэффициента в линейной регрессии.

Вот такая последовательность действий происходит:

  1. Модель сделала прямой проход предсказала вероятности появления каждого слова из словаря (50 257 для GPT3) в качестве следующего. То есть это вектор вероятностей с размерностью словаря, сумма чисел в котором даёт 1.
    При этом модель знает, какое именно слово для этого примера ожидается (ведь мы специально нарезали и подготовили тренировочные данные). И она готовит вектор такой же размерности, где будут все 0 и одна 1 на позиции, соответствующей ожидаемому слову — он называется one-hot.
    И далее считается разница между предсказанным вектором и реальным.

    В деталях функция ошибки

    Выше мы использовали Mean Squared Error (MSE) для вычисления ошибки, потому что мы решали задачу регрессии — предсказание числа. И она отлично подходит для непрерывных данных: количество осадков, цена квадратного метра, загруженность дорог итд. В трансформерах мы решаем задачу классификации: какое следующее слово из списка нужно выбрать. И тут MSE справляется плохо, а вместо него используется Cross-Entropy Loss.

    One-hot вектор поэлементно умножается на логарифм вектора предсказания, все его значения суммируются и получается одно число.

    L = -Σ(y_i × log(ŷ_i))
    Где
    - y_i — истинная метка (0 или 1) для класса i (One-hot)
    - ŷ_i — предсказанная вероятность класса i
    - Σ — сумма по всем классам (токенам) от i=1 до C
    

    Но, поскольку one-hot — это вектор с одной 1, а все остальные нули, формула упрощается и выглядит так:

    L = -log(ŷ_correct)
    Где 
    - ŷ_correct —  вероятность правильного класса.
    

    А поскольку мы считаем не на одном примере, а на батче, то ошибку мы усредняем по всем примерам. Так:

    L_batch = -(1/N) × Σ(log(ŷ_j_correct))
    

    Эта разница — и есть ошибка. Она говорит, где мы находимся. Если вернуться к примеру с горой, то можно сказать, что ошибка говорит, на какой высоте.
    Чем выше забрались, тем хуже. И нам надо начать спускаться вниз, в долину. В долину наименьшей ошибки.

  2. Чтобы понять в каком направлении и какой длины шаг делать, нам как раз нужны корректирующие матрицы (градиенты), которые будут уменьшать ошибку.
    Если ошибка говорит, на какой высоте мы находимся, то градиент — в каком направлении двигаться, чтобы спуститься.
    Градиент — это тензор той же формы, что и параметры, содержащий для каждого параметра данного слоя число, на которое его нужно изменить.

  3. Градиенты считаются последовательно через всю модель слой за слоем. Это обратный проход или backpropagation.

  4. Последним шагом изменения применяются — все веса модели обновляются.

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

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

Важно, что как параметры сохраняются только веса матриц и векторов модели: FFN, нормализации,смещения, матрицы внимания Wq, Wk, Wv.
НЕ сохраняются векторы активации слоёв, Q, K, V, потому что они уникальны для каждого примера.


Обучающие данные

Обучающих примеров должно быть достаточно много. Например, современные LLM видели практически всю оцифрованную и публично доступную текстовую информацию — это сотни миллиардов токенов.

Обычно обрабатывается не один пример и не датасет целиком, а батч — это определённый кусок обучающих данных.
Обрабатывать по одному примеру, во-первых супер-долго, во-вторых градиенты получаются очень шумными и модель обучается либо слишком медленно, либо вообще некорректно.
Целиком датасет обрабатывать очень дорого, и, скорее, даже невозможно — невозможно все обучающие данные разом засунуть в память кластера.

Иными словами обновление весов модели происходит один раз для всего батча по усреднённому градиенту по всем примерам.
Размер батча может выбираться просто по длине токенов. При обучении GPT-3 , например, размер батча был 3.2 млн токенов. Чем больше батч — тем стабильнее градиент. Стабильнее — значит его не штормит от каж��ого нового примера то в жар холодных числ, то в дар божественных видений.

В случае LLM в среднем каждый обучающий пример только один раз проходит через модель. Но качественные данные могут прогоняться и несколько раз.
При этом на этапе Fine-tuning'а (дообучения) какие-то примеры из дополнительных датасетов могут прогоняться и несколько раз.


Размеры и качество модели, MoE

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

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

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

Одним из методов разреженной активации является MoEMixture of Experts. Внутри нейросети появляются группы нейронов, специализирующиеся на определённой тематике — эксперты.
Это могло бы быть так:

  • программирование,

  • наука,

  • художественная литература,

  • история,

  • рецепты

  • итд.

Перед экспертами ставится роутер, определяющий, какой из них лучше обработает запрос.

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

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

Повторим, что специализация экспертов неможет не быть понятна человеку — не он её определяет. Она не обязательно основана на семантических темах. Иногда эксперты специализируются на позициях в последовательности. Или на частотности токенов (редкие vs частые слова)

Как это реализуется технически?
На самом деле почти всё то же самое, что для обычной модели с двумя отличиями:

  1. Вместо одной пары матриц FFN в каждом блоке появляется несколько пар матриц. Это и есть эксперты.

  2. Добавляется роутер, который выбирает путь в нужного эксперта.

MoEм MoEм трубочиста! Чисто-чисто. Чисто-чисто
MoEм MoEм трубочиста! Чисто-чисто. Чисто-чисто

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

Эксперты могут быть не в каждом блоке, а только в некоторых. Блоки без экспертов — плотные, с ними — разреженные. Как их располагать, определяется архитектурой модели.
Количество экспертов — это гиперпараметр модели.


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

Простая арифметика

Итого, для GPT-3 у нас получается:

  • 96 слоёв трансформеров + embedding + output

  • ~175 млрд параметров

  • Параметры занимают 350ГБ памяти (2 байта на каждый (FP16))

То есть только сама обученная модель весит 350 ГБ.
На инференс потребуется намного больше, потому что нужно хранить промежуточные данные: векторы Q, K, V (или KV-кэш), векторы активации, временные расчёты при умножении матриц итд

Для обучения же нужно ещё несколько терабайт памяти.
Это сам батч обучающих данных (сотни гигабайт), промежуточные активации слоёв (сотни гигабайт), градиенты (350 ГБ по числу параметров), даже само обновление параметров требует дополнительно 700 ГБ, а ещё есть Master weights в те же 700 ГБ. Итд.

Теперь прикинем на современном железе.
Посчитаем: модель NVidia H100 содержит 80ГБ памяти. То есть одна карта не может вместить даже просто все параметры модели.
Нужно как минимум 5 карт, чтобы просто вместить модель GPT-3 со всеми параметрами. По факту больше.

А для её обучения нужно порядка 3-6 ТБ памяти, а следовательно минимум 40 карточек H100.
Но это минимум, просто чтобы вместить все числа. Для эффективного обучения нужно в несколько раз больше — тысячи карт, чтобы сошлось за месяцы.
Для понимания масштаба. Одна итерация обучения (прямой, обратный проход и обновление параметров) может длиться пару минут. Вот и считаем 100 000 примеров по 2 минуты — это 4.6 месяцев.

Для сравнения GPT-4 по слухам содержит 1.8 триллиона параметров, что потребует уже больше несколько десятков тысяч карточек.

Даже новейшая NVidia B300 — это 288ГБ памяти на борту GPU.

Поэтому единственный выход для больших моделей, как LLM, так и почти любых других — это параллелизация.


Параллелизация

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

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

Всё интересное, вроде LLM или крупных свёрточных сетей для создания изображений, требует сотен и тысяч GPU-карт при обучении.

И что с этим делать?
Глобально подхода два:

  • разложить саму модель на несколько разных GPU

  • распараллелить тренировочные данные: разные экземпляры модели учатся на разных данных.

Обычно они комбинируются.

Для проектирования сети и кластера, то как параллелится обучение — ключевой момент — будут ли купленные на последние деньги карточки отрабатывать 100% вложений или простаивать большую часть времени из-за медленной сети или памяти.

Давайте разбираться.

Виды параллелизации

  • Data Parallelism — нарезаем пачку примеров на пачки поменьше.

  • Sequence Parallelism — нарезаем один длинный пример на несколько кусочков покороче.

  • Model Parallelism — нарезаем модель.


Data Parallelism (DP)

Нарезаем пачку примеров на пачки поменьше.

Илия данных на батчи данных.

Например, у нас есть 10 GPU и 500 обучающих примеров. Делим на 10 групп по 50 примеров и раздаём их на наши 10 карточек.

DataParallelism
DataParallelism

То есть по сути — мы просто одновременно запускаем несколько копий модели на разных GPU или группах GPU и отправляем в них разные обучающие данные.

И у каждой в результате будут свои веса в линейных матрицах и векторах смещения.
А н��м нужно получить не 10 моделей, обученных каждая на своих примерах, а одну, обученную на всех.

Для этого используется механизм AllReduce.
Разберём, как это работает.

  1. В самом начале создаётся 10 копий модели с одинаковыми весами и гиперпараметрами.

  2. Каждая копия сама делает прямой проход, предсказание, вычисляет ошибку и градиенты.

  3. Копии с помощью AllReduce обмениваются своими градиентами так, что в итоге все 10 копий знают все 10 градиентов.

  4. Каждая копия самостоятельно вычисляет среднее значение градиента — на всех одинаковое.

  5. И этим усреднённым градиентом уже обновляет веса матриц — веса на всех копиях получаются одинаковые.

Таким образом на каждом цикле обучения матрицы всех копий имеют одинаковые значения.

AllReduce собирает матрицы одинаковой размерности со всех копий (All) и делает из них одну (Reduce) засчёт усреднения значений.

На самом деле в некоторых случаях (BatchNorm) нужно синхронизировать ещё и статистики нормализации на прямом проходе, чтобы сквозь всю итерацию обучения на батче были согласованные значения для нормализации. А также синхронизировать в этих случаях нужно и градиенты для поправки компонентов нормализации тем же AllReduce.

Это вид параллелизации имеет среднюю чувствительность к задержкам и полосе. Рекомендуется локализовать его в пределах стойки.


Sequence Parallelism (SP)

Нарезаем один длинный пример на несколько кусочков покороче и раздаём на разные GPU.

Что может быть примером таких обучающих данных, которые не помещаются в одну карточку, но должны быть обработаны именно вместе?

  • Геномные последовательности

  • Климатические данные

  • Научные статьи с полным контекстом литературы

У нас есть 10 GPU с копиями модели и 1 длинный обучающий пример на 1 000 000 токенов. Делим на 10 последовательностей по 100 000 токенов и раздаём их на наши 10 карточек.

SequenceParallelism
SequenceParallelism

Как и в DP мы разбиваем обучающие данные на отдельные части.
Но в чём отличие?
Когда модель обрабатывает переданный текст, в нём каждое слово «видит» все другие слова.
Так, во фразе «Ох, у ямы холм с кулями» слово «у» видит все другие: «ямы», «холм», «с», «кулями, и даже запятую. Слово «ямы» аналогично. И так далее. Это позволяет составлять общий смысл фразы.

Но, когда мы разделили фразу на две карты:

  • «Ох, у ямы»

  • «холм с кулями»

«Ямы» не может увидеть слово «холм». А это критически важно для понимания смысла.

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

То есть для SP нужно синхронизировать:

  • градиенты один раз на батч

  • промежуточные активации

  • при инференсе нужно синхронизировать ещё и KV-кэш (при обучении делать этого не нужно)

В деталях как происходит синхронизации

Синхронизация вектора активации

Напомню: это результат работы слоя или блока, который передаётся в следующий слой/блок. Или другими словами — это то, во что превращается изначальный вектор эмбеддинга, пройдя через все предыдущие слои.

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

То есть при наличии 4 GPU. У первой карточки есть информация для 1, 2, 3 и 4, и у 2 — для 1, 2, 3, 4 (и для других, и для себя). А в конце синхронизации у неё должны быть все кусочки, предназначенные ей от 2, 3 и 4. На второй — все её кусочки итд.

Реализует это механизм AlltoAll, позволяющий GPU обменяться нужными данными.
Каждая GPU отправляет другим только те части матрицы, которые им нужны.
Адресно: GPU0 на GPU1 только кусочек для GPU1, GPU0 на GPU2 только кусочек для GPU2. Итд.

AlltoAll
Такую аналогию можно провести с реальным миром: трое друзей вернулись из путешествий и привезли друг для друга подарки (и про себя не забыли):

Вася: [Для Васи, Для Пети, Для Маши]
Петя: [Для Васи, Для Пети, Для Маши]
Маша: [Для Васи, Для Пети, Для Маши]

В процессе AlltoAll Вася оставляет свой подарок себе, подарок для Пети передаёт Пете, а для Маши — Маше. Петя дарит Васе и Маша по отдельности, а Маша — Васе и Пете.
Все обменялись.

Получается так:

Вася: [Для Васи, Для Васи, Для Васи]
Петя: [Для Пети, Для Пети, Для Пети]
Маша: [Для Маши, Для Маши, Для Маши]

Синхронизация градиентов

Это мы уже проходили — передача градиентов по всей сети и их усреднение. Используется AllReduce.

Синхронизация KV-кэшей

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

При инференсе синхронизация KV-кэшей кусочки кэша на каждом шаге с каждого GPU передаются другим GPU, где они конкатенируются в целевую матрицу.
Для этого используется механизм AllGather.
Он похож на AllReduce, но в результате получается не матрица усреднённых значений, а большая матрица, составленная из многих маленьких (Gather).
Однако именно из-за того, что их нужно многократно синхронизировать на каждом проходе, SP редко используется при инференсе.

SP рекомендуется локализовать как можно ближе — оптимально в пределах сервера, но можно и в стойке при наличии NVLink или InfiniBand


Model: Pipeline Parallelism (PP)

Нарезаем модель по слоям или блокам

Это довольно простой вид параллелизма. Применяется, когда модель целиком не помещается в память одной GPU-карты.
Модель нарезается по слоям (блокам), и каждая карточка занимается обработкой примера на своих слоях.
Например:

  • GPU0 вычисляет 1-32 слои

  • GPU1 — 33-64

  • GPU2 — 65-96

PipelineParallelism
PipelineParallelism

При прямом проходе после того, как GPU0 отработала, она отправляет свою матрицу активации на GPU1, а та, отработав, — на GPU2.
При обратном проходе аналогично распространяются градиенты.

Никакой синхронизации не нужно.

Это как конвейер на заводе: каждый участок отвечает за свою операцию. Закончил — передал дальше, взял новое.

Для того, чтобы, пока работает одна GPU, две другие не простаивали, список примеров делится на так называемые микробатчи. И получается как раз конвейер — пока один пример обрабатывается на GPU1, в GPU0 подаётся следующий. GPU1 закончила, передаёт первый пример на GPU2, а в себя принимает второй пример. И так далее по конвейеру.

PP наименее чувствителен к полосе и задержкам и может локализоваться как угодно.


Model: Tensor Parallelism

нарезаем модель по столбцам/строкам матриц.

Здесь в модели нарезаются матрицы — по столбцам или строкам. То есть один слой распределяется по нескольким GPU — у каждой своя часть весов.

Простой аналог — это таблица умножения чисел 1000х1000. Первые столбцов 250 мы отдаём на GPU0, следующие 250 — на GPU1, далее на GPU3 и 4 — и каждая считает свою пачку. А потом сводим всё обратно в одну таблицу.

В случае GPT-3 в блоке FFN, например, нам нужно умножить вектор X на матрицу W. Её размерность [12288×49152]. Мы можем распараллелить её так:

GPU0 хранит столбцы 0-12287
GPU1 хранит столбцы 12288-24575
GPU2 хранит столбцы 24576-36863
GPU3 хранит столбцы 36864-49151

Каждая GPU умножает свою часть, а потом результаты складываются обратно в таблицу.
Распараллелили!

TensorParallelism
TensorParallelism

При обучении в этом случае нужно обмениваться дополнительно матрицами внимания:

  • матрицами внимания

  • векторами активации

  • градиентами

Блок трансформера:
│
├─ LayerNorm (локально)
├─ Multi-Head Attention
│  ├─ QKV projection (column-parallel) ❌
│  ├─ Attention compute (локально) ❌
│  └─ Output projection (row-parallel) ? All-Reduce #1
│
├─ Residual + LayerNorm (локально)
├─ FFN
│  ├─ Up-projection (column-parallel) ❌
│  ├─ GeLU (локально) ❌
│  └─ Down-projection (row-parallel) ? All-Reduce #2
│
└─ Residual connections (локально)

Forward: 2 All-Reduce
Backward: 2 All-Reduce
Итого: 4 All-Reduce на слой

Как именно это происходит, мы уже видели выше.

TP сверхчувствителен к полосе и задержкам, поэтому локализуется строго в пределах сервера.


Model: Expert Parallelism

нарезаем модель по экспертам в MoE

В случае больших моделей мы можем распараллелить экспертов — то есть обработку через матрицы FFN: GPU0 за физику и математику, GPU1 за программирование и БД, песни и пляски — на GPU3 итд.

ExpertParallelism
ExpertParallelism

Поскольку и входные токены, и эксперты могут быть распределены по разным устройствам, нужно, чтобы сначала одни GPU распределили все кусочки по всем целевым GPU, а потом последние собрали всё обратно для нормализации и активации — и там, и там AlltoAll.

Требования по полосе и задержкам средние. Локализовать чем ближе, тем лучше, но в пределах стойки приемлемо, с оговорками — и в пределах ДЦ.


Подведём итог способам параллелизации

Во-первых, мы, распараллеливая, преследуем две цели:

  1. Распределить модель, не помещающуюся на одну GPU-карточку

  2. Ускорить обучение и инференс

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

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


Методы синхронизации

Исторически в вычислительных кластерах специальные центральные узлы (мастера) помогали вычислять и распределять данные по другим узлам. Примером может служить Apache Hadoop.

Но у этого очевидного подхода очевидные же минусы:

  • Узкое место
    Представьте себе, что все узлы шлют свои градиенты на один? А потом он рассылает их обратно.

  • Плохо масштабируется
    По той же причине.

  • Единая точка отказа
    Вылетает мастер-узел — встаёт весь кластер.

  • Увеличивающиеся задержки
    Все узлы кластера должны отправить данные на центральный и получить ответ. Обработка на нём происходит последовательно, а не параллельно.
    Кроме того образуется очередь на мастер-узле, потому что его ресурсы (CPU, память, сеть) ограничены.
    Итого, общая эффективность кластера падает.

Все современные кластера для HPC, AI/ML используют коллективные операции, когда все вычисления происходят распределённо, то есть каждый узел передаёт в сеть то, что нужно другим, и сам считает то, что нужно ему.

Выше мы встречали разные механизмы, обеспечивающие вычисления параметров, которые нужно синхронизировать:

Теперь детальнее посмотрим, что делает каждая из них и вспомним, где применяется.


AllReduce

Собирает тензоры со всех GPU, применяет операцию редукции (например, сумму или усреднение) и распределяет результат на все GPU
Reduce она называется, потому что из большого количества матриц одинакового размера она делает одну — редуцирует их количество до одной, применяя операцию (сумма, среднее итд).

Пример использования — градиенты для обновления параметров при Data Parallelism.

Иллюстрации коллективных операций здесь и далее из https://textbook.cs168.io/beyond-client-server/collective-operations.html
Иллюстрации коллективных операций здесь и далее из https://textbook.cs168.io/beyond-client-server/collective-operations.html

AllGather

Собирает тензоры со всех GPU и составляет из них один мега-тензор, то есть конкатенирует много маленьких в один большой.
В результате на каждой GPU не один вычисленный ею кусочек тензора, а собранный со всех GPU и одинаковый на всех.

Пример использования — матрицы внимания при Tensor Parallelism.

allgather
allgather

AlltoAll

Все всем. Каждая GPU вычислила несколько кусочков тензора, и каждый кусочек предназначен для конкретной GPU в кластере. И теперь эти кусочки распределяются между карточками — каждая собирает свой паззл.
GPU 0 имеет данные для 0, 1, 2, 3. GPU1 — для 0, 1, 2, 3 итд

В результате на каждой GPU собираются все её кусочки, необходимые ей для дальнейшей работы.
На GPU0 всё, что преднзначено именно для неё, на GPU1 — для неё итд.

Пример использования — промежуточные активации при Tensor Parallelism.


Reduce-Scatter

Похоже на AllReduce — данные собираются вместе, редуцируются (например, усредняются), но потом на каждой GPU остаётся только та часть тензора, которая нужна именно ей.
То есть сначала делаем Reduce (агрегируем данные), потом Scatter (распределяем фрагменты результата).

Пример использования — градиенты при Data Parallelism — на каждой GPU только свой кусочек матрицы весов (и активации, и нормализации) и хранить огромный градиент целиком, съедая память, нет необходимости.

Reduce-Scatter
Reduce-Scatter

Тут, если вернуться к AllReduce, становится понятно, что это ReduceScatter + AllGather:

AllReduce
AllReduce

Broadcast

Это единственная операция, предполагающая наличие root-узла. И этот узел может не участвовать в вычислениях — он централизованно распределяет общие для всего кластера вещи, например, гиперпараметры, матрицу эмбеддингов, изначальную матрицу весов.
А также он шлёт управляющие сигналы, такие, например, как «остановить обучение».

Например, Broadcast используется при инициализации кластера, когда все GPU должны оказаться в согласованном состоянии. Но не только.

broadcast
broadcast

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

Для того, чтобы это всё заработало, нужно знать как устроена сеть, а поверх неё создать виртуальную топологию, и поверх этой топологии уже составить программу обмена данными, понимать как работать с памятью GPU, как управлять CUDA.
Непросто?
Непросто!

Будь я админом кластера или модельки, я не хотел бы вручную писать все эти схемы обмена, честно говоря (я же не олимпиадник).

Поэтому человечество придумало коллективные библиотеки.


Коллективные библиотеки

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

Три наиболее распространённые Open Source библиотеки:

  • NCCL — Nvidia Collective Communication Library

  • MPI — Message Passing Interface

  • Gloo — библиотека от Meta

Что делают эти движки?

  • Отвечают за работу с CUDA

  • Управляют памятью GPU

  • Реализуют низкоуровневое взаимодействие с сетевыми картами и сетью. Они знают, что такое NVLink, RDMA, InfiniBand.

  • Изучают топологию сети (Fat Tree, DragonFly, HyperCube, Torus итд)

  • Составляют нужную виртуальную топологию для обмена данными, оптимизированную под реальную.

CUDA ты меня послал?!

CUDACompute Unified Device Architecture — SDK от NVidia к GPU, позволяющий использовать их не только для вывода изображения, но и для того, чтобы просто заказать вычисления.
То есть CPU через CUDA ставит задачу на GPU. GPU сверхбыстро перемножает матрицы, а общий контроль над задачами остаётся на CPU.

Виртуальные топологии

Физически у нас может быть Clos, DragonFly, Flattened Butterfly, Torus, что угодно, но поверх неё коллективные библиотеки умеют собирать виртуальные топологии. Они же схемы виртуальных соединений между карточками.

Мы не будем вдаваться в детали их устройства и места применения, но рассмотреть хотя бы коротко, придётся.

Итак, топологий не очень-то и много:

  • Ring

  • Tree

  • Bruck’s Algorithm

  • Recursive Doubling / Halving

  • PAT (Pipeline AlltoAll Topology)

В деталях давайте на примере Ring посмотрим, как оно выглядит

Итак, у нас может быть физическая топология Dragonfly.
К каждому Leaf подключено по 2 сервера с 8 GPU-картами, у каждой из которых свой интерфейс в сеть.

Реальная топология Dragonfly
Реальная топология Dragonfly

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

Виртуальная топология Ring
Виртуальная топология Ring

А может собрать из них дерево.

Виртуальная топология Tree
Виртуальная топология Tree

Коллективная библиотека исследует сеть, выясняет топологию, технологии (NVLink, InfiniBand, RoCE), измеряет задержки, сверяется с тем, какие данные и какого объёма нужно передавать и какую использовать операцию (AllReduce итд) и на основе этого выбирает схему соединений.

Тут важно понимать, что она динамически выбирает схему соединений на основе того, какую операцию над какими данными сейчас нужно выполнить.

Например для AllReduce она может выбрать топологию Ring.
Возьмём за пример синхронизацию градиентов.

В деталях AllReduce как пример коллективной операции

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

Дальше это похоже на шаговый механизм:
Щелчок, колесо чуть-чуть прокрутилось: каждый узел передаёт своему соседу один кусочек. Все суммируют полученные кусочки с тем, что есть у них.
Щелчок, колесо чуть-чуть прокрутилось: каждый узел передаёт своему соседу другой кусочек. Все суммируют полученные кусочки с тем, что есть у них
Щелчок..

Так числа «путешествуют» по кольцу, накапливая суммы, пока не попадут на worker, расположенный на одну позицию раньше того, откуда началась!

Но давайте ещё детальнее взглянем: ведь в действительности всё сложнее, чем на самом деле.

На самом деле AllReduce состоит из двух фаз:

  • SCATTER-REDUCE Сначала данные нарезаются на N кусочков (по числу узлов), далее передаются по кольцу и суммируются. После N-1 итераций на каждом узле окажется свой полностью просуммированный кусочек.

  • AllGather Затем эти просуммированные кусочки распространяются по всем узлам кольца.

Формально процесс выглядит так:
Фаза SCATTER-REDUCE
Формулы для каждого узла i:

  • Отправляет chunk в позиции: (i - k + 1) mod N

  • Получает от worker: (i - 1) mod N

  • Добавляет к своей позиции: (i - k) mod N

Фаза AllGather

  • Каждый узел i отправляет кусочек данных на узел (i+1) mod N

  • Каждый узел i получает от узла (i-1) mod Nи записывает к себе

  • Без сложения! Просто копируем полученное значение.

Сложно? Сложно!

Давайте рассмотрим на примере простой топологии с 4 GPU. То есть N=4
Соединены они могут быть так

Реальная топология
Реальная топология

Библиотека собирает их в кольцо

Виртуальная топология кольцо
Виртуальная топология кольцо

После этапа обучения на каждом из узлов есть вычисленный градиент для обновления весов. Сейчас этот градиент свой на каждом узле.
На узле А: a
На узле B: b
На узле C: c
На узле D: d

Нам нужно, чтобы градиент был одинаковый на всех.
Простой наивный подход: каждый отправляет каждому его кусочек. Но это супер-неоптимально, поэтому делают так.

Каждый узел делит свой градиент на N кусочков (SCATTER)

A: [a₀, a₁, a₂, a₃]
B: [b₀, b₁, b₂, b₃]
C: [c₀, c₁, c₂, c₃]
D: [d₀, d₁, d₂, d₃]

В результате мы хотим получать:

A: a+b+c+d
B: a+b+c+d
C: a+b+c+d
D: a+b+c+d

Или:

A: [a₀+b₀+c₀+d₀, a₁+b₁+c₁+d₁, a₂+b₂+c₂+d₂, a₃+b₃+c₃+d₃]
B: [a₀+b₀+c₀+d₀, a₁+b₁+c₁+d₁, a₂+b₂+c₂+d₂, a₃+b₃+c₃+d₃]
C: [a₀+b₀+c₀+d₀, a₁+b₁+c₁+d₁, a₂+b₂+c₂+d₂, a₃+b₃+c₃+d₃]
D: [a₀+b₀+c₀+d₀, a₁+b₁+c₁+d₁, a₂+b₂+c₂+d₂, a₃+b₃+c₃+d₃]

Начинаем REDUCE. Для этого потребуется N-1 итераций. В нашем случае 3.

Фаза SCATTER-REDUCE

Шаг 1
На шаге k=1, согласно алгоритму выше:
Узел i отправляет chunk в позиции: (i - 1 + 1) mod 4 или i mod 4
Добавляет к своей позиции то, что получил сам: (i - 1) mod 4

  • A отправляет на B a₀
    А получает от D d₃, суммирует его с a₃ и сохраняет на позицию 3

    A: [a₀, a₁, a₂, a₃+d₃]
    
  • B отправляет на C b₁
    B получает от A a₀, суммирует его с b₀ и сохраняет на позицию 0

    B: [b₀+a₀, b₁, b₂, b₃]
    
  • C отправляет на D c₂
    C получает от B b₁, суммирует его с c₁ и сохраняет на позицию 1

    C: [c₀, c₁+b₁, c₂, c₃]
    
  • D отправляет на A d₃
    D получает от C c₂, суммирует его с d₂ и сохраняет на позицию 2

    D: [d₀, d₁, d₂+c₂, d₃]
    

И получаем

A: [a₀, a₁, a₂, a₃+d₃] 
B: [b₀+a₀, b₁, b₂, b₃ ]
C: [c₀, c₁+b₁, c₂, c₃ ] 
D: [d₀, d₁, d₂+c₂, d₃ ]

Шаг 2

Согласно алгоритму выше при k=2:

Узел i отправляет: (i - 2 + 1) mod 4 = (i - 1) mod 4
Добавляет к своей позиции то, что получил: (i - 2) mod 4

То есть:

  • A отправляет на B позицию 3a₃+d₃ ((0-2+1) mod 4 = 3)
    А получает от D d₂+c₂, суммирует его в позицию 2 ((0-2) mod 4) и получаем a₂ + (d₂+c₂)

    A: [a₀, a₁, a₂ + (d₂+c₂), a₃+d₃]
    
  • B отправляет на C позицию 0a₀+b₀ ((1-2+1) mod 4)
    B получает от A a₃+d₃, суммирует его в позицию 3 ((1-2) mod 4) и получаем b₃ + (a₃+d₃)

    B: [b₀+a₀, b₁, b₂, b₃ + (a₃+d₃)]
    
  • С отправляет на D позицию 1с1+b₁ ((2-2+1) mod 4)
    C получает от B a₀+b₀, суммирует его в позицию 0 ((2-2) mod 4) и получаем с0 + (a₀+b₀)

    C: [c₀ + (a₀+b₀), c₁+b₁, c₂, c₃]
    
  • D отправляет на A позицию 2d₂+c₂ ((3-2+1) mod 4)
    D получает от C c₁+b₁, суммирует его в позицию 1 ((3-2) mod 4) и получаем d₁ + (c₁+b₁)

    D: [d₀, d₁+(c₁+b₁), d₂ +c₂, d₃]
    

И получаем:

A: [a₀, a₁, a₂ + (d₂+c₂), a₃+d₃]
B: [b₀+a₀, b₁, b₂, b₃ + (a₃+d₃)]
C: [c₀ + (a₀+b₀), c₁+b₁, c₂, c₃]
D: [d₀, d₁+(c₁+b₁), d₂ +c₂, d₃]

Шаг 3

Согласно алгоритму выше при k=3:

Узел i отправляет (i - 3 + 1) mod 4 = (i - 2) mod 4
Добавляет к своей позиции то, что получил сам: (i - 3) mod 4

То есть:

  • A отправляет на B позицию 2a₂+d₂+с2 ((0-2) mod 4)
    А получает от D d₁+c₁+b₁, суммирует его в позицию 1 ((0-3) mod 4) и получаем a₁ + (d₁+c₁+b₁)

    A: [a₀, a₁+(d₁+c₁+b₁), a₂ + (d₂+c₂), a₃+d₃]
    
  • B отправляет на C позицию 3b₃+a₃+d₃ ((1-2) mod 4)
    B получает от A a₂+d₂+c₂, суммирует его в позицию 2 ((1-3) mod 4) и получаем b₂ + (a₂+d₂+c₂)

    B: [b₀+a₀, b₁, b₂ + (a₂+d₂+c₂), b₃ + (a₃+d₃)]
    
  • С отправляет на D позицию 0с0+a₀+b₀ ((2-2) mod 4)
    C получает от B b₃+a₃+d₃, суммирует его в позицию 3 ((2-3) mod 4) и получаем с3 + (b₃+a₃+d₃)

    C: [c₀ + (a₀+b₀), c₁+b₁, c₂, c₃ + (b₃+a₃+d₃)]
    
  • D отправляет на A позицию 1d₁+c₁+b₁ ((3-2) mod 4)
    D получает от C c₀+a₀+b₀, суммирует его в позицию 0 ((3-3) mod 4) и получаем d₀ + (c₀+a₀+b₀)

    D: [d₀ + (c₀+a₀+b₀), d₁+(c₁+b₁), d₂ +c₂, d₃]
    

И получаем:

A: [a₀, a₁+d₁+c₁+b₁, a₂+d₂+c₂, a₃+d₃]
B: [b₀+a₀, b₁, b₂+a₂+d₂+c₂, b₃+a₃+d₃]
C: [c₀+a₀+b₀, c₁+b₁, c₂, c₃+b₃+a₃+d₃]
D: [d₀+c₀+a₀+b₀, d₁+c₁+b₁, d₂ +c₂, d₃]

На каждом узле у нас получилась полная сумма всех компонентов одной из четырёх частей. Причём, заметьте, не именно его частей, а просто каких-то. Так A (нулевой узел) собрал всю первую часть, а D (третий) — всю нулевую

А нам нужно прийти к картине, в которой на всех узлах одна и та же матрица.
Поэтому мы переходим к стадии AllGather.

Фаза AllGather

Он так же будет состоять из N-1 итераций. В нашем примере 3.

Каждый узел i отправляет кусочек узлу (i+1) mod N
Каждый узел i получает кусочек от узла (i-1) mod N
Без сложения! Просто копируем полученное значение

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

Шаг 1
A отправляет в B позицию 1a₁+d₁+c₁+b₁.
B в C позицию 2b₂+a₂+d₂+c₂
C в D 3c₃+b₃+a₃+d₃
D в A 0d₀+c₀+a₀+b₀

И получаем:

A: [d₀+c₀+a₀+b₀, a₁+d₁+c₁+b₁, a₂+d₂+c₂, a₃+d₃]
B: [b₀+a₀, a₁+d₁+c₁+b₁, b₂+a₂+d₂+c₂, b₃+a₃+d₃]
C: [c₀+a₀+b₀, c₁+b₁, b₂+a₂+d₂+c₂, c₃+b₃+a₃+d₃]
D: [d₀+c₀+a₀+b₀, d₁+c₁+b₁, d₂ +c₂, c₃+b₃+a₃+d₃]

Шаг 2
Каждый узел отправляет последнюю сумму, что получил.
A в B позицию 0
B в C1
C в D2
D в A3

И получаем:

A: [d₀+c₀+a₀+b₀, a₁+d₁+c₁+b₁, a₂+d₂+c₂, c₃+b₃+a₃+d₃]
B: [d₀+c₀+a₀+b₀, a₁+d₁+c₁+b₁, b₂+a₂+d₂+c₂, b₃+a₃+d₃]
C: [c₀+a₀+b₀, a₁+d₁+c₁+b₁, b₂+a₂+d₂+c₂, c₃+b₃+a₃+d₃]
D: [d₀+c₀+a₀+b₀, d₁+c₁+b₁, b₂+a₂+d₂+c₂, c₃+b₃+a₃+d₃]

Шаг3
Осталась последняя итерация.

A в B позицию 3
B в C0
C в D1
D в A2

И получаем:

A: [d₀+c₀+a₀+b₀, a₁+d₁+c₁+b₁, b₂+a₂+d₂+c₂, c₃+b₃+a₃+d₃]
B: [d₀+c₀+a₀+b₀, a₁+d₁+c₁+b₁, b₂+a₂+d₂+c₂, c₃+b₃+a₃+d₃]
C: [d₀+c₀+a₀+b₀, a₁+d₁+c₁+b₁, b₂+a₂+d₂+c₂, c₃+b₃+a₃+d₃]
D: [d₀+c₀+a₀+b₀, a₁+d₁+c₁+b₁, b₂+a₂+d₂+c₂, c₃+b₃+a₃+d₃]

Синхронизировано!

Почему так сложно?

Почему нельзя просто передать по кругу всю матрицу с каждого узла, суммируя значения по ходу?
Можно. Это будет называться Broadcast. Просто в этом случае нам нужно умножить размер матрицы M на число узлов N и на количество передач (N - 1) — итого M * N * (N - 1). Или O(MN²).
Многовато-то — зависит от квадрата числа узлов. Чем больше кластер, тем квадратично больше платим.

При этом Ring AllReduce даёт суммарный объём трафика в сети O(2MN).
Как так?
У нас всё тот же размер матрицы M, но каждый узел нарезает его на N частей и передаёт только его N-1 раз. И так дважды — на этапах SCATTER-REDUCE и на AllGather.
Получается 2 * M/N * (N - 1) для каждого узла. И умножаем для N узлов для суммарного объёма.
Так и получаем 2 * M * (N - 1).

На примерных цифрах:
Пусть объём памяти под параметры модели: 350GB, а Data Parallelism настроен на 64 узла.
Для Broadcast получаем 350GB * 64 * 63 = 1 411 200 GB
Для Ring: 2* 350 * 63 = 44 100.


«Подожди» — скажет любознательный читатель. Зачем тут Broadcast и Full-mesh, когда A может передать на B всю матрицу, B их просуммирует, передаст на C, тот их просуммирует, передаст дальше итд? И общий объём будет M * (N - 1).
Ну, во-первых, в лоб не сработает — на итерации N-1 полная сумма будет только на узле А, а как его дотащить до остальных узлов — нужно ещё придумать.
А, во-вторых, проблема в том, что это последовательные операции. Пока А передаёт, всё остальные ждут.
Если время передачи полной матрицы равно T, то для полной передачи потребуется как минимум N * T.
В случае Ring AllReduce нужно будет 2 * (T / N) * (N - 1). 2 прохода, где передаётся 1 / N часть матрицы N-1 раз. Получается около 2T, то есть константное время.
А время передачи.. Ух как важно при работе на дорогущих GPU.

Другие механизмы мы разбирать в этой статье не будем. Мне хотелось лишь показать, как работает параллелизация и к каким ухищрениями приходится прибегать, чтобы минимизировать расходы на сеть и время простоев.


NCCL, MPI и Gloo — это низкоуровневые библиотеки, которые позволяют разработчикам не задумываться о том, как им реализовать коллективные операции на сотнях и тысячах карточек.
Но работать с ними из того же, например, Питона, очень непросто — вам нужно реализовать все нюансы взаимодействия с библиотекой.
NCCL и товарищи ничего не знают о нейросетях, об их слоях, о градиентах, они не знают, как и когда запускать прямой и обратный проходы. Они работают напрямую с сырыми GPU-буферами.
NCCL поддерживает только NVIDIA GPU, а MPI и Gloo — кроссплатформенные.

Если вы готовы во всё это нырять, управлять памятью и писать тысячи строк сишного кода, то вам сюда или сюда, а если нет, то для вас придумали фреймворки.


Фреймворки

Это как HTTP-серверы. Вы можете из питона напрямую работать с nginx, а можете использовать flask/django. Второй способ явно удобнее.
Самый известный и достаточный в 90% случаях — это PyTorch. Он предоставляет очень простой интерфейс взаимодействия с NCCL, скрывая все сложности для реализации обучения моделей.
Для сравнения простая задача вычисления градиентов на пачке GPU занимает 5 строчек на PyTorch:

import torch
import torch.distributed as dist

dist.init_process_group("nccl", rank=0, world_size=2)
tensor = torch.tensor([1.0, 2.0, 3.0]).cuda()
dist.all_reduce(tensor, op=dist.ReduceOp.SUM)

При прямом использовании NCCL вам нужно написать под 100 строчек сишного кода, где вы вручную оперируете GPU-карточками и памятью на них, подгружаете данные в GPU, создаёте CUDA-stream, вычитываете данные из памяти GPU через указатели и возвращаете в CPU.
И это только один простой этап в обучении — синхронизация градиентов — это даже не обновление весов.


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

  • Одна модель не помещается на одну GPU

  • М��ллиарды обучающих примеров выгоднее считать одновременно на разных группах

  • Очень большие входные примеры могут также не помещаться в память одной карты

  • При инференсе нужен ответ в человеческом масштабе в несколько секунд

Поэтому это всегда гибридные схемы параллелизации, например, pipeline+tensor+data.
То есть модель разрезается по слоями, но даже так полностью не помещается в память, поэтому матрицы разделяется по строкам или столбцам и распределяется на группы карточек.
Таких экземпляров модели запускается несколько штук, а на них обрабатываются разные обучающие примеры.

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


Сетевые технологии в кластере

Этим параграфом мы забегаем вперёд в одну из следующих статей. Но именно здесь уместно сделать связку между абстрактными матрицами и реальным железом.

В кластерах ML и HPC есть 4 сегмента коммуникации с принципиально разными характеристиками.

  1. Внутри GPU. Канал чип ↔ HBM-память

  2. Внутри сервера. GPU ↔ NVLink ↔ GPU.

  3. Внутри стойки. Сервер(GPU) ↔ InfiniBand/RoCE ↔ Сервер(GPU)

  4. Между стойками. Сервер(GPU) ↔ InfiniBand/RoCE ↔ Сервер(GPU)

1. Внутри GPU

Современные GPU — это сложнейшие устройства, где на одном основании (интерпозере) располагаются сравнительно небольшой GPU и огромная батарея чипов памяти. Обычно это HBMHigh Bandwidth Memory.

Чип GPU или SoC
Чип GPU или SoC

Именно в этой памяти хранятся параметры модели, все промежуточные вычисления (активации, Q, K, V и прочее) и KV-кэш.

И вот первый сегмент сети — это между GPU и HBM.
Скорость 2-3 ТБ/с. Задержка ~100нс.

Технически, тут, как и в CPU, есть L1-кэш внутри самого GPU (200Тб/с на ядро) и L2-кэш (10Тб/с на карту) и только потом появляется объёмный и чуть более медленный HBM.

Немного почитать об их практическом применении можно тут.

2. Внутри сервера

В кластерах ML в каждом сервере по несколько GPU-карточек (часто — 8), а между ними используется специальная сверхбыстрая сеть.

Вообще это может быть и простой PCI-E, но сегодня чаще всего это NVLink.
NVLink — проприетарная технология Nvidia для интерконнекта между карточками в пределах одного сервера.
Поколение 4.0 — это 900Гб/с и задержки около 5мкс. А 5.0 — это уже 1.8 Тб/с.
Для полнодуплексного соединения используется специальный коммутатор — NVSwitch.

NVLink и NVSwitch
NVLink и NVSwitch
Карта GPU
Карта GPU

Открытый PCI-E 5.0 при этом даёт 128Гб/с и 10мкс, а PCI-E 6.0 — 256Гб/с и те же 10мкс. 1Тб/с фулл-дуплекса обещают в PCI-E 8.0, но это когда ещё!
И есть ещё его эволюционный последователь для когерентной памяти — Compute Express Link. Базируется на том же PCI-E, но засчёт новых протоколов шустрее и гибче.

Альтернативы не то чтобы прям существуют.
Это AMD Infinity Fabric для тех, кто готов строить свой кластер на AMD.
Это Intel Xe Link.
И нарождающийся где-то в сумрачных лабораториях мега-корпораций UALink.

3-4. Внутри стойки и между стойками

Исторически тут властвует InfiniBand, переживший уже полдюжины поколений.
Текущая актуальная версия — NDR (Next Data Rate) выдаёт 400Гб/с с карточки и задержки в единицы мкс. Также уже есть XDR (800Гб/с)

InfiniBand
InfiniBand
InfiniBand коммутатор
InfiniBand коммутатор

Но в силу ряда особенностей и проблем на сцену популярности заглядывает сейчас Ethernet, надев маску Loss-Less сети — RoCE — RDMA over Converged Ethernet.
Сегодня он даёт те же 400Гб/с с порта и задержки в единицы мкс. И также уже есть реализации 800Гб/с.

RoCE — RDMA over Converged Ethernet
RoCE — RDMA over Converged Ethernet

И тут тоже сумрачный гений рождает нового кадавра UltraEthernet, про которого нам ещё, возможно, придётся услышать

Про InfiniBand и RoCE будут отдельные статьи в цикле.
А пока можно послушать подкасты про InfiniBand и RoCE.


Короткий итог:

|-------------------------------------------------------------------|
| Технология | Пропускная способность | Задержка | Где используется |
|------------|------------------------|----------|------------------|
| HBM3/4     | 3 TB/s                 | ~100 ns  | GPU↔Memory       |
| NVLink 4.0 | 900 GB/s               | ~5 μs    | GPU↔GPU (узел)   |
| PCIe 5.0   | 128 GB/s               | ~1 μs    | GPU↔GPU (узел)   |
| IB NDR     | 400 Gb/s               | ~1 μs    | Узел↔Узел        |
| RoCE v2    | 400 Gb/s               | ~1-3 μs  | Узел↔Узел        |
|-------------------------------------------------------------------|

Схемы параллелизации

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

Пойдём от самых критичных ко времени и полосе к менее.

Тензорный параллелизм — это частые обмены небольшими объёмами данных, очень критичные к задержкам передачи.
Совершенно нельзя терять никаких данных — иначе всё на смарку.
Поэтому его нужно использовать строго внутри сервера, где NVLink обеспечивает минимальное время доставки.

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

Параллелизм данных — очень редкие обмены очень большими объёмами данных. По сути это однократная синхронизация градиентов на цикл обучения.
Здесь не важны задержки. Неплохо бы иметь широкую полосу. И потеря данных некритична — просто сожгли немного электричества зря, просчитывая потерянный батч.
Межстоечная сеть вполне подходит для этих целей.

Экспертный параллелизм — требует AllToAll коммуникаций, но не так критичен к задержкам.
Может быть в пределах стойки или между стойками.

Вот такая вот логичная и незамысловатая* схема:

  • TP — в пределах сервера

  • PP — в пределах стойки

  • DP, EP — между стойками

* на первый наивный взгляд. На второй может быть так, например: TP+DP внутри узла (без PP)


На этом заканчивается вводная часть про нейросети. Больше в таких деталях к этой теме мы возвращаться уже не будем. Поэтому сейчас — самое время рассказать короткую историю нейросетей.


Историческая справка

  • В 1676 году Лейбниц в письмах изложил основы математического анализа — дифференциалы/производные и интегралы. Спасибо дедушке Лейбницу!

  • В 1943 Маккаллок и Питтс предложили математическую модель нейрона — без практической реализации. Но какова идея!!

  • В 1949 нейропсихолог Хебб описал правило обучения нейронных связей: «Взаимодействующие клетки объединяются».
    Его работа стала мостом между нейробиологией и искусственным интеллектом, задав направление для развития самоорганизующихся и адаптивных моделей.

  • В 1958 появилась первая рабочая нейросеть — перцептрон Розенблатта. Он умел распознавать печатные символы на изображении.

    Перцептрон был однослойным и был реализован в железе на основе переменных резисторов.
    Технически, у него было два слоя: входной (принимает данные) и выходной (выдаёт результат). Но фактически нейросеть однослойная, потому что только в одном из слоёв — выходном — есть обучающиеся веса.
    В такой сети, очевидно, не было никаких скрытых слоёв: вход, ассоциативные элементы, анализирующие изображение, выходные элементы, выдающие результат.

    Перцептрон Розенблата
    Перцептрон Розенблата

    В перцептроне были две базовые концепции современных нейросетей: обучаемые (настраиваемые) веса, пороговая активация элементов — когда те или иные элементы выдавали +1, если сигнал на вход мощнее порога, и -1, иначе.

    Перцептрон показал техническую возможность аппаратной реализации нейросетей. Но не стал коммерческим из-за того, что мог делать только очень примитивный анализ — не мог распознать сдвинутые, повёрнутые или частично закрытые символы.
    Для этого ему не хватало скрытых слоёв и нелинейности.

    Со временем стало понятно, что необходимо несколько слоёв внутри сети: нижние слои выделяют простые признаки, верхние — сложные закономерности.

  • Но тут настала первая зима нейросетей: 60-70-е. Не было аппаратной базы, не было мат.аппарата для обучения и для обеспечения нелинейности распознавания, чтобы продвинуться вперёд.

  • Сеппо Илмари Линнаинмаа в 1970 впервые описал метод обратного распространения ошибки (reverse mode of automatic differentiation). Backpropagation использовался и прежде, но в этой работе было предоставлено строгое математическое описание алгоритма, которое и легло в основу современной его реализации. Но до 1986-го года про работу забыли.

  • Первой сетью с нелинейной функцией стал Когнитрон Фукусимы в 1975, также распознающий изображения.
    Его архитектура послужила прообразом для большинства последующий нейросетей: от нижних к верхним слоям: от примитивных форм на ограниченной области к абстрактным и обобщённым признакам по всему изображению.

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

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

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

  • В 1986 году, благодаря Дэвиду Румелхарту, Джеффри Хинтону и Рональду Уильямс получил широкое распространение алгоритм обратного распространения ошибки (Backpropagation). Это по сути обучение с учителем. Модель выдаёт результат, который сравнивается с заранее известным, вычисляется отклонение, на основе которого создаются матрицы корректировок, которые распространяются из конца в начало.
    А вместе с Backpropagation, появились и многослойные обучаемые сети и в частности MLP — MultiLayer Perceptron, который совершил революцию в распознавании символов.

  • В конце 80-х появились системы оптического распознавания символов OCR, ставшие первыми коммерчески успешными приложениями нейросетей. Они использовались для распознавания символов на чеках и индексов на письмах.

  • Затем наступила долгая вторая зима: 90-е и нулевые. На любые иные применения не хватало ни вычислительных мощностей, ни подходов.

  • Пока в 2012 на конференции ImageNet Large Scale Visual Recognition Challenge Алекс Крижевский, Илья Суцкевер и Джеффри Хинтон не привезли AlexNet.
    Это была искра, запустившая в небеса ракету глубокого обучения.
    Сеть разорвала на конкурсе всех. И вот почему.

    • 8 слоёв (5 свёрточных и 3 полносвязных)

    • 60 млн параметров. Одна из крупнейших на тот момент.

    • Обучение на нескольких GPU с параллелизацией. До этого было обучение или на одном GPU, или с параллелизация, но на CPU

    • Использование CUDA для ускорения вычислений

    • Обучение на больших данных (ImageNet)

    • Использовали ReLU вместо сигмоиды

    AlexNet показала не просто параллелизацию, а экономически оправданную параллелизацию. Именно после AlexNet параллелизация на GPU превратилась в обязательный элемент современных нейросетевых фреймворков (PyTorch, TensorFlow).

  • Дальше в течение 10-х был взрывной рост вычислительных мощностей (GPU, TPU, HBM), ренессанс нейронных сетей и появление GAN, ResNet, трансформеров.

  • 20-е знаменуются развитием LLM ( GPT-3 /4/5, Gemini, Claude, DeepSeek), мультимодальными моделями, (когда в одном интерфейсе вы работаете с текстом, изображением и звуком) публикацией обученных моделей в OpenSource (LLaMA, Mistral, Deepseek), крупными свёрточными сетями. И самое главное — коммерческое использование и астрономические инвестиции.


Заключение

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

Она не понимает, что во фразе «стакан упал и разбился» есть огромный объём информации, считываемый человеком. Мы знаем, что стакан упал под действием гравитации, что он сделан, вероятно, из стекла, которое является хрупким материалом, что пока стакан летел, он ускорялся и что он падал с достаточной высоты, чтобы разбиться, что произошло какое-то событие, заставившее его упасть. Да что там! Даже концепций «стакан», «упал» и «разбился» она не знает. Она просто обнаруживает математические закономерности между этими словами.

Технические же выводы, которые важны нам для следующих статей цикла следующие:

  • Типов нейросетей на сегодняшний день не великое, но множество. И устроены они все примерно одинаково с разницей в деталях.

  • В основе лежит строго детерминированный механизм с набором определённых шагов.

  • Для вычислений используется простой, но тяжёлый матаппарат с огромным количеством перемножений матриц.

  • Размеры моделей и промежуточных данных таковы, что нам необходима параллелизация и распределённые вычисления.

  • Есть несколько разных способов параллелизации с различными требованиями к полосе и задержкам, которые обычно объединяются в хитрые гибридные схемы.

  • Все распределённые вычисления требуют массивного обмена данными во время как обучения, так и инференса.

  • Обучение и инференс предъявляют очень разные требования к сети, поэтому почти всегда разделяются.

  • Есть несколько типов физических сетей в кластерах GPU, у каждой из которых своя специфика.

  • Нет такого греха, на который не пойдут компании ради оптимизации высокопроизводительных вычислений.

Поэтому параллелизация, оптимизации коллективных операций, ZeRO, кэши, сверхдорогая память HBM, SoC, механизмы Kernel Bypass, Zero Copy, DMA, специализированные сети, вроде NVLink и InfiniBand, тонкий тюнинг Ethernet — всё это находит своё применение в работе с нейросетями.
И именно о философии универсальных медленных решений против частных быстрых мы и поговорим в следующей статье.

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

Запустили обучение на осцилографе
Запустили обучение на осцилографе

Полезные ссылки


Спасибы

  • Алисе AI, Claude Sonet 4.5, DeepSeek V3.1. За нечеловечное терпение к моим бесконечным вопросам, за рецензии готовой статьи и за код изображений manim

  • Дмитрию Афанасьеву flow за уже человеческую рецензию к статье, а также пожизненный запас ссылок для изучения

  • Павлу Остапенко за вычитку и рекомендации

  • Александру Демиденко за стилистические и фактические правки

  • Андрею Щербину за комментарии

  • Всем близким и друзьям, много месяцев терпевшим от меня непрошеные рассказы про нейронки.

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


  1. mSnus
    04.01.2026 15:16

    Пришли Нероны сжигать нейроны.. отличная статья, но почему-то подписи к картинкам часто упоминают Императоров


    1. eucariot Автор
      04.01.2026 15:16

      Не думал, что так быстро спалят))


      1. mSnus
        04.01.2026 15:16

        Нерон
        недоволен своими весами
        недоволен своими весами


      1. mSnus
        04.01.2026 15:16

        Если серьезно, то не очень понятно, как происходит вот этот ключевой момент:

        в первом скрытом слое зажигаются те нейроны, которые определяют разные простые формы

        то есть там есть какой-то сумматор?


        1. eucariot Автор
          04.01.2026 15:16

          Ну в каком-то смысле трансформеры — это последовательность сумматоров, и компараторов (нелинейных функций).


  1. 010011011000101110101
    04.01.2026 15:16

    в нейросетях нет никакой магии — это просто множество простых операций над числами, которые выполняются на компьютерах со специальными чипами.

    Так ни в чём нет никакой магии. Человеческая жизнь это просто множество химических процессов. А человеческое сознание - это такая же нейросеть, только аналоговая, ещё проще. Только вот крошечная нейросеть из 768 нейронов превращает кучку железок лежащих на полу в почти живое существо. По крайней мере, оно начинает двигаться как живое.


    1. eucariot Автор
      04.01.2026 15:16

      Как говорил Докинз в "Бог как иллюзия", что полное понимание процессов во время секса, не мешает наслаждаться им)


  1. Jogker
    04.01.2026 15:16

    Начало пафосное, но сразу настораживает про "...доменную область..."


    1. eucariot Автор
      04.01.2026 15:16

      А почему настораживает?


  1. Moog_Prodigy
    04.01.2026 15:16

    Годная статья. Так вот как оно параллелится! Я конечно предполагал, что при обучении по шинам эти десятки терабайт никто гонять не должен (вроде) но не понимал как именно оно работает. ХитрО.

    А про распределенный инференс будет статья?


    1. eucariot Автор
      04.01.2026 15:16

      Я конечно предполагал, что при обучении по шинам эти десятки терабайт никто гонять не должен

      Так в том-то и дело, что терабайты данных нужно гонять по сети. Если под шиной вы имеете в виду NVLink, то он лучше — и по полосе и по задержкам.

      А про распределенный инференс будет статья?

      Отдельной — нет. Обзорно я коснулся в этой статье. А дальше я буду уходить в специфику инфраструктуры для кластеров. И даже более точечно — сетей.


  1. VanGorod
    04.01.2026 15:16

    Чтобы это написать понадобится воистину титанический труд, уважаемо


  1. StasTukalo
    04.01.2026 15:16

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


    1. eucariot Автор
      04.01.2026 15:16

      Здравствуйте. Вы правы. Но ирония в том, что в области именно нейросетей у меня опыта нет — я строю сеть для кластеров.