NVIDIA выпустила CUDA Rust — платформу для написания GPU-ядер на Rust с нативной компиляцией в PTXпромежуточное представление NVIDIA, в которое компилируются GPU-ядра перед запуском на устройстве. Это не отдельный крейт, а набор host-side крейтов для работы с драйвером CUDA и device-side крейтов и инструментов. Внутри — две модели программирования ядер и общий runtime.
Платформа описана как ранний исследовательский проект: ожидаются баги, неполные функции и ломающие изменения API.
Что именно изменилось
CUDA Rust включает две модели для написания ядер.
cuda-oxide — кастомный codegen-бэкендчасть компилятора, которая превращает код программы в машинный код для конкретной платформы rustcкомпилятор Rust из официального тулчейна, cargo-утилита и крейты для традиционной модели SIMT (Single Instruction, Multiple Threads). Здесь пользователю доступны все детали device-side CUDA-программирования.
cutile-rs — proc-macro плагин и крейты для модели CUDA Tile. Вычисления выполняются над тайламиблоками данных, каждый из которых обрабатывается как единое целое: тело ядра выполняется один раз над одним под-тензором, а компилятор сам решает, сколько реальных GPU-потоков это обеспечивает, а не над скалярами.
Общий 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стабильный публичный интерфейс rustc к промежуточному представлению компилятора, позволяющий инструментам работать с кодом без привязки к внутренним структурам и пайплайн MIRпромежуточное представление Rust, по которому компилятор анализирует и преобразует код: тело каждой функции транслируется в dialect-mirдиалект MIR в составе Pliron IR, после чего результат понижается до представления, близкого к машинному коду, и из него получается PTX. Весь host-код делегируется LLVM-бэкенду.
Как работает сбор device-функций
Сбор начинается с поиска точек входа ядра: перебираются CGUединицы кодогенерации, на которые rustc разбивает крейт, и для каждого элемента-функции проверяется, является ли он ядром. Найденные ядра добавляются как корни, после чего обход идёт по графу вызовов из каждого ядра. Если ядер нет, сканируются отдельные 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.