트렌딩 딥다이브 · 2026-07-22 · TrendShift Daily #2 · MARCELROED

marcelroed/gigatoken 딥다이브
— LLM 토크나이징을 초당 기가바이트로 끌어올린 Rust 엔진

gigatoken(기가토큰)은 대규모 언어모델(LLM) 학습용 텍스트를 초당 수 기가바이트(GB/s) 속도로 토큰화하는 Rust 토크나이저다. 놀라운 건 배수다 — 이미 멀티스레드 Rust로 돌아가는 HuggingFace tokenizers보다도 최대 약 1000배 빠르면서, 출력 토큰은 완전히 동일한 드롭인(drop-in) 교체품이다. GPT-2 토크나이저 기준, 144코어 서버에서 24.5 GB/s가 나온다. 비결은 단순한 "코드 튜닝"이 아니라 발상의 전환이다. 대부분의 토크나이저가 병목으로 삼는 정규식(regex) 전처리를 버리고, 그 자리를 SIMD 비트마스크 스캐너로 갈아끼웠다. 여기에 캐시 라인 하나에 딱 맞춘 커스텀 해시테이블, huge page, dual-cursor 파이프라이닝까지 얹어 CPU를 한계까지 짜낸다.

(저장소 marcelroed/gigatoken · PyPI gigatoken · v0.9.0 · 순수 Rust(edition 2024, nightly) + pyo3 Python 바인딩 · CUDA 없음(CPU/SIMD 전용) · Rust ~2.56만 줄 · MIT · 저자 Marcel Rød(Stanford) · ★90 · 2025-11 시작 · 클론 소스 직접 분석)
목차
  1. 프로젝트 한 줄 요약
  2. 왜 주목받는가 — tiktoken·HF와 뭐가 다른가
  3. 기술 스택 전체 지도
  4. 아키텍처 심화 — 3개의 속도 축
  5. 디렉토리 구조 해부
  6. 학습 포인트 — 여기서 훔쳐올 설계
  7. 하드웨어 / 시스템 요구사항
  8. 직접 해볼 수 있는 실습 과제
  9. 관련 기술 심화 학습 로드맵
  10. 핵심 키워드 사전
  11. 참고 링크

1프로젝트 한 줄 요약

"Tokenize your text data at GB/s!" — 텍스트 데이터를 초당 기가바이트로
한 문장으로

gigatoken = LLM 학습 데이터를 "인터넷 규모"로 토큰화하기 위해, CPU의 SIMD·캐시·파이프라인을 극한까지 쓴 초고속 BPE 토크나이저

pip install gigatoken 한 줄이면 설치된다. 기존 코드에서 토크나이저 객체만 gt.Tokenizer("openai-community/gpt2")로 바꾸면, HuggingFace가 16초 걸리던 일을 1.4초에 끝낸다. 출력 토큰 ID는 원본과 한 개도 다르지 않게 검증한다.

저자의 주장을 그대로 옮기면, EPYC 서버 속도라면 Common Crawl 전체(사실상 인터넷 전체, 약 130조 토큰)를 6.5시간 미만에 토큰화할 수 있다. LLM을 처음부터 학습시키는 사람에게 토큰화는 "데이터 파이프라인의 숨은 며칠"인데, 그걸 몇 시간으로 접는 도구다.

토크나이저가 뭔지부터 짚자. LLM은 글자를 직접 먹지 않는다. 텍스트를 토큰(token)이라는 정수 조각들로 쪼갠 뒤, 그 정수 배열을 입력으로 받는다. 이 "텍스트 → 정수 배열" 변환을 하는 게 토크나이저다. GPT 계열은 대부분 BPE(Byte-Pair Encoding)라는 방식을 쓴다.

용어
BPE (Byte-Pair Encoding, 바이트 쌍 부호화)
텍스트를 바이트 단위로 시작해, "가장 자주 붙어 나오는 쌍"을 반복해서 하나의 토큰으로 합쳐 나가는 압축식 토큰화 기법이다. 예를 들어 t+h가 자주 붙으면 th를 새 토큰으로 만들고, 다시 th+e가 자주 나오면 the를 만드는 식이다. 미리 학습해 둔 merge 규칙 목록을 순서대로 적용하면 어떤 텍스트든 토큰 배열로 바뀐다. GPT-2, Llama, Qwen, DeepSeek 등 거의 모든 주류 LLM이 BPE 계열을 쓴다.

문제는 양(量)이다. 현대 LLM은 수조~수십조 토큰으로 학습하는데, 그 전에 원본 텍스트(수 TB)를 전부 토큰화해야 한다. 이때 토크나이저 속도가 느리면 GPU가 놀고 데이터 준비에만 며칠이 날아간다. gigatoken은 바로 이 "학습 전 대량 토큰화" 구간을 겨냥한 순수 처리량(throughput) 특화 도구다. 챗봇이 한 문장 토큰화하는 지연시간(latency)이 아니라, "11.9 GB짜리 OpenWebText를 통째로 얼마나 빨리 삼키느냐"가 목표다.

구현은 순수 Rust다. GPU도 CUDA도 쓰지 않는다. 오직 CPU 한 대의 SIMD 명령어와 캐시 계층을 마지막 한 방울까지 짜내서 이 속도를 낸다. Python 사용자는 pyo3 바인딩을 통해 이 Rust 엔진을 그냥 import gigatoken으로 쓴다. 저자 Marcel Rød는 스탠퍼드 소속이며, 이 프로젝트는 스탠퍼드의 유명한 강의 CS336("Language Modeling from Scratch")의 BPE 과제에서 출발해 프로덕션 패키지로 자라난 것으로 보인다.

2왜 주목받는가 — tiktoken·HF와 뭐가 다른가

"이미 빠른" 경쟁자들보다 세 자릿수 배수 빠르다는, 검증 가능한 주장

토큰화는 이미 최적화가 많이 된 분야다. OpenAI의 tiktoken도, HuggingFace의 tokenizers속도 때문에 Rust로 작성됐고 멀티스레드로 돈다. 그런데 gigatoken은 "이미 빠른" 그들보다 다시 100~1300배 빠르다고 주장한다. 보통 이런 주장은 과장이지만, gigatoken은 같은 입력에서 토큰이 한 개도 다르지 않음을 검증하고(공정 비교), 재현 명령까지 제공한다는 점에서 눈길을 끈다.

실측 벤치마크 (owt_train.txt = OpenWebText 11.9 GB)

토크나이저gigatokenHF tokenizerstiktokenvs HF
GPT-2 (EPYC 144코어)24.53 GB/s24.8 MB/s36.0 MB/s989×
GPT-2 (M4 Max 16코어)8.79 GB/s~6.9 MB/s~63 MB/s1268×
Llama 3 계열22.15 GB/s48.5 MB/s457×
Qwen 3 계열22.16 GB/s34.2 MB/s648×
DeepSeek V3/R119.69 GB/s26.2 MB/s750×
Gemma 3 (SentencePiece)3.43 GB/s357 MB/s9.6×

표를 정직하게 읽는 법: BPE 계열(GPT-2·Llama·Qwen·DeepSeek)은 15~24 GB/s로 압도적이지만, SentencePiece 계열(Gemma·Mistral)은 3~4 GB/s로 뚝 떨어진다. 저자 스스로 "SentencePiece는 아직 최적화가 덜 됐다(우선순위 낮음)"라고 README에 명시한다. 즉 "1000배"는 BPE에서의 최고치이고, 실제 배수는 토크나이저 종류에 따라 10배~1300배로 갈린다. 이렇게 약점을 숨기지 않는 태도가 오히려 신뢰를 준다.

비유

기존 토크나이저 = 글자를 한 자씩 짚어가며 읽는 사서. 정규식 엔진이 "여기가 단어 경계인가?"를 글자마다 물어본다. 성실하지만 느리다.

gigatoken = 페이지 전체를 한눈에 스캔하는 속독가. 64바이트를 한 번의 SIMD 명령으로 통째로 분류해 "단어 경계 지도"를 만들고, 이미 본 단어는 통째로 외워둔 캐시에서 즉시 꺼낸다. 글자를 하나씩 짚는 대신 문단을 통으로 훑는다.

왜 이렇게까지 빠른가 — 핵심 통찰 하나

gigatoken의 가장 중요한 발견은 이것이다: "토큰화의 병목은 BPE 병합이 아니라, 그 앞 단계인 사전 토큰화(pretokenization)다." 대부분의 토크나이저는 GPT-2가 정한 복잡한 정규식('s, ?\p{L}+, ?\p{N}+ 같은 규칙)으로 텍스트를 먼저 "단어 비슷한 조각"으로 자른다. 이 정규식 처리가 전체 시간의 절반을 먹는다. gigatoken은 이 정규식을 SIMD 명령어로 손수 재구현해 이 절반을 통째로 날려버렸다. 나머지 절반(BPE 병합, 캐시 조회)도 캐시 친화적으로 다시 짰다. 세부는 4장에서 뜯어본다.

용어
사전 토큰화 (pretokenization)
BPE 병합을 적용하기 전에, 텍스트를 "단어 비슷한 조각(pretoken)"으로 먼저 쪼개는 단계다. 예컨대 "Hello world"["Hello", " world"]로 나눈다. BPE 병합은 이 조각 안에서만 일어나고 조각 경계를 넘지 않는다. 이렇게 미리 잘라두면 병합 대상이 짧아져 빠르고, 공백·구두점 처리가 일관돼진다. 문제는 이 "자르기" 규칙이 복잡한 유니코드 정규식이라 느리다는 것 — gigatoken이 정조준한 지점이다.

드롭인 호환 — 갈아끼우기만 하면 된다

빠르기만 하면 안 쓴다. 기존 코드를 다 바꿔야 한다면 아무도 옮겨오지 않기 때문이다. gigatoken은 .as_hf(), .as_tiktoken() 어댑터로 HuggingFace fast tokenizer APItiktoken Encoding API 양쪽을 흉내 낸다. 특수 토큰, eot_token, n_vocab까지 맞춘다. 즉 기존 학습 스크립트에서 import 한 줄만 바꾸면 되는 이 "이식 비용 제로" 설계가, 순수 속도만큼이나 사람들이 몰리는 이유다.

3기술 스택 전체 지도

GPU 없이 순수 CPU만으로 — Rust 코어 + 얇은 Python 껍데기 + maturin 배포

gigatoken의 기술 선택은 "속도를 위한 것"과 "쓰기 편하게 하기 위한 것"으로 딱 나뉜다. 무거운 계산은 전부 Rust가 하고, 사용자와 만나는 얇은 표면만 Python이다. 특이하게도 GPU·CUDA·C++이 전혀 없다 — CPU 한 대의 SIMD와 캐시만으로 승부한다.

계층별 지도

계층핵심 기술역할
언어/툴체인Rust edition 2024 · nightly · portable_simd안정 버전에 없는 SIMD 기능을 쓰려고 nightly를 씀
Python 다리pyo3 0.29 (abi3-py310) · numpy 0.29Rust 함수를 import gigatoken으로 노출
병렬성rayon 1.11work-stealing 스레드 풀로 청크를 전 코어에 분배
속도용 자료구조rustc-hash(FxHash) · dashmap · 커스텀 해시테이블표준 해시보다 빠른 조회, 학습 때 동시성 맵
SIMD 입출력simdutf(UTF-8 검증) · sonic-rs(JSON) · memchr파일 읽기·검증까지 SIMD로 가속
파일 입력memmap2(mmap) · parquet+arrow · zstd/flate2거대 파일을 mmap, 압축·컬럼포맷 직접 해독
토크나이저 로딩ureq(rustls) · aho-corasick · icu · spm_precompiledHF Hub 직접 다운로드, 특수토큰 매칭, 유니코드/SP 정규화
Python 출력awkward(가변길이 배열) · typer(CLI)문서별 길이가 다른 토큰 배열을 효율적으로 담기
빌드/배포maturin · manylinux2014 wheels · criterion(벤치)Rust+Python 혼합 패키지를 pip 휠로 빌드
용어
SIMD (Single Instruction, Multiple Data)
CPU 명령어 하나로 여러 데이터를 동시에 처리하는 기능이다. 보통 명령어가 숫자 1개를 더한다면, SIMD 명령어는 16개·32개·64개를 한 방에 더한다. gigatoken은 이걸로 "64바이트를 한 번에 분류"한다. CPU 종류마다 명령어 집합이 달라서(NEON=ARM/애플, AVX2/AVX-512=인텔·AMD) gigatoken은 실행 시점에 CPU를 감지해 맞는 코드를 고른다.
용어
pyo3 · maturin · abi3
pyo3는 Rust 함수를 Python에서 부를 수 있게 다리를 놓는 라이브러리, maturin은 그 Rust+Python 혼합 프로젝트를 pip install용 휠(wheel)로 빌드하는 도구다. abi3는 "Python 3.10 이상 어느 버전에서도 도는 하나의 바이너리"를 만드는 안정 ABI인데, gigatoken은 배포 편의를 위해 이걸 쓰되 "그 대가로 Python 반복 루프가 조금 느려진다"고 알려진 이슈에 적어뒀다(향후 2배 개선 여지).
비유

Rust 코어 = 공장의 자동화 생산라인, Python = 주문 창구. 손님(사용자)은 창구에서 쉬운 말로 주문만 하고(tokenizer.encode(...)), 실제 대량 생산은 뒤편 Rust 라인이 전 코어를 돌려 처리한다. 가장 빠른 경로(encode_files)에서는 원자재(텍스트 파일)조차 창구를 거치지 않고 라인으로 직행한다 — Python을 거치는 순간 느려지기 때문이다.

4아키텍처 심화 — 3개의 속도 축

SIMD 마스크 스캐너 + 캐시라인 해시테이블 + 병렬·Python우회, 이 셋의 합작

gigatoken이 GB/s를 뽑는 비결은 하나의 마법이 아니라 세 개의 축이 곱해진 결과다. 전체 파이프라인을 먼저 그림으로 보자.

입력 파일 (owt_train.txt, 11.9 GB) ← gz/zst면 메모리에 해제 │ ▼ Rust가 직접 mmap (텍스트가 Python을 거치지 않음 = 최속 경로) ┌──────────────────────────────────────────────────┐ │ 청크 분할기 (batch.rs) │ │ 문서/단어 경계에서 1MB 이상 청크로 자름 │ │ 스레드당 ~16청크 → rayon work-stealing 로드밸런싱 │ └──────────────────────────────────────────────────┘ │ │ │ │ ▼ ▼ ▼ ▼ (CPU 코어마다 병렬) ┌────────────────────────────────────────┐ │ 워커 1개의 인코딩 파이프라인 │ │ │ │ ① SIMD 마스크 스캐너 (pretokenize) │ ← 시간의 49% │ 64바이트 → 클래스 비트마스크 │ │ 비트 대수로 "단어 경계"를 한 번에 │ │ │ pretoken(단어) 하나씩 │ │ ▼ │ │ ② pretoken 캐시 (커스텀 해시테이블) │ ← 시간의 14% │ 99.4% 히트 → 저장된 토큰 즉시 반환 │ │ │ (miss일 때만) │ │ ▼ │ │ ③ BPE 병합 (tiktoken.rs) │ ← 시간의 15% │ PairRankTable로 최소 rank 쌍 병합 │ └────────────────────────────────────────┘ │ │ │ │ ▼ ▼ ▼ ▼ ┌──────────────────────────────────────────────────┐ │ awkward Array (플랫 버퍼 1개 + 오프셋 배열) │ │ 문서마다 numpy 배열을 새로 만들지 않음 │ └──────────────────────────────────────────────────┘

축 ① — 정규식을 SIMD 비트마스크로 대체 (가장 큰 승부수)

일반 토크나이저는 GPT-2가 정한 정규식으로 단어 경계를 찾는다. gigatoken은 이걸 simdjson 스타일 마스크 스캐너로 손수 짰다. 64바이트를 SIMD로 읽어 바이트마다 "글자/숫자/공백/구두점" 클래스를 비트마스크로 만들고, 단어 경계 비트를 레지스터 안 비트 연산만으로 유도한다. 워커는 경계 비트를 하나씩 pop하며 pretoken을 뽑는다 — 바이트마다 if로 분기하던 걸 없앤 것이다.

SIMD가 없는 CPU를 위한 스칼라 폴백도 배울 게 많다. 인트린식 없이 순수 산술로 8바이트를 병렬 판정하는 SWAR 기법이다:

// 분기(if) 없이 산술만으로 바이트 종류 판정 — SWAR
#[inline(always)]
fn is_letter(b: u8) -> bool { (b | 0x20).wrapping_sub(b'a') < 26 }
fn is_digit(b: u8)  -> bool {  b.wrapping_sub(b'0')         < 10 }

b | 0x20은 대문자를 소문자로 접는 트릭(비트 하나만 켜면 Aa)이고, wrapping_sub 뒤 범위 비교는 "a~z 사이냐"를 분기 없이 답한다. 분기가 없으면 CPU의 분기 예측 실패가 사라져 파이프라인이 안 멈춘다.

용어
SWAR (SIMD Within A Register)
전용 SIMD 명령어 없이, 그냥 64비트 정수(레지스터) 하나에 8바이트를 담아 일반 산술·비트 연산으로 8개를 동시에 처리하는 기법이다. 특별한 CPU 기능이 필요 없어 이식성이 최고다. "가난한 자의 SIMD"라 불리기도 하지만, gigatoken에선 SIMD가 없는 환경의 든든한 폴백이자 스칼라 경로의 기반이 된다.

축 ② — 캐시 라인에 딱 맞춘 pretoken 캐시

같은 단어(pretoken)는 텍스트에 계속 나온다. the, of, and... 그래서 한 번 계산한 단어의 토큰 결과를 외워두면 BPE를 다시 돌릴 필요가 없다. gigatoken의 ShortPretokenCache는 1GB OpenWebText에서 99.4% 적중한다. 설계가 지독하게 하드웨어 친화적이다:

비유

pretoken 캐시 = 자주 쓰는 단어를 통째로 외운 속기사. "the"가 나올 때마다 철자를 하나씩 분석하지 않는다. 눈에 들어오는 순간 기호 하나로 적는다. 100번 중 99번은 이렇게 "외운 대로" 처리하고, 처음 보는 낯선 단어(1%)만 실제로 BPE 병합을 돌린다.

축 ③ — 병렬성과 "Python 우회"

마지막 축은 규모다. Rust가 파일을 직접 mmap해 텍스트가 Python 인터프리터를 아예 거치지 않고 rayon 풀로 흩어진다. 거대한 단일 문서조차 단어 경계에서 쪼개 전 코어를 쓰되, 토큰 결과는 바이트 단위로 동일하게 재조립한다. 출력은 문서마다 numpy 배열을 새로 만드는 대신 awkward Array(플랫 버퍼 1개 + 오프셋)로 담아 할당 폭발을 막는다. 불변(immutable) 모델 테이블은 Arc로 공유해 프로세스 fork도 O(1)이다.

설계 교훈
"경계를 넘나드는 비용"이 진짜 적

이 프로젝트가 반복해서 증명하는 것: 순수 계산보다 계층 경계를 넘는 비용(Python↔Rust 왕복, 캐시 미스, TLB 미스, 분기 예측 실패)이 더 크다는 사실이다. 그래서 세 축 모두 "경계 넘기를 없애는" 방향으로 설계됐다 — 정규식 엔진 호출 제거, 메모리 접근 1회로 축소, Python 우회.

물리적 바닥 — 더는 못 빠른 이유

흥미롭게도 저자는 프로파일링으로 "여기가 한계"임을 증명한다. 성능 카운터를 보면 Bad speculation(분기 예측 실패)이 25%인데, 단어 경계는 텍스트 내용에 따라 제멋대로라 CPU가 원리적으로 예측할 수 없다. 즉 이 25%는 튜닝으로 못 없애는 물리적 바닥이다. PGO(프로파일 기반 최적화)를 시도했다가 "데이터 의존 분기라 효과 없음"으로 되돌린 기록도 남아 있다.

5디렉토리 구조 해부

src(Rust 코어) + gigatoken(Python 껍데기) + profiling(최적화 일지)
gigatoken/ ├── src/ # Rust 코어 (~2.56만 줄) │ ├── lib.rs # pyo3 모듈: BPETokenizer / SentencePieceTokenizer │ ├── main.rs # 독립 실행 바이너리(벤치용) │ ├── batch.rs # ★ 병렬 청크 인코딩 엔진 (mmap→분할→rayon) │ ├── bpe_train.rs # BPE 학습(dashmap + priority-queue) │ ├── pretokenize/ # ★ SIMD 사전토큰화 │ │ ├── mod.rs # 타입 디스패치, span 인프라 │ │ ├── unicode.rs # 유니코드 클래스 패킹 테이블 │ │ ├── fast/ # 스킴별 고속 구현 │ │ │ ├── mask.rs # ★ 마스크 스캐너 공통(NEON/AVX512/AVX2) │ │ │ ├── r50k.rs # GPT-2 계열 │ │ │ └── qwen2·deepseek_v3·kimi·nemotron.rs │ │ └── reference/ # 스칼라 레퍼런스(정답 검증용) │ ├── bpe/ │ │ ├── tiktoken.rs # ★ byte-level BPE 인코더 + 메모이즈 │ │ ├── sentencepiece.rs # SP + byte fallback │ │ ├── pretoken_cache.rs # ★ 커스텀 캐시라인 해시테이블 │ │ └── mod.rs # ByteRemapping, PairRankTable │ ├── bindings/ # pyo3 글루(bridge, sources, padding, train, hub) │ ├── input/ # 파일 리더(parquet, jsonl, decompress, mmap) │ └── load_tokenizer/ # 로더(hf, tiktoken, hub — HF Hub 직접 다운로드) ├── gigatoken/ # Python 패키지 (~2,300줄) │ ├── _tokenizer.py # 통합 Tokenizer 래퍼 │ ├── _hf_compat.py # transformers fast-tokenizer 드롭인 │ ├── _tiktoken_compat.py # tiktoken.Encoding 드롭인 │ ├── _cli.py # `gigatoken bench` CLI │ └── _parallel.py # multiprocessing 워커 자동 감지 ├── benches/ # criterion 벤치 ├── benchmarks/ # 라이브러리 간 비교 + results.json / families.json ├── profiling/ # ★ 상세 프로파일 리포트(campaign_report, report) ├── tests/ # pytest + Rust 테스트(HF/tiktoken 패리티) ├── examples/ # quickstart, hf_drop_in, encode_files ├── README.md · design_doc.md · pretokenizer_optimization_log.md └── Cargo.toml · pyproject.toml · rust-toolchain.toml · LICENSE(MIT)

구조만 봐도 "어디를 먼저 읽어야 하는지"가 보인다. 속도의 핵심 3개는 src/batch.rs(병렬 엔진), src/pretokenize/fast/mask.rs(SIMD 스캐너), src/bpe/pretoken_cache.rs(캐시)다. 그런데 학습 자료로 가장 값진 파일은 코드가 아니라 문서다 — 루트의 pretokenizer_optimization_log.mdprofiling/ 폴더. 여기엔 "무엇을 시도해서 얼마가 나왔고, 무엇이 실패했으며 왜인지"가 단계별로 적혀 있다.

용어
byte-level BPE · byte fallback
byte-level BPE는 글자가 아니라 바이트(0~255)에서 병합을 시작하는 방식이다. 그러면 세상의 어떤 문자·이모지·깨진 텍스트도 항상 표현할 수 있다(모든 것은 바이트니까). byte fallback은 SentencePiece 계열이 "사전에 없는 글자를 만나면 바이트로 쪼개 처리"하는 안전장치다. gigatoken은 로딩 시 tokenizer.jsonbyte_fallback 플래그를 보고 두 Rust 백엔드(BPETokenizer/SentencePieceTokenizer) 중 알맞은 것을 자동으로 고른다.

6학습 포인트 — 여기서 훔쳐올 설계

토크나이저를 안 만들어도 배울 게 많다 — 고성능 시스템 프로그래밍의 교본

gigatoken이 특별한 이유는 "빠른 토크나이저"라서가 아니라, 고성능 CPU 코드를 어떻게 짜고 어떻게 증명하는지를 통째로 보여주는 살아있는 교본이라서다. 토큰화에 관심이 없어도 아래 다섯 가지는 어떤 성능 작업에나 그대로 옮겨 쓸 수 있다.

배울 것 ①

병목은 "당연한 곳"에 없다 — 먼저 재라

모두가 "BPE 병합이 느리겠지"라 생각할 때, 저자는 프로파일링으로 정규식 사전토큰화가 절반임을 찾아냈다. 최적화의 첫 걸음은 코드를 고치는 게 아니라 어디가 진짜 병목인지 측정하는 것. profiling/report.md의 "walker 49% / BPE miss 15% / cache 14%" 분해가 모든 결정의 출발점이었다.

배울 것 ②

분기를 없애면 CPU가 안 멈춘다 — SWAR·비트마스크

if가 많은 코드는 CPU 분기 예측이 틀릴 때마다 파이프라인이 비워진다. is_letter(b|0x20).wrapping_sub(b'a') < 26 같은 분기 없는 산술로 바꾸는 SWAR, 그리고 조건을 비트마스크로 모아 한 번에 처리하는 기법은 파서·검증기·스캐너 어디에나 통한다.

배울 것 ③

자료구조를 캐시 라인 단위로 설계하라

같은 알고리즘도 메모리 접근 패턴이 속도를 가른다. "키를 u128로 패킹해 비교 1회", "엔트리를 32바이트로 맞춰 조회당 캐시 라인 1개", "값을 인라인 저장해 2차 접근 제거", "huge page로 TLB 미스 방지" — 표준 해시맵을 이기는 이 네 수는 성능이 필요한 모든 조회 구조에 응용된다.

배울 것 ④

처리량과 레이턴시는 다르다 — dual-cursor ILP

최적화 일지의 백미. 병목이 "일이 많아서"가 아니라 의존 체인이 길어서(레이턴시)임을 발견하고, 독립적인 커서 2개를 번갈아 처리(인터리브)해 CPU의 비순차 실행이 파이프라인 빈틈을 채우게 했다 — 코드량은 그대로인데 25% 공짜 향상. 처리량 최적화와 레이턴시 최적화는 접근이 정반대라는 걸 보여준다.

배울 것 ⑤

실패를 기록하고, 정확성으로 가둬라

pretokenizer_optimization_log.md는 성공만 적지 않는다 — "hot/cold 분리는 되돌림(LLVM 인라인 방해)", "2-pass SWAR는 2.5배 느림", "PGO 효과 없음"까지 이유와 함께 남긴다. 게다가 모든 변경은 "출력 토큰이 HF와 완전히 동일한가" 게이트를 통과해야만 채택된다. 빠르지만 틀린 코드는 무가치하다는 원칙이 프로젝트 전체를 관통한다.

7하드웨어 / 시스템 요구사항

GPU 불필요 — 대신 최신 CPU·nightly Rust·(가급적) Linux

gigatoken은 GPU가 필요 없다. 대신 성능을 다 뽑으려면 SIMD를 지원하는 최신 CPU와 몇 가지 시스템 조건이 붙는다.

항목요구/권장메모
CPUNEON(ARM/애플) 또는 AVX2/AVX-512(x86)없으면 스칼라(SWAR) 폴백 — 느려짐
GPU불필요순수 CPU/SIMD 구현, CUDA 없음
OSLinux 권장 · macOS 지원 · Windows는 WSLhuge page·madvise는 Linux 전용 이득
메모리입력 파일을 mmap할 여유실제 상주는 페이지 단위라 파일 크기보다 적을 수 있음
Python3.10 이상abi3-py310 휠
빌드(소스 시)Rust nightly + maturinpip install 휠 쓰면 빌드 불필요
코어 수많을수록 선형 이득144코어 EPYC에서 24 GB/s, 16코어에서 6~9 GB/s
주의
모든 토크나이저가 GB/s는 아니다

BPE 계열(GPT-2·Llama·Qwen·DeepSeek)은 15~24 GB/s가 나오지만, SentencePiece 계열(Gemma·Mistral)은 3~4 GB/s로 훨씬 느리다. 저자가 "SP는 최적화 우선순위가 낮다"고 밝혀둔 만큼, Gemma/Mistral 위주라면 기대 배수를 낮춰 잡아야 한다. 또 WordPiece(BERT 계열)는 아직 미지원이다.

주의
Beta 단계 · Windows 테스트 부족

패키지 상태는 Beta(v0.9.0)이고 저자가 "Windows 테스트가 부족하니 WSL을 권장"한다고 명시했다. 프로덕션 파이프라인에 넣기 전, 내가 쓰는 토크나이저·플랫폼에서 --validate로 토큰 동일성부터 확인하는 게 안전하다.

8직접 해볼 수 있는 실습 과제

설치 5분부터, 최적화 일지 재현까지 — 난이도별 5단계
과제 1 · 난이도 ★☆☆☆☆ · 10분

설치하고 "토큰이 같은지" 두 눈으로 확인

pip install gigatoken tiktoken 후, 같은 문장을 gigatoken과 tiktoken으로 각각 인코딩해 결과 배열이 정확히 일치하는지 비교한다.

import gigatoken as gt
tok = gt.Tokenizer("openai-community/gpt2")
ids = tok.encode("Tokenize your text data at GB/s!")
print(ids, tok.decode(ids).decode("utf-8"))

배우는 것: 토크나이저의 입출력이 "텍스트 ↔ 정수 배열"이라는 감각, 드롭인 교체의 의미.

과제 2 · 난이도 ★★☆☆☆ · 30분

내 파일로 벤치마크 돌려 배수 재현

텍스트 파일 하나(수백 MB)를 준비해 CLI 벤치를 돌리고, --validate로 HF와 토큰이 같은지까지 확인한다. "1000배"가 내 CPU에선 몇 배인지 직접 재보는 게 핵심.

uvx --with tokenizers gigatoken bench 'openai-community/gpt2' \
     my_text.txt --validate --doc-separator "<|endoftext|>"

배우는 것: 처리량(MB/s·Mtok/s) 읽는 법, CPU 코어 수·SIMD 지원이 배수에 미치는 영향.

과제 3 · 난이도 ★★★☆☆ · 반나절

SWAR is_letter를 직접 짜고 벤치

Rust(또는 C)로 "바이트가 글자인지"를 ① 평범한 if 버전과 ② SWAR/분기 없는 버전으로 각각 짜서, 1GB 텍스트에 대해 처리 속도를 비교한다. pretokenizer_optimization_log.md의 수치(47 MiB/s → 830 MiB/s 여정)를 참고 삼는다.

배우는 것: 분기 제거가 실제로 얼마나 빠른지, 마이크로벤치 짜는 법(criterion).

과제 4 · 난이도 ★★★★☆ · 1~2일

미니 마스크 스캐너 만들기

"공백으로 단어를 나눈다"는 단순 규칙만이라도 SIMD(또는 SWAR)로 비트마스크 스캐너를 구현해 본다. 64바이트를 읽어 "공백 위치 비트마스크"를 만들고, 그 비트를 pop하며 단어 경계를 뽑는다. gigatoken의 mask.rs가 정답지.

배우는 것: SIMD 인트린식, 비트 연산으로 경계 유도, "바이트당 분기" 없애기.

과제 5 · 난이도 ★★★★★ · 주말 프로젝트

perf로 "물리적 바닥" 관찰하기

Linux perf stat으로 자신의 스캐너를 프로파일해 branch-misses·cache-misses·IPC를 읽는다. gigatoken이 말한 "bad speculation 25%가 예측 불가능한 바닥"을 내 코드에서도 재현·측정하고, 어디까지가 튜닝 가능하고 어디부터가 물리 한계인지 선을 그어본다.

배우는 것: 하드웨어 성능 카운터 해석, 레이턴시 vs 처리량 진단, "그만 최적화할 때"를 아는 감각.

9관련 기술 심화 학습 로드맵

"토크나이저 하나"에서 "고성능 시스템 프로그래밍"으로 6주 코스

gigatoken을 제대로 씹어먹으려면 여러 층의 배경지식이 필요하다. 밑에서부터 쌓는 6주 로드맵을 제안한다.

주차주제할 일 / 재료
1주차토큰화의 원리BPE를 손으로 이해하기. Karpathy의 minbpe로 200줄짜리 BPE를 직접 구현. GPT-2 정규식이 무슨 규칙인지 뜯어보기.
2주차Rust 시스템 기초소유권·slice·unsafe·#[inline]. criterion으로 마이크로벤치 짜는 법. gigatoken의 bpe/mod.rs 읽기.
3주차SIMD & SWARSWAR로 바이트 병렬 판정 → std::simd(portable_simd) 맛보기 → NEON/AVX2 인트린식. 과제 3·4 수행.
4주차메모리 계층캐시 라인·TLB·huge page·프리페치·false sharing. "왜 자료구조가 알고리즘보다 중요할 때가 있나". pretoken_cache.rs 정독.
5주차병렬성rayon work-stealing, mmap, 스레드 오버구독/데드락. batch.rs의 청크 분할 전략 분석.
6주차측정과 증명perf·top-down 분석(retiring/bad-speculation), PGO, "레이턴시 vs 처리량". profiling/ 리포트 재현. 과제 5 완성.
비유

이 로드맵은 "요리를 배우는 순서"와 같다. 1주차에 무슨 요리인지(토큰화) 알고, 2주차에 칼질(Rust)을 익히고, 3~5주차에 불·팬·오븐(SIMD·캐시·병렬)을 다루고, 마지막 6주차에 간을 보는 법(측정)을 배운다. 재료(gigatoken 소스)는 이미 최고급이니, 순서대로 따라가면 된다.

10핵심 키워드 사전

이 문서에 나온 개념들, 한 줄로 다시 정리
토크나이저 / 토큰
텍스트를 LLM이 먹는 정수 배열로 바꾸는 도구. 그 정수 조각 하나가 토큰.
BPE (Byte-Pair Encoding)
자주 붙는 바이트 쌍을 반복 병합해 토큰을 만드는 방식. GPT·Llama·Qwen 등 주류 LLM의 표준.
사전 토큰화 (pretokenization)
BPE 병합 전에 텍스트를 단어 비슷한 조각으로 먼저 자르는 단계. gigatoken이 SIMD로 갈아끼운 최대 병목.
SIMD
명령어 하나로 여러 데이터를 동시 처리. NEON(ARM/애플)·AVX2·AVX-512(x86)가 대표.
SWAR
전용 SIMD 명령 없이 64비트 정수 하나로 8바이트를 산술 연산해 병렬 처리하는 이식성 좋은 기법.
처리량(throughput) vs 레이턴시(latency)
처리량 = 초당 몇 GB 삼키나(대량 학습용), 레이턴시 = 한 요청이 얼마나 빨리 끝나나(챗봇용). gigatoken은 처리량 특화.
캐시 라인 · TLB · huge page
CPU가 메모리를 64바이트 덩어리(캐시 라인)로 읽고, 주소 변환을 dTLB로 캐싱한다. 큰 테이블은 huge page로 묶어야 TLB가 안 터진다.
ILP (Instruction-Level Parallelism)
한 코어가 독립적인 명령을 겹쳐 실행하는 능력. 독립 커서 2개를 인터리브하면 파이프라인 빈틈을 채워 공짜 속도가 난다.
분기 예측 실패 (bad speculation)
CPU가 if 결과를 미리 찍었다가 틀리면 파이프라인을 비운다. 단어 경계처럼 예측 불가능한 분기는 근본적 성능 바닥이 된다.
mmap · rayon
mmap=파일을 메모리에 직접 매핑해 복사 없이 읽기. rayon=Rust의 work-stealing 병렬 처리 라이브러리.
pyo3 · maturin
Rust를 Python에서 부르게 하는 다리(pyo3)와, 그 혼합 프로젝트를 pip 휠로 빌드하는 도구(maturin).
tiktoken · SentencePiece · WordPiece
각각 OpenAI·Google이 만든 토크나이저 계열. gigatoken은 tiktoken/HF-BPE에 강하고 SP는 느리며 WordPiece는 미지원.
awkward Array
문서마다 길이가 다른 가변길이 배열을 "플랫 버퍼 1개 + 오프셋"으로 담는 Python 라이브러리. 할당 폭발을 막는다.

11참고 링크

원본부터 배경 지식까지

프로젝트

핵심 소스(먼저 읽을 파일)

기반 기술 / 배경