장치 독립적인 가상 ISA(Instruction Set Architecture)
개발자가 작성한 CUDA 코드는 하드웨어로 바로 전달되지 않고 PTX(Parallel Thread Execution)라는 가상 ISA 단계를 먼저 거친다. PTX는 물리적 레지스터 수의 제한이 없는 가상 계층으로, 특정 장치에 종속되지 않는 범용성을 확보하는 역할을 한다. 하드웨어의 제약을 배제한 상태이기에 주소 형성 과정에서 실제 기계어보다 더 많은 명령어가 필요할 수 있다.
이후 ptxas(PTX 어셈블러)가 PTX를 특정 GPU 아키텍처에 최적화된 실제 머신 코드인 SASS(Streaming Assembler)로 변환한다. 이 과정에서 무한했던 가상 레지스터는 하드웨어의 실제 레지스터로 매핑된다. 예를 들어 10여 개의 가상 레지스터가 7개의 실제 레지스터로 압축되며 물리적 실행 환경에 맞춰 최적화된다.
명령어 융합을 통한 효율화도 여기서 일어난다. 두 개의 mul.wide 명령어와 add 시퀀스를 하나의 IMAD.WIDE 명령어로 합치는 식이다. 결국 ptxas가 가상 레지스터를 매핑하고 명령어를 융합하는 정교한 과정을 거쳐야만 GPU가 즉시 실행 가능한 물리적 명령어가 완성된다.
확인해야 할 핵심 지점
하드웨어의 32개 레인이 동시에 데이터를 읽어야 하는 제약은 데이터 전달 방식의 효율성을 결정한다. 이를 위해 커널 인자들은 모든 스레드가 동시에 읽을 수 있는 '상수 뱅크 0(constant bank 0)' 영역에 저장된다. `c[0x0][…]`와 같은 피연산자들이 드라이버가 관리하는 이 작은 메모리 영역에 배치되는 이유다. 포인터와 크기 같은 핵심 인자를 상수 캐시를 통해 브로드캐스트 방식으로 전달함으로써 전송 지연을 막고 연산 속도를 높인다.
실제 배포되는 CUDA 실행 파일은 SASS와 PTX를 모두 포함한 fatbin(패트바이너리) 구조를 취한다. RTX 4090 같은 특정 아키텍처에서는 SASS가 즉시 구동되지만, 지원하지 않는 다른 세대의 GPU에서는 PTX가 폴백(fallback) 역할을 한다. 드라이버가 로드 시점에 PTX를 JIT(적시 컴파일)하여 해당 하드웨어에 맞는 SASS를 새로 생성하는 방식이다. 하드웨어 의존적인 기계어와 독립적인 가상 코드를 동시에 보유해 세대 간 실행 유연성을 확보한 설계다.
CUDA 프로그램은 nvcc를 통해 호스트 코드와 디바이스 코드로 분리된다
`nvcc` 명령어 한 줄 뒤에서는 복잡한 코드 분리 작업이 작동한다. `nvcc`는 단순한 컴파일러가 아니라 여러 컴파일러의 출력을 결합하는 드라이버 프로그램이다. 소스코드를 호스트 코드와 디바이스 코드로 나누어, 호스트 코드는 시스템 컴파일러로, 디바이스 코드는 LLVM 기반의 `cicc`를 통해 PTX로 변환한 뒤 최종적으로 `ptxas`가 SASS로 바꾼다.
실행 단계에서는 `cudafe++`(프론트엔드 컴파일러)가 삽입한 생성자가 fatbinary를 CUDA 런타임에 등록하며 시작된다. 컴파일러는 `<<<...>>>` 같은 커널 실행 구문을 발견하면 이를 '호스트 런칭 스텁(host launch stub)'으로 대체한다. 이 스텁은 커널 인자들을 호스트 메모리 버퍼의 특정 바이트 오프셋에 맞춰 정렬하고 패킹한다. GPU로 명령을 보내기 전 호스트 메모리에서 데이터를 정렬하는 이 과정이 커널 런칭 오버헤드가 발생하는 구체적인 지점이다.
결과적으로 단 한 번의 CUDA 커널을 실행하기 위해 CPU는 수천만 개의 명령어를 쏟아내고 900번의 ioctl 호출을 수행한다. 익숙한 컴파일 과정 이면에는 PTX가 SASS로 변환되고 호스트 메모리가 정렬되는 정교한 파이프라인이 숨어 있다.
성능 최적화의 근거는 추측이 아니라 하드웨어의 명령 전달 체계를 뜯어보는 데 있다. 런타임 오버헤드가 발생하는 정확한 지점을 이해해야만 불필요한 호출을 줄이고 GPU 컴퓨팅의 실질적인 성능을 끌어올릴 수 있다.



