《GPU Mode》
L019
Data Processing on GPUs
High priority
transcript · available
Data Processing on GPUs
“ML 이 아닌 GPU 컴퓨팅” 의 영역 — CSV/Parquet 파싱, dataframe filter/groupby/join 같은 SQL-스타일 연산을 어떻게 GPU 위에서 수십 GB/s 로 돌리는가. 전 cuDF 메인테이너이자 Voltron Data 의 Devavret Makkar 가 보여주는 scan · 해시맵 · atomic · dictionary encoding 같은 GPU 데이터 엔지니어링의 빌딩블록들.
cuDF
CSV/Parquet
scan
hashmap on GPU
atomic
dictionary encoding
1-Billion-Row Challenge
groupby
Voltron Data
D
Speaker
Devavret Makkar
Voltron Data · ex-cuDF maintainer (NVIDIA)
§ 01강의가 풀려는 문제· why GPUs for data?
“ML 안 하는 GPU 컴퓨팅” 의 가장 큰 영역 — data engineering
GPU Mode 시리즈의 다른 강의들이 거의 다 모델 학습/추론 에 초점을 맞췄다면, 이 강의는 명시적으로 “데이터 엔지니어링이 GPU 에서 어떻게 일어나는가” 를 깐다. SQL 쿼리, ETL 파이프라인, dataframe API — pandas 같은 인터페이스인데 backend 는 CUDA.
강의가 깐 큰 질문 세 개.
- 왜 데이터 처리를 GPU 에서 하는가 — 의외로 “GPU 가 빠르니까” 가 아니다. 정확한 답은 HBM 의 대역폭(§ 02).
- CSV 같은 가변길이 레코드를 GPU 에서 어떻게 파싱하는가 — sequential 하게 line-by-line 도는 게 inherent 한 알고리즘인데, GPU 는 sequential 을 못 한다. scan 으로 푼다(§ 04).
- groupby/aggregation 같은 SQL 기본기는 어떻게 짜는가 — atomic + GPU hashmap 의 조합(§ 05–06).
강의의 인지적 frame
“같은 작업을 CPU 에서 30 GB/s 로 한다 → GPU 에서 1.5 TB/s HBM 으로 한다” 의 50× 가 데이터 엔지니어링이 GPU 로 옮겨가는 진짜 이유. 모델 학습보다 I/O 가 dominant 한 영역이라, 더더욱 메모리 대역폭이 중요하다.
“useful 한 데이터셋의 평균 크기가 LLM 모델보다 훨씬 크다 — 그게 GPU 데이터 처리가 의미 있는 이유.”Devavret Makkar (요약)
§ 02왜 데이터가 GPU 로 가는가· bandwidth math
같은 SQL 쿼리, 50× 짧은 시간 — 산수의 이유
강의 도입의 한 figure. CPU DRAM 의 대역폭과 GPU HBM 의 대역폭을 같은 축에 놓는다.
FIG · CPU DRAM vs GPU HBM 대역폭같은 데이터, 다른 read 시간
DDR5 — CPU server~100 GB/s
HBM2e — A100~2.0 TB/s
HBM3 — H100~3.0 TB/s
HBM3e — H200/B200~4.8 TB/s
memory-bound op (filter, groupby, scan…) 의 시간은 거의 트래픽/대역폭. HBM 이 30× 빠르면 같은 op 가 30× 빠르다. 단순한 산수.
그런데 데이터가 처음부터 GPU 위에 있을 리는 없다 — disk → CPU memory → GPU memory 의 PCIe 단계를 지난다. 그래서 “disk 에서 한 번만 읽고, 그 다음 모든 처리는 GPU 안에서” 의 패턴이 표준. cuDF 의 read_csv · read_parquet 가 처음부터 GPU 메모리로 직접 적재하는 이유다.
PCIe 우회 — GPUDirect Storage
최신 setup 에서는 NVMe → GPU memory 를 PCIe 직접 DMA 로 — CPU 안 거침. cuDF + KvikIO + GDS 조합이 그 영역. 이 강의에서는 짧게 언급되지만, 데이터 엔지니어링 측면에서 점점 중요해지는 자리.
§ 03CSV 파싱이 왜 어려운가· variable-length records
“line 하나 = thread 하나” 가 안 되는 이유
행렬 곱 같은 GPU 일은 — “element 위치가 자기 thread 가 어디 있는지” 가 곧바로 결정된다. CSV 는 다르다. line N 이 어디서 시작하는지를 알려면 line 0..N-1 을 다 읽어야 한다. inherent 한 sequential dependency.
FIG · 같은 byte stream 안의 row 경계byte offset → row index
byte0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
dataP,1,3\nQ,2,4\nR,3,5
row 0[0..3] "P,1,3"
row 1[4..7] "Q,2,4"
row 2[8..14] "R,3,5"
\n 의 위치를 모르면 어느 byte 가 어느 row 인지 알 수 없다. 그리고 byte 의 “이게 \n 인지” 는 byte 자체로 결정 — sequential dependency 가 아니다. parallel 하게 \n 위치 찾기 만 가능하면, 거기서부터 다시 row 단위 처리가 parallel 해진다.
강의의 작은 트릭 — bytestream 을 청크로 나누고 각 청크의 시작 부근에서 가까운 \n 까지 skip 한 자리를 row 시작으로 본다. partial line 은 두 청크의 경계에서 합치기. 이렇게 하면 대략적인 parallelism 은 얻지만 정확한 row index 는 아직 모른다 — 거기서 scan 이 들어온다.
§ 04scan 으로 CSV 파싱하기· prefix-sum based parser
“이 byte 가 몇 번째 row 의 몇 번째 column 인가” — 한 prefix-sum 으로 답한다
강의에서 Devavret 이 가장 흥미롭다고 강조한 부분. 동료의 PhD thesis 였던 아이디어 — “CSV/JSON 파싱 전체를 scan 의 합성으로 표현”. 핵심은 한 byte 의 “role” (newline, separator, quote, escape, …) 이 inputs 자체에서만 결정되고, 그 role 의 누적이 곧 row/col index 라는 사실.
FIG · CSV 파싱의 scan 분해byte → token → cell index
1
tokenize
byte → {NL, COMMA, OTHER} · 1:1 map (parallel)
2
scan(NL → +1)
prefix-sum: 누적 newline 수 = 현재 row index
3
scan(COMMA → +1, NL → 0 reset)
segmented scan: 현재 row 안 col index
4
gather
(row, col) → cell start byte / end byte
5
parse cell
parallel — 각 cell 을 자기 thread 가 atoi/atof
단계 2, 3 은 정확히 scan / segmented scan —
L020 의 알고리즘이 여기서 데이터 엔지니어링의 핵심 기본기로 등장한다.
scan 은 GPU 데이터 엔지니어링의 가장 중요한 primitive.
quote 안에 들어 있는 comma 는 어떻게 처리하나? — quote 의 toggle 도 scan 으로 표현된다 (XOR 누적). 강의는 깊이 들어가지 않지만, 같은 idea 의 확장.
JSON 파싱도 같은 패턴
cuDF 의 JSON 파서도 거의 같은 구조 — bracket nesting depth 를 scan 으로 누적. 그리고 그 depth 가 변하는 자리가 token boundary. simdjson 이 CPU 위에서 같은 idea 를 SIMD 로 구현한 것.
“같은 데이터를 한 번만 통과하면서 — newline 누적, comma 누적, quote 토글이 모두 scan 의 합성으로 표현된다. CSV 파싱이 곧 prefix-sum 문제다.”Devavret Makkar (요약)
§ 05groupby — atomic 으로 누적· hash · atomic · aggregate
“같은 key 를 만난 thread 들이 어떻게 같은 자리에 더하는가”
SQL 의 가장 흔한 연산 — SELECT city, AVG(temp) FROM measurements GROUP BY city. 이걸 GPU 에서 짜는 표준 패턴이 hash → atomic add. 강의의 핵심 두 figure 중 하나.
FIG · groupby aggregate 의 핵심 흐름thread → hash → atomic 누적
L0 · row한 row → 한 threadrow 의 key 와 value 를 register 에 loadparallel
L1 · hashhash(key) → bucket indexopen addressing — linear probeon-chip
L2 · atomicatomicAdd(&sum[bucket], value)
atomicAdd(&count[bucket], 1)같은 bucket 을 친 thread 들끼리 직렬화global memory
L3 · finalizeavg = sum / countbucket 별 1 thread 가 한 번씩parallel
contention — 같은 도시가 자주 나오면 그 bucket 위에서 atomic 이 직렬화. 하지만 GPU atomic 은 매우 빠르다. 특히 같은 SM 안의 thread 끼리는 L2 위에서 atomic 이 끝남(HBM 까지 안 감). § 06 의 hashmap 디자인이 이 contention 을 더 줄이는 트릭.
강의의 한 가지 디테일 — bucket 수가 너무 작으면 collision 이 폭발하고, 너무 크면 메모리 낭비. 일반적인 cuDF/cuco 의 default 는 load factor 50% 정도로 bucket array allocate. dictionary 처럼 unique key 를 미리 알면 perfect hash 도 가능.
왜 sort-based 가 아닌가
CPU 의 표준 groupby 는 “sort 후 같은 key 의 연속 구간을 reduce”. GPU 에서는 sort 가 비싸다 (O(n log n)). hash + atomic 이 거의 항상 빠르다. 다만 cardinality 가 매우 높을 때(unique 가 많을 때) sort-then-reduce 가 메모리 측면에서 유리한 경우도 있다.
§ 06GPU 위 hashmap 의 구조· cuco · open addressing
“GPU 에서는 hashmap 이 자료구조가 아니라 array 이다”
강의에서 Devavret 이 강조한 메시지. CPU 의 std::unordered_map 같은 chained hashmap 은 GPU 에서 잘 안 도는다 — pointer chasing 이 random read 를 만든다. GPU 는 항상 open addressing 의 flat array.
CPU style — chained
- bucket → linked list of (key, value) nodes
- collision → list 에 append
- random pointer chasing → cache miss
- CUDA 에서 거의 못 씀
GPU style — open addressing
- flat array of slots — slot = (key, value)
- hash → start index, collision → probe (linear/quadratic)
- contiguous read → coalesced
- insertion 도 atomic CAS 한 번으로 끝
- cuCollections (cuco) 의 표준 구조
// GPU hashmap insert — 핵심만
__device__ void insert(slot_t* table, int capacity, K key, V val) {
int idx = hash(key) % capacity;
while (true) {
// CAS — empty slot 을 잡으면 내가 차지
K prev = atomicCAS(&table[idx].key, EMPTY, key);
if (prev == EMPTY) { // 새 entry — value 초기화
atomicAdd(&table[idx].value, val);
return;
}
if (prev == key) { // 이미 같은 key 의 entry — 누적
atomicAdd(&table[idx].value, val);
return;
}
idx = (idx + 1) % capacity; // linear probe
}
}
그리고 강의의 또 한 자리 — shared memory 안 hashmap. 작은 cardinality (수백~수천 unique) 면 block 하나가 자기 hashmap 을 SRAM 안에 두고 끝낸다. 그 다음 block 들의 결과를 hierarchy 로 합친다. cuDF 의 groupby 가 정확히 이 패턴.
Devavret 이 직접 다시 짠 사례
강의 끝부분에서 Parquet writer 의 dictionary encoding 커널 이야기 — 기존 hand-tuned 커널이 device memory 까지 자주 갔는데, shared memory 안에 hashmap 을 두고 거의 안 나가게 다시 짰더니 더 빨라졌다는 일화. on-chip 우선의 구조적 이득이 직접 손에 잡히는 자리.
§ 071-Billion-Row Challenge· tailor-made vs general API
같은 문제를 두 가지로 — 손으로 짠 커널 vs cuDF/SQL 일반 API
강의의 가장 구체적인 케이스 스터디. 1 Billion Row Challenge — 10억 줄의 (도시, 온도) 측정값에서 도시별 min/avg/max 를 뽑아라. CPU 에서 ~10초 안에 푸는 게 목표. GPU 에서 두 접근.
접근 A — tailor-made kernel
- 도시 unique 가 ~400 개 (작음 — 사전 알려져 있음)
- shared memory 안에 도시별 (sum, count, min, max) 4-tuple 두기
- 한 번 read · per-thread parse · atomic update on shared
- block 간 합치기로 마무리
- 한 kernel 안에서 끝. 데이터를 한 번만 통과.
접근 B — general SQL/cuDF
read_csv → 모든 column dtype/parse
groupby('city').agg(['min','mean','max'])
- SQL 엔진은 plan 만들어 — scan, hash, aggregate 단계가 분리됨
- 중간 buffer 가 HBM 에 머무름
- generic 한 만큼 데이터 통과 횟수가 많음
강의에서 보여준 결과 — tailor-made 가 더 빠르긴 하지만 차이가 생각만큼 크진 않다. SQL engine 도 well-engineered 면 데이터 통과를 1–2 회로 압축한다. “손으로 짠 커널의 이득은 알고리즘적 통합이고, SQL 엔진의 이득은 generality” 의 trade-off 가 명확히 드러나는 사례.
tailor-made 의 큰 이점
“한 번만 통과해도 된다”. min/avg/max/count 를 한 atomic 묶음으로 누적. SQL plan 은 (a) parse, (b) groupby aggregate (c) result projection 의 3 단계가 따로 데이터를 만진다. 그 차이가 cardinality 가 클 때 더 벌어진다.
“tailor-made 커널은 이 데이터에 대해 한 번만 통과하면 된다는 사전 지식을 직접 utilize. 일반 API 는 그 사전 지식을 표현할 자리가 없다.”Devavret Makkar (요약)
§ 08Parquet · dictionary encoding· file format on GPU
columnar 파일이 GPU 와 잘 맞는 이유
CSV 가 가변길이의 row-oriented 면, Parquet 는 columnar + 사전 디코드 가능한 메타데이터를 들고 있다. row group / page / dictionary 의 계층. 각 column 이 contiguous 하게 저장되어 있어 GPU coalesced load 와 자연스럽게 맞는다.
FIG · Parquet 파일의 계층row group → column chunk → page
1
file
N row groups · footer 에 메타데이터
2
row group
M columns · 각 column chunk 가 독립 압축
3
column chunk
K pages · 각 page 가 independent encode
4
page
RLE / DICT / PLAIN — encode scheme
중요한 건 — page 단위가 독립적으로 디코드 가능. GPU 에서는 page 하나당 block 하나를 띄워 parallel decode. cuDF 의 Parquet reader 가 그 구조.
그리고 dictionary encoding — column 의 값이 적은 unique 면, 사전(dictionary) 한 번 + 인덱스 array. 압축 + lookup 효율 둘 다 좋아진다. 강의에서 Devavret 이 “cuDF Parquet writer 의 dictionary encoding 커널을 다시 짰던” 일화 (§ 06 참고) 가 이 자리.
§ 09Voltron — query engine 으로· SQL → GPU plan
cuDF 가 dataframe 이라면, Voltron 은 SQL engine
Devavret 의 현재 회사 Voltron Data 의 제품 — Theseus / Velox 같은 GPU-native query engine. SQL 을 받아서 GPU 친화적 execution plan 으로 컴파일해서 돌린다. cuDF 가 “손으로 호출하는 라이브러리” 라면 Voltron 은 “DB 처럼 쓴다”.
SQL → GPU plan 의 단계
(1) SQL parse → logical plan. (2) optimizer → join order, predicate pushdown, projection pruning. (3) GPU-aware physical plan — 어떤 op 가 GPU 에서 도는지, multi-GPU 면 어떤 column 이 어디 있는지. (4) execution — scan/filter/groupby/join 이 § 04–06 의 primitive 위에서 도는 것.
이 영역의 큰 통찰 — “데이터 엔지니어링이 GPU 로 옮겨가는 건, ML 이 GPU 로 옮겨간 패턴의 5–10년 뒤 반복”. 같은 hardware/software 스택을 다른 도메인에서 다시 쓰는 흐름.
§ 10기억할 메모와 코드· key takeaways · repo
다시 열었을 때 5분 안에 손으로 잡혀야 할 것
데이터 엔지니어링이 GPU 에서 어떻게 돌아가는지의 첫 mental model — 그 핵심 사실들.
대역폭 산수
CPU DDR5 ~100 GB/s, GPU HBM3 ~3 TB/s. 같은 memory-bound op 가 약 30× 빠른 게 GPU 데이터 엔지니어링의 본질.
CSV = scan 문제
newline 누적이 row index, comma 누적이 col index — segmented scan 으로 분해.
L020 scan 이 이 강의의 사전 지식.
groupby = hash + atomic
한 row 한 thread → hash bucket → atomic add. shared memory 안 hashmap 으로 contention 을 줄임.
open addressing only
GPU hashmap 은 항상 flat array + linear probe. chained map 은 pointer chasing 으로 느림. cuCollections 가 표준.
tailor-made vs general
한 도메인 사전 지식이 있으면 tailor-made 가 데이터 통과를 1 회로 압축. SQL engine 은 generality 의 가격으로 N 회 통과.
Parquet 의 page
독립 디코드 가능. block 하나당 page 하나로 parallel decode. dictionary encoding 으로 압축 + lookup.
GPUDirect Storage
NVMe → GPU 직접 DMA. CPU 안 거치고. cuDF + KvikIO 조합으로 큰 데이터 read 의 PCIe 비용을 잡음.
data engineering ⊃ ML
평균 데이터셋이 모델보다 훨씬 큼. GPU 가 ML 을 거쳐 데이터로 확장되는 흐름.
손에 새기기 — 실습 시퀀스
- cuDF first run — 1 GB 정도의 CSV 를 pandas 와 cuDF 로 같은 groupby 돌려보고 시간 비교. 차이가 30× 가까운지 확인.
- scan 으로 \n 위치 찾기 — 작은 byte stream 위에서 newline 위치를 직접 prefix-sum 으로 누적하는 CUDA 커널 짜기.
cub::DeviceScan 활용.
- shared memory hashmap — 100 unique key 에 대해 block 하나가 SRAM 위에 hashmap 하나 두고 atomic 누적. 이후 block 간 합치기.
- 1BRC tailor kernel — 자기가 직접 작은 1BRC clone (10M rows) 을 짜고 — SQL/pandas vs cuDF vs tailor kernel 세 시간 비교.
- cuCollections 직접 써보기 —
cuco::static_map 으로 unique counting 같은 작업. open addressing 의 insert 함수 source 직접 읽기.
- Parquet page-level parallel — 작은 Parquet 파일의 column chunk 안 page 수를 footer 에서 읽고, page 단위 parallel decode 가 가능한지 head 만 본다.
§ 11다른 강의로 이어지는 길· connections
이 강의의 primitive 가 다른 자리에서 어떻게 다시 등장하는지
§ 12열린 질문· open questions
다음에 다시 들었을 때 직접 검증해야 할 것들
- scan-based CSV parser 의 정확한 구현 — 강의에서 reference 한 PhD thesis/paper 의 정확한 출처는 자막에서 명시되지 않음. 추적 필요 (확인 필요).
- cuDF 의 groupby kernel 위치 — repo 의
cpp/src/groupby/hash/ 안 코드를 직접 읽으면 § 05–06 의 mental model 과 정확히 어떻게 매칭되는지 확인 가능.
- 1BRC 의 GPU 결과 — 강의에서 정확한 시간 숫자가 명시되지 않음. tailor-made vs cuDF 의 차이 % 가 얼마인지 자기 GPU 에서 재현 필요.
- Voltron 의 internals — Theseus 가 actually 어떻게 SQL → GPU plan 을 만드는지 강의에서 깊이 들어가지 않음. 별도로 paper / blog 추적.
- JSON 파싱의 stack-based 분해 — § 04 의 callout 에서 언급한 nesting depth scan. simdjson 의 알고리즘과 GPU 버전의 차이는 후속 자료로 보강 필요.
- multi-GPU groupby — 강의는 single-GPU 위주. multi-GPU 에서 hash partition + shuffle 이 어떻게 도는지는 RAPIDS 의 dask-cuDF 영역.
검증 메모
이 노트의 대역폭 그래프와 % 분포는 표준 spec sheet 에 근거한 예시. 실제 환경에서 측정값은 thermal/PCIe topology 등에 따라 차이가 날 수 있다.