gpumode · 강의 아카이브
《GPU Mode》 L043 2025 · FEB · 09 High priority transcript · 약 80분 · available

int8 tensorcore matmul for Turing

RTX 2060 같은 Turing GPU 위에서 INT8 tensor core 만으로 matmul 을 단계별로 빠르게 — naive → column-major → shared memory → register tiling. 같은 NCU diff 화면을 매번 다시 보면서 어떤 transformation 이 어떤 stall 을 무슨 비율로 바꾸는지 추적한 educational 기획. Erik Schultheis 의 직접 CUDA 작성 시퀀스 학습 노트.

Turing INT8 IMMA mma_sync NCU diff shared memory register tiling column-major warp stalls
E
Speaker
Erik Schultheis
GPU Mode core contributor · LMC 개발자
강의 번호
L043
스피커
Erik Schultheis
학습 우선순위
High · 정독
다시 볼 때
NCU diff 를 직접 재현
§ 01강의가 풀려는 문제· why educational int8 on Turing

“peak 빠르게 짜는 법” 이 아니라 “느린 데서 빠른 데로 가는 사다리”

강의 도입에서 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) 의 같은 화면에서 본다.

강의의 인지적 frame

모든 단계의 진단은 한 질문으로 회수된다 — “where's my data?” Steven Jones 의 GTC 슬라이드에서 빌려온 표현. row-major 가 strided read 를 만든다, SMEM 으로 stage 가 안 되어 L1-L2 traffic 이 폭발한다, register tile 이 부족해서 같은 데이터를 반복해서 fetch 한다 — 모두 데이터가 잘못된 자리에 있어서 생기는 stall.

“tensor core 만 쓴다고 빨라지지 않는다. 1.5초에서 1초로 줄이는 첫 걸음은 column-major 한 줄 — 그 다음부터 SMEM, register, occupancy 의 사다리.”Erik Schultheis · 32:00 정리

이 강의의 가치 — 같은 도구(NCU)가 같은 코드의 5개 변형에서 어떻게 다른 hint 를 주는지의 reading 자료. NCU 를 처음 본 사람에게 가장 좋은 1시간짜리 worked example 이다.

§ 02왜 INT8 인가· CPU vs GPU · DP4A · IMMA

integer matmul 이 floating-point matmul 과 어떻게 다른지부터

강의 첫 5분이 집중한 자리. INT8 matmul 이 단순히 “float 을 int 로 바꾼” 게 아니라는 것 — register width 가 안 맞는다. INT8 두 개를 곱하면 결과는 INT16, accumulator 는 INT32. 즉 input 폭과 output 폭이 다르다. 이게 hardware instruction 의 모양을 결정한다.

강의에서 깐 비교점.

  • CPU (AVX-512 VNNI)VPDPBUSD 같은 4-way INT8 dot product instruction. 부호 있는/없는 INT8 의 조합이 한 instruction 안에 박혀 있다.
  • CUDA non-tensor core__dp4a intrinsic. INT8×4 vs INT8×4 → INT32 dot product 누적. CPU 의 VNNI 와 사실상 동치.
  • CUDA tensor core (Turing+)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);
§ 03Turing 의 mma.sync 와 fragment· 16×16×16 · m16n8k16

tensor core 가 한 번에 무엇을 처리하는가

Turing 의 INT8 tensor core 는 warp 단위 instruction 이다. 32개 thread 가 같이 호출해야 한다. 한 번의 호출이 16×16×16 matmul 을 처리. 16 = M, 16 = N, 16 = K.

FIG · m16n16k16 INT8 tensor core 호출 한 번warp = 32 thread
A · 16×16
a
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
a
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
a
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
a
·
·
·
·
·
·
·
·
·
·
·
·
INT8 · row-major 일 때 strided
×
B · 16×16
b
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
b
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
b
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
b
·
·
·
·
·
·
·
·
·
·
·
·
INT8 · column-major 가 자연
+
C · 16×16
c
c
c
c
·
·
·
·
·
·
·
·
·
·
·
·
c
c
c
c
·
·
·
·
·
·
·
·
·
·
·
·
c
c
c
c
·
·
·
·
·
·
·
·
·
·
·
·
c
c
c
c
·
·
·
·
·
·
·
·
·
·
·
·
INT32 accumulator
한 warp 가 모든 fragment 를 분담해서 들고 있는다. load_matrix_sync 가 input 을 GMEM/SMEM 에서 register fragment 로 분산 적재. mma_sync 가 32 thread 의 register 위에서 직접 matmul. store_matrix_sync 로 결과를 출력.

강의에서 직접 인용한 사실들.

  • fragment 안 layout 은 “unspecified” — NVIDIA docs 가 이렇게 표시한다. 즉 thread → register 매핑이 architecture 별로 다를 수 있다. 사용자는 fragment 를 직접 인덱스 하지 않는다 — load/store/mma sync 만 부른다.
  • fragment 는 일반 register 다 — “special” 한 register 가 아니다. 일반적인 register pressure 가 그대로 적용. 한 warp 가 너무 많은 fragment 를 들고 있으면 occupancy 떨어진다.
  • load_matrix_sync 의 access 가 곧 일반 GPU load — 즉 coalescing 이 그대로 중요하다. row-major B 행렬은 K 차원 따라 strided read 가 된다 = 큰 문제.
// 강의의 첫 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);
}
§ 04naive 결과와 첫 NCU 진단· 4 TFLOPs · L1↔L2 630 GB

tensor core 만으로는 빠르지 않다 — 어디로 가야 하는지를 NCU 가 가리킨다

강의 첫 큰 surprise. 위의 코드를 15000×15000 크기 입력으로 돌리면 — 1.5초, 약 4 TFLOPs. Turing 의 INT8 tensor core peak 가 60+ TFLOPs 이므로 일 자릿수 utilization. 이유를 NCU 가 가리킨다.

FIG · naive INT8 tensor core 커널의 NCU 메모리 그래프15000² · Turing
device → L2device memory ↔ L2 cache 통신량206 GB
L2 → L1L2 ↔ L1 통신량 — 같은 데이터가 수십 번 다시 fetched630 GB
register실제 사용된 register footprintsmall
device memory 가 8–16 GB 인 GPU 에서 이미 device→L2 가 206 GB. L2→L1 은 그 3배. 같은 데이터를 수십 번 다시 가져온다는 뜻. cache reuse 가 거의 0.

그리고 NCU 가 같이 보여주는 두번째 화면 — warp stall reasons.

FIG · NCU warp stall 분포 (naive)높을수록 cycle 낭비
Stall LG Throttle
~28%
Stall Long Scoreboard
~22%
Stall Math Pipe
~9%
Stall Selected (productive)
~5%
LG throttle = load 명령이 너무 많아서 in-flight load queue 가 가득 차서 새 load 를 발행조차 못 함. Long scoreboard = load 는 발행됐는데 결과가 도착하기 전에 다음 instruction 이 그 데이터를 기다림. 둘 다 “데이터가 제때 안 와서” stall.
진단의 핵심

NCU 가 가리키는 두 stall 모두 memory traffic 자체가 너무 많다는 신호. tensor core 는 거의 idle. 다음 단계의 모든 transformation 은 이 두 metric 을 줄이는 방향으로만 진행된다.

§ 05column-major 의 효과· uncoalesced 제거

한 줄 변경 — B 를 column-major 로 — 1.5s → 1.0s

강의의 첫 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 의 답.

  • matmul 은 O(N³) compute, transpose 는 O(N²) memory.
  • 15000² → transpose 는 약 6 ms, matmul 은 1.5 s. 0.4% 비용.
  • memory bound transpose 도 어차피 빨리 끝난다 — 그리고 한 번만 돌면 된다.

결과적으로 한 줄 변경 + 미리 transpose 의 비용으로 1초로 단축. NCU 의 두 metric 도 변한다.

FIG · column-major 적용 후 NCU diffprevious vs current
v0naive row-major Buncoalesced strided reads on K1.50 s4 TFLOPs
v1column-major Bcoalesced K-stride loads0.97 s7 TFLOPs
memory request 수 −60%, L1↔L2 traffic −30%, device→L2 −40%. 기본 access pattern 한 줄로 잡힌 자리.
청중 질문 — 카운터 직관

“stall ratio 가 줄어들 거라 기대했는데, 오히려 비슷하거나 늘었다”. Erik 의 설명 — stall 은 normalized metric. 분모(=실제 instruction issue cycle) 도 같이 줄었다. instruction count 가 −45% 줄어서 (index 계산 instruction 이 사라져서), ratio 가 큰 변화 없이 보일 뿐 — 절대 cycle 수는 줄었다. "NCU diff 는 절대값과 상대값을 같이 봐야 한다" 의 교훈.

§ 06shared memory tile· SMEM 위에 stage

HBM 까지 안 가도 되도록 — block 안에서 reuse

다음 fix. block 안의 여러 warp 가 같은 A 행, 같은 B 열을 반복해서 읽는다. 한 번 SMEM 에 올려두면 — 그 block 안에선 register 로 stage 가능. PMPP 책의 표준 tiling 패턴이 그대로.

FIG · GPU 메모리 계층 — “데이터가 어디 있는가”크기 ↓ · 속도 ↑
HBM (device)8–16 GB · 수백 GB/s · 모든 데이터~500 GB/s
L2 cache~5 MB · device 전체에서 공유~2 TB/s
SMEM / L1SM 당 ~96 KB · block 단위로 stage~10 TB/s
registerthread 당 직접 접근 · 가장 빠름peak
naive 커널은 모든 fragment 가 directly HBM ↔ register. SMEM tile 을 끼우면 block 안의 warp 들이 같은 데이터를 SMEM 에서 공유. HBM 까지 가는 횟수가 tile 크기에 비례해서 감소.

강의의 SMEM 추가 시퀀스.

  1. block 단위 tile size 결정 — 보통 128×128.
  2. K 차원을 BLOCK_K (예: 32) 로 쪼개서 stage.
  3. warp 들이 협력해서 GMEM → SMEM 으로 한 stage 분량 load.
  4. warp 별로 자기 부분 SMEM → fragment register 로 load.
  5. __syncthreads() 로 stage 사이 동기화.

결과: 0.97s → ~0.6s. NCU 가 다시 가리키는 다음 자리는 register pressure.

SMEM 도입 시 함정

SMEM bank conflict — INT8 8개 element 가 한 word 에 들어간다. row 별로 4 byte stride 면 같은 bank 에 걸린다. 강의에서 Erik 이 한 코드 비교에서 보여준 — padding 으로 stride 어긋남(예: shared memory array 의 한 차원에 +4 padding) 으로 conflict 회피.

§ 07register tiling· thread 당 더 많은 출력

같은 데이터를 register 에 더 오래 잡아두기 — 한 thread 가 여러 출력 계산

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 를 다른 메모리 계층에서 보는 것이다.

FIG · 5단계 커널의 누적 효과15000² · Turing INT8
v0naivetensor core 호출만1.50 s4 TFLOPs
v1column-major Bcoalesced K loads0.97 s7 TFLOPs
v2SMEM tileblock-level reuse~0.6 s11 TFLOPs
v3register tilethread 당 더 많은 출력~0.3 s22 TFLOPs
v4double bufferingSMEM stage 와 compute overlap~0.2 s33 TFLOPs
강의의 orders of magnitude 표현은 절대값보다 NCU diff 의 양상을 본다. 각 단계마다 “직전 대비” 어떤 metric 이 어떻게 변하는지가 핵심.

register tiling 의 trade-off — register 더 많이 쓰면 occupancy 가 떨어진다. 즉 SM 위에 동시에 띄울 수 있는 warp 가 줄어든다. 그래서 무한정 키울 수 없다.

강의에서 Erik 가 강조 — "두 효과가 균형 맞춰지는 자리가 있다". NCU 의 achieved occupancytensor core utilization 두 metric 을 같이 보면서 sweet spot 을 찾는다. Turing 의 register file (256 KB per SM, 64 KB per warp 에서 시작) 한계 안에서.

§ 08warp stall 카테고리· long scoreboard · LG throttle · math pipe

NCU 의 stall reasons 가 무엇을 의미하는지 — 5분 정리

강의에서 별도 섹션처럼 풀어준 자리. NCU 의 “Warp State Statistics” 가 보여주는 stall 카테고리들의 의미. 매 단계 NCU diff 를 읽을 때 이 어휘가 손에 있어야 한다.

Stall Long Scoreboard
load 가 발행됐고, 결과가 와야 다음이 진행될 수 있는데 안 옴. memory latency가 hide 되지 않는 상태. 보통 SMEM/register reuse 부족.
Stall LG Throttle
너무 많은 load 가 in-flight 라 새 load 를 발행 못 함. queue 가 꽉 참. traffic 자체가 너무 많다는 신호. coalescing / tile 부족.
Stall Short Scoreboard
SMEM access 또는 비교적 짧은 latency 의 dependency. SMEM bank conflict 의 흔적이 여기에 잡히기도 한다.
Stall Math Pipeline
tensor core 또는 ALU 가 이미 일하고 있어서 다음 instruction 이 못 들어감. 좋은 stall — 여기 비중이 크면 compute bound 에 가깝다.
Stall Barrier
__syncthreads() 또는 fence 에서 다른 warp 를 기다림. tile size 와 warp 수의 균형 문제.
Stall Selected (eligible)
stall 이 아닌 자리. 이 비율이 곧 productive cycle. NCU 의 “warp 가 일하고 있는 비율”.

강의에서 Erik 가 한 정리 — "naive 에서 column-major 로 가면 LG throttle 이 가장 많이 줄고, SMEM tile 추가하면 long scoreboard 가 줄고, register tile 추가하면 short scoreboard 까지 줄어든다. math pipeline stall 이 늘어나면 compute bound 에 다가가는 것".

§ 09Turing 과 H100 의 차이· no TMA · no WGMMA · no async copy

이 강의의 결과가 어디까지 직접 transfer 되고 어디부터 안 되는가

다음 주 강의(L045 H100 cuBLAS 격파)와 이 강의의 코드 시퀀스가 같은 도구로 풀리지 않는 이유 — Hopper 가 새로운 hardware 의 자리를 추가했기 때문.

Aasync copycp.async (Ampere+) · TMA (Hopper+)Turing XH100 ✓
BWGMMAwarp-group MMA · HopperTuring XH100 ✓
Ctensor memory acceleratorSMEM bulk transfer descriptorTuring XH100 ✓
Dblock clusterSM 간 SMEM 통신Turing XH100 ✓
Emma.syncwarp 단위 MMA · 본 강의의 도구Turing ✓H100 ✓

그래서 두 강의의 관계는 — 이 강의의 5단계 사다리 (column-major → SMEM → register tile → double buffering) 가 H100 강의의 출발점 이다. Hopper 위에서는 그 위에 producer-consumer warp specialization, TMA descriptor 사용, cluster multicast 같은 새 도구가 추가된다.

즉 이 강의의 도구를 손에 익히고 H100 강의로 가는 게 자연스러운 학습 곡선.

Turing 을 일부러 고른 이유

"on a more recent GPU you have a lot more confounding factors". Turing 은 새 도구가 적어서 — async copy 도 없고, cluster 도 없다 — 결과를 확인할 변수가 적다. old GPU = clearer signal. 같은 5단계 분석을 H100 에서 하면 async copy 가 같이 변해서 “어느 변경이 무엇을 잡았는지” 분리하기 어렵다.

§ 10기억할 메모와 코드· key takeaways · repo

다시 열었을 때 5분 안에 손에 잡혀야 할 것

강의에서 6개월 뒤 다시 돌아왔을 때 가장 빨리 복원해야 하는 사실들과 — 직접 손에 박아야 할 코드 자료들.

tensor core peak 만으로는 부족
naive 는 peak 의 5–10%. column-major + SMEM + register tile 의 3단계가 모두 들어가야 30%+.
where's my data?
Steven Jones 의 GTC 표현. naive→x10 의 모든 transformation 이 이 한 질문의 변형.
column-major B
두 행렬이 다 row-major 면 K 차원 sum 에서 strided. transpose 는 O(N²) 라 무료. 첫 fix.
SMEM tile
block 안의 warp 들이 같은 데이터 reuse. __syncthreads() 로 stage 동기화.
register tile
warp 당 출력 tile 키우기. fragment reuse 늘리되 register pressure / occupancy trade-off.
NCU diff 보는 법
stall ratio 와 절대 instruction count 둘 다. 분모가 같이 줄면 ratio 만 보면 잘못된 결론.
long scoreboard vs LG throttle
전자 = latency 미해소, 후자 = traffic 과다. fix 방향이 다르다.
integer = deterministic
test 가 단순. tolerance 결정 안 해도 됨. exact-match.
YouTube youtube.com/watch?v=BgGe_erJB1A · 약 80분
관련 슬라이드 lecture_042/int8_mm_turing.pdf (slides 가 lecture_042 폴더에 있음)
참고 코드 CUDA toolkit samples · cudaTensorCoreGemm · immaTensorCoreGemm
PMPP 참조 Ch. 5 (Memory architecture) · Ch. 7 (Tiled matrix multiplication) — 같은 패턴

손에 새기기 — 실습 시퀀스

  1. naive INT8 mma.sync 커널 베이스라인 — wmma fragment 로 16×16×16 호출. 5000² 정도로 작게 시작. cuda.Event 로 ms 측정.
  2. NCU 한 번 돌리기ncu --set full -o trace ./naive. 메모리 그래프와 warp stall 분포 확인. 특히 LG Throttle / Long Scoreboard 비율.
  3. column-major B — 별도 transpose 커널 작성, B 를 column-major 로 변환 후 fragment 의 layout flag 변경. NCU diff 로 traffic 감소 확인.
  4. SMEM tile 추가 — block tile size 128×128, K-stage 32. __shared__ int8_t As[128][32] 같은 declare. bank conflict 회피용 padding 한 줄 추가.
  5. register tile 키우기 — warp 당 출력 32×32 또는 32×64. fragment 여러 개를 한 warp 가 들고 있게.
  6. NCU diff 5단계 모두 비교ncu compare 또는 같은 trace 두 개를 GUI 에서 열기. stall 분포 변화의 양상을 보고서로 정리.
  7. cuBLAS INT8 baseline 측정 — 같은 크기로 cuBLAS 의 INT8 GEMM. 본 강의의 결과가 cuBLAS 의 몇 % 인지 확인. 완벽 격차가 아니라 격차의 origin 이 학습 목표.
§ 11다른 강의로 이어지는 길· connections

이 강의의 도구가 다음에 어디에 다시 등장하는지

L043 의 5단계 사다리 (column-major → SMEM → register tile → double buffering) 가 시리즈 안에서 어디에 다시 호출되는지 묶어둔다.

§ 12열린 질문· open questions

다음에 들었을 때 직접 검증해야 할 것들

학습 노트로 정리하면서 의도적으로 비워둔 자리들 — 강의 안에서 부분적으로만 등장한 주제, 또는 후속 자료가 더 깊게 다룰 주제.

검증 메모

본 노트의 절대 수치 (1.5s, 1.0s, 0.6s 등)는 강의 transcript 에서 인용. 실제 GPU 모델 (RTX 2060? T4?) 을 캡션이 명시하지 않은 부분이 있다. "Turing" 이라는 말만 분명. 정확한 hardware 베이스라인은 영상 직접 확인 권장. NCU 의 metric 명칭(stall LG throttle 등)은 NVIDIA 표준 명칭으로 확정.

← Lecture 042 Mosaic GPU — Adam Paszke Lecture 044 → NVIDIA Profiling — speaker TBD