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.
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.x | cuda.grid(1) |
| threadIdx.x / blockIdx.x / blockDim.x | cuda.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 H2D | d_a = cuda.to_device(a) |
| cudaMemcpy D2H | a = 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, малые данные
своём или в Colab
Все Python-примеры этой и следующих страниц выполнены именно так — в симуляторе, на машине без NVIDIA (результаты сверены с эталонами; скрипты — в папке сode). Настоящую скорость даст Colab из C01: тот же блокнот, только вместо %%writefile и nvcc — просто ячейки с @cuda.jit; Toolkit там уже стоит, T4 к вашим услугам.
4. Тренажёр: переводчик C++ → Numba
Дана строка из CUDA C++ линии C. Выберите её точный Numba-эквивалент — один из вариантов содержит типичную ошибку перевода.
Контрольные вопросы
-
Общее — всё существенное: код одного треда, запуск гридом из блоков, те же threadIdx/blockIdx, невозможность вернуть значение, результат через переданные массивы. Различия синтаксические: декоратор вместо спецификатора, kernel[grid, block] вместо тройных скобок, cuda.grid(1) как готовая формула индекса.
-
cuda.grid(1) возвращает глобальный индекс треда — blockIdx.x·blockDim.x + threadIdx.x. cuda.grid(2) — пару (x, y) для двумерного грида: обе координаты по той же формуле по своим осям. Это сокращения; развёрнутые встроенные переменные тоже доступны.
-
Numba неявно копирует его в память устройства перед запуском и результат обратно после. Удобно для прототипа, но копирование происходит на каждый вызов ядра — для цепочек ядер и замеров нужно явное управление через cuda.to_device / copy_to_host (P05).
-
Подходит для проверки корректности и отладки: ядра выполняются интерпретатором на CPU, работают print и отладчик, видеокарта не нужна вовсе. Не подходит для замеров: выполнение в тысячи раз медленнее и устроено принципиально иначе — тайминги симулятора не значат ничего.
-
Ядро тоже компилируется в машинный код (PTX) через тот же вывод типов — динамические структуры Python в него не помещаются. Добавляется специфика устройства: нельзя создавать массивы внутри ядра, вызывать можно только device-функции, стиль — скаляры и индексация, код одного треда.
Источники
- Numba for CUDA GPUs. Overview; Writing CUDA Kernels // Numba Documentation : [сайт]. — URL: https://numba.readthedocs.io/en/latest/cuda/kernels.html (дата обращения: 09.07.2026).
- Debugging CUDA Python with the the CUDA Simulator // Numba Documentation : [сайт]. — URL: https://numba.readthedocs.io/en/latest/cuda/simulator.html (дата обращения: 09.07.2026).
- Supported Python features in CUDA Python // Numba Documentation : [сайт]. — URL: https://numba.readthedocs.io/en/latest/cuda/cudapysupported.html (дата обращения: 09.07.2026).