RTX 2060 같은 Turing GPU 위에서 INT8 tensor core 만으로 matmul 을 단계별로 빠르게 — naive → column-major → shared memory → register tiling. 같은 NCU diff 화면을 매번 다시 보면서 어떤 transformation 이 어떤 stall 을 무슨 비율로 바꾸는지 추적한 educational 기획. Erik Schultheis 의 직접 CUDA 작성 시퀀스 학습 노트.
강의 도입에서 Erik 이 분명히 한 자리 — "this is not state-of-the-art fast or even close. it's about getting the basics right." 같은 시리즈 두 주 뒤에 H100 cuBLAS-beat 강의가 잡혀 있는 자리에서, 일부러 고른 old GPU(Turing) + 가장 작은 도구셋(CUDA C++ 만, no CUTLASS, no inline asm). 추상이 다 걷힌 자리에서 tensor core 가 어떻게 도는지를 손에 잡히게 만드는 게 목표다.
그래서 이 강의는 한 큰 결과를 보여주는 게 아니라 — 점진적으로 빨라지는 5개의 커널과 그 사이의 NCU diff를 본다. naive (~4 TFLOPs) → column-major → SMEM tile → register tile → ... 의 단계마다 어떤 metric 이 무엇 때문에 어떻게 변하는지를 같은 도구(Nsight Compute) 의 같은 화면에서 본다.
모든 단계의 진단은 한 질문으로 회수된다 — “where's my data?” Steven Jones 의 GTC 슬라이드에서 빌려온 표현. row-major 가 strided read 를 만든다, SMEM 으로 stage 가 안 되어 L1-L2 traffic 이 폭발한다, register tile 이 부족해서 같은 데이터를 반복해서 fetch 한다 — 모두 데이터가 잘못된 자리에 있어서 생기는 stall.
이 강의의 가치 — 같은 도구(NCU)가 같은 코드의 5개 변형에서 어떻게 다른 hint 를 주는지의 reading 자료. NCU 를 처음 본 사람에게 가장 좋은 1시간짜리 worked example 이다.
강의 첫 5분이 집중한 자리. INT8 matmul 이 단순히 “float 을 int 로 바꾼” 게 아니라는 것 — register width 가 안 맞는다. INT8 두 개를 곱하면 결과는 INT16, accumulator 는 INT32. 즉 input 폭과 output 폭이 다르다. 이게 hardware instruction 의 모양을 결정한다.
강의에서 깐 비교점.
VPDPBUSD 같은 4-way INT8 dot product instruction. 부호 있는/없는 INT8 의 조합이 한 instruction 안에 박혀 있다.__dp4a intrinsic. INT8×4 vs INT8×4 → INT32 dot product 누적. CPU 의 VNNI 와 사실상 동치.mma.sync with INT8 inputs. 한 warp 가 같이 내는 16×16×16 matmul. Turing 부터 지원되어 — “정말 오래된 GPU 가 아니어도 시도 가능” — 가 강의가 Turing 을 고른 이유.강의의 Erik 이 강조 — integer matmul 의 추가 미덕 한 가지는 deterministic. floating point 의 reordering 따라 결과가 미세하게 변하는 문제가 없다. 그래서 testing 이 단순하다 — “정확한 일치 여부” 만 보면 된다.
INT8 matmul 은 quantized inference의 backbone. LLM weight 를 INT8 로 quantize 하면 메모리 사용량이 절반, bandwidth 절반, INT8 tensor core throughput 이 FP16 의 2배 (Turing 기준). 이 강의의 결과가 직접 production 가는 건 아니지만 — pyramid 의 첫 칸이 어떻게 동작하는지 파악되어야 위 칸 (smoothquant, AWQ, INT4 GPTQ) 이 눈에 들어온다.
// CPU (AVX-512 VNNI) — 한 instruction
__m512i acc = _mm512_dpbusd_epi32(acc, a, b);
// ^^^^ accumulate INT32
// dot product of 4×INT8
// CUDA non-tensor core (DP4A)
int acc = __dp4a(a4, b4, acc);
// CUDA tensor core (Turing+)
nvcuda::wmma::fragment<...> a_frag, b_frag, c_frag;
mma_sync(c_frag, a_frag, b_frag, c_frag);
Turing 의 INT8 tensor core 는 warp 단위 instruction 이다. 32개 thread 가 같이 호출해야 한다. 한 번의 호출이 16×16×16 matmul 을 처리. 16 = M, 16 = N, 16 = K.
load_matrix_sync 가 input 을 GMEM/SMEM 에서 register fragment 로 분산 적재. mma_sync 가 32 thread 의 register 위에서 직접 matmul. store_matrix_sync 로 결과를 출력.강의에서 직접 인용한 사실들.
// 강의의 첫 tensor core kernel — 약식
__global__ void tc_matmul(
const int8_t* A, const int8_t* B,
int* C, int M, int N, int K)
{
// 16x16 fragments
fragment<matrix_a, 16,16,16, int8_t, row_major> aF;
fragment<matrix_b, 16,16,16, int8_t, row_major> bF; // ← row major B = strided
fragment<accumulator, 16,16,16, int> cF;
fill_fragment(cF, 0);
for (int k = 0; k < K; k += 16) {
load_matrix_sync(aF, &A[i*K + k], K);
load_matrix_sync(bF, &B[k*N + j], N);
mma_sync(cF, aF, bF, cF);
}
store_matrix_sync(&C[i*N + j], cF, N, mem_row_major);
}
강의 첫 큰 surprise. 위의 코드를 15000×15000 크기 입력으로 돌리면 — 1.5초, 약 4 TFLOPs. Turing 의 INT8 tensor core peak 가 60+ TFLOPs 이므로 일 자릿수 utilization. 이유를 NCU 가 가리킨다.
그리고 NCU 가 같이 보여주는 두번째 화면 — warp stall reasons.
NCU 가 가리키는 두 stall 모두 memory traffic 자체가 너무 많다는 신호. tensor core 는 거의 idle. 다음 단계의 모든 transformation 은 이 두 metric 을 줄이는 방향으로만 진행된다.
강의의 첫 fix. row-major 두 행렬에서 K 차원으로 sum 을 돌면, B 의 access 는 K 만큼 strided = uncoalesced. column-major 로 두면 K 차원이 contiguous = coalesced. 변경은 fragment 의 layout flag 한 줄과 — B 를 미리 column-major 로 transpose 하는 별도 kernel.
“transpose 비용은?” 청중 질문. Erik 의 답.
결과적으로 한 줄 변경 + 미리 transpose 의 비용으로 1초로 단축. NCU 의 두 metric 도 변한다.
“stall ratio 가 줄어들 거라 기대했는데, 오히려 비슷하거나 늘었다”. Erik 의 설명 — stall 은 normalized metric. 분모(=실제 instruction issue cycle) 도 같이 줄었다. instruction count 가 −45% 줄어서 (index 계산 instruction 이 사라져서), ratio 가 큰 변화 없이 보일 뿐 — 절대 cycle 수는 줄었다. "NCU diff 는 절대값과 상대값을 같이 봐야 한다" 의 교훈.
다음 fix. block 안의 여러 warp 가 같은 A 행, 같은 B 열을 반복해서 읽는다. 한 번 SMEM 에 올려두면 — 그 block 안에선 register 로 stage 가능. PMPP 책의 표준 tiling 패턴이 그대로.
강의의 SMEM 추가 시퀀스.
__syncthreads() 로 stage 사이 동기화.결과: 0.97s → ~0.6s. NCU 가 다시 가리키는 다음 자리는 register pressure.
SMEM bank conflict — INT8 8개 element 가 한 word 에 들어간다. row 별로 4 byte stride 면 같은 bank 에 걸린다. 강의에서 Erik 이 한 코드 비교에서 보여준 — padding 으로 stride 어긋남(예: shared memory array 의 한 차원에 +4 padding) 으로 conflict 회피.
SMEM 으로 block 안에서 reuse 했지만, warp 안의 같은 register fragment 는 여전히 여러 번 다시 load 된다. 한 warp 가 더 큰 출력 tile (예: 32×32 또는 32×64) 를 계산하면 — 같은 A fragment 를 여러 B fragment 와 곱한다 = register reuse.
Erik 가 강조한 자리. "in registers and the only way you can do that is if each thread handles more of the output ... so in that sense both these strategies kind of are the same thing from different perspectives". SMEM tile 키우기 vs register tile 키우기는 같은 reuse 를 다른 메모리 계층에서 보는 것이다.
register tiling 의 trade-off — register 더 많이 쓰면 occupancy 가 떨어진다. 즉 SM 위에 동시에 띄울 수 있는 warp 가 줄어든다. 그래서 무한정 키울 수 없다.
강의에서 Erik 가 강조 — "두 효과가 균형 맞춰지는 자리가 있다". NCU 의 achieved occupancy 와 tensor core utilization 두 metric 을 같이 보면서 sweet spot 을 찾는다. Turing 의 register file (256 KB per SM, 64 KB per warp 에서 시작) 한계 안에서.
강의에서 별도 섹션처럼 풀어준 자리. NCU 의 “Warp State Statistics” 가 보여주는 stall 카테고리들의 의미. 매 단계 NCU diff 를 읽을 때 이 어휘가 손에 있어야 한다.
__syncthreads() 또는 fence 에서 다른 warp 를 기다림. tile size 와 warp 수의 균형 문제.강의에서 Erik 가 한 정리 — "naive 에서 column-major 로 가면 LG throttle 이 가장 많이 줄고, SMEM tile 추가하면 long scoreboard 가 줄고, register tile 추가하면 short scoreboard 까지 줄어든다. math pipeline stall 이 늘어나면 compute bound 에 다가가는 것".
다음 주 강의(L045 H100 cuBLAS 격파)와 이 강의의 코드 시퀀스가 같은 도구로 풀리지 않는 이유 — Hopper 가 새로운 hardware 의 자리를 추가했기 때문.
그래서 두 강의의 관계는 — 이 강의의 5단계 사다리 (column-major → SMEM → register tile → double buffering) 가 H100 강의의 출발점 이다. Hopper 위에서는 그 위에 producer-consumer warp specialization, TMA descriptor 사용, cluster multicast 같은 새 도구가 추가된다.
즉 이 강의의 도구를 손에 익히고 H100 강의로 가는 게 자연스러운 학습 곡선.
"on a more recent GPU you have a lot more confounding factors". Turing 은 새 도구가 적어서 — async copy 도 없고, cluster 도 없다 — 결과를 확인할 변수가 적다. old GPU = clearer signal. 같은 5단계 분석을 H100 에서 하면 async copy 가 같이 변해서 “어느 변경이 무엇을 잡았는지” 분리하기 어렵다.
강의에서 6개월 뒤 다시 돌아왔을 때 가장 빨리 복원해야 하는 사실들과 — 직접 손에 박아야 할 코드 자료들.
__syncthreads() 로 stage 동기화.cudaTensorCoreGemm · immaTensorCoreGemm
cuda.Event 로 ms 측정.ncu --set full -o trace ./naive. 메모리 그래프와 warp stall 분포 확인. 특히 LG Throttle / Long Scoreboard 비율.__shared__ int8_t As[128][32] 같은 declare. bank conflict 회피용 padding 한 줄 추가.ncu compare 또는 같은 trace 두 개를 GUI 에서 열기. stall 분포 변화의 양상을 보고서로 정리.L043 의 5단계 사다리 (column-major → SMEM → register tile → double buffering) 가 시리즈 안에서 어디에 다시 호출되는지 묶어둔다.
학습 노트로 정리하면서 의도적으로 비워둔 자리들 — 강의 안에서 부분적으로만 등장한 주제, 또는 후속 자료가 더 깊게 다룰 주제.
--ptxas-options=-v 로 확인.본 노트의 절대 수치 (1.5s, 1.0s, 0.6s 등)는 강의 transcript 에서 인용. 실제 GPU 모델 (RTX 2060? T4?) 을 캡션이 명시하지 않은 부분이 있다. "Turing" 이라는 말만 분명. 정확한 hardware 베이스라인은 영상 직접 확인 권장. NCU 의 metric 명칭(stall LG throttle 등)은 NVIDIA 표준 명칭으로 확정.