Эта программа складывает два массива по миллиону чисел на RTX 4090. За кажущейся простотой скрывается сложный путь от кода до результата — компиляция, драйвер, ioctl, DMA и работа с памятью.
nvcc запускает каскад компиляторов. Сначала cicc превращает код в PTX — виртуальный ассемблер с бесконечным числом регистров. Потом ptxas переводит PTX в SASS — машинный код для конкретной архитектуры. В PTX расчёт адреса занимает три инструкции, в SASS — одну IMAD.WIDE. Аргументы ядра (a, b, c, n) попадают в константный банк 0: их одинаково читают все потоки.
Готовый SASS упаковывается в fatbin вместе с исходным PTX. PTX нужен для совместимости — если запустить программу на другом GPU, драйвер скомпилирует его на лету. fatbin встраивается в .nv_fatbin секцию исполняемого файла.
При запуске программы срабатывает скрытый конструктор: он регистрирует fatbin в CUDA runtime. Когда встречается vadd<<<4096, 256>>>, рантайм упаковывает аргументы в буфер и вызывает cuLaunchKernel. Начиная с CUDA 12.2 модули загружаются лениво: SASS копируется в видеопамять только при первом запуске конкретного ядра.
Запуск устроен сложнее. В оперативной памяти создаётся pushbuffer — область с командами для GPU. На «кольцевой буфер» GPFIFO указывают два курсора: GP_GET (сколько прочитал GPU) и GP_PUT (сколько записал драйвер). Курсоры живут в USERD — структуре в памяти устройства. Когда драйвер заполняет pushbuffer и обновляет GP_PUT, он «дергает дверной звонок» (doorbell) — пишет в специальный MMIO-регистр. После этого host engine GPU считывает команды через DMA и передаёт QMD — дескриптор запуска — «compute work distributor».
Distributor распределяет 4096 блоков по 128 SM. Каждый SM может держать до 6 блоков (48 warp-ов). Warp состоит из 32 потоков. Scheduler каждым тактом выбирает готовый к исполнению warp. Готовность определяют статические подсказки из кода SASS: счётчик тактов для предсказуемых операций и аппаратные scoreboard-барьеры — для непредсказуемых (например, чтения из глобальной памяти). Потоковые загрузки (LDG.E) выставляют барьер B2. Пока данные не вернулись, warp неактивен, а scheduler переключается на другой.
При загрузке 32 потока читают соседние float — 128 байт подряд. Блок запроса (coalescing) превращает это в четыре запроса по 32 байта. Данные идут через L1 кеш, L2 (72 МБ на RTX 4090) и, если мимо, — в GDDR6X. Профилировщик ncu показывает: GPU занят на 82,77%, инструкции выдаются лишь 5,17% времени — ядро упёрлось в пропускную способность памяти (почти 80% пика). Вся работа заняла 10,78 микросекунд.
Результат оседает в L2 кеше. После завершения ядра cudaMemcpy обратно инициирует DMA-перенос с GPU на CPU — и в консоли появляется c[0]=2.0.