고성능 GPU를 투입해도 모델의 실제 추론 속도가 기대치에 못 미치는 경우가 많다. 하드웨어 스펙 시트의 수치와 실제 실행 속도 사이의 간극은 대부분 코드 레벨의 병목에서 발생한다. PyTorch는 이를 정밀하게 분석하기 위해 torch.profiler 모듈을 제공한다.

이번 분석은 행렬 곱셈(matmul)과 덧셈 연산을 통해 프로파일링의 실질적인 활용법을 다룬다. CPU가 커널을 준비하고 런칭하는 시간과 GPU가 실제로 연산을 수행하는 시간을 분리해 측정하여, 연산 규모가 작을 때 발생하는 오버헤드-바운드(Overhead-bound) 현상을 수치로 증명하고 이를 컴퓨트-바운드(Compute-bound) 상태로 전환하는 과정을 추적한다.

NVIDIA A100 기반 torch.profiler의 데이터 추출 방식

NVIDIA A100-SXM4-80GB GPU 환경에서 01_matmul_add.py 스크립트를 실행하면, torch.profiler 모듈은 연산 효율 진단을 위한 두 가지 결과물을 생성한다. 하나는 이벤트별 CPU와 GPU 소요 시간 및 호출 횟수를 기록한 텍스트 기반의 프로파일러 테이블 파일이며, 다른 하나는 Perfetto UI에서 시각화가 가능한 JSON 형식의 트레이스 파일이다.

.txt 파일은 연산 비용을 정량적으로 파악하는 지표다. 테이블의 각 행은 트리거된 이벤트를 나타내며, 열은 CPU와 GPU 각각의 소요 시간을 기록한다. 호출 횟수를 나타내는 # of Calls 열과 시간 수치를 대조하면 특정 연산이 시스템 자원을 얼마나 빈번하고 무겁게 사용하는지 식별할 수 있다.

.json 파일은 연산의 시간적 간극을 시각화한다. Perfetto UI에 이 파일을 업로드하면 CPU와 GPU 레인이 나란히 표시되며, 막대의 길이는 이벤트 지속 시간을, 수직적 배치는 호출 계층 구조를 나타낸다. CPU와 GPU 레인 사이의 빈 공간은 연산이 수행되지 않는 유휴 시간이나 커널 디스패치 지연을 나타낸다. 데이터 분석 시에는 초기화 과정에서 발생하는 노이즈를 제외하고 실제 연산이 집중되는 구간을 선별해야 한다.

Self Time과 Total Time의 구분 및 Perfetto 시각화

PyTorch 프로파일러는 병목 지점이 함수 자체에 있는지 아니면 하위 함수에 있는지 구분하기 위해 Self CPU/CUDA와 CPU/CUDA Total 지표를 제공한다. Self 지표는 하위 자식 이벤트를 제외하고 해당 이벤트 내부에서만 소비된 순수 시간을 측정한다. 반면 Total 지표는 해당 이벤트와 그로 인해 트리거된 모든 자식 이벤트의 시간을 합산해 기록한다. 예를 들어 matmul_add 함수의 CPU Total 시간은 함수 자체의 실행 시간과 내부에서 호출된 모든 하위 연산 시간을 포함한다.

수치 데이터로 파악하기 어려운 실행 흐름은 JSON 아티팩트를 통해 시각화한다. Perfetto UI(웹 기반 트레이스 분석 도구)에 해당 파일을 업로드하면 이벤트 간의 호출 계층과 시간 축을 확인할 수 있다. 직접 링크를 생성하려면 다음 명령어를 사용한다.

bash
uvx trace-util traces -b traces

Perfetto 인터페이스에서는 W, A, S, D 키를 이용해 트레이스 화면을 탐색하고 확대 및 축소한다. CPU 레인에서는 이벤트 발생 순서와 호출 깊이를 확인하고, GPU 레인에서는 실제 커널이 실행된 시점과 지속 시간을 대조한다. 두 레인 사이의 빈 공간은 CPU가 커널을 제출한 뒤 GPU가 실행을 시작하기까지의 대기 시간으로, 런칭 오버헤드를 분석하는 핵심 지표가 된다.

64x64 행렬의 오버헤드-바운드 vs 대형 행렬의 컴퓨트-바운드

64x64 크기의 소형 행렬 연산을 수행하면 하드웨어 성능과 실제 속도 사이의 괴리가 뚜렷하게 나타난다. PyTorch 프로파일러로 측정한 결과, GPU 커널인 `ampere_bf16_s16816gemm`의 실행 시간은 CPU 연산 시간의 1% 미만으로 측정되었다. 이는 연산 자체보다 커널 준비와 런칭, 데이터 전송 과정에서 발생하는 오버헤드가 전체 실행 시간을 지배하는 오버헤드-바운드(Overhead-bound) 상태이기 때문이다. GPU는 연산 명령이 올 때까지 대부분의 시간 동안 유휴(Idle) 상태로 대기하며, 이 단계에서는 GPU 성능을 높여도 전체 속도가 개선되지 않는다.

행렬 크기를 확장하면 병목의 위치가 이동한다. 대형 행렬을 사용하여 다시 측정한 결과, `ampere_bf16_s16816gemm` 커널이 GPU 내부에서 실제로 계산을 수행하는 시간이 CPU 런칭 시간보다 커지며 전체 실행 시간을 지배하게 된다. 연산량이 증가함에 따라 CPU의 준비 비용이 차지하는 비중이 낮아지고 하드웨어 자원 활용도가 상승한다.

이러한 변화는 시스템을 컴퓨트-바운드(Compute-bound) 상태로 전환시킨다. 이 상태에서는 CPU의 런칭 오버헤드보다 GPU의 순수 연산 능력이 전체 처리 속도를 결정한다. 결국 연산 단위의 크기를 적절히 확보하는 것이 GPU의 잠재 성능을 끌어내는 핵심 조건이며, 이는 배치 크기나 행렬 규모 최적화가 실질적인 성능 향상을 가져온다는 것을 입증한다.

228µs의 '데드 윈도우'와 웜업(Warmup)의 필요성

프로파일러 분석 결과, record_function("matmul_add")에 진입한 시점부터 실제 커널이 디스패치되는 시점까지 약 228µs의 공백이 발생한다. 이 데드 윈도우는 워크스페이스 할당, cuBLAS(NVIDIA GPU 가속 선형 대수 라이브러리)의 휴리스틱 계산, 지연 모듈 로딩 과정에서 발생하는 일회성 비용이다. 이를 포함해 프로파일링을 진행하면 측정값이 왜곡되어 실제 연산 성능을 정확히 파악하기 어렵다.

정확한 측정을 위해 대상 함수를 미리 실행하여 초기화 노이즈를 제거하는 웜업(Warmup) 전략이 필수적이다. PyTorch 프로파일러는 초기화 노이즈를 건너뛰는 wait, 기록 없이 함수를 실행하는 warmup, 실제 데이터를 수집하는 active 단계로 스케줄을 구성하는 기능을 제공한다.

실무에서는 코드 수준의 루프 실행과 프로파일러 내부의 warmup 인자 설정을 병행하여 내부 캐시와 라이브러리 로딩을 완료해야 한다. 웜업을 적용하면 각 프로파일 스텝이 일정한 시간 내에 수렴하며 일회성 비용이 배제된다. 다만, CPU가 커널을 제출한 후 GPU에서 실행이 시작되기까지 발생하는 2.5ms 수준의 오프셋은 웜업만으로 제거되지 않는 구조적 지연 지점이다.

한국 AI 실무자를 위한 LLM 추론 최적화 시사점

CPU 레인에서 이벤트가 발생한 뒤 GPU 레인에서 실제 커널이 실행되기까지 발생하는 물리적 지연 시간은 단순한 벤치마크 수치 뒤에 숨은 실제 지연의 원인이 된다. 특히 작은 크기의 행렬 곱셈을 반복적으로 수행하는 환경에서는 GPU의 실제 연산 시간보다 커널 런칭 오버헤드가 지배적이며, 연산 단위가 작을수록 CPU의 커널 생성 속도가 GPU의 처리 능력을 따라가지 못해 자원 낭비가 심화된다.

최적화의 방향은 작은 연산을 빈번하게 호출하는 구조를 탈피해 연산을 묶어 GPU 점유율을 극대화하는 배치 최적화로 향해야 한다. 행렬 크기를 키워 한 번의 호출로 처리하는 데이터 양을 늘리면 시스템은 컴퓨트-바운드 상태로 전환되며, CPU 런칭 오버헤드의 비중이 감소해 토큰당 생성 속도가 실질적으로 상승한다.

실무자는 추론 파이프라인 내에 산재한 불필요한 CPU-GPU 동기화 지점을 찾아 제거하는 작업에 집중해야 한다. 충분한 워밍업 단계를 설정해 일회성 오버헤드를 제거하고, 커널 런칭 횟수를 줄이는 구조적 재설계가 필요하다. 동기화 횟수를 최소화하고 GPU 점유 시간을 확보하는 것이 추론 효율을 결정짓는 핵심이다.

PyTorch 프로파일러가 기록한 커널 실행 시간과 동기화 지연 수치는 하드웨어 자원 낭비가 발생하는 구체적인 지점과 규모를 증명하는 데이터다. 병목 구간을 명확히 특정하지 않은 상태에서의 파라미터 튜닝은 실질적인 성능 향상 없이 수치상의 착시만 일으킬 가능성이 크다. 결국 모델의 추론 속도와 학습 효율은 데이터 흐름의 정체 구간을 얼마나 정교하게 제거하느냐로 결정된다.

결국 모델의 추론 속도와 학습 효율은 알고리즘의 복잡도가 아니라 데이터 흐름의 정체 구간을 얼마나 정교하게 제거하느냐로 결정된다.