아주 간단한 CUDA(쿠다) 벡터 덧셈 프로그램조차도 GPU에서 최종 결과값을 얻기까지는 매우 복잡하고 정교한 내부 과정을 거칩니다. 개발자가 작성한 코드는 컴파일 파이프라인, 드라이버 호출, GPU 명령 큐, 워프(warp) 스케줄링, 메모리 계층, 그리고 완료 신호(세마포어)에 이르기까지 수많은 단계를 거쳐야 비로소 실행됩니다. 이는 단순한 연산 한 번에도 CPU와 GPU 간의 긴밀한 협업과 하드웨어의 복잡한 작동 방식이 숨어 있음을 보여줍니다.
엔비디아(NVIDIA)의 컴파일러인 nvcc는 호스트(CPU) 코드와 디바이스(GPU) 코드를 분리하여 처리합니다. 디바이스 코드는 먼저 가상 명령어 집합인 PTX(Parallel Thread Execution)로 변환된 후, 특정 GPU 아키텍처에 최적화된 실제 명령어인 SASS(Streaming Assembler)로 컴파일됩니다. 이 SASS와 PTX는 'fatbin'이라는 형태로 묶여 최종 실행 파일에 포함됩니다. 프로그램이 실행되면, 호스트 코드는 CUDA 런타임과 드라이버(libcuda.so.1)를 통해 GPU에 작업을 전달합니다. GPU는 CPU처럼 함수를 직접 호출하는 대신, PCIe 버스를 통해 호스트 메모리에 있는 드라이버 명령 스트림을 읽어 작업을 처리합니다. 이 과정에서 QMD(Queue Meta Data)라는 실행 정보가 GPU에 전달되어, 커널의 실행 방식, 병렬 구성, 메모리 주소 등을 지시합니다.
GPU는 QMD를 받아 컴퓨트 워크 분배기(compute work distributor)를 통해 작업을 스트리밍 멀티프로세서(SM)에 분산합니다. 예를 들어, 엔비디아 지포스 RTX 4090 GPU는 128개의 SM을 활용하여 수천 개의 블록과 스레드를 워프 단위로 실행합니다. 각 SM은 여러 워프를 동시에 관리하며, 특정 워프가 메모리 접근 등으로 인해 지연될 경우 다른 워프로 빠르게 전환하여 지연 시간(latency)을 숨깁니다. 이러한 워프 스케줄링은 컴파일러가 예측 가능한 타이밍을 설정하고 하드웨어 스코어보드(scoreboard)가 예측 불가능한 부분을 처리하며 이루어집니다. 이처럼 복잡한 과정을 통해 GPU는 높은 병렬성을 달성하고 방대한 데이터를 효율적으로 처리할 수 있게 됩니다. 결국, 우리가 보는 간단한 CUDA 실행 결과 뒤에는 수천만 개의 CPU 명령, 수백 개의 드라이버 호출, 그리고 정교하게 설계된 GPU 하드웨어와 소프트웨어 스택이 유기적으로 작동하고 있는 것입니다.