Траектория «Параллельные вычисления» · линия P · конспект 4 из 6

CUDA без C++: ядра на Python

О чём эта тема
Декоратор @cuda.jit: те же ядра, гриды, блоки и треды, что в линии C, — но на Python, без nvcc и Visual Studio. Плюс симулятор, позволяющий отлаживать CUDA-код на машине без видеокарты вообще.
Аннотация
Numba умеет компилировать не только для процессора: @cuda.jit транслирует функцию в PTX — тот самый ассемблер NVIDIA из конспекта C01 — и запускает её на видеокарте как настоящее ядро. Конспект строит мост между линиями: сложение векторов из C04 переписывается на Python строка в строку, конфигурация запуска записывается в квадратных скобках kernel[blocks, threads], а формула глобального индекса сворачивается в вызов cuda.grid(1). Сводная таблица соответствий C++ ↔ Numba покрывает всё, что встречалось в линии C. Отдельный раздел — симулятор NUMBA_ENABLE_CUDASIM: ядра выполняются на CPU медленно, зато с print и отладчиком, и именно так проверены все примеры конспекта (машина автора — без NVIDIA). Тренажёр — переводчик: по строке CUDA C++ выбрать её Numba-эквивалент.
Пререквизиты
C02 и C04 — модель грид/блок/тред и формула индексации: здесь они те же, меняется только синтаксис. P01 — как Numba компилирует.
Мотивация
Линия C дала понимание, как устроена видеокарта, — ценой C++, nvcc и ручного управления памятью. Но модель CUDA не привязана к языку: грид остаётся гридом, варп — варпом. Numba позволяет применять всё выученное, не покидая Python: прототип ядра за минуты, numpy-массивы вместо malloc, и тот же ноутбук Colab. Это не «CUDA для бедных» — это та же CUDA с другой клавиатурой: PTX на выходе честный, скорость ядер — сопоставимая.

1. То же ядро, другой язык

Возьмём сложение векторов из C04 и поставим два листинга рядом. C++:

__global__ void vecAdd(const float* a, const float* b, float* c, int n)
{
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    if (i < n) c[i] = a[i] + b[i];
}
// запуск: vecAdd<<<grid, block>>>(da, db, dc, n);

Python с Numba:

from numba import cuda
import numpy as np

@cuda.jit                          # «__global__ по-питоновски»
def vec_add(a, b, c):
    i = cuda.grid(1)               # = blockIdx.x*blockDim.x + threadIdx.x
    if i < c.size:                 # та же защита от хвостовых тредов
        c[i] = a[i] + b[i]

n = 1000
a = np.arange(n, dtype=np.float32)
b = 2 * a
c = np.zeros(n, dtype=np.float32)

threads = 256
blocks = (n + threads - 1) // threads          # округление вверх — как в C04
vec_add[blocks, threads](a, b, c)              # конфигурация — в квадратных скобках

print(np.allclose(c, 3 * a))                   # True — проверено

Отличий по существу три. Конфигурация запуска пишется в квадратных скобках перед аргументами — kernel[blocks, threads] вместо тройных угловых. Формула глобального индекса спрятана в cuda.grid(1) — но никуда не делась: развёрнутые blockIdx/blockDim/threadIdx тоже доступны, и для двумерного грида px, py = cuda.grid(2) возвращает пару координат сразу. И третье: мы передали в ядро обычные numpy-массивы — Numba молча скопировала их на устройство и обратно. Удобно для старта, дорого для скорости: явное управление копированием данных — тема P05.

Типичная ошибка Ждать от @cuda.jit-ядра возвращаемого значения или писать в нём операции над целыми массивами (c = a + b). Ядро — это код одного треда: возвращать ему нечего и некому (знакомо по C02), а массивные операции внутри ядра не поддерживаются — каждый тред трогает свои элементы по индексу. Весь стиль мышления — из линии C, Python лишь укорачивает записи.

2. Словарь соответствий C++ ↔ Numba

Всё, что встречалось в линии C, переводится механически:

CUDA C++Numba
__global__ void kernel(...)@cuda.jit def kernel(...)
__device__ float f(...)@cuda.jit(device=True) def f(...)
kernel<<<grid, block>>>(args)kernel[grid, block](args)
blockIdx.x * blockDim.x + threadIdx.xcuda.grid(1)
threadIdx.x / blockIdx.x / blockDim.xcuda.threadIdx.x / cuda.blockIdx.x / cuda.blockDim.x
dim3 grid(gx, gy); dim3 block(bx, by)kernel[(gx, gy), (bx, by)](args)
__shared__ float t[256]t = cuda.shared.array(256, dtype=float32)
__syncthreads()cuda.syncthreads()
atomicAdd(&x, v)cuda.atomic.add(x, i, v)
cudaMalloc + cudaMemcpy H2Dd_a = cuda.to_device(a)
cudaMemcpy D2Ha = d_a.copy_to_host()
cudaDeviceSynchronize()cuda.synchronize()

Не переводится (и это важно знать заранее): внутри ядра нельзя создавать numpy-массивы, вызывать не-device-функции, использовать списки и словари — ядро подчиняется тем же ограничениям nopython-режима, что и @njit (P01), плюс своим: код одного треда, скаляры и индексация. Печать print внутри ядра работает — как printf в C01, с той же оговоркой про синхронизацию.

3. Симулятор: CUDA без видеокарты

У Numba есть режим, которого лишена CUDA C++: симулятор. Если до импорта numba установить переменную окружения, все ядра будут выполняться обычным Python-кодом на CPU — тред за тредом, блок за блоком:

# вариант 1: в терминале
#   Windows (PowerShell):  $env:NUMBA_ENABLE_CUDASIM = "1"
#   Linux/macOS:           export NUMBA_ENABLE_CUDASIM=1

# вариант 2: в начале скрипта, ДО импорта numba
import os
os.environ["NUMBA_ENABLE_CUDASIM"] = "1"
from numba import cuda

print(cuda.is_available())      # True — «устройство» есть, хоть и нарисованное

Симулятор чудовищно медленный — он для корректности, не для скорости. Зато внутри «ядра» работают print для любого треда, точки останова отладчика и вся интроспекция Python: можно остановиться в треде (5, 3) блока (2, 1) и рассмотреть его переменные — роскошь, недоступная настоящему железу. Рабочий цикл получается такой:

написать ядро
и мини-тест к нему
отладить в симуляторе
CUDASIM=1, малые данные
запустить на GPU
своём или в Colab

Все Python-примеры этой и следующих страниц выполнены именно так — в симуляторе, на машине без NVIDIA (результаты сверены с эталонами; скрипты — в папке сode). Настоящую скорость даст Colab из C01: тот же блокнот, только вместо %%writefile и nvcc — просто ячейки с @cuda.jit; Toolkit там уже стоит, T4 к вашим услугам.

Типичная ошибка Мерить производительность в симуляторе. CUDASIM выполняет треды по одному интерпретатором — он в тысячи раз медленнее и настоящей видеокарты, и обычного @njit; любые его тайминги бессмысленны. Симулятор отвечает на вопрос «правильно ли ядро считает», и только Colab или живая карта — на вопрос «насколько быстро».

4. Тренажёр: переводчик C++ → Numba

Тренажёр · переведите строку

Дана строка из CUDA C++ линии C. Выберите её точный Numba-эквивалент — один из вариантов содержит типичную ошибку перевода.

Серия не начата.

Контрольные вопросы

Источники

  1. Numba for CUDA GPUs. Overview; Writing CUDA Kernels // Numba Documentation : [сайт]. — URL: https://numba.readthedocs.io/en/latest/cuda/kernels.html (дата обращения: 09.07.2026).
  2. Debugging CUDA Python with the the CUDA Simulator // Numba Documentation : [сайт]. — URL: https://numba.readthedocs.io/en/latest/cuda/simulator.html (дата обращения: 09.07.2026).
  3. Supported Python features in CUDA Python // Numba Documentation : [сайт]. — URL: https://numba.readthedocs.io/en/latest/cuda/cudapysupported.html (дата обращения: 09.07.2026).