Перейти к содержимому
ПереводыMeta, NVIDIA, Mistral, Amazon

Accelerated Computing Hub, часть 2: GPU, CUDA и Python — основы

Перевод вводных глав NVIDIA Accelerated Python User Guide и урока «What Are GPUs»: чем GPU отличается от CPU, как устроены SM, потоки, варпы, блоки и иерархия памяти CUDA, и почему Python-разработчику хватает высокоуровневых библиотек вроде CuPy.

Это перевод вводных материалов NVIDIA из репозитория Accelerated Computing Hub: первых трёх глав Accelerated Python User Guide («Руководство по ускоренному Python») и короткого урока What Are GPUs из учебного курса Accelerated Python. Тексты репозитория распространяются по лицензии CC BY-NC-SA 4.0, поэтому и у этой страницы та же лицензия: можно делиться и переделывать с указанием авторства, некоммерчески и на тех же условиях.

Путеводитель по всему репозиторию — в части 1, практика с CuPy и своими ядрами на Numba — в части 3.


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

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

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

(В оригинале здесь картинка-заставка — куб NVIDIA.)

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

  • одна инструкция, один поток данных (SISD, Single instruction, single data);
  • одна инструкция, много данных (SIMD, Single instruction, multiple data);
  • много инструкций, один поток данных (MISD, Multiple instructions, single data);
  • много инструкций, много данных (MIMD, Multiple instructions, multiple data).

Большинство CPU — многоядерные процессоры с архитектурой MIMD. GPU, напротив, используют архитектуру SIMD. Из-за этого различия GPU хорошо подходят для сильно параллельных задач, где одну и ту же операцию нужно выполнить над множеством элементов данных.

(Примечание переводчика: сама NVIDIA в руководстве по CUDA называет модель исполнения своих GPU SIMT — Single Instruction, Multiple Threads, «одна инструкция, много потоков». Отличие от классического SIMD в том, что программист пишет код для одного потока, а железо группирует потоки по 32 — это варп, см. главу 2 — и исполняет одну инструкцию сразу для всей группы.)

Из-за изначального назначения GPU раньше требовали знания специализированных языков вроде OpenGL. Эти языки использовались только для GPU, учить их было непрактично, и это создавало барьер для использования.

В 2007 году, с выходом платформы NVIDIA CUDA, этот барьер был сломан и доступ к ресурсам GPU стал гораздо шире. CUDA основана на C и предоставляет API, с помощью которого разработчики могут применять GPU для других задач.

(Примечание переводчика: в главе 2 указан ноябрь 2006 года — тогда CUDA анонсировали вместе с GPU G80; первые версии инструментов для разработчиков вышли в 2007-м.)

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

Центральные процессоры (CPU, Central Processing Units) — самый распространённый тип процессоров в современных компьютерах; долгое время они были «рабочей лошадкой» аналитики данных. CPU состоят из нескольких ядер, которые выполняют инструкции и последовательно, и параллельно, — поэтому CPU универсальны и подходят для широкого круга задач.

  • Универсальность. CPU справляются с широким кругом задач общего назначения: логическими сравнениями, пересылкой данных между регистрами, работой с файлами. CPU быстрее GPU в обработке данных в оперативной памяти, операциях ввода-вывода и администрировании операционной системы.
  • Многозадачность. CPU могут выполнять несколько задач одновременно.
  • Высокая тактовая частота. Обычно частота у CPU выше, чем у GPU, поэтому однопоточные задачи CPU выполняет быстрее.
  • Гибкость. CPU умеют переключаться между несколькими задачами и быстро менять контекст.
  • Кэш-память. У CPU большая локальная кэш-память.
  • Совместимость. CPU совместимы со всеми типами систем, а GPU может потребоваться специальное оборудование.
  • Ограниченный параллелизм. CPU не рассчитаны на сильно параллельные нагрузки.
  • Высокое энергопотребление. CPU могут потреблять много энергии, что увеличивает эксплуатационные расходы.
  • Ограниченная пропускная способность памяти. По сравнению с GPU у CPU пропускная способность памяти ниже, поэтому на больших наборах данных они работают медленнее.
  • Параллельная обработка. CPU хуже справляются с задачами, где нужны миллионы одинаковых операций.
  • Ограниченное развитие. CPU — очень зрелая технология, у неё меньше потенциала для улучшения, чем у GPU.

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

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

  • Параллельная обработка. GPU выполняют тысячи вычислений одновременно над множеством элементов данных. Сотни ядер GPU позволяют вести массово-параллельные вычисления — например, умножение матриц.
  • Высокая пропускная способность памяти. GPU быстрее обращаются к данным и обрабатывают их.
  • Энергоэффективность. GPU могут потреблять меньше энергии, чем CPU, что снижает эксплуатационные расходы.
  • Настраиваемые архитектуры. GPU можно настраивать под конкретные приложения и нагрузки, они гибкие и адаптируемые.
  • Научные вычисления. GPU дают огромное ускорение в специализированных задачах вроде генеративного ИИ, глубокого обучения и обработки больших данных.
  • Ограниченная многозадачность. GPU умеют повторять одну задачу в огромном масштабе, но не умеют выполнять задачи общего назначения так, как CPU.
  • Сложность. GPU плохо справляются с ветвлениями, последовательными операциями и другими сложными шаблонами программирования.
  • Совместимость ПО. Чтобы GPU работали эффективно, может понадобиться дополнительное программирование или оптимизация.

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

(Примечание переводчика: «энергоэффективность» здесь — в пересчёте на операцию: одна видеокарта потребляет сотни ватт, но на матричных вычислениях делает на порядки больше операций за ватт, чем CPU.)

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

flowchart TB
subgraph GPU["GPU"]
  subgraph PC1["Кластер (PC)"]
    SM1["SM: ядра + L1"]
    SM2["SM: ядра + L1"]
  end
  subgraph PC2["Кластер (PC)"]
    SM3["SM: ядра + L1"]
    SM4["SM: ядра + L1"]
  end
  L2["Общий кэш L2"]
end
MEM[("Глобальная память GPU<br/>GDDR / HBM")]
SM1 & SM2 & SM3 & SM4 --> L2 --> MEM
Упрощённая схема архитектуры GPU (перерисовано переводчиком по рисунку gpu-arch.png из оригинала)

Один GPU состоит из нескольких кластеров процессоров (PC, Processor Clusters), в каждом из которых несколько потоковых мультипроцессоров (SM, Streaming Multiprocessors). В каждом SM есть кэш инструкций первого уровня и связанные с ним ядра. Обычно SM использует свой кэш первого уровня (L1) и общий кэш второго уровня (L2), прежде чем обращаться к глобальной памяти GDDR-5 (или GDDR-6 в более новых моделях GPU). Архитектура GPU терпима к задержкам памяти.

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

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

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

(Примечание переводчика: у серверных GPU (A100, H100 и новее) вместо GDDR стоит память HBM с гораздо большей пропускной способностью. Число SM у своей видеокарты можно узнать в PyTorch: torch.cuda.get_device_properties(0).multi_processor_count — у RTX 4080 их 76.)

Вариантов много: можно выбрать потребительские GPU, GPU для дата-центров или готовые рабочие станции.

(В оригинале — рисунок с тремя классами оборудования.)

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

Актуальная информация о видеокартах NVIDIA GeForce: https://www.nvidia.com/en-us/geforce/graphics-cards/

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

Актуальная информация о решениях NVIDIA для дата-центров: https://www.nvidia.com/en-us/data-center/data-center-gpus/

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

Актуальная информация о серверах NVIDIA DGX: https://www.nvidia.com/en-us/data-center/dgx-platform/

GPU обычно применяют для:

  • научного моделирования и симуляций;
  • машинного обучения и нейросетей;
  • глубокого обучения.

GPU вместе с CPU быстро выполняют численно тяжёлые операции вроде научных симуляций и моделирования. Конструкция GPU традиционно нацелена на максимум блоков операций с плавающей точкой и на операции с многомерными массивами. GPU особенно хороши там, где полезны линейная алгебра и матричные операции. Это возможно благодаря тензорным ядрам (Tensor Cores) GPU — блокам, оптимизированным для умножения матриц.

Реализации на GPU есть для многих задач научных вычислений, включая молекулярное моделирование, гидродинамику, астрофизику и климатические симуляции.

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

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

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

Глубокое обучение с массово-параллельными входами

Заголовок раздела «Глубокое обучение с массово-параллельными входами»

После появления CUDA было разработано несколько фреймворков глубокого обучения, например PyTorch и TensorFlow. Они скрывают сложность прямого программирования на CUDA и сделали вычисления на GPU доступными для современного глубокого обучения.

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

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

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

(Примечание переводчика: «до трёх раз» — очень осторожная оценка. На умножении больших матриц современный GPU с тензорными ядрами быстрее серверного CPU в десятки и сотни раз; поэтому LLM и обучают на GPU. Ваш собственный замер — в части 3 и в уроке «Как устроена видеокарта».)


В ноябре 2006 года NVIDIA® представила CUDA® (Compute Unified Device Architecture) — платформу параллельных вычислений общего назначения и модель программирования. Она использует параллельный вычислительный движок GPU NVIDIA, чтобы решать многие сложные вычислительные задачи эффективнее, чем на CPU.

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

Чтобы выполнить функцию (её называют ядром, kernel) на GPU, в научном коде обычно нужны три главных шага:

  1. Скопировать входные данные из памяти CPU в память GPU.
  2. Загрузить и выполнить ядро на GPU.
  3. Скопировать результаты из памяти GPU в память CPU.

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

sequenceDiagram
participant CPU as CPU и его память (host)
participant GPU as GPU и его память (device)
CPU->>GPU: 1. Копируем входные данные
CPU->>GPU: 2. Запускаем ядро
Note over GPU: тысячи потоков<br/>выполняют ядро
GPU->>CPU: 3. Копируем результат
Три шага запуска вычислений на GPU (схема переводчика)

CUDA работает по модели гетерогенных вычислений, в которой участвуют и CPU, и GPU.

Ядро CUDA — глобальная функция, которая выполняется на GPU. Параллельная часть вашего приложения выполняется K раз параллельно K разными потоками CUDA — в отличие от обычных последовательных функций на CPU, которые выполняются один раз.

В CUDA ядро выполняется потоками (threads). Поток — абстрактная сущность, которая представляет выполнение ядра. Это наименьшая единица исполнения в CUDA, у каждого потока есть уникальный идентификатор (Thread ID).

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

Группа потоков называется блоком CUDA (block). Потоки блока могут взаимодействовать через разделяемую память и синхронизировать доступ к памяти по ходу исполнения. Блоки CUDA объединяются в сетку (grid). Ядро исполняется как сетка из блоков потоков. Все блоки массива выполняют одно и то же ядро, и параллельное исполнение масштабируется на все потоки.

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

flowchart LR
K["Ядро (kernel)"] --> G["Сетка (grid)"]
G --> B1["Блок 0"] & B2["Блок 1"] & B3["Блок …"]
B1 --> W1["Варп 0:<br/>потоки 0–31"] & W2["Варп 1:<br/>потоки 32–63"]
B1 -.->|"исполняется<br/>на одном"| SM["SM"]
Иерархия исполнения: ядро → сетка → блоки → варпы по 32 потока (по рисунку gpu-kernel-exec.png из оригинала)

Иерархическая структура этих компонентов не только помогает управлять исполнением потоков, но и отражает физическую архитектуру GPU с поддержкой CUDA. Потоки исполняются одновременно на нескольких потоковых мультипроцессорах (SM).

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

Основные виды:

  • Глобальная память (global memory) — доступна всем потокам.
  • Разделяемая память / кэш L1 (shared memory) — общая для потоков одного блока.
  • Локальная память (local) — личная для каждого потока, но физически часть глобальной памяти.
  • Регистры (registers) — личные для каждого потока.

(В оригинале — рисунок с видами памяти.)

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

  • Константная память (constant memory) — кэшируемая память только для чтения, доступная всем потокам. У GPU NVIDIA разделяемая память, кэш L1 и кэш константной памяти находятся внутри потокового мультипроцессора. Поэтому они быстрее кэша L2 и оперативной памяти GPU.
  • Текстуры (textures) — память только для чтения, оптимизированная для фильтрации, методов интерполяции и произвольного доступа всеми потоками.

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

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

flowchart TB
R["Регистры<br/>(свои у каждого потока)"] --> S["Разделяемая память / L1<br/>(общая в блоке, внутри SM)"]
S --> L2["Кэш L2<br/>(общий для всех SM)"]
L2 --> GM["Глобальная память GPU<br/>(видна всем потокам)"]
GM <-->|"PCIe"| HM["Память CPU (host)"]
Иерархия памяти: чем выше, тем быстрее и меньше (по рисунку gpu-memory-hierarchy.png из оригинала)

Архитектура создаёт пул управляемой памяти (managed memory): каждый блок, выделенный из этого пула, доступен и на хосте, и на устройстве по одному и тому же адресу или указателю. Система сама переносит данные между хостом и устройством.

Умение эффективно передавать данные между CPU и GPU — ключ к высокой производительности приложений, ускоренных на GPU. Передача данных между CPU и GPU идёт по шине PCIe, и это может ограничивать скорость доступа к локальной памяти и на CPU, и на GPU.

Эти виды выделения памяти приведены в главе для справки, но подробности исполнения ядер, потоков и управления памятью обычно скрыты во многих высокоуровневых пакетах CUDA Python. Сгенерированный шаблонный код и разумные значения по умолчанию дают производительный код без обращения к низкоуровневым функциям CUDA. Python-разработчики, которые не знакомы с C++, оценят и «питоничность» этих API.

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


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

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

Кроме того, потоки Python ведут себя не так, как потоки операционной системы, потому что в интерпретаторе одновременно может выполняться только один поток. Глобальная блокировка интерпретатора (GIL, Global Interpreter Lock), по сути, не даёт потокам работать параллельно, и потоки в Python ведут себя как на одноядерном CPU. Поэтому Python-программистам часто приходится добиваться параллелизма через конкурентные программы (например, несколько процессов), а не через многопоточность.

Многие внешние библиотеки Python написаны на других языках, например C или C++, чтобы пользоваться многопоточностью. Одна из них — NumPy, библиотека, на которой строятся численные вычисления в Python.

(Примечание переводчика: начиная с Python 3.13 есть экспериментальная сборка без GIL (free-threaded), но на момент перевода основная сборка Python по-прежнему с GIL.)

NumPy в экосистеме научных вычислений на Python

Заголовок раздела «NumPy в экосистеме научных вычислений на Python»

NumPy (сокращение от Numerical Python) появился в 2005 году в результате слияния библиотеки Numarray с Numeric. С тех пор открытая библиотека NumPy стала незаменимой для научных вычислений на Python. Она — фундамент многих других научных библиотек: SciPy, Scikit-learn, Pandas и других. Научное сообщество так ценит NumPy за удобный интерфейс Python для эффективной работы с многомерными массивами; структуру данных «массив NumPy» называют также ndarray — сокращение от n-dimensional array, «n-мерный массив».

NumPy в основном написан на C, а Python служит «языком-клеем». Но главная причина его эффективности в численных вычислениях в том, что массивы NumPy занимают непрерывные блоки памяти, которые CPU умеет эффективно кэшировать. Списки Python, напротив, — это массивы указателей на объекты, разбросанные по памяти в случайных местах; их трудно кэшировать, и поиск в памяти обходится дороже. Но за вычислительную эффективность и малый расход памяти приходится платить: массивы NumPy имеют фиксированный размер и однородны, то есть все элементы должны быть одного типа. Однородность ndarray позволяет NumPy выполнять операции эффективным кодом на C и избегать дорогих проверок типов и других накладных расходов API Python. Добавлять и удалять элементы в конце списка Python очень дёшево, а вот менять размер массива NumPy очень дорого: для этого нужно создать новый массив и перенести в него содержимое старого, который мы хотим увеличить или уменьшить.

Кроме того, что NumPy эффективнее обычного кода на Python для численных вычислений, он ещё и позволяет писать изящнее и понятнее благодаря векторизованным операциям и broadcasting (их мы разберём дальше).

(Примечание переводчика: векторизация и broadcasting подробно разобраны в уроке «Broadcasting и векторизация».)

(В оригинале — два рисунка: области, где используют NumPy, и экосистема научного Python, в центре которой NumPy.)

Сегодня NumPy — основа экосистемы научных вычислений на Python (см. numpy.org). Уже разрабатываются версии этих фундаментальных библиотек с ускорением на GPU, но возможность воспользоваться такими оптимизациями есть у всех научных библиотек.

Класс cupy.ndarray — ядро библиотеки CuPy и замена классу numpy.ndarray из NumPy. Поэтому CuPy — отличная точка входа в экосистему CUDA для Python, особенно для разработчиков научных библиотек, чьи программы зависят от NumPy.

Цель проекта CuPy — дать пользователям Python ускорение на GPU без глубокого знания нижележащих технологий GPU. Команда CuPy стремится обеспечить:

  • полное покрытие API NumPy и SciPy, чтобы CuPy был полноценной заменой «один в один» (drop-in replacement), а также продвинутые возможности CUDA для максимальной производительности;
  • зрелую и качественную библиотеку как фундаментальный пакет для всех проектов, которым нужно ускорение, — от лабораторного окружения до крупного кластера.

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

Под капотом CuPy использует библиотеки CUDA Toolkit — cuBLAS, cuRAND, cuSOLVER, cuSPARSE, cuFFT, cuDNN и NCCL, — чтобы в полной мере задействовать архитектуру GPU.


Дополнение: что такое GPU (урок 00 курса Accelerated Python)

Заголовок раздела «Дополнение: что такое GPU (урок 00 курса Accelerated Python)»

Оригинал: tutorials/accelerated-python/notebooks/fundamentals/00__what_are_gpus.ipynb.

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

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

(В оригинале — рисунок сравнения CPU и GPU.)

Ядро (kernel) похоже на функцию: это блок кода, который принимает входные данные и выполняется процессором.

Отличия ядра от функции:

  • ядро ничего не может вернуть — вместо этого оно должно изменить память;
  • для ядра нужно указать иерархию потоков (потоки и блоки).

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

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

(В оригинале — рисунок из руководства по программированию CUDA C++: сетка из блоков, блоки из потоков.)

Выполнять код на GPU — это очень похоже на выполнение кода на втором компьютере по сети.

Если бы я хотел отправить программу на Python на другую машину и выполнить её там, мне понадобились бы:

  • способ скопировать данные и код на удалённую машину (SCP, SFTP, SMB, NFS и т. д.);
  • способ войти и запустить программы на удалённой машине (SSH, VNC, удалённый рабочий стол и т. д.).

Чтобы сделать то же самое с GPU, нужно использовать CUDA через шину PCI. Но идея та же: перенести данные и код на устройство и выполнить этот код.

(В оригинале — две схемы: «два компьютера в сети» и «компьютер и GPU, связанные через CUDA».)

CUDA (Compute Unified Device Architecture) — платформа параллельных вычислений и модель программирования, созданная NVIDIA. Она позволяет разработчикам использовать GPU NVIDIA для вычислений общего назначения, а не только для графики, и задействовать массовый параллелизм GPU, чтобы значительно ускорять вычислительно тяжёлые приложения в искусственном интеллекте, научных симуляциях и анализе данных.

CUDA Python — точка доступа к платформе CUDA из Python. Она состоит из нескольких компонентов:

  • cuda.core — «питоничный» доступ к среде выполнения CUDA и другим базовым возможностям;
  • cuda.bindings — низкоуровневые привязки Python к C API CUDA;
  • cuda.pathfinder — утилиты, которые находят компоненты CUDA, установленные в окружении Python;
  • cuda.coop — модуль Python с переиспользуемыми примитивами CCCL уровня блока и варпа для ядер Numba CUDA;
  • cuda.compute — модуль Python для удобного доступа к высокоэффективным настраиваемым параллельным алгоритмам CCCL (сортировка, префиксная сумма, свёртка, преобразование и т. д.), которые вызываются с хоста;
  • numba.cuda — цель компиляции Numba для программирования GPU на CUDA: ограниченное подмножество Python напрямую компилируется в ядра CUDA и функции устройства по модели исполнения CUDA;
  • nvmath-python — «питоничный» доступ к математическим библиотекам NVIDIA для CPU и GPU, с API и для хоста, и для устройства (через nvmath.device). Также даёт низкоуровневые привязки Python к C API хоста (через nvmath.bindings).

ENRU
GPU, CPUграфический процессор, центральный процессор
host, deviceхост (CPU и его память), устройство (GPU)
Streaming Multiprocessor (SM)потоковый мультипроцессор
kernelядро (функция, исполняемая на GPU)
thread, warp, block, gridпоток, варп (32 потока), блок, сетка
SIMD / SIMTодна инструкция — много данных / много потоков
global / shared / local memoryглобальная / разделяемая / локальная память
registersрегистры
latency, bandwidth, throughputзадержка, пропускная способность (памяти), производительность (пропускная способность вычислений)
Tensor Coresтензорные ядра
managed memoryуправляемая (унифицированная) память
Global Interpreter Lock (GIL)глобальная блокировка интерпретатора
drop-in replacementзамена «один в один»