Назад к блогу

NVIDIA добавила в Rust два трека для написания GPU-ядер

NVIDIA добавила в Rust два трека для написания GPU-ядер

NVIDIA представила CUDA Rust — ранний исследовательский набор инструментов для написания GPU-ядер на Rust с нативной компиляцией в PTX, охватывающий как классическую SIMT-модель, так и тайловые вычисления. Проект переносит правила владения и заимствования Rust за границу запуска на GPU, что открывает путь к безопасной работе с памятью устройства без отказа от привычных языковых гарантий. Материал будет полезен тем, кто следит за проникновением Rust в HPC и хочет разобраться, как устроена генерация PTX и сбор device-функций изнутри.

NVIDIA выпустила CUDA Rust — платформу для написания GPU-ядер на Rust с нативной компиляцией в PTX. Это не отдельный крейт, а набор host-side крейтов для работы с драйвером CUDA и device-side крейтов и инструментов. Внутри — две модели программирования ядер и общий runtime.

Платформа описана как ранний исследовательский проект: ожидаются баги, неполные функции и ломающие изменения API.

Что именно изменилось

CUDA Rust включает две модели для написания ядер.

cuda-oxide — кастомный codegen-бэкенд rustc, cargo-утилита и крейты для традиционной модели SIMT (Single Instruction, Multiple Threads). Здесь пользователю доступны все детали device-side CUDA-программирования.

cutile-rs — proc-macro плагин и крейты для модели CUDA Tile. Вычисления выполняются над тайлами, а не над скалярами.

Общий runtime образуют крейты cuda-bindings, cuda-core (с cuda-core-derive) и cuda-async: привязки к драйверу, контексты, потоки, буферы устройства, события и ленивые композируемые операции.

Обе модели распространяют правила владения Rust за границу запуска на GPU: изменяемые буферы разбиваются на непересекающиеся части до запуска, неизменяемые — разделяются, а лаунчеры удерживают эти заимствования, пока работа на GPU выполняется.

Предыстория

Rust-GPU/rust-cuda — экосистема библиотек и инструментов для написания и запуска GPU-кода полностью на Rust. Проект находился в ранней разработке и предупреждал об ошибках, проблемах безопасности и неработающих вещах.

NVIDIA отдельно указывает, что Rust на GPU — не новость, и что существуют работы, предшествующие их проекту и продолжающиеся параллельно; они работают с мейнтейнерами rust-cuda по мере развития обоих проектов.

Как устроен codegen-бэкенд cuda-oxide

Главная точка входа бэкенда — codegen_crate, где происходит разделение device- и host-кода. Сначала он получает мономорфизированные элементы, затем считает ядра (функции в зарезервированном пространстве имён cuda_oxide_kernel_) и device-функции, и определяет, содержит ли крейт device-код:

contains_device_code = kernel_count > 0 || device_fn_count > 0

Если device-код есть, обход идёт по графу вызовов от ядер, а затем device-код генерируется через stable_mir и пайплайн MIR: тело каждой функции транслируется в dialect-mir, после чего результат понижается до представления, близкого к машинному коду, и из него получается PTX. Весь host-код делегируется LLVM-бэкенду.

Как работает сбор device-функций

Сбор начинается с поиска точек входа ядра: перебираются CGU, и для каждого элемента-функции проверяется, является ли он ядром. Найденные ядра добавляются как корни, после чего обход идёт по графу вызовов из каждого ядра. Если ядер нет, сканируются отдельные device-функции — они добавляются как некорневые корни и попадают в PTX как .func, а не как .entry, то есть их нельзя запустить напрямую с хоста, но можно вызывать из ядер и других device-функций.

Крейт std запрещён, потому что это OS, I/O, threads — они не могут работать на GPU. Два исключения: собственные float-обёртки std собираются как обычные device-функции, а std::sys::cmath-шимы пропускаются и понижаются до libdevice.

Хост-CPU-интринсики запрещены, так как MIR ядра приходит из host-target сессии, и модуль core::arch хоста виден device-коду без PTX-понижения. Проверка отсекает всё под ::core_arch::, кроме nvptx, simd и macros.

Как генерируется PTX

PTX генерируется вызовом llc, которому всегда передаётся безусловный набор аргументов: выбор цели и два флага, отключающих позднее свёртывание ветвлений.

Флаг -switch-to-lookup=false отключает преобразование плотного switch в таблицу констант, потому что при результатах-указателях на shared-память такая таблица недопустима: ptxas отвергает обе кодировки с ошибкой «Variable used as initial value not in .global or .const state space», а на GPU модуль падает при JIT с ошибкой 218. Вместо таблицы switch сохраняет ветвевую форму, и llc понижает плотные switch в таблицы меток brx.idx, которые PTX кодирует корректно.

Флаги -disable-branch-fold и -disable-block-placement отключают поздние машинные проходы, чтобы циклы сохраняли двухпереходную разметку, которую распознаёт SASS-разворачиватель ptxas; свёрнутая форма с отрицанием условия ломает это распознавание. Оба флага — generic disable-only опции, они пропускают преобразования разметки и не меняют семантику.

Измерения показывают, что свёрнутая разметка снижает регистры с 38 до 26 и FFMA с 18 до 4, теряя 26% пропускной способности (7218 → 5708 GFLOPS), тогда как с флагами — 38 регистров и 7226 GFLOPS.

Макросы и атрибуты cuda_device

#[kernel] помечает функцию как CUDA-ядро: сохраняет имя функции в бинарнике и помечает функцию для обнаружения бэкендом. Поддерживает параметры: #[grid_constant] для передачи значения по значению в параметрах запуска, список типов для обобщённых ядер (создаёт по одной PTX-точке входа на каждый тип), launch_context для именованной привязки, unchecked_indexing для удаления проверок границ при индексации, и разворачивание циклов — полное для циклов с известным на этапе компиляции числом итераций или по N итераций работы за проход с остаточным циклом для остатка.

#[device] помечает функцию как CUDA-устройственную: сохраняет имя функции в бинарнике, переименовывает функцию в зарезервированное пространство имён и помечает её для извлечения бэкендом. Такие функции могут возвращать значения, вызываться из ядер и других device-функций, использовать обобщения и разворачивание циклов.

#[launch_bounds] задаёт границы запуска ядра, испуская директивы PTX .maxntid и .minnctapersm. Первый параметр обязателен (максимум потоков на блок), второй опционален (минимум блоков на SM).

#[launch_contract] объявляет контракт геометрии запуска и ресурсов. #[cuda_module] использует это для генерации подготовленного пути запуска, а соотношения размеров буферов и скаляров, которые должны выполняться для доступа в границах, проверяются на CPU перед запуском.

#[cluster_launch] задаёт размеры кластера блоков на этапе компиляции, испуская директиву PTX .reqnctapercluster. Поддерживает 1D, 2D и 3D формы, требует sm_90+ и внедряет вызов конфигурации кластера в начало ядра.

#[cooperative_launch] помечает ядро для кооперативного запуска, что гарантирует одновременное присутствие всех блоков сетки на устройстве — это предварительное условие для сетевых барьеров. Он ничего не меняет в PTX, а #[cuda_module] направляет все сгенерированные методы запуска через расширенный вызов запуска с установленным кооперативным атрибутом.

Как устроен Tile-трек cutile-rs

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

Макрос #[cutile::module] встраивает AST ядра в host-бинарь и компилирует его при первом фактическом запуске ядра. Host и device код живут в одном файле, собираются одной командой и не требуют отдельного kernel-крейта.

В примере модуль содержит функцию add с атрибутом #[cutile::entry()], которая принимает Tensor<f32, {[B]}> как эксклюзивный выход и Tensor<f32, {[-1]}> как общие входы, где -1 — динамическая размерность, разрешаемая при запуске. Ленивые операции api::ones и api::zeros ничего не трогают на GPU. Разбиение на тайлы по 128 элементов даёт каждому тайлу эксклюзивное владение своим куском, фиксирует сетку в 1024/128 = 8 тайлов и задаёт B. Ничего не выполняется на GPU до синхронизации с потоком.

Почему это сделали

Systems layer для AI (inference engines, serving, drivers, agent runtimes) постоянно меняется и всё больше пишется на Rust, который ловит целые классы багов на этапе компиляции без потери производительности. Но GPU-ядро оставалось исключением: его можно запускать из Rust, но само ядро часто приходилось писать на другом языке. CUDA Rust закрывает этот разрыв, позволяя писать GPU-ядра на Rust с нативной компиляцией в PTX, а не через обёртку.

Конкретная проблема, которую ловит компилятор, — классическая ошибка алиасинга: тысячи потоков обращаются к одним буферам в неопределённом порядке, и когда два потока попадают по одному адресу и один пишет, порядок определяет результат. Такие баги редко воспроизводятся по требованию и проходят тесты до отказа в продакшене.

Отдельно упоминается, что SIMT-трек всё ещё требует закреплённого nightly-тулчейна, а Cargo и crates создают ожидание лёгкого старта, тогда как GPU-программирование исторически было противоположным.

Что это меняет на практике

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

Оба примера перехватывают ошибку во время компиляции, но проводят границу в разных местах: cuda-oxide проверяет каждый вызов запуска, а в cutile-rs владение следует за тензорами через границу запуска, что является более сильным утверждением.

Рабочий процесс начинается с установки подкоманды Cargo:

cargo +nightly-2026-04-03 install --git https://github.com/NVlabs/cuda-oxide.git cargo-oxide

Затем создаётся проект командой cargo oxide new vecadd_demo, после чего нужно перейти в каталог. Далее cargo oxide doctor проверяет всё необходимое окружение, включая опциональный системный LLVM. Запуск выполняется через cargo oxide run; первый запуск собирает backend кодогенерации, поэтому занимает время, а последующие запуски переиспользуют кэш. Успешный результат выводится как PASSED: all 1024 elements correct.

Ограничения и открытые вопросы

Для работы нужен Linux, GPU с compute capability 8.0 или выше, CUDA toolkit 12.x или новее, clang с заголовками libclang и закреплённый nightly-тулчейн. Для Tile-трека требуется стабильный Rust 1.89 или новее, CUDA 13.3 и Linux, но не требуется nightly-тулчейн и собственный LLVM.

Оба проекта ранние: cuda-oxide — early alpha, cutile-rs дальше по зрелости, опубликован на crates.io и уже используется вне NVIDIA в HuggingFace Grout и mistral.rs, но покрытие неполное и API будут меняться.

SIMT-трек всё ещё требует закреплённого nightly-тулчейна, а в SIMT общая память сегодня требует unsafe, и сделать этот путь безопасным — активная работа. CUDA Rust в целом — early-stage research project, где следует ожидать багов, неполных функций и ломающих изменений API.

Источники

Похожее