Перейти к содержимому
ПереводыGoogle и DeepMind

How To Scale Your Model, глава 12: как думать о GPU

Перевод ядра двенадцатой главы книги Google DeepMind: устройство GPU (SM, тензорные ядра, CUDA-ядра, иерархия памяти), сравнение с TPU, сеть NVLink и InfiniBand, коллективные операции и roofline для параллелизма данных, тензорного, экспертного и конвейерного на GPU.

Мы в Google любим TPU, но и GPU — это здорово. Эта глава подробно разбирает GPU NVIDIA: как устроен каждый чип, как они связываются в сеть и что это значит для LLM, особенно в сравнении с TPU. — подзаголовок главы.

Современный GPU для ML (например, H100, B200) — это, по сути, набор вычислительных ядер, специализированных на умножении матриц (их называют потоковыми мультипроцессорами, Streaming Multiprocessors, SM), подключённых к «планке» быстрой памяти (её называют HBM).

Рисунок в оригинале (gpu-diagram.png): абстрактная схема GPU H100 или B200. У H100 132 SM, у B200 — 148. Термином «планировщик варпов» (Warp Scheduler) авторы довольно широко называют набор из 32 SIMD-ядер CUDA и планировщик, который раздаёт им работу. Обратите внимание, насколько это похоже на TPU!

Каждый SM, как и TensorCore у TPU, имеет специальное ядро умножения матриц (к сожалению, тоже называемое тензорным ядром, Tensor Core1), векторный арифметический блок (его называют планировщиком варпов, Warp Scheduler2) и быстрый кэш на кристалле (его называют SMEM). В отличие от TPU, где не больше 2 независимых «TensorCore», у современного GPU больше 100 SM (132 у H100). Каждый из этих SM гораздо слабее TensorCore у TPU, но система в целом гибче. Каждый SM более-менее полностью независим, так что GPU может одновременно выполнять сотни отдельных задач3.

Рассмотрим SM у H100 подробнее.

Рисунок в оригинале (blackwell-sm.png): схема SM у H100 с 4 подразделами (subpartitions); в каждом — тензорное ядро, планировщик варпов, регистровый файл и наборы CUDA-ядер разной точности. «L1 Data Cache» внизу — это SMEM на 256 КБ. У B200 похоже, но добавлен заметный объём тензорной памяти (TMEM) для питания громоздких тензорных ядер.

Каждый SM разбит на 4 одинаковые четверти, которые NVIDIA называет подразделами SM (SM subpartitions); в каждой — тензорное ядро, 16 тысяч 32-битных регистров и векторный арифметический блок SIMD/SIMT, называемый планировщиком варпов, дорожки (ALU) которого NVIDIA называет CUDA-ядрами. Главный компонент каждого подраздела — пожалуй, тензорное ядро: оно умножает матрицы и даёт подавляющую часть FLOPs/s, — но не единственный, о котором стоит знать.

  • CUDA-ядра. В каждом подразделе есть набор ALU, называемых CUDA-ядрами, которые выполняют векторную арифметику SIMD/SIMT. Каждый ALU обычно выполняет 1 арифметическую операцию за такт, например f32.add4. В каждом подразделе 32 ядра fp32 (и меньшее число ядер int32 и fp64), и все они на каждом такте выполняют одну и ту же инструкцию. Как VPU у TPU, CUDA-ядра отвечают за ReLU, поточечные векторные операции и редукции (суммы)5.
  • Тензорное ядро (TC). В каждом подразделе своё тензорное ядро — специальный блок умножения матриц, как MXU у TPU. Тензорные ядра дают подавляющую часть FLOPs/s GPU (например, у H100 990 TFLOPs/s в bf16 на тензорных ядрах против всего 66 TFLOPs/s на CUDA-ядрах).
    • 990 TFLOPs/s в bf16 при 132 SM на частоте 1,76 ГГц значит, что каждое тензорное ядро H100 выполняет 7.5e12 / 1.76e9 / 4 ~ 1024 FLOPs в bf16 за такт — примерно умножение 8×8×86.
    • Как и TPU, GPU умножают матрицы в пониженной точности быстрее (например, у H100 FLOPs/s в fp8 вдвое больше, чем в fp16). Обучение или обслуживание в пониженной точности может быть заметно быстрее.
    • С поколения Volta каждое следующее поколение GPU увеличивало размер тензорного ядра (хорошая статья об этом). У B200 тензорное ядро стало настолько большим, что его входы больше не помещаются в SMEM, поэтому в B200 появилось новое пространство памяти — TMEM7.

CUDA-ядра гибче, чем VPU у TPU. CUDA-ядра GPU (начиная с V100) используют модель программирования SIMT (Single Instruction Multiple Threads, «одна инструкция — много потоков»), а VPU у TPU — модель SIMD (Single Instruction Multiple Data, «одна инструкция — много данных»). Как и ALU в VPU у TPU, CUDA-ядра одного подраздела на каждом такте должны выполнять одну и ту же операцию (например, если одно ядро складывает два числа, то и все остальные CUDA-ядра подраздела должны это делать). Но, в отличие от VPU, у каждого CUDA-ядра (или «потока» в модели программирования CUDA) свой указатель инструкций, и его можно программировать независимо. Когда двум потокам одного варпа велено выполнять разные операции, фактически выполняются обе операции, а ядра, которым расходящаяся операция не нужна, маскируются.

Рисунок в оригинале (warp-divergence.png): пример расхождения варпа (warp divergence) в наборе потоков; белые промежутки — простои хотя бы части физических CUDA-ядер.

Это даёт гибкость программирования на уровне потоков, но ценой тихого падения производительности, если варпы расходятся слишком часто. Потоки также гибче в том, к какой памяти обращаться: VPU работает только с непрерывными блоками памяти, а CUDA-ядра могут обращаться к отдельным числам в общих регистрах и хранить собственное состояние каждого потока.

Планирование CUDA-ядер тоже гибче. SM работают отчасти как многопоточные процессоры в том смысле, что могут «планировать» много программ (варпов) одновременно (до 64 на SM), но каждый планировщик варпов на каждом такте выполняет только одну программу8. Планировщик варпов автоматически переключается между активными варпами, чтобы скрыть операции ввода-вывода, например загрузки из памяти. TPU по сравнению с этим, как правило, однопоточные.

Помимо вычислительных блоков, у GPU есть иерархия памяти: самая большая — HBM (основная память GPU), а дальше ряд меньших кэшей (L2, L1/SMEM, TMEM, регистры).

  • Регистры. У каждого подраздела свой регистровый файл: 16 384 32-битных слова у H100/B200 (4 * 16384 * 4 = 256kiB на SM), доступные CUDA-ядрам.
    • Каждое CUDA-ядро может одновременно обращаться не более чем к 256 регистрам, поэтому, хотя на SM можно запланировать до 64 «резидентных варпов», при 256 регистрах на поток одновременно поместятся только 8 (256 * 1024 / (4 * 32 * 256)).
  • SMEM (кэш L1). У каждого SM свой кэш на кристалле объёмом 256 КБ — SMEM; программист может управлять им как «общей памятью» (shared memory), или железо использует его как кэш. В SMEM хранятся активации и входы для умножений на тензорных ядрах.
  • Кэш L2. Все SM делят9 сравнительно большой кэш L2 на ~50 МБ, который сокращает обращения к основной памяти.
    • По размеру он похож на VMEM у TPU, но гораздо медленнее и не управляется программистом. Получается своего рода «жуткое дальнодействие»: программисту приходится менять порядок обращений к памяти, чтобы кэш L2 использовался хорошо10.
    • NVIDIA не публикует пропускную способность L2 своих чипов, но замеры дают около 5,5 ТБ/с. Это примерно в 1,6 раза больше пропускной способности HBM, но канал полнодуплексный, так что эффективная двусторонняя пропускная способность ближе к трёхкратной. Для сравнения: VMEM у TPU вдвое больше и гораздо быстрее (около 40 ТБ/с).
  • HBM — основная память GPU, где хранятся веса модели, градиенты, активации и т. д.
    • Объём HBM сильно вырос: с 32 ГБ у Volta до 192 ГБ у Blackwell (B200).
    • Пропускную способность от HBM к тензорным ядрам называют пропускной способностью HBM или памяти; у H100 это около 3,35 ТБ/с, у B200 — 9 ТБ/с.

Вот сводка характеристик последних моделей GPU. Число SM, тактовая частота и FLOPs немного различаются между вариантами одного GPU. Сначала объёмы памяти:

GPUПоколениеТактовая частотаSM на чипSMEM на SML2 на чипHBM на чип
V100Volta1,25/1,38 ГГц8096 КБ6 МБ32 ГБ
A100Ampere1,10/1,41 ГГц108192 КБ40 МБ80 ГБ
H100Hopper1,59/1,98 ГГц132256 КБ50 МБ80 ГБ
H200Hopper1,59/1,98 ГГц132256 КБ50 МБ141 ГБ
B200Blackwell?148256 КБ126 МБ192 ГБ

У всех поколений по 256 КБ регистров на SM. Blackwell добавляет ещё 256 КБ TMEM на SM. Теперь FLOPs и пропускные способности:

GPUПоколениеHBM, байт/с на чипFLOPs/s на чип (bf16/fp16)FLOPs/s на чип (fp8/int8)FLOPs/s на чип (fp4)
V100Volta9.0e11———
A100Ampere2.0e123.1e146.2e14—
H100Hopper3.4e129.9e142.0e15—
H200Hopper4.8e129.9e142.0e15—
B200Blackwell8.0e122.3e154.5e159.0e15

B100 мы не включаем: в массовое производство он не пошёл11. Некоторые характеристики немного зависят от точной версии GPU: GPU NVIDIA не так стандартизованы, как TPU.

Удобная шпаргалка соответствия компонентов GPU и TPU:

GPUTPUЧто это?
потоковый мультипроцессор (SM)TensorCoreосновная «ячейка», содержащая остальные блоки
планировщик варповVPUвекторный арифметический блок SIMD
CUDA-ядроALU в VPUSIMD ALU
SMEM (кэш L1)VMEMбыстрая кэш-память на кристалле
тензорное ядроMXUблок умножения матриц
HBM (она же GMEM)HBMпамять с большой пропускной способностью и объёмом

GPU начинались как устройства для отрисовки видеоигр, но с тех пор как в 2010-х взлетело глубокое обучение, они всё больше ведут себя как специализированные машины для умножения матриц — то есть всё больше похожи на TPU12. Отчасти эта история объясняет, почему современные GPU выглядят именно так. Их проектировали не только под LLM или ML-модели, а как универсальные ускорители, и железо стремится к «универсальности», которая может быть и благом, и проклятием. GPU гораздо чаще «просто работают» на новых задачах и гораздо меньше зависят от хорошего компилятора, чем TPU. Но из-за этого о них гораздо труднее рассуждать и труднее добиться от них производительности на уровне roofline: очень многие особенности компилятора могут создать узкие места.

GPU модульнее. У TPU 1–2 больших TensorCore, а у GPU — сотни маленьких SM. Точно так же у каждого TensorCore один большой VPU из 4 независимо программируемых блоков 8×128 (всего 4096 ALU), а у H100 132 × 4 = 528 независимых SIMD-блоков шириной 32 (всего 16 тысяч ALU). Вот сравнение GPU и TPU один к одному, которое это подчёркивает:

GPUTPUЧисло у H100Число у TPU v5p
SM (потоковый мультипроцессор)TensorCore1322
планировщик варповслоты VPU5288
SMEM (кэш L1)VMEM32 МБ128 МБ
регистрывекторные регистры (VREG)32 МБ256 КБ
тензорное ядроMXU5288

Такая разница в модульности, с одной стороны, делает TPU гораздо дешевле в производстве и проще для понимания, но, с другой, перекладывает больше ответственности на компилятор. У TPU один поток управления и только векторные инструкции шириной во весь VPU, поэтому компилятор должен вручную конвейеризовать все загрузки из памяти и работу MXU/VPU, чтобы не было простоев. Программист GPU может просто запустить десятки разных ядер (kernels), каждое на совершенно независимом SM. Зато эти ядра могут работать ужасно медленно, потому что «перемалывают» кэш L2 или не объединяют загрузки из памяти; поскольку железо само управляет столь многим во время выполнения, трудно понять, что происходит за кулисами. В итоге TPU часто приближаются к пиковой производительности roofline с меньшими усилиями.

Исторически отдельный GPU мощнее (и дороже) сравнимого TPU. Один H200 даёт почти вдвое больше FLOPs/s, чем TPU v5p, и в 1,5 раза больше HBM. При этом в Google Cloud H200 стоит около 10 долл. в час, а TPU v5p — около 4 долл. в час. TPU в целом сильнее полагаются на объединение многих чипов в сеть, чем GPU.

У TPU гораздо больше быстрой кэш-памяти. VMEM у TPU гораздо больше, чем SMEM (+TMEM) у GPU, и эту память можно использовать для хранения весов и активаций так, чтобы загружать и использовать их очень быстро. Это может сделать TPU быстрее в инференсе LLM, если удаётся постоянно хранить веса модели в VMEM или заранее предзагружать их туда.

Вопрос 3 [интенсивность матмула на GPU]. Какова пиковая интенсивность умножения матриц в fp16 у H100? У B200? А в fp8? Под интенсивностью понимаем отношение FLOPs/s умножения матриц к пропускной способности памяти.

Ответ

У H100 пик — 990e12 FLOPs в fp16 и 3,35e12 байт/с пропускной способности. Значит, критическая интенсивность — 990e12 / 3.35e12 = 295, довольно близко к 240 у TPU. У B200 — 2250e12 / 8e12 = 281, очень похоже. Значит, как и на TPU, чтобы умножение матриц упиралось в вычисления, нужен батч около 280 токенов.

И у H100, и у B200 FLOPs в fp8 ровно вдвое больше, поэтому пиковая интенсивность тоже удваивается — до 590 и 562 соответственно, хотя в каком-то смысле она остаётся прежней, если учесть, что веса, скорее всего, тоже будут загружаться в fp8.

Вопрос 4 [время умножения матриц]. По ответу на вопрос 3 оцените, сколько займёт умножение fp16[64, 4096] * fp16[4096, 8192] на одном B200. А fp16[512, 4096] * fp16[4096, 8192]?

Ответ

Из сказанного выше мы знаем, что при батче меньше 281 токена упираемся в связь. Значит, первое умножение целиком ограничено пропускной способностью. Мы читаем или пишем 2BD+2DF+2BF2BD + 2DF + 2BF байт (2*64*4096 + 2*4096*8192 + 2*64*8192=69e6) при 8e12 байт/с, так что это займёт около 69e6 / 8e12 = 8.6us. На практике мы, скорее всего, получим лишь часть пропускной способности, так что может выйти ближе к 10–12 мкс. С ростом батча мы полностью упираемся в вычисления и ожидаем T=2*512*4096*8192/2.3e15=15us. И снова мы ожидаем лишь часть FLOPs, так что может выйти ближе к 20 мкс.

Вопрос 7 [время сложения на H100]. По числам выше посчитайте, сколько должно занимать сложение двух векторов fp32[N] на одном H100. Посчитайте и TmathT_\text{math}, и TcommsT_\text{comms}. Какова арифметическая интенсивность этой операции? Если есть доступ, попробуйте выполнить её в PyTorch или JAX для N = 1024 и N=1024 * 1024 * 1024. Как это соотносится с расчётом?

Ответ

Сложение двух векторов fp32[N] — это N FLOPs, при этом нужно загрузить 4 * N * 2 байт и записать обратно 4 * N байт, всего 3 * 4 * N = 12N. Отношение: всего FLOPs / всего байт = N / 12N = 1 / 12 — совсем плохо.

Как мы посчитали выше, без учёта FMA можно выполнять примерно 33,5 TFLOPs/s с ускорением частоты. Это — если задействованы все CUDA-ядра. При N = 1024 можно задействовать не больше 1024 CUDA-ядер, или 8 SM, что займёт больше времени (примерно в 16 раз, если упираемся в вычисления). Пропускная способность памяти — 3,35e12 байт/с. Значит, пиковая интенсивность железа — 33.5e12 / 3.35e12 = 1013. Так что мы безнадёжно упрёмся в связь, и время — просто

T=max⁡(Tcomms,Tmath)=12⋅N3,35⋅1012=N2,8⋅1011T = \max(T_\text{comms}, T_\text{math}) = \frac{12 \cdot N}{3{,}35 \cdot 10^{12}} = \frac{N}{2{,}8 \cdot 10^{11}}

Для N = 65,536 это около 0,23 мкс. На практике в JAX выходит около 1,5 мкс, и это нормально: здесь мы, ожидаемо, сильно упираемся в задержку. Для N = 1024 * 1024 * 1024 roofline даёт около 3,84 мс, а на деле — 4,1 мс. Отлично!

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

GPU же используют более традиционную иерархическую древовидную коммутируемую сеть. Наборы из 8 GPU, называемые узлами (nodes; до 72 у GB20014), соединены друг с другом за 1 переход с помощью быстрых межсоединений NVLink, а эти узлы соединяются в более крупные блоки (SU, Scalable Units, «масштабируемые блоки») более медленной сетью InfiniBand (IB) или Ethernet через сетевые карты (NIC), подключённые к каждому GPU. Их, в свою очередь, можно объединять в сколь угодно большие блоки коммутаторами более высокого уровня.

Рисунок в оригинале (superpod-diagram.png): типичная сеть H100. Набор из 8 GPU соединён в узел, или домен NVLink, коммутаторами NVSwitch (их также называют коммутаторами NVLink), а узлы соединены коммутируемой сетью InfiniBand. У H100 около 450 ГБ/с исходящей пропускной способности на GPU внутри домена NVLink, а у каждого узла — 400 ГБ/с исходящей пропускной способности в сеть IB.

Узел GPU — небольшой блок, обычно из 8 GPU (до 72 у GB200), соединённых «каждый с каждым» межсоединениями NVLink с полной пропускной способностью и низкой задержкой15. В каждом узле несколько быстрых коммутаторов NVSwitch, которые передают пакеты между всеми локальными GPU. Реальная топология узла со временем сильно менялась, включая число коммутаторов на узел, но у H100 на узел 4 NVSwitch, а GPU подключены к ним по схеме 5 + 4 + 4 + 5 каналов.

Рисунок в оригинале (nvlink-nodes.png): схемы узлов (доменов NVLink) начиная с Pascal (P100). С Volta (V100) внутри узла есть связность «каждый с каждым» через набор коммутаторов. В узле H100 4 NVSwitch подключены ко всем 8 GPU каналами по 25 ГБ/с.

В поколении Hopper (NVLink 4.0) каждый канал NVLink даёт 25 ГБ/с в полнодуплексном режиме16 (50 ГБ/с у B200), что даёт 18 * 25=450GB/s полнодуплексной пропускной способности от каждого GPU в сеть. У огромных NVSwitch до 64 портов NVLink, так что узел 8×H100 с 4 коммутаторами может пропустить до 64 * 25e9 * 4=6.4TB/s. Вот как эти числа менялись от поколения к поколению:

Поколение NVLinkПоколение NVSwitchПоколение GPUПропускная способность NVLink (ГБ/с, полный дуплекс)Портов NVLink на GPUGPU ↔ GPU в узле (ГБ/с, полный дуплекс)Размер узла (домен NVLink)NVSwitch на узел
3.02.0Ampere251230086
4.03.0Hopper251845084
5.04.0Blackwell50189008/722/18

У Blackwell (B200) узлы по 8 GPU. GB200 NVL72 поддерживает большие домены NVLink — на 72 GPU. В таблице приведены данные для обеих систем — на 8 и на 72 GPU.

Вопрос [стоимость AllGather]. Сколько займёт AllGather (ограниченный пропускной способностью) массива из B байт на узле 8×H100? Посчитайте для bf16[DX, F], где D=4096, F=65,536. Перед ответом стоит прочитать раздел о коллективных операциях TPU в главе 3.

Ответ

Каждый GPU отправляет 450 ГБ/с, и у каждого GPU B/NB / N байт (где N=8 — размер узла). Можно представить, что каждый узел по очереди отправляет свои байты каждому из остальных N−1N - 1 узлов; всего получается N−1N - 1 раундов, каждый — Tcomms=B/(N⋅Wunidirectional)T_\text{comms} = B / (N \cdot W_\text{unidirectional}), то есть Tcomms=(N−1)⋅B/(N⋅Wunidirectional)T_\text{comms} = (N - 1) \cdot B / (N \cdot W_\text{unidirectional}). Это примерно B/WuniB / W_\text{uni}, или B/450⋅109B / 450 \cdot 10^{9}.

Для данного массива B = 4096 * 65536 * 2 = 536e6 байт, так что общее время — 536e6 * (8 - 1) / (8 * 450e9) = 1.04ms (или 536e6 / 450e9 = 1.19ms в приближении). Операция может упираться в задержку, так что на практике она может занять больше (на деле — около 1,5 мс).

Пересказ раздела «Beyond the node level» с переводом выводов.

За пределами узла топология сети GPU менее стандартизована. NVIDIA публикует эталонную архитектуру DGX SuperPod: 1024 GPU H100 — это 128 узлов по 8 GPU, у каждого GPU своя сетевая карта InfiniBand на 400 Гбит/с. Узлы группами по 32 (256 GPU) подключены к 8 «листовым» (leaf) коммутаторам IB — такая группа называется Scalable Unit (SU); четыре SU соединены 16 «хребтовыми» (spine) коммутаторами. Такая сеть — толстое дерево (fat tree): кабели и коммутаторы подобраны так, чтобы над уровнем узла была полная бисекционная пропускная способность — 400 ГБ/с на узел. Значит, если разделить узлы пополам, каждый узел может одновременно отправлять 400 ГБ/с узлу из другой половины, и пропускная способность AllReduce в масштабирующей сети (scale-out network) примерно постоянна, сколько бы узлов ни участвовало. Заказчики вправе менять эту схему: например, Meta обучала LLaMA-3 на сети Ethernet с трёхуровневой коммутацией и переподпиской на верхнем уровне.

УровеньGPUКоммутаторов на блокТипПропускная способность блока (ТБ/с)GPU ↔ GPU (ГБ/с)Толстое дерево (ГБ/с)
Узел84NVLink3,6450450
Лист2568IB12,850400
Хребет102416IB51,250400

Для сравнения: у TPU v5p около 90 ГБ/с исходящей пропускной способности на канал, или 540 ГБ/с по всем осям трёхмерного тора. Это не соединение «точка-точка», так что его можно использовать только для ограниченных однородных схем обмена, но оно даёт гораздо большую пропускную способность TPU ↔ TPU и масштабируется до сколь угодно больших топологий (по крайней мере до 8960 TPU).

GB200 NVL72. Новые кластеры NVIDIA GB200 NVL72 объединяют 72 GPU в одном домене NVLink с полными 900 ГБ/с между GPU, а исходящая пропускная способность узла в листовой уровень — 3,6 ТБ/с, в 9 раз больше, чем у H100 (ровно во столько же раз в узле больше GPU). Из-за этого критическая исходящая пропускная способность узла намного выше, и пропускная способность коллективных операций между узлами может оказаться даже ниже, чем внутри узла.

Тип узлаGPU на узелИсходящая пропускная способность GPUИсходящая пропускная способность узла
H1008450e9400e9
B2008900e9400e9
GB200 NVL7272900e93600e9

GPU умеют выполнять те же коллективные операции, что и TPU: ReduceScatter, AllGather, AllReduce и AllToAll. В отличие от TPU, то, как они работают, зависит от того, выполняются ли они на уровне узла (по NVLink) или выше (по InfiniBand). Эти операции NVIDIA реализует в библиотеках NVSHMEM и NCCL (произносится «никель»); NCCL открыта здесь. NCCL использует разные реализации в зависимости от требований к задержке и топологии (подробности), но дальше мы будем обсуждать теоретически оптимальную модель для коммутируемой древовидной сети.

AllGather или ReduceScatter. AllGather или ReduceScatter на уровне узла можно выполнить по кольцу, как на TPU, используя на каждом переходе полную пропускную способность GPU ↔ GPU. Упорядочите GPU произвольно и передавайте часть массива по кольцу с полной пропускной способностью17. Стоимость каждого перехода — Thop=bytes/(N⋅исходящая пропускная способность GPU)T_\text{hop} = \text{bytes} / (N \cdot \text{исходящая пропускная способность GPU}), так что общая стоимость

TAG или RS=bytes⋅(N−1)N⋅исходящая пропускная способность GPU→bytesисходящая пропускная способность GPUT_\text{AG или RS} = \frac{\text{bytes} \cdot (N - 1)}{N \cdot \text{исходящая пропускная способность GPU}} \rightarrow \frac{\text{bytes}}{\text{исходящая пропускная способность GPU}}

Заметьте: это ровно то же, что на TPU. AllReduce, как обычно, можно сложить из RS + AG — вдвое дороже.

Анимация в оригинале (all-gather.gif): оптимальный по пропускной способности кольцевой AllGather. Для B байт он отправляет B / X байт через коммутаторы верхнего уровня X − 1 раз.

Если вас беспокоит задержка (например, массив очень маленький), можно выполнить редукцию деревом: AllReduce в парах, затем в четвёрках, затем в восьмёрках — всего log⁡N\log N переходов вместо N−1N - 1, хотя общая стоимость остаётся той же.

Быстрый вопрос 1 [время AllGather]. Сколько займёт AllGather(bf16[BX, F]) на узле 8×H100 с полнодуплексной пропускной способностью 450 ГБ/с? Пусть B=1024B=1024, F=16 384F=16\,384.

Ответ

Всего 2⋅B⋅F2 \cdot B \cdot F байт при односторонней пропускной способности 450e9. Это займёт примерно Tcomms=(2⋅B⋅F)/450⋅109T_\text{comms} = (2 \cdot B \cdot F) / 450 \cdot 10^{9}, а точнее (2⋅B⋅F⋅(8−1))/(8⋅450⋅109)(2 \cdot B \cdot F \cdot (8 - 1)) / (8 \cdot 450 \cdot 10^{9}). По данным значениям выходит примерно (2⋅1024⋅16384)/450⋅109=75(2 \cdot 1024 \cdot 16384) / 450 \cdot 10^{9} = 75 мкс, а точнее — 65 мкс.

AllToAll. У GPU внутри узла связность «каждый с каждым», поэтому AllToAll выполняется, ну, довольно просто. Каждый GPU просто отправляет данные прямо в пункт назначения. Внутри узла для B байт у каждого GPU B/NB / N байт, и он отправляет по (B/N2)(B / N^2) байт N−1N - 1 получателям, всего

TAllToAll=B⋅(N−1)W⋅N2≈BW⋅NT_\text{AllToAll} = \frac{B \cdot (N - 1)}{W \cdot N^2} \approx \frac{B}{W \cdot N}

Сравните с TPU, где стоимость B/(4W)B / (4W). То есть внутри одного узла получаем теоретическое ускорение в 2 раза (B/4WB / 4W против B/8WB / 8W).

Для моделей со смесью экспертов (MoE) часто нужен разреженный (ragged) AllToAll, где гарантированно не больше kk из NN шардов по выходному измерению ненулевые. Его стоимость уменьшается в k/Nk/N раз — всего около min⁡(k/N,1)⋅B/(W⋅N)\min(k/N, 1) \cdot B / (W \cdot N). В MoE ненулевые значения часто выбираются независимо и случайно, так что есть шанс, что ненулевых окажется меньше kk, и получается примерно (N−1)/N⋅min⁡(k/N,1)⋅B/(W⋅N)(N-1)/N \cdot \min(k/N, 1) \cdot B / (W \cdot N).

Замеры на практике. Замер пропускной способности AllReduce на узле 8×H100 (рисунок в оригинале) показывает: «шинная» пропускная способность (Bus BW, 2⋅W⋅(8−1)/82 \cdot W \cdot (8 - 1) / 8 — теоретически мера реальной пропускной способности каналов) достигает почти 370 ГБ/с — меньше 450 ГБ/с, но довольно близко, — однако только при сообщениях около 10 ГБ на устройство. То есть оценки теоретически верны, но, чтобы их достичь, нужны большие сообщения.

Это серьёзная проблема: она заметно усложняет любые теоретические утверждения. Например, даже AllReduce массива разумного размера — скажем, MLP у LLaMA-3 70B (размера bf16[8192, 28672], или при 8-путевом шардировании модели bf16[8192, 3584] = 58MB) — достигает лишь около 150 ГБ/с против пиковых 450 ГБ/с. TPU, для сравнения, выходят на пиковую пропускную способность при гораздо меньших сообщениях.

Редукции в сети. С поколения Hopper коммутаторы NVIDIA поддерживают SHARP (Scalable Hierarchical Aggregation and Reduction Protocol) — «редукции в сети»: сами коммутаторы выполняют редукцию и рассылают результат нескольким GPU. Теоретически это почти вдвое удешевляет AllReduce (T=bytes/WGPU egressT = \text{bytes} / W_\text{GPU egress} вместо удвоенного), но на практике прирост пропускной способности около 30%, а не ожидаемые 75%, — примерно до 480 ГБ/с эффективной пропускной способности коллективной операции.

Коллективные операции между узлами (в сокращении)

Заголовок раздела «Коллективные операции между узлами (в сокращении)»

Пересказ раздела «Cross-node collectives» с переводом выводов.

За пределами узла редукцию по дереву можно представлять снизу вверх: сначала внутри узла, затем на листовом уровне, затем на хребтовом, каждый раз обычным алгоритмом. Для AllReduce это значит, что после AllReduce внутри узла наверх нужно отправить лишь BB байт, а не B⋅NB \cdot N. В первом приближении, благодаря полной бисекционной пропускной способности, AllGather или ReduceScatter стоит примерно размер буфера, делённый на исходящую пропускную способность узла, независимо от деталей древовидной редукции:

TAG или RS=bytesWnode egress=H100bytes400⋅109T_\text{AG или RS} = \frac{\text{bytes}}{W_\text{node egress}} \underset{H100}{=} \frac{\text{bytes}}{400 \cdot 10^{9}}

Проще всего представить кольцевую редукцию по всем узлам кластера: благодаря толстому дереву между любыми двумя узлами можно построить кольцо с пропускной способностью Wnode egressW_\text{node egress}. Редукция на уровне узла почти никогда не узкое место, но в общем случае

Ttotal=max⁡[bytesWGPU egress,bytesWnode egress]T_\text{total} = \max\left[\frac{\text{bytes}}{W_\text{GPU egress}}, \frac{\text{bytes}}{W_\text{node egress}}\right]

(Точный расчёт по уровням дерева для эталонного SuperPod даёт 514 ГБ/с на уровне узла, 413 ГБ/с на листовом и 17,1 ТБ/с на хребтовом, итого ~413 ГБ/с.) AllReduce по-прежнему вдвое дороже, если нет SHARP. А вот AllToAll между узлами дорожает сильно: он не «иерархический», и бисекционную пропускную способность узла использовать не получается. Для N-путевого AllToAll на M=N/8M = N / 8 узлах

TAllToAll=B⋅(M−1)M2⋅Wnode egress≈BM⋅Wnode egressT_\text{AllToAll} = \frac{B \cdot (M - 1)}{M^2 \cdot W_\text{node egress}} \approx \frac{B}{M \cdot W_\text{node egress}}

то есть фактически 50 ГБ/с вместо 400: от B/(8⋅450⋅109)B / (8 \cdot 450 \cdot 10^{9}) внутри одного узла H100 к B/(2⋅400⋅109)B / (2 \cdot 400 \cdot 10^{9}) на двух узлах — ухудшение больше чем вчетверо.

УровеньЧисло GPUСтепень (число потомков)Пропускная способность коммутаторов (ТБ/с)Пропускная способность кабелей (ТБ/с)Пропускная способность коллективных операций (ГБ/с)
Узел886,43,6450
Лист (SU)2563225,612,8400
Хребет1024451,251,2400

«Пропускная способность коллективных операций» — эффективная пропускная способность, с которой можно отправлять данные из GPU или из узла; она же равна бисекционная пропускная способность⋅2/N\text{бисекционная пропускная способность} \cdot 2 / N.

Редукции массива, шардированного по другой оси. Если массив, по которому делается AllReduce по оси XX, сам шардирован по другой оси YY, то на TPU стоимость падает в YY раз. На GPU — только если внутренняя ось YY охватывает несколько узлов:

Tна уровне узла=bytesWGPU egress⋅1min⁡(Y,Dnode),Tв масштабирующей сети=bytesWnode egress⋅Dnodemax⁡(Dnode,Y)T_\text{на уровне узла} = \frac{\text{bytes}}{W_\text{GPU egress}} \cdot \frac{1}{\min(Y, D_\text{node})}, \qquad T_\text{в масштабирующей сети} = \frac{\text{bytes}}{W_\text{node egress}} \cdot \frac{D_\text{node}}{\max(D_\text{node}, Y)}

Теперь посмотрим, к чему всё это вело: поймём roofline для масштабирования LLM на GPU. Это дополнение к главе об обучении на TPU (глава 5). Как и там, цель — посмотреть на общие TmathT_\text{math} и TcommsT_\text{comms} для разных стратегий параллелизма и понять, в какой момент Tcomms>TmathT_\text{comms} > T_\text{math}. Как и раньше, рассматриваем только блок MLP с операциями

MLP(x)≡x[B,D]∗DWin[D,F]⋅FWout[F,D]\text{MLP}(x) \equiv x[B, D] *_D W_\text{in}[D, F] \cdot_F W_\text{out}[F, D]

где BB — глобальный размер батча в токенах (то есть B=размер батча⋅длина последовательностиB = \text{размер батча} \cdot \text{длина последовательности}).

Повторим таблицу эффективных пропускных способностей на уровне GPU и узла:

Тип узлаGPU на узелИсходящая пропускная способность GPUИсходящая пропускная способность узла
H1008450e9400e9
B2008900e9400e9
GB200 NVL7272900e93600e9

Замечание: roofline наших LLM определяют обе пропускные способности — и GPU, и узла. Через WcollectiveW_\text{collective} будем обозначать пропускную способность GPU или узла — в зависимости от того, работаем мы внутри узла или выше.

Рассмотрим roofline «вычисления — связь», как делали для TPU, для параллелизма данных, тензорного, конвейерного и экспертного параллелизма и их сочетаний. До конца раздела в конкретных расчётах ориентируемся на H100 (обозначим его пиковые FLOPs/s через CC). У GB200-NVL72 roofline в целом те же, но из-за большей исходящей пропускной способности узла узким местом иногда может стать уровень узла.

Как отмечалось, параллелизм данных (DP) и шардирование ZeRO требуют в обратном проходе либо AllReduce весов, либо ReduceScatter + AllGather. Стоимость у них одинакова, поэтому, чтобы чистый параллелизм данных или FSDP без редукций в сети упирались в вычисления, нужно (на слой, в обратном проходе, для оси размера X):

Tmath=2⋅2⋅2⋅BDFX⋅CT_\text{math} = \frac{2 \cdot 2 \cdot 2 \cdot BDF}{X \cdot C} Tcomms=2⋅2⋅2⋅DFWcollectiveT_\text{comms} = \frac{2 \cdot 2 \cdot 2 \cdot DF}{W_\text{collective}}

Поэтому для Tmath>TcommsT_\text{math} > T_\text{comms} нужно B/(XC)>1/WcollectiveB / (XC) > 1 / W_\text{collective}, или

BX>CWcollective\frac{B}{X} > \frac{C}{W_\text{collective}}

где WcollectiveW_\text{collective} — исходящая пропускная способность GPU или узла, в зависимости от того, шардируем мы внутри узла или между узлами. Значит:

  • внутри узла достаточно, чтобы батч на GPU в токенах был больше 990⋅1012/450⋅109=2200990 \cdot 10^{12} / 450 \cdot 10^{9} = 2200;
  • внутри SU или на хребтовом уровне — больше 990⋅1012/400⋅109=2475990 \cdot 10^{12} / 400 \cdot 10^{9} = 2475.

Это заметно больше, чем на TPU, где это число — 850 при всех трёх осях. Например, LLaMA-3, обучавшейся на 16 000 H100, понадобился бы батч не меньше 40 млн токенов (для сравнения: использовали 16 млн). DeepSeek v3, обучавшейся на 2048 GPU H800 с более низкой пропускной способностью 300 ГБ/с (вместо 450 ГБ/с у H100), понадобилось бы 990⋅1012/300⋅109=3300990 \cdot 10^{12} / 300 \cdot 10^{9} = 3300 токенов на GPU, или около 6,7 млн (на практике использовали 4 млн).

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

MoE-модели. Для модели со смесью экспертов (MoE), где E экспертов и k экспертов на токен, это растёт до

Tmath=2⋅2⋅2⋅k⋅BDFX⋅CT_\text{math} = \frac{2 \cdot 2 \cdot 2 \cdot k \cdot BDF}{X \cdot C} Tcomms=2⋅2⋅2⋅EDFWcollectiveT_\text{comms} = \frac{2 \cdot 2 \cdot 2 \cdot EDF}{W_\text{collective}}

что увеличивает батч на GPU в токенах в E/kE/k раз:

BX>EkCWcollective\frac{B}{X} > \frac{E}{k} \frac{C}{W_\text{collective}}

Например, у новой открытой модели OpenAI с k=4k=4 и E=128E=128 это вырастает до 32 * 2475 = 79,200 между узлами — какое-то смехотворно большое число.

Что, если X мал? Когда параллелизм данных, например, всего на 2 узла, помогает множитель (X−1)/X(X - 1) / X:

Tmath=2⋅2⋅2⋅BDFN⋅CT_\text{math} = \frac{2 \cdot 2 \cdot 2 \cdot BDF}{N \cdot C} Tcomms=2⋅2⋅2⋅DF⋅(X−1)X⋅WcollectiveT_\text{comms} = \frac{2 \cdot 2 \cdot 2 \cdot DF \cdot (X-1)}{X \cdot W_\text{collective}}

где X — число узлов, а N=8⋅XN = 8 \cdot X. Тогда для плотной модели B/N>α⋅(X−1)/XB / N > \alpha \cdot (X - 1) / X, например B/N>1237B / N > 1237 — вдвое меньше значения выше. Поэтому двухпутевой параллелизм данных встречается довольно часто.

Тензорный параллелизм (TP) требует AllGather и ReduceScatter активаций, которые нужно совмещать с FLOPs блока MLP. То есть в прямом проходе

Tmath=2⋅2⋅BDFY⋅CT_\text{math} = \frac{2\cdot 2 \cdot BDF}{Y \cdot C} Tcomms=2⋅2⋅BDWcollectiveT_\text{comms} = \frac{2\cdot 2 \cdot BD}{W_\text{collective}}

откуда для упора в вычисления получаем правило

Y<F⋅WcollectiveCY < \frac{F \cdot W_\text{collective}}{C}

Внутри узла это примерно F/2200F / 2200, а за его пределами — F/2475F / 2475. Для F=28 000F = 28\,000, как у LLaMA-3, это около 11-путевого TP (или, с округлением вниз, около 8-путевого — ровно размер узла). Как и выше, если охватить ровно 2 узла, получаем дополнительный двукратный выигрыш в пропускной способности, так что обычно можно делать 16-путевой тензорный параллелизм (F>2475⋅(Y−8)F > 2475 \cdot (Y - 8)), что теоретически даёт до 19-путевого параллелизма модели.

Как уже отмечалось, модели со смесью экспертов (MoE) дают в E раз больше весов модели при лишь в k раз большем числе FLOPs, и параллелизм данных становится заметно труднее. Это можно отчасти смягчить, шардируя веса по измерению экспертов, то есть Win[EZ, D, F]. Для блока MLP нужно ввести два AllToAll, чтобы отправить активации нужным экспертам.

Как отмечено выше, стоимость такого AllToAllZ→k([B, D, k]), если он охватывает несколько узлов, — примерно TAllToAll=2⋅B⋅D⋅(Z−8)/Z⋅min⁡(8⋅k/Z,1)T_\text{AllToAll} = 2 \cdot B \cdot D \cdot (Z-8)/Z \cdot \min(8 \cdot k / Z, 1), так что для чистого экспертного параллелизма нужно

Tmath=4⋅B⋅k⋅D⋅FZ⋅CT_\text{math} = \frac{4 \cdot B \cdot k \cdot D \cdot F}{Z \cdot C} Tcomms=4⋅B⋅D⋅(Z−8)W⋅Z⋅min⁡(8⋅kZ,1)T_\text{comms} = \frac{4 \cdot B \cdot D \cdot (Z-8)}{W \cdot Z} \cdot \min\left(\frac{8 \cdot k}{Z}, 1\right)

Нужно либо k>Z/8k > Z/8 при F>α⋅(Z−8)/kF > \alpha \cdot (Z - 8)/k, либо Z≫kZ \gg k и F>8⋅αF > 8 \cdot \alpha, где α=C/W\alpha = C/W. Получаются две области, где экспертный параллелизм возможен: одна — с небольшой степенью экспертного параллелизма (примерно на 2 узла) и малым FF, другая — с большим FF и сколь угодно большим ZZ (вплоть до E-путевого экспертного параллелизма).

На практике встречаются оба случая: либо немного экспертного параллелизма (как у DeepSeek v3 с очень маленьким F и сравнительно небольшим, ограниченным межузловым экспертным параллелизмом), либо модели с большим F, где можно делать значительный межузловой EP вместе с TP.

Конвейерный параллелизм (PP) делит слои между узлами с крайне низкой стоимостью связи: раз в несколько слоёв отправляются лишь небольшие микробатчи активаций. Исторически конвейеры страдали от «пузырей», но с новыми подходами без пузырей (zero-bubble) обычно можно обойтись без них.

Общая стоимость связи в конвейере крошечная: при NMBN_\text{MB} микробатчах и NstagesN_\text{stages} стадиях Tна переход=2⋅B⋅D/(W⋅NMB)T_\text{на переход} = 2 \cdot B \cdot D / (W \cdot N_\text{MB}), а переходов NMB+Nstages−2N_\text{MB} + N_\text{stages} - 2, так что примерно

Tвся связь PP=2BDW⋅NMB⋅(NMB+Nstages−2)T_\text{вся связь PP} = \frac{2BD}{W \cdot N_\text{MB}} \cdot (N_\text{MB} + N_\text{stages} - 2) Tсвязь на слой≈1,5⋅2BDW⋅NlayersT_\text{связь на слой} \approx 1{,}5 \cdot \frac{2BD}{W \cdot N_\text{layers}}

Поскольку мы делим на NlayersN_\text{layers}, это гораздо меньше любых других затрат. Иначе говоря, с точки зрения связи конвейер практически бесплатен. Почему же тогда не использовать только конвейер? Причин несколько.

(1) Сложность кода. Конвейер не так хорошо вписывается во фреймворки автоматического параллелизма (вроде GSPMD в XLA), как другие подходы. Он вводит микробатчи, чтобы скрыть пузыри, и тем меняет структуру программы, а собственные расписания конвейера без пузырей ещё усугубляют проблему, требуя сложного чередования прямого и обратного проходов.

(2) Конвейер затрудняет параллелизм данных и FSDP. Пожалуй, главная причина не использовать конвейер — он плохо сочетается с FSDP и параллелизмом данных. Особенно плохо работает шардирование ZeRO-3: оно требует AllGather весов на каждом микробатче, а это не окупается, когда на микробатч приходится лишь B/NmicrobatchesB / N_\text{microbatches} токенов. Кроме того, в обратном проходе нельзя выполнить AllReduce или ReduceScatter градиентов, пока последний микробатч не прошёл данную стадию, — значит, появляется заметное время связи, которое не перекрывается вычислениями.

Рисунок в оригинале (pipeline-bubble.png): пример конвейера из 2 стадий и 2 микробатчей. F — прямой проход стадии, B — обратный (вдвое дороже), G — AllReduce параллелизма данных, который может быть заметно длиннее одного микробатча.

(3) Пузыри конвейера и дисбаланс шагов. Как видно из (плохого) расписания выше, при наивном расписании легко получить заметные пузыри (то есть потерянные вычисления). Выше вторая стадия простаивает на шаге 0, первая — с шага 2 по 3, а вторая снова простаивает на последнем шаге. Аккуратным планированием это отчасти можно обойти, но какие-то пузыри часто остаются. Кроме того, активации нужно передавать от стадии к стадии на критическом пути, что добавляет накладные расходы (pipeline-transfer.png: стоимость передачи сдвигает стадии друг относительно друга и увеличивает пузыри).

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

Оговорка о задержке. Как отмечалось, GPU с трудом достигают полной пропускной способности AllReduce даже на довольно больших сообщениях. Значит, даже если теоретически можно масштабировать, скажем, AllToAll экспертного параллелизма на несколько узлов, на деле может не получиться выжать и 50% общей пропускной способности. Поэтому TP или EP стараются держать в пределах небольшого числа узлов, чтобы уменьшить накладные расходы на задержку.

Что делает DeepSeek? Для справки: DeepSeek V3 обучалась на 2048 GPU H800 с:

  • 64-путевым экспертным параллелизмом (EP), охватывающим 8 узлов;
  • 16-путевым конвейерным параллелизмом (PP);
  • 2-путевым параллелизмом данных ZeRO-1 (DP).

Установившийся размер батча — 4096 * 15360 = 62,914,560 токенов, или 30 тысяч токенов на GPU. Это уже довольно много, но и модель очень разреженная (k=8, E=256), так что нужен довольно большой батч. Видно, что при 64-путевом EP и 16-путевом PP получается в сумме 1024-путевой параллелизм модели, а значит, AllReduce выполняется на хребтовом уровне, и, поскольку он лишь 2-путевой, на практике получаем в 2/(2−1)=22 / (2 - 1) = 2 раза больше пропускной способности. Это также помогает удешевить финальный AllReduce параллелизма данных, перекрывающийся с последними стадиями конвейера.

Что делает LLaMA-3? LLaMA-3 обучается с батчем 16 млн токенов на 16 тысячах GPU, то есть около 1 тысячи токенов на GPU. Используются:

  • 8-путевой тензорный параллелизм внутри узла (TP);
  • 16-путевой конвейерный параллелизм (PP);
  • 128-путевой параллелизм данных ZeRO-1.

Это плотная модель, так что всё в целом довольно просто. 16-путевой PP в 16 раз удешевляет AllReduce параллелизма данных, что помогает снизить критический размер батча.

Отступим на шаг и подведём общий итог того, что мы узнали:

  • Параллелизм данных или FSDP (ZeRO-1/3) требует локального батча около 2500 токенов на GPU, хотя теоретически редукции в сети плюс чистый DP могут это немного снизить.
  • Тензорный параллелизм упирается в вычисления примерно до 8-путевого, но пропускной способности не хватает, чтобы масштабироваться заметно дальше, не упёршись в связь. В основном это ограничивает нас одним доменом NVLink (то есть одним узлом, или нужен GB200NVL72 с числом GPU до 72).
  • Любой вид параллелизма модели, охватывающий несколько узлов, может ещё удешевить FSDP, поэтому часто хочется смешать PP + EP + TP, чтобы охватить много узлов и снизить стоимость FSDP.
  • Конвейерный параллелизм хорошо работает, если вы справляетесь со сложностью кода конвейера без пузырей и держите размеры батча достаточно большими, чтобы избежать узких мест параллелизма данных. Конвейер обычно делает ZeRO-3 невозможным (пришлось бы делать AllGather на каждой стадии конвейера), но вместо него можно использовать ZeRO-1.

В целом это даёт рецепт шардирования больших моделей на GPU:

  • Для сравнительно небольших плотных моделей отлично работает агрессивный FSDP, если хватает размера батча, — возможно, с небольшой долей конвейерного или тензорного параллелизма, если нужно.
  • Для более крупных плотных моделей хорошо работает сочетание TP на 1–2 узла + PP на много узлов + чистый DP.
  • Для MoE действует то же правило, но можно использовать и экспертный параллелизм, который обычно предпочтительнее TP. Если F>8⋅C/WnodeF > 8 \cdot C / W_\text{node}, можно делать много межузлового экспертного параллелизма, иначе мы ограничены примерно 2-узловым EP.
flowchart TD
A["Модель помещается в одну GPU с оптимизатором?"] -->|"да"| DP["DDP: батч ≥ ~2500 токенов на GPU (на PCIe — больше)"]
A -->|"нет"| B["Плотная модель среднего размера?"]
B -->|"да"| FSDP["FSDP (ZeRO-3), при нужде + немного PP или TP"]
B -->|"нет, очень большая"| C["TP внутри узла + PP между узлами + DP (ZeRO-1)"]
C --> D{"MoE?"}
D -->|"да"| EP["Экспертный параллелизм вместо части TP"]
Рецепт выбора параллелизма на GPU по итогам главы (схема переводчика)

Дальше в оригинале — квиз 5 о roofline для LLM, благодарности и приложения о GB200 и деталях сети (не переведены).

EnglishРусский
Streaming Multiprocessor (SM), SM subpartitionпотоковый мультипроцессор, подраздел SM
Tensor Core, CUDA coreтензорное ядро, CUDA-ядро
warp, warp scheduler, warp divergenceварп, планировщик варпов, расхождение варпа
SIMT / SIMDодна инструкция — много потоков / данных
register file, SMEM (shared memory), L2, TMEM, HBMрегистровый файл, общая память, кэш L2, тензорная память, HBM
FMA (fused multiply-add)совмещённое умножение-сложение
node, NVLink domain, NVSwitchузел, домен NVLink, коммутатор NVSwitch
InfiniBand, NIC, leaf / spine switchInfiniBand, сетевая карта, листовой / хребтовый коммутатор
fat tree, bisection bandwidthтолстое дерево, бисекционная пропускная способность
scale-out networkмасштабирующая сеть (между узлами)
full-duplex, egress / ingressполный дуплекс, исходящий / входящий трафик
AllGather, ReduceScatter, AllReduce, AllToAllсбор, редукция с раздачей, общая редукция, «все всем»
ragged AllToAllразреженный AllToAll
in-network reduction (SHARP)редукция в сети
data / tensor / expert / pipeline parallelismпараллелизм данных / тензорный / экспертный / конвейерный
ZeRO-1, ZeRO-3, FSDPшардирование состояния оптимизатора / всего, полностью шардированный DP
microbatch, pipeline bubble, zero-bubbleмикробатч, пузырь конвейера, конвейер без пузырей
  1. Тензорное ядро GPU — подблок SM для умножения матриц, а TensorCore у TPU — объемлющий блок, содержащий MXU, VPU и другие компоненты. ↩

  2. У NVIDIA нет хорошего названия для этого блока, поэтому мы используем это — как лучшее из нескольких плохих. Планировщик варпов — прежде всего блок, раздающий работу набору CUDA-ядер, но здесь мы называем так и управляющий блок, и набор ядер, которым он управляет. ↩

  3. Хотя SM независимы, ради пиковой производительности им часто приходится согласовывать работу: все они делят кэш L2 ограниченного объёма. ↩

  4. Новые GPU поддерживают инструкции FMA (fused multiply-add, совмещённое умножение-сложение), которые формально выполняют две FLOPs за такт, — этим NVIDIA беззастенчиво пользуется, удваивая заявленные характеристики. ↩

  5. Исторически, до появления тензорных ядер, CUDA-ядра были главным компонентом GPU и использовались для отрисовки, включая пересечения лучей с треугольниками и затенение. В современных игровых GPU они по-прежнему выполняют основную работу по отрисовке, а тензорные ядра используются для повышения разрешения (DLSS): GPU рисует в меньшем разрешении (меньше пикселей — меньше работы) и повышает его с помощью ML. ↩

  6. NVIDIA раскрывает мало аппаратных деталей тензорных ядер, так что это скорее догадка, чем точный факт, — и уж точно ничего не говорит о том, как тензорное ядро реализовано. Известно, что V100 выполняет 256 FLOPs на тензорное ядро за такт, A100 — 512, H100 — 1024, а для B200 детали не опубликованы, но, вероятно, около 2048, поскольку 2250e12 / (148 * 4 * 1.86e9) — примерно 2048. Некоторые подробности подтверждены здесь. ↩

  7. В Ampere тензорное ядро можно было питать из одного варпа, в Hopper нужен целый SM (warpgroup), а в Blackwell его питают 2 SM. Умножения в Blackwell стали такими большими, что аргументы (точнее, аккумулятор) больше не помещаются в регистры/SMEM, поэтому в Blackwell добавили TMEM. ↩

  8. Варпы, запланированные на данном SM, называют «резидентными». ↩

  9. Строго говоря, кэш L2 разделён на две половины, так что у H100 половина SM имеет доступ к 25 МБ каждая. Половины связаны каналом, но с меньшей пропускной способностью. ↩

  10. То, что кэш L2 общий для всех SM, фактически вынуждает программиста всё равно запускать SM довольно согласованно, хотя в принципе это независимые блоки. ↩

  11. NVIDIA выпустила поколение B100, но его продавали и производили недолго — якобы из-за конструктивных недостатков, которые не давали работать близко к заявленным характеристикам. Ему было трудно выходить на пиковые FLOPs без снижения частоты из-за нагрева и энергопотребления. ↩

  12. До бума глубокого обучения GPU («графические процессоры») занимались, собственно, графикой — в основном для видеоигр. Видеоигры представляют объекты миллионами маленьких треугольников, а игра отрисовывает (растеризует) эти треугольники в двумерное изображение, которое показывается на экране 30–60 раз в секунду (эта частота называется частотой кадров). Растеризация включает проецирование треугольников в систему координат камеры и вычисление, какие треугольники перекрывают какие пиксели, — миллиарды раз в секунду. Это очень дорого, и это только начало: затем каждый пиксель нужно раскрасить, смешав цвета, возможно, нескольких полупрозрачных треугольников, которые пересекает луч. GPU проектировали так, чтобы выполнять эти операции очень быстро и при этом универсально: одновременно нужно запускать много разных задач GPU (их называют шейдерами), и ни одна операция не доминирует. Поэтому потребительские графические GPU умеют умножать матрицы, но это не их главная функция. ↩

  13. Примечательно, что эта интенсивность остаётся постоянной в последних поколениях GPU: у H100 — 33,5 / 3,35, у B200 — 80 / 8. Почему так, неясно, но наблюдение интересное. ↩

  14. Термин «узел» перегружен и может значить две вещи: домен NVLink, то есть набор GPU, полностью связанных межсоединениями NVLink, или набор GPU, подключённых к одному хосту с процессором. До B200 это обычно совпадало, но в GB200 NVL72 домен NVLink — 72 GPU, а к каждому хосту по-прежнему подключено 8 GPU. Здесь мы называем узлом домен NVLink, хотя это спорно. ↩

  15. Полный дуплекс здесь означает 25 ГБ/с в каждую сторону, причём направления независимы. Всего по каналу можно отправить 50 ГБ/с, но не больше 25 ГБ/с в каждом направлении. ↩

  16. Можно представить и так: каждый GPU отправляет свой кусок размером bytes/N\text{bytes} / N каждому из остальных N−1N - 1 GPU, всего (N−1)⋅N⋅bytes/N(N - 1) \cdot N \cdot \text{bytes} / N переданных байт, — ответ тот же. ↩