AI VIDEO BRIEFING
GPU 학습 성능 최적화: CPU-GPU 동기화 제거부터 커널 퓨전, 커스텀 CUDA 커널까지
제인스트리트 엔지니어들이 GPU 학습을 느리게 만드는 세 가지 병목을 짚었다. 숨은 CPU-GPU 동기화와 메모리 대역폭, 커널 실행 오버헤드를 프로파일러로 찾아내고 커스텀 커널로 20밀리초를 30마이크로초까지 줄인 과정을 정리했다.

핵심 메시지
쉽게 이해하기
금융 트레이딩 회사 제인스트리트의 엔지니어 두 명이 사내 기술 강연에서 GPU로 신경망을 학습시킬 때 성능이 어디서 새는지를 처음부터 짚었다. 이들은 먼저 상식을 뒤집는 질문을 던진다. 부동소수점 연산 하나를 처리하는 데 CPU는 약 1나노초가 걸리지만 GPU는 약 1마이크로초, 즉 1000배가 더 걸린다는 것이다. 그럼에도 GPU를 쓰는 이유는 단 하나, 병렬성이다. 2048×2048 행렬곱처럼 출력 원소마다 독립적으로 계산할 수 있는 작업에서는 CPU가 28밀리초를 쓰는 동안 GPU가 200마이크로초 만에 끝낸다.
파이토치 코드에 `.to("cuda")` 몇 줄을 더하면 연산이 GPU로 넘어가지만, 그 뒤에서 실제로 벌어지는 일은 단순하지 않다. 커널 하나를 실행하려면 CPU가 실행 파라미터를 계산해 드라이버에 넘기고, 드라이버가 GPU에 명령을 전달한다. 다행히 이 과정은 비동기라서 CPU는 GPU의 완료를 기다리지 않고 다음 커널을 계속 큐에 쌓는다. 발표자들은 엔비디아 Nsight Systems 프로파일러를 띄워놓고, GPU 실행을 나타내는 막대가 끊김 없이 채워져 있어야 한다고 강조했다. 막대에 틈이 보이면 그만큼 비싼 GPU가 놀고 있다는 뜻이다.
이 파이프라인을 무너뜨리는 주범은 CPU와 GPU 사이의 동기화다. 강연에서는 학습 루프 안에 숨은 동기화 세 곳을 관객과 함께 찾아냈는데, 지표를 모으려고 `.cpu()`를 부르는 곳, 손실이 NaN인지 검사하며 텐서를 `if` 조건에 넣는 곳, 그리고 의외로 CPU에서 GPU로 데이터를 복사하는 곳이다. 두 번째가 특히 까다롭다. `.isNaN()` 자체가 아니라 그 결과를 불리언으로 강제 변환하는 `if` 문이 동기화를 일으키기 때문에 코드만 봐서는 알아채기 어렵다. 해결책은 각각 달라서, 지표는 GPU 커널로 계산해 루프 끝에서만 가져오고, NaN 검사는 비동기로 돌리며, CPU에서 GPU로의 복사는 `pin_memory`로 메모리를 고정한 뒤 `non_blocking=True`를 넘겨 GPU가 DMA로 직접 읽어가게 만든다.
실행 막대를 가득 채웠다고 GPU를 잘 쓰는 것은 아니다. 여기서부터는 하드웨어 구조를 알아야 한다. H100에는 132개의 SM(스트리밍 멀티프로세서)이 있고, SM끼리는 사실상 대화하지 못하므로 문제를 SM 단위 조각으로 쪼개는 것이 프로그래밍의 출발점이다. 메모리 계층의 속도 차이도 결정적이다. 글로벌 메모리가 초당 3테라바이트, L2 캐시가 7테라바이트, 공유 메모리가 26테라바이트로 벌어지기 때문에 가능한 한 위쪽 계층에 데이터를 붙잡아두는 것이 효율적인 커널의 핵심이 된다.
마지막으로 두 사람은 실제 사례를 보여줬다. 연구자가 모든 그래디언트의 평균을 기록하려고 수천 개 텐서의 합을 파이썬 for 루프로 돌렸더니 20밀리초가 걸렸다. 커널 하나를 큐에 넣는 데 CPU가 5마이크로초를 쓰는데 정작 GPU 실행은 1마이크로초도 걸리지 않아 GPU가 굶주린 것이다. torch.compile을 붙이자 오히려 조금 더 느려졌는데, 컴파일러가 for 루프를 펼쳐볼 뿐 병렬 커널로 재구성하지는 못하는 데다 트라이톤 템플릿이 엔비디아 손수 최적화 커널의 80~90% 성능에 그치기 때문이다. 결국 텐서 하나를 SM 하나에 배정하는 CUDA 커널을 직접 써서 30마이크로초, 약 1000배 빠른 결과를 얻었다.
주요 인사이트
- "파란 막대가 꽉 찼다"는 것은 GPU를 쉬지 않고 쓴다는 뜻일 뿐, 잘 쓴다는 뜻은 아니다. 두 지표를 분리해서 봐야 최적화 순서를 잘못 잡지 않는다.
- 동기화는 코드에 이름을 드러내지 않는다. 텐서를 조건문·출력·형변환에 넣는 모든 지점이 후보이며, 전체 그래프 컴파일 옵션으로 컴파일해 오류를 내보는 것이 숨은 동기화를 찾는 실용적인 방법이다.
- torch.compile은 만능이 아니다. 그래프 브레이크가 많으면 퓨전 기회가 사라지는데, 파이토치가 모르는 문법이나 커스텀 연산자, 숨은 CPU-GPU 동기화가 모두 그래프를 끊는다.
- 커널을 직접 쓸 때는 스레드가 메모리에서 서로 붙어 있는 값을 읽도록 배치해야 한다. 한 스레드에 연속 구간을 통째로 맡기는 직관적인 분할이 오히려 느리다.
- 발표자들은 최적화 순서를 순수 파이토치 → 트라이톤 → CUDA로 제시했다. 커스텀 커널은 초보자에게 일주일, 숙련자에게 반나절이 드는 비용이므로 마지막 수단이다.
자주 묻는 질문
GPU가 CPU보다 연산 하나를 처리하는 속도가 느린데도 딥러닝에 쓰는 이유는 무엇인가요?
단일 연산만 보면 CPU가 약 1나노초, GPU가 약 1마이크로초로 CPU가 1000배 빠릅니다. 하지만 행렬곱처럼 출력 원소마다 스레드를 독립적으로 붙일 수 있는 작업에서는 GPU의 대규모 병렬성이 압도합니다. 강연에서 든 2048×2048 행렬곱 예시에서 CPU는 28밀리초, GPU는 200마이크로초가 걸렸습니다.
학습 코드에서 CPU-GPU 동기화가 생기는 대표적인 지점은 어디인가요?
지표를 모으려고 `.cpu()`를 호출하는 곳, 손실이 NaN인지 검사한 결과를 `if` 문에 넣어 불리언으로 바꾸는 곳, 그리고 CPU에서 GPU로 데이터를 복사하는 곳입니다. 세 번째는 복사가 끝날 때까지 해당 메모리가 스왑되거나 해제되지 않도록 붙잡아둬야 하기 때문에 발생하며, `pin_memory`와 `non_blocking=True`로 피할 수 있습니다.
GPU 성능 병목에는 어떤 종류가 있고 각각 어떻게 대응하나요?
연산 병목, 메모리 병목, 커널 실행 오버헤드 세 가지입니다. 연산 병목에는 행렬곱 타일을 처리하는 텐서 코어가, 메모리 병목에는 여러 연산을 한 커널로 묶는 커널 퓨전이 쓰입니다. 커널 오버헤드는 퓨전으로 줄이거나, 작은 커널을 많이 띄워야 한다면 CUDA 그래프로 실행 파라미터를 한 번만 기록해 재생하는 방식으로 완화합니다.
torch.compile을 쓰면 오히려 느려지는 경우도 있나요?
있습니다. 강연에서 수천 개 텐서의 합을 for 루프로 계산한 사례가 그랬습니다. 컴파일러는 for 루프를 병렬 커널로 재구성하지 못하고 펼쳐본 뒤 부분적으로만 퓨전하는데, 이때 쓰이는 트라이톤 템플릿의 성능이 엔비디아가 직접 최적화한 커널의 80~90% 수준이라 퓨전 이득이 없으면 손해만 남습니다.
원문과 출처
이 글은 원본 영상의 자막을 바탕으로 한국어 독자를 위해 요약했습니다. 전체 맥락과 최신 정보는 원문에서 확인하세요.
YouTube 원본 영상 보기 ↗