CUDA 커널을 실행할 때 발생하는 일: 소스에서 SASS까지
CUDA 커널을 실행할 때 발생하는 일: 소스에서 SASS까지
CUDA 커널을 시작하는 것은 호스트 CPU, NVIDIA 사용자 모드 드라이버, 커널 모드 드라이버, 그리고 GPU 하드웨어 사이의 복잡한 오케스트레이션을 포함합니다. 이 과정은 고수준 C++ 코드를 하드웨어 전용 명령과 SASS 명령 스트림으로 변환하고, 수천 개의 병렬 스레드에서 실행을 트리거하기 위해 특수한 메모리 매핑 도어벨 시스템을 활용합니다.
컴파일 파이프라인: PTX와 SASS
CUDA 프로그램을 실행하려면 장치에 독립적인 코드와 하드웨어 전용 기계 명령 사이의 격차를 메우기 위해 여러 컴파일 단계가 필요합니다.
nvcc는 여러 컴파일러를 조정하는 드라이버 프로그램 역할을 합니다. 호스트 코드는 일반 호스트 컴파일러에 전달되고, 장치 코드는 특정 경로를 따릅니다: cicc(LLVM 기반 컴파일러)가 **PTX(Parallel Thread Execution)**를 생성하고, 그 다음 ptxas가 이를 **SASS(Streaming Assembler)**로 변환합니다.
PTX vs. SASS
- PTX는 가상 명령 집합 아키텍처(ISA)입니다. 장치에 독립적이며 무한한 수의 타입 레지스터를 사용합니다. 이는 전방 호환성을 위한 폴백 역할을 하며, 바이너리가 사전 컴파일된 SASS가 지원하지 않는 GPU 아키텍처에서 실행될 경우 드라이버가 로드 시점에 PTX를 JIT‑컴파일하여 새로운 SASS를 생성할 수 있게 합니다.
- SASS는 GPU에서 실제로 실행되는 기계 코드입니다. 가상 레지스터를 유한한 물리 레지스터 집합에 매핑하고 복잡한 PTX 연산을 단일 하드웨어 명령(예:
IMAD.WIDE)으로 결합합니다.
최종 바이너리는 fatbin이며, 여기에는 SASS(ELF 컨테이너 안)와 압축된 PTX가 모두 포함됩니다. 이 fatbin은 호스트 실행 파일의 .nv_fatbin 섹션에 내장됩니다.
호스트에서 GPU 트리거하기
GPU가 PCIe 버스를 통해 연결되어 있기 때문에 CPU가 단순히 "GPU 함수를 호출"할 수 없습니다. 대신 드라이버가 관리하는 작업 큐를 통해 통신해야 합니다.
호스트 런치 스텁
nvcc가 커널 런치를 컴파일할 때(예: vadd<<<4096, 256>>>), 해당 표현식을 생성된 호스트 런치 스텁으로 대체합니다. 이 스텁은 커널 인자를 호스트 메모리의 특정 바이트 오프셋에 있는 버퍼에 패킹하고, 이후 __cudaLaunch를 호출합니다. __cudaLaunch는 호스트 측 함수 포인터를 조회 키로 사용해 fatbin 안의 대응되는 장치 측 심볼을 찾습니다.
드라이버 브리지
CUDA 런타임은 폐쇄형 사용자 모드 드라이버(libcuda.so)와 상호 작용하고, 이는 다시 커널 모드 드라이버(nvidia.ko)와 /dev/nvidiactl 같은 디바이스 파일에 대한 ioctl 호출을 통해 통신합니다.
CUDA 12.2부터 모듈 로딩은 기본적으로 lazy(지연) 방식입니다. 드라이버는 해당 커널이 처음 실행될 때까지 SASS cubin을 GPU 메모리로 업로드하는 작업을 미룹니다.
하드웨어 런치 메커니즘
GPU 실행은 호스트 메모리에서 읽어들인 명령 스트림에 의해 구동되며, 이는 세 가지 주요 구조로 이루어진 "채널"을 통해 조정됩니다:
- Pushbuffer: 드라이버가 GPU 동작을 정의하는 "메서드"(레지스터 주소와 값)를 기록하는 호스트 메모리 영역.
- GPFIFO: 푸시버퍼의 어느 구간을 읽을지 GPU에 알려주는 포인터 링 버퍼.
- USERD: 장치 메모리 내에 있는 작은 구조로, 작업 소비를 추적하기 위한 커서(
GP_GET및GP_PUT)를 포함합니다.
도어벨
현대 GPU(Turing 이후)는 GP_PUT 커서를 스누프하지 않습니다. 대신 드라이버는 도어벨이라 불리는 메모리 매핑 레지스터에 작업 제출 토큰을 기록합니다. 이는 GPU의 호스트 엔진에게 업데이트된 GP_PUT을 가져오고 DMA를 통해 푸시버퍼에서 메서드를 끌어오도록 신호를 보냅니다.
Queue Meta Data (QMD)
가장 중요한 메서드 중 하나는 Queue Meta Data (QMD) 스트리밍입니다. QMD는 GPU에 다음 정보를 전달하는 런치 디스크립터입니다:
- 그리드와 블록 차원(예: 4096 블록, 각 256 스레드)
- 스레드당 필요한 레지스터와 공유 메모리 양
- SASS 코드의 메모리 주소
- 커널 인자를 담고 있는 상수 뱅크 주소
스트리밍 멀티프로세서(SM)에서의 실행
QMD가 Compute Work Distributor(GigaThread Engine)에게 전달되면 GPU는 선형 SASS 명령을 사용 가능한 스트리밍 멀티프로세서(SM)들에 매핑합니다.
자원 제한과 점유율
RTX 4090(AD102)에서 SM은 스레드 용량과 레지스터 파일 크기에 의해 제한됩니다. 스레드당 16 레지스터를 사용하고 블록당 256 스레드를 사용하는 커널을 예로 들면:
- 레지스터 용량: $65,536 / (256 \times 16) = 16$ 블록
- 스레드 용량: $1,536 / 256 = 6$ 블록
스레드 용량이 더 빡빡한 병목이므로 각 SM은 최대 6개의 레지스터 블록(48 워프)만 보유할 수 있습니다.
워프 적합성 및 레이턴시 은폐
CPU와 달리 GPU는 복잡한 Out‑of‑Order 실행 로직을 사용하지 않습니다. 대신 많은 레지던트 워프 사이를 전환하면서 레이턴시를 은폐합니다. 워프는 ptxas가 SASS 명령에 패킹한 제어‑코드 페이로드에 따라 실행 가능 여부가 결정됩니다:
- Static Stall Count: 고정 레이턴시 연산에 대해 컴파일러가 정확히 몇 사이클 동안 워프가 대기해야 하는지를 인코딩합니다.
- Yield Hint: 스케줄러가 다른 워프를 우선시하도록 제안하는 비트.
- Dependency‑Barrier Indices: 가변 레이턴시 연산(예: 전역 메모리 로드
LDG)에 대해 하드웨어는 6개의 물리 스코어보드 배리어를 사용합니다. 워프는 자신이 기다리는 특정 배리어가 해제될 때까지 실행 불가능합니다.
메모리 계층 및 데이터 전송
워프가 로드(LDG.E)를 발행하면 SM의 Load/Store 유닛은 **요청 병합(request coalescing)**을 수행합니다. 워프 내 32개의 스레드가 연속적인 4바이트 float에 접근하면 하드웨어는 이를 4개의 32바이트 섹터 요청으로 합쳐 버스 트래픽을 최소화합니다.
데이터 흐름은 L1 데이터 캐시 → L2 캐시 → 메모리 컨트롤러 → GDDR6X VRAM 순입니다. 벡터 덧셈과 같이 연산 강도가 낮은 커널에서는 일반적으로 DRAM 버스 대역폭이 성능을 제한하며, 계산 능력보다 메모리 대역폭이 병목이 됩니다.
결과를 CPU로 반환하기
마지막 블록이 종료되면 GPU는 QMD에 정의된 완료 세마포어를 게시합니다. 이후 GPU의 복사 엔진이 L2 캐시(또는 VRAM)에서 결과를 DMA로 호스트 메모리로 전송합니다. 복사가 끝나면 복사 엔진이 자체 세마포어를 게시하고, 호스트의 cudaMemcpy 호출이 반환되어 CPU가 실행을 재개합니다(예: printf 호출).
요약: CUDA 커널의 전체 수명 주기를 깊이 있게 살펴보며, nvcc 컴파일 및 PTX/SASS 생성 단계부터 RTX 4090에서의 하드웨어 수준 실행까지의 흐름을 추적합니다.
제목: CUDA 커널을 실행할 때 발생하는 일: 소스에서 SASS까지