gpumode · 강의 아카이브
《GPU Mode》 L021 Scan Algorithm — Part 2 High priority transcript · slides · available

Scan Algorithm Part 2

Part 1 의 single-block scan 을 GB 단위 array 로 키우는 길 — multi-block scan, warp-level scan, thread coarsening 의 합성. Izzat El Haj 가 reduction tree 로 다시 쌓아올리고, __syncthreads() 가 latency-bound 의 dominant cost 임을 NCU 로 보여주는 part 2 의 학습 노트.

multi-block scan warp-level scan thread coarsening reduction tree __shfl_up_sync register-resident 3 kernel launches latency-bound
I
Speaker
Izzat El Haj
Professor · American University of Beirut · PMPP co-author
강의 번호
L021
스피커
Izzat El Haj
학습 우선순위
High · 정독
다시 볼 때
L020 부터 다시
§ 01강의가 풀려는 문제· scaling beyond 1024

한 block 안 1024 element 의 scan 을 — GB 단위 array 로 키우기

Part 1 (L020) 은 한 block 안의 scan 까지 깔았다. 실전에서 scan 을 하는 array 는 거의 항상 그것보다 훨씬 크다 — 10M, 1B element. 한 block 의 thread 한도(1024)를 넘어선다. 이걸 어떻게 짜는가가 part 2 의 본론.

강의가 깐 큰 질문 세 개.

  1. multi-block scan 의 구조 — 한 block 안 scan 을 어떻게 “이어 붙이는가”. 같은 reduction tree 가 한 단계 더 위로 올라간다(§ 03–04).
  2. 한 단계 더 내려가서 — warp 안에서 scan 을 더 빠르게__shfl_up_sync 로 register-only scan(§ 05).
  3. 왜 더 빨라지지 않는가 — Izzat 이 NCU 로 보여주는 self-finding: scan 이 memory-bound 처럼 보이지만 실은 __syncthreads() 의 latency 가 dominant. 이건 § 09 의 핵심 지점이고 L024 의 출발점.
part 2 의 frame

같은 알고리즘 (Brent-Kung) 을 3 단계 hierarchy 로 다시 짠다 — warp → block → grid. 각 단계에서 같은 idea 가 한 번 더 적용된다. recursive 한 구조가 자연스럽게 드러나는 자리.

“같은 reduction tree 를 다른 size 의 단위에 한 번씩 — warp 32, block 1024, grid N. 이게 GPU scan 의 정식 hierarchy.”Izzat El Haj (요약)
§ 02part 1 의 짧은 복습· block-level scan

Brent-Kung 한 block 의 모양 — 5분 안에 다시 박기

part 2 가 part 1 의 코드 위에 올라간다. 잊었으면 못 따라간다 — 5분 복습.

part 2 의 출발점

이 block-level scan 이 “multi-block 을 짤 때의 한 단위” 가 된다. 같은 코드를 부분적으로 호출해서 hierarchy 의 첫 layer 로 쓴다.

§ 03multi-block 의 핵심 idea· 3 kernel launches

각 block 이 자기 segment 의 partial — 그 partial 들을 다시 scan — 다시 더한다

강의의 첫 큰 figure. 같은 idea 의 3 단계 launch 패턴. 아이디어 자체는 단순한데 — index 계산과 buffer 관리가 까다롭다.

FIG · multi-block scan 의 3 launch같은 알고리즘이 한 단계 위로
입력 B0[0..1023]B1[1024..2047]B2[2048..3071]B3[3072..4095]B4B5B6B7
launch 1 scan B0scan B1scan B2scan B3scan B4scan B5scan B6scan B7
partial sum B0sum B1sum B2sum B3sum B4sum B5sum B6sum B7
launch 2 scan partials → exclusive prefix·······
launch 3 B0 (그대로)B1 + p1B2 + p2B3 + p3B4 + p4B5 + p5B6 + p6B7 + p7
launch 1 — 각 block 이 독립적으로 자기 segment scan. 마지막 element 가 그 block 의 sum. launch 2 — partial sum array (block 수만큼) 를 단일 block 으로 다시 scan (exclusive). launch 3 — 각 block 의 모든 element 에 자기 block 의 prefix sum 을 더함.

이 패턴의 weakness — 중간 partial sum 을 HBM 에 적었다 다시 읽는다. 한 array 를 두 번 통과. § 09 에서 measurement 가 이 비용을 정확히 보여준다.

§ 04reduction tree 로 본 scan· recursive 구조

같은 algorithm 이 단위만 바뀌는 — fractal 한 구조

Izzat 이 강의에서 강조한 mental model. multi-block scan 은 사실 recursive 다. block 의 partial sum array 가 또 너무 크면, 그 array 도 같은 multi-block scan 으로 풀면 된다.

L0 — element한 element 가 한 자리0차원의 base case — scan 안 함register
L1 — warp32 element 의 scanwarp shuffle 만으로 — shared memory 안 씀register
L2 — block~1024 element 의 scanwarp 32 개의 partial sum 을 또 scanshared memory
L3 — grid~1B element 의 scanblock 들의 partial sum 을 또 scan (3 launch)global memory

각 layer 가 자기 위 layer 의 “unit” 이다. 같은 합산 idea 를 서로 다른 단위 (32, 1024, N) 에 한 번씩 적용. 이게 hierarchical scan 의 정신.

왜 이렇게 짜는가

각 layer 의 “단위” 가 hardware 의 동기화 단위와 일치. warp 안에서는 implicit lockstep + shuffle, block 안에서는 __syncthreads() + shared memory, grid 안에서는 kernel launch barrier + global memory. “가장 빠른 동기화 도구를 그 layer 안에서 쓴다.”

§ 05warp-level scan· __shfl_up_sync

shared memory 를 안 거치고 — register 만으로

강의의 가장 만족스러운 트릭 중 하나. 32 thread 의 scan 을 — shared memory 도 __syncthreads() 도 안 쓰고 — warp shuffle instruction 만으로 5 step 안에 끝낸다.

// warp-level Hillis-Steele inclusive scan — 32 thread
__device__ float warp_inclusive_scan(float v) {
  for (int stride = 1; stride < 32; stride *= 2) {
    float n = __shfl_up_sync(0xffffffff, v, stride);
    if (threadIdx.x % 32 >= stride) v += n;
  }
  return v;
}
왜 이게 빠른가

__shfl_up_sync 는 같은 warp 의 다른 lane 의 register 를 직접 읽는 instruction — shared memory 도 거치지 않고 hardware shuffle network 가 cycle 단위로 처리. memory 명령이 0 개. 같은 일을 shared memory 로 하면 매 step 마다 store/load 와 sync 가 든다.

강의에서 Izzat 이 직접 측정한 차이 — warp-level scan 으로 바꾸면 block-level scan 의 시간이 약 30–40% 감소. scan 이 latency-bound 임을 보여주는 첫 강한 신호(§ 09).

§ 06block-level: warp scans 합성· warp 의 partial 들을 다시 scan

1024 thread = 32 warp — warp scans + warp-of-partials scan

block 안 1024 thread 를 32 warp 으로 본다. 각 warp 가 독립적으로 § 05 의 warp scan 을 한다. 그 다음 — 32 개의 “warp 마지막 값” 을 다시 scan (한 warp 으로). 마지막으로 모든 warp 가 자기 prefix 를 받는다.

FIG · block-level scan 의 3 phasewarp scan → warp-of-warps scan → broadcast
A
warp scan
32 warp 가 각각 독립 — register only
B
warp last → shared
각 warp 의 마지막 값 (= warp sum) 을 shared 에 32 개
C
scan of warp sums
한 warp 가 그 32 개를 또 warp scan
D
broadcast + add
각 thread 가 자기 warp 의 prefix sum 을 add
__syncthreads() 는 단 2 번 (B 후, C 후). Brent-Kung 의 2log(BLOCK) ≈ 20 step 보다 훨씬 적은 sync 수. 이게 § 09 의 결정적 차이.
// block-level scan with warp scans — 약식
__device__ float block_inclusive_scan(float v) {
  int tid  = threadIdx.x;
  int lane = tid & 31;
  int wid  = tid >> 5;
  __shared__ float warp_sums[32];

  v = warp_inclusive_scan(v);                     // A — warp scan
  if (lane == 31) warp_sums[wid] = v;          // B
  __syncthreads();

  if (wid == 0) {                                  // C — warp-of-warp-sums scan
    float w = (lane < 32) ? warp_sums[lane] : 0.0f;
    w = warp_inclusive_scan(w);
    warp_sums[lane] = w;
  }
  __syncthreads();

  if (wid > 0) v += warp_sums[wid - 1];          // D — broadcast
  return v;
}
§ 07thread coarsening· 한 thread 여러 element

같은 thread 가 K 개 element 를 처리 — block 의 “effective size” 를 K 배

강의의 마지막 큰 트릭. 한 block 의 thread 수가 1024 로 cap 되어 있으니, “각 thread 가 K 개 element 를 들고 있게” 하면 block 하나가 1024·K 개 element 의 scan 을 처리.

각 thread 의 일.

  1. K 개 element 를 register 에 load — vectorized load (float4) 면 효율적.
  2. thread 안에서 sequential scan — K 가 작으니 (4, 8) 빠르다.
  3. 각 thread 의 “마지막 값” (= thread 안 K 개의 합) 만 들고 block-level scan 에 들어간다 — § 06.
  4. 받은 prefix 를 자기 thread 의 K 개 element 에 다시 더해서 store.
왜 이게 큰 이득인가

block 의 sync 비용은 BLOCK 의 thread 수에만 의존 — element 수에는 의존 안 한다. K 배의 element 를 같은 sync 비용으로 처리. 그리고 thread 안 sequential scan 은 register 에서 끝나니 거의 공짜. 두 효과가 곱한 만큼 빨라짐.

“thread coarsening 은 같은 sync 비용에 더 많은 일을 시키는 트릭이다 — sync 가 dominant cost 인 알고리즘에서 가장 큰 이득.”Izzat El Haj (요약)
§ 08register-resident 와 occupancy· trade-off 회계

K 가 클수록 빠르다 — 그런데 register 가 모자라면 occupancy 가 떨어진다

thread coarsening 의 trade-off. K 개 element 를 register 에 들고 있으려면 — 한 thread 의 register footprint 가 K 만큼 커진다. 그게 너무 크면 SM 위에 띄울 수 있는 block 수가 줄어든다 (occupancy 감소).

K = 1 (no coarsening)
register 적게. occupancy 높음. 그러나 sync 비용 dominant. 가장 느림.
K = 4 ~ 8
sweet spot. register 충분히 작고 sync 비용 분산. 보통 가장 빠름.
K = 16 ~ 32
register footprint 가 매우 커져서 occupancy 가 떨어지기 시작. 한 SM 에 한 두 개 block 만 띄움. memory latency hide 가 어려워짐. 종종 더 느려짐.
K = 64+
register spill 이 시작. local memory (실은 HBM) 에 떨어진다. 거의 항상 더 느림.
강의의 강한 메시지

Izzat 이 한 말 — “occupancy 가 무조건 높아야 좋은 게 아니다. matmul 같은 일부 커널은 의도적으로 낮은 occupancy 로 register 를 충분히 써서 더 빠르다.” register 가 가까운 메모리, occupancy 가 latency hiding. 둘은 trade-off, 정답이 따로.

§ 09latency-bound 의 정체· __syncthreads() 가 dominant

scan 은 “memory-bound 처럼 보이는” latency-bound 알고리즘

강의의 가장 통찰적인 자리. Izzat 이 NCU 로 측정해 보여주는 사실 — naive 한 Brent-Kung block scan 은 DRAM throughput 이 peak 의 30–40% 정도밖에 안 나온다. 흔한 첫 직관 — “더 vectorize 해야지, 더 coalesce 해야지” — 가 맞지 않는다.

실제 dominant cost 는 __syncthreads(). 매 step 마다 모든 thread 가 barrier 를 친다. 1024 thread block 에서 10 step → 10 번의 sync. sync 자체가 latency 를 만든다 — instruction 자체보다 “모두가 도착하기를 기다리는” 시간.

FIG · scan 변형들의 NCU bandwidth 비율 (개념)같은 알고리즘, 다른 hierarchy
구현peak bandwidth 비율비고
naive block-level Brent-Kung~35%
+ warp-level scan (§ 05–06)~55%
+ thread coarsening K=4 (§ 07)~75%
decoupled lookback (L024)~95% · 다음 강의
% 는 강의 narrative 를 재구성한 개념적 값 (확인 필요). 핵심 메시지 — 각 트릭이 sync 와 HBM 왕복을 줄일 때마다 throughput 이 올라간다. 그리고 절대 한계 (memcpy bandwidth) 는 L024 의 본격 주제.
“scan 은 memory-bound 가 아니다 — latency-bound 다. __syncthreads() 와 kernel launch 가 dominant cost. 그걸 인정하면 모든 최적화 전략이 명확해진다.”Izzat El Haj (요약)
§ 10기억할 메모와 코드 자료· key takeaways

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

3-단계 hierarchy
warp → block → grid. 같은 reduction tree 가 다른 size 단위에 한 번씩. 각 단계가 자기 동기화 도구를 씀.
multi-block 의 3 launch
launch 1 — block scan, launch 2 — partial sum scan, launch 3 — block prefix add. naive 패턴.
__shfl_up_sync
warp 안에서 다른 lane register 직접 read. shared memory 도 sync 도 안 씀. 32-thread Hillis-Steele 의 표준.
block scan = warp + warp-of-warp-sums
__syncthreads() 가 단 2 번. 20 번보다 훨씬 빠름. 이게 latency-bound 의 sync 절약.
thread coarsening
한 thread K 개 element. block 의 effective size K 배. sync 비용 분산.
register vs occupancy
K 의 sweet spot 4–8. K 너무 크면 occupancy 떨어지고 register spill.
latency-bound
scan 은 sync/launch 가 dominant. memory-bound 처럼 보이지만 NCU 가 다르게 답한다.
다음 단계
3 launch → 1 launch + decoupled lookback. L024 의 본격 주제.

손에 새기기 — 실습 시퀀스

  1. 3 launch multi-block scan — 1M element 위에서 직접 짜기. block scan + partial scan + add. CPU 결과와 비교.
  2. warp_inclusive_scan__shfl_up_sync 만 써서 32-thread scan. shared memory 안 쓰는 버전.
  3. block scan with warp scans — § 06 의 구조. __syncthreads() 가 정말 2 번만 들어가는지 코드로 검증.
  4. thread coarsening sweep — K ∈ {1, 2, 4, 8, 16} 로 측정. 어디서 best 인지, occupancy 가 어디서 떨어지기 시작하는지.
  5. NCU latency 분석 — “Long scoreboard stalls” 가 얼마인지. __syncthreads 의 stall % 가 차지하는 비중 확인.
  6. CUB vs 직접 구현cub::DeviceScan::ExclusiveSum 과 자기 구현의 시간 비교. 차이가 어디서 오는지 hint 만 봐도 풍부함.
  7. 1B element scaling — 1B element 까지 키워보고 시간이 N 에 정확히 linear 한지. 만약 N log N 이면 어딘가 알고리즘이 나쁜 것.
§ 11다른 강의로 이어지는 길· connections

이 강의 다음으로 어디로 가는가

§ 12열린 질문· open questions

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

검증 메모

이 노트의 단계별 코드는 강의의 narrative 를 따라 약식으로 재구성한 것. 정식 production 코드는 CUB/Thrust source 가 정답이다.

← Lecture 020 Scan Algorithm — Izzat El Haj part 1 Lecture 022 → Hacker's Guide to Speculative Decoding in vLLM — Cade Daniel