
Привет, Хабр! Я Ефим Головин, старший MLOps-инженер в Selectel. Некоторое время назад мы в отделе Data/ML начали задаваться вопросом: а как там поживает AMD? Понятно, что у них масса дел, но нас интересовало, скорее, что у них в плане AI/DL/ML. С NVIDIA все плюс-минус ясно, это стандарт. А вот AMD — что-то неизвестное. Я вообще предполагал, что у «красных» хотя бы в плане терминологии и документации все должно быть плюс-минус аналогично тому, как оно есть у NVIDIA. Но решил убедиться в этом, поэтому отправился изучать документацию обеих компаний и попал в дивный мир хаоса, бардака и разброса в терминах. Не могу держать в себе, давайте разбираться вместе. Начнем, как ни странно, с поиска истины в документации NVIDIA.
Закажите сервер с GPU от AMD. Под капотом: 16-ядерный AMD RyzenTM 9 9950X 3,4 ГГц, видеокарта AMD Radeon RX 7900XT 20 ГБ GDDR6, 48 ГБ RAM DDR5 и 1 000 ГБ SSD NVMe M.2.
Используйте навигацию, если не хотите читать текст полностью:
→ Стоит ли всерьез рассматривать AMD для AI-задач
→ Compute Capability
→ LLVM и NVCC
→ Что еще за SM
→ Virtual vs Actual
→ Подведем небольшой итог
Стоит ли всерьез рассматривать AMD для AI-задач
Справедливости ради отмечу, что вопрос применимости железа и софта от AMD к задачам AI/DL/ML занимает далеко не только меня с коллегами. Для примера: на Reddit можно найти тему «AMD GPUs for Ai?».
Обсуждению уже больше двух лет. То есть еще два года назад люди рассуждали, что перенос CUDA — просто вопрос времени, у AMD есть свой API, а CUDA — это только про NVIDIA, да и вообще, AMD будет вас кормить завтраками, а вот NVIDIA доминирует в AI и ML уже сегодня. Выглядит это все не очень в пользу AMD. Но за пару лет что-то могло и поменяться, не так ли?
Видимо так, потому что 12-го сентября 2022-го года AMD вошла в состав основателей PyTorch Foundation. Позже она начала прикладывать вполне ощутимые усилия к тому, чтобы влиться в AI/DL/ML-тусовку.
Так вот, 14 сентября 2023 наш продакт-менеджер и сооснователь сообщества MLечный путь Антон Чунаев выступил на нашей одноименной открытой встрече с докладом. В нем он в числе прочего упомянул очень показательный кейс применения видеокарт от AMD для обучения больших языковых моделей. По описанию кейса можно предположить, что AMD серьезно вложились в то, чтобы код PyTorch не требовал изменений при перехода от NVIDIA на AMD. Еще спустя какое-то время Антон предложил мне исследовать эту тему глубже. Что ж…
Сразу объясню, зачем нам нырять в терминологические трущобы и пытаться там сориентироваться. С моей точки зрения, это необходимо, чтобы на фундаментальном уровне понимать вопросы, связанные с GPU.
Вы, наверное, не раз натыкались на термины типа «SM», «Compute Capability», «kernel», «Warp», «Thread», «Block», «Grid» и нечто подобное. Особенно если вы пишете код на CUDA. В документации NVIDIA можно найти исчерпывающее описание того, что такое «Compute Capability», да и для термина «kernel» там же есть вполне годное описание. Но поверьте, если вы попытаетесь копнуть чуть глубже, то откроете для себя много нового.

Compute Capability
На термине «Compute Capability» хочется остановиться чуть поподробнее. В документации NVIDIA по поводу данной сущности сказано, что она («compute capability», или вычислительные возможности) представлена номером версии, также иногда называемой «версией SM»:

Если немного поискать в интернете, действительно можно наткнуться на словосочетание «SM version» (например, на форуме разработчиков NVIDIA). Но почти везде всплывают еще два термина: sm_XX и compute_XX.
Есть ли что-нибудь об этом в документации? Ну а как же:

Стало понятнее? Вот и мне нет.
К фразе со скриншота о том, что «двоичный код зависит от архитектуры» («Binary code is architecture-specific»), мы еще вернемся ниже.
Понятно хотя бы, что sm_XX — это одно из допустимых значений параметра -code, который используется при компиляции исходников. А что там со вторым термином — compute_XX? В документации говорится, что, например, compute_90 позволяет использовать функции Compute Capability 9.0, но не Compute Capability 9.0a. В то же время compute_90a позволяет использовать полный набор функций, то есть и 9.0, и 9.0a.

Надо полагать, это тоже является допустимым значением какого-нибудь параметра компилятора. Да, так и есть. В одном из разделов документации в качестве примера упоминается -arch=compute_50 (или выше):

Итак, говорится, что компилятор использует параметр -arch, чтобы указать вычислительные возможности, которые предполагаются в наличии на том устройстве, на котором будет выполняться код. Например, для Warp Shuffle функций предполагаемое значение параметра должно быть равно compute_50.
Тут может возникнуть вопрос: что такое PTX code? А это, собственно, Parallel Thread Execution — некий промежуточный ассемблер, в который перегоняется C/C++ CUDA код, когда мы его компилируем. Если вдруг интересно, то вот к нему документация.
LLVM и NVCC
И вот тут придется чуть-чуть нырнуть непосредственно в то, как происходит компиляция кода с помощью NVCC. А еще в то, как во всем этом безобразии замешан LLVM.
Если коротко, то LLVM — это основа NVCC. Собственно, NVCC предоставляет имплементации компонентов LLVM (а именно — NVPTX Back-end), специфичных для работы с CUDA-исходниками.
Нужны пруфы? Идем на главную страницу NVCC, где сказано примерно все то же самое:

Говоря начистоту, LLVM сам по себе тянет на серию статей, пару десятичасовых лекций и вообще его бы пару-тройку месяцев изучать. Дабы не закапываться в него слишком глубоко, вот вам в помощь классная статья на Хабре о том, что такое LLVM.
Но пойдем дальше. Чуть позже мы еще вернемся к этой теме (спойлер: AMD тоже немного причастен к LLVM). А пока вновь обратимся к документации NVIDIA. Сходу натыкаемся вот на такую замечательную диаграмму (The CUDA Compilation Trajectory):

Ну и уж теперь-то, конечно же… Все равно ничего не понятно. Поэтому продолжим читать документацию.
Ниже натыкаемся на раздел «GPU Generations», где документация начинает разъяснять суть и смысл присутствия терминов sm_XX и compute_XX. В частности, здесь говорится, что надо развивать и улучшать архитектуру. При этом, в силу того, что команды в наборе инструкций определенной архитектуры кодируются по-разному, нельзя на 100% гарантировать, что будет соблюдена совместимость на уровне бинарных файлов.

Напомню еще раз, что «двоичный код зависит от архитектуры» и «компиляция кода, который будет выполняться на GPU, нацелена на конкретные вычислительные возможности GPU, и фича, которая появляется в GPU-шном коде, должна быть доступна в вычислительных возможностях GPU».
Идем дальше и еще раз пропускаем через себя смысл цифр после sm_ и compute_. В частности, находим в документации описание того, что собой представляет XY-нумерация в sm_ и compute_:

XY-нумерация соответствует конкретной версии конкретной архитектуры GPU. X здесь отвечает за поколение GPU, а Y — за версию этого поколения. Плюс, в данную схему наименования сразу закладывается информация о том, какие фичи/функции/возможности доступны для конкретной версии конкретного поколения GPU.
Это как раз и демонстрируется на примере: есть sm_x1y1 и sm_x2y2 и из того, что x1y1 ≤ x2y2, четко следует, что фичи/функции/возможности, доступные в sm_x1y1, уже включены в sm_x2y2.
И кстати, ISA на скриншоте выше — это Instruction Set Architecture. Если вдруг хочется разобраться, что это такое, настоятельно рекомендую посмотреть видео, а мы пока пойдем дальше. Версии ISA конкретно для NVIDIA можно найти в документации.
Что еще за SM
Ниже есть даже табличка с сопоставлениями архитектур и sm’ок:

Возможно, у вас возник вопрос о том, что же такое «SM»? Вопрос хороший и если вы просто попытаетесь погуглить что-нибудь в стиле «SM nvidia doc», то наткнетесь немного не на то:

А вообще, в контексте GPU «SM» означает «Stream(ing) Multiprocessor». Об истории их развития можно почитать отличный глоссарий от компании Modal или заглянуть в блог к Фабьену Санграру. Там есть, в частности, статья об истории стриминг-мультипроцессоров.
Также, дабы немного освежить в памяти понятие, скажем, шейдера и его роли в пайплайне рендеринга, можно прочитать вводную статью по шейдерам. Просто чтобы понять, почему структура графического процессора G71 выглядит следующим образом:

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


«ТУПИК»… Оптимистично, правда?
Собственно, попыткой справиться с нарастающей сложностью анализа узких горлышек архитектуры стала полная переработка всей архитектуры. Так появилась архитектура Tesla и один из ее важнейших компонентов — SM (Stream(ing) Multiprocessor).
Virtual vs Actual
Но вернемся к фразе с одного из скриншотов выше: «if we abstract from the instruction encoding». Вопрос: а как именно мы абстрагируемся от кодирования инструкций? Ведь еще выше было сказано, что наборы инструкций отличаются от поколения к поколению. Да и далее в документации написано, что NVIDIA не может гарантировать двоичную совместимость, не жертвуя возможностями улучшения GPU:

Так как же тогда абстрагироваться от этих «инструкций кодирования»? Ответ на этот вопрос находится еще чуть ниже по тексту документации:

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

И вот у нас получается следующая схема.
- Перегоняя мой C/C++ CUDA код в промежуточную PTX-репрезентацию, я использую compute_XX, чтобы сообщить компилятору, какой набор команд и, соответственно, какую функциональность требует мой код для корректного выполнения.
- Формируя итоговый бинарь, я использую sm_XX, чтобы сообщить компилятору, какой реальный набор команд должен быть использован для преобразования моей ассемблерной репрезентации команды в бинарный код.
Схематично в документации эти стадии изображены следующим образом:

Как думаете, можно ли это считать примером реализации понятного и известного подхода WOCA (Write once, compile anywhere)? Или это, скорее, WORA (Write once, run anywhere)? Или, может, что-нибудь вроде WOTCIARSIYGRL (Write Once Then Compile It And Run Somewhere If You Get Reaaaally Lucky)? В попытках найти ответ читаем документацию дальше:

Здесь NVIDIA сообщает нам, что есть два
Я, честно говоря, немного теряюсь с тем, как именно перевести слово «fatbinary». «Жирный бинарь»? «Толстяк Бин»? Впрочем, как оказалось, это не какая-то локальная придумка от NVIDIA, а вполне себе стандартный термин. Русскоязычного аналога я не нашел и остановился на Толстяке Бине (жирный бинарь звучит как-то обидно). По сути, это программа (или либа), которая была собрана под несколько наборов инструкций и, соответственно, содержит код для них. Собственно, поэтому другое название fatbinary — multiarchitecture binary.
И вот дальше становится интересно: выбор реальной архитектуры, под которую будет создан итоговый бинарь, остается за рантаймом.

Далее документация отсылает к еще одному интересному разделу, который почему-то расположен не в документе по компилятору, а именно 3.1.1. Compilation Workflow, но мы сильно на нем останавливаться не будем и пойдем дальше.
Недостаток JIT-подхода, как справедливо заметили в документации, в том, что он замедляет процесс запуска программы. Так и хочется спросить: а можно ли обойтись без него? И опять же, на это в документации есть ответ: чтобы избежать задержки запуска при использовании JIT, можно указать несколько экземпляров кода (например, sm_50 и sm_52):

Подведем небольшой итог
Кажется, уже можно что-то понять из того, что мы вытащили из документации выше.
- В процессе компиляции исходного кода мы можем пойти двумя путями, выбрав JIT или fatbinary. При желании можно использовать и гибридный подход fatbinary + JIT, который описан в документации.
- Мы можем сгенерировать несколько версий кода, подходящих под разные архитектуры GPU. Некоторые из этих версий могут быть в PTX-формате, а некоторые — в бинарном.
- Все сгенерированное можно уместить в мульти-архитектурный бинарь, который, соответственно, можно будет выполнять на разных GPU.
- Категории sm_XY и compute_XY нужны, чтобы гибко управлять процессом генерации разных версий исполняемого кода.
- X отвечает за конкретное поколение архитектуры, а Y — за конкретную модификацию этого поколения.
- «SM» отвечает за «Stream(ing) Multiprocessor» — ключевой компонент унифицированной архитектуры, впервые введенной NVIDIA в рамках Tesla.
Отдельно на полях выделим, что название Tesla вступает в коллизию с его другим значением, тоже введенным NVIDIA. Ну да ладно — это так, небольшое отступление в сторону.
А на этом предлагаю сделать небольшую паузу. В следующей статье, которая выйдет на следующей неделе, мы посмотрим, что происходит в документации AMD, и постараемся разобраться в их терминологии. Да, путь к оценке перспектив AMD в ML будет долгим, но ведь вас это не пугает?
Комментарии (4)
feanoref Автор
15.05.2025 12:38Вот тут (https://chipsandcheese.com/p/testing-amds-giant-mi300x) можно посмотреть тесты по MI300X.
Выглядит, конечно, впечатляюще, но хотелось бы воспроизвести))
RolexStrider
15.05.2025 12:38Такой длинный пост про AMD - и ни одного упоминания про ROCm. А ведь и на Хабре про него писали: https://habr.com/ru/companies/ruvds/articles/847792/
feanoref Автор
15.05.2025 12:38Да, я видел данную работу.
Все будет!))
Я просто планировал плавненько подойти к ROCm, а перед этим сделать несколько шагов назад и проговорить несколько моментов касательно терминологии.
Всему свое время =)
Alex-Freeman
Хотелось бы увидеть тесты производительности карт amd, но не бытовых как в сервере выше, что вы предлагаете, а нормальных из линейки Instinct MI300/350