2025년 12월 scrap
01 Dec, 2025
1주차
2주차
3주차
4주차
Lecture 8: CUDA Performance Checklist
cuda에서 performance를 개선시키기 위한 general한 방법들을 다룸.
마지막에 flash attention 1에서 softmax를 최적화하는 방법에 다루긴 하는데 복잡한건 아니였음.
Lecture 14: Practitioners Guide to Triton
cute-dsl을 공부하다보니 triton이 궁금해짐.
torch 코드 최적화
torch.compile을 하면 내부적으로 Triton 커널이 자동 생성됨.
mode="reduce-overhead" 옵션을 주면 CUDA graph도 활용되어 커널 launch overhead가 줄어듦.
그래프 브레이크(그래프가 하나로 연결되지 않고 끊기는 현상)가 발생하면 성능이 저하되므로, 하나의 그래프로 만들 수 있게 코드를 수정하는 게 좋음.
그럼에도 충분히 빠르지 못하면 느린 부분을 확인하고 custom titon kernel이나 cuda kernel을 작성함.
os.environ['TRITON_INTERPRET'] = '1'을 설정하면 debugging하기 정말 쉬움. 따로 유틸 함수 도 작성함.
cuda와의 핵심 차이점은 cuda 프로그래밍 모델은 각 쓰레드가 scalar값을 처리하게 하는 것처럼 작성하게 되는 것에 비해 triton은 쓰레드 블록(cta)이 벡터값을 처리하는데에 있다고 보임.
분기문을 다룰 때에 각 쓰레드가 if 문을 하지 않고 값의 참여 여부를 mask를 통해 벡터로 다룸.
사실 이 부분이 내가 처음 cute-dsl에서 엄청 안와닿았던 부분이기도 하다.
Lecture 1 How to profile CUDA kernels in PyTorch
torch.autograd.profiler로 cpu와 gpu에서 어떤 함수들이 실행되고 시간을 어떻게 잡아 먹는지 확인 가능함.
PyTorch Profiler는 chrome_trace를 지원하여 결과 json을 크롬으로 붙이면 시각적으로 확인 가능함.
TORCH_LOGS="output_code"와 torch.compile을 활용하면 triton kernel이 생성되어 기준점으로 잡기 좋다고함.
ncu profiler
How To Write A Fast Matrix Multiplication From Scratch With Tensor Cores
npu profiler를 봐야 되는건 알겠는데 어떤 항목을 보고 어떤 생각을 하는지 쉽지 않은 문제인것 같다. 그런 부분에서 매우 유익한 도움을 주는 글이었다.
Warp State Statistics은 평균 워프가 발행된 평균 명령당 정지 상태로 보내는 클럭 사이클 수를 다양한 범주로 분류하여 알려줌.
커널이 실행되는 동안 각 워프는 스케줄러에 의해 명령을 발행받음. 이상적인 세상에서는 스케줄러가 각 클럭 사이클마다 새 명령을 발행할 수 있을 것.
실제로는 커널 동기화, 메모리 지연, 파이프라인 용량 제한 등의 이유로 매우 사이클마다 새 명령을 발행할 수 있는 커널을 작성하는 것은 매우 어려움.
Scoreboarding은 다음 명령어를 실행하는 데 필요한 데이터 종속성이 레지스터에 도착했는지 추적하기 위해 대부분의 프로세서 하드웨어에 구현된 알고리즘
대부분의 최신 CPU는 피연산자가 준비된 명령어가 아직 레지스터에 도착하지 않은 피연산자를 가진 명령어보다 먼저 실행될 수 있도록 명령어를 즉석에서 재정렬할 수 있음
GPU는 처리량에 최적화되어 있으므로 이러한 트랜지스터를 텐서 코어와 같은 곳에 사용하는 것이 더 낫기 때문이라고 생각하기 때문에 명령어를 실행할 때 재정렬하지 않음
하지만 GPU는 데이터 종속성을 추적함. 다만 CPU에 비해 컴파일러의 도움을 훨씬 더 많이 받음.
다음 명령어를 실행하는 데 필요한 데이터가 레지스터 메모리에 도착하지 않으면, 실행 중인 워프는 데이터가 도착할 때까지 기다림.
"Long Scoreboard Stall"은 워프가 데이터 종속성을 기다리며 멈춰 있는 평균 사이클 수
이 스톨 원인이 워프가 유휴 상태로 보내는 전체 사이클의 약 50%를 차지한다는 사실은 커널 1의 성능이 주로 메모리 지연 시간에 의해 제한된다는 것을 알려준다고 함.
이는 전역 메모리에서 칩으로 데이터를 이동하는 코드에 집중하고, 이동된 바이트당 지연 시간을 최소화하는 방법을 찾아야 함을 의미한다고...
타일 메모리 복사의 loop를 unrolling하고, 명령어당 로드되는 바이트 수를 늘리는 것으로 해결 함.
SASS에서 가장 넓은 로드 명령어는 LDG.128 이며, 이는 128비트/16바이트를 로드함.
MIO Throttle는 GPU의 메모리 입출력 파이프라인(특수 수학 함수, 공유 메모리, 동적 분기 처리 담당)에 명령어가 몰려서 큐가 꽉 찼을 때 발생하는 병목임.
이 경우에는 동적 분기가 없고, 특수 수학 함수도 쓰지 않기 때문에 공유 메모리 뱅크 충돌 문제라고 유추할 수 있다고 함.
(이상적으로) 공유메모리 사용시 어떤 스레드가 어떤 바이트에 접근하든 상관없이, 모든 접근이 뱅크 전체에 고르게 분산되도록 데이터를 정렬할 수 있어야 함
메모리 뱅크 충돌을 피하기위해 padding이나 swizzling같은 기법을 사용할 수 있음.
swizzle은 인덱스를 새 인덱스로 매핑하는 함수 f으로 구현할 수 있음. A가 원래 배열이고, A_s가 스위즐된 배열, i가 요소의 인덱스라면, A_s[f(i)] = A[i]
쓰기 시 뱅크 충돌을 피하기 위해 어떤 요소도 행을 바꾸지 않아야 함. 이는 f가 열을 인코딩하는 비트를 수정하고 행을 인코딩하는 비트는 그대로 두어야 함을 의미함.
각 행에 다른 순열을 적용하고 싶고, 주어진 열에 있는 요소들이 스위즐된 배열의 네 열 전체에 퍼지도록 해야 함.
XOR 함수를 사용하여 이 두 가지 목표를 달성할 수 있음. 각 요소의 행 비트와 열 비트를 XOR하고 그 결과를 새로운 열 비트로 사용함.
f(i) = i ^ ((i & 0b1100) >> 2)로 4*4 규모에서 적용 가능하고, 이는 자기역원함수로 f(f(i)) = i임.
스위즐링은 패딩 기법보다 좀 더 많은 계산이 필요하며, 스위즐 함수의 선택은 공유 메모리 배열 차원과 읽기/쓰기에 사용하는 벡터 너비에 따라 달라짐.
뱅크 충돌을 제거하고 이 과정에서 공유 메모리 공간을 늘리고 싶지 않다면 스위즐링이 필수적
각 최적화는 이전 커널에서 성능이 가장 낮은 부분을 다룸. 각 최적화를 적용한 후, 만약 효과가 있었다면 커널에서 성능이 가장 낮은 부분이 바뀌어야 함.
공유 메모리 뱅크 충돌을 해결하기 전에는 내부 루프 내의 공유 메모리 작업이 병목 현상이였고, 뱅크 충돌을 제거한 후에는 내부 루프가 훨씬 효율적이 되고, 병목 현상은 다시 전역 메모리에서 공유 메모리로의 전송 지연 시간이 되었다고함 .
처음 최대 문제는 long scoreboard stall(전역 데이터 로딩)이였고 그다음은 MIO throttle(공유 메모리)이였음.
다시 문제는 long scoreboard stall로 전역 메모리에서 공유 메모리로 복사하는 라인임.
고성능 GEMM 커널은 일반적으로 점유율이 낮음.
즉, 스레드당 더 많은 공유 메모리와 레지스터 메모리를 사용하며, 한 번에 SM에 상주하는 스레드 수가 적음. 이는 주로 높은 산술 강도(arithmetic intensity)의 필요성 때문임.
제한된 메모리 대역폭으로 컴퓨팅 유닛을 계속 바쁘게 유지하려면 메모리 계층의 낮은 수준에서 스레드당 더 많은 컴퓨팅이 이루어질수록 좋음.
그러나 낮은 점유율의 단점은 GPU가 컨텍스트 스위칭을 통한 자동 지연 시간 숨기기에 덜 효과적이라는 점.
우리는 컴퓨팅과 데이터 이동 간의 오버랩을 허용하도록 커널을 구성함으로써 이러한 트레이드오프를 처리할 수 있으며, 파이프라인 기법임.
#scrap