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 시작 · 클론 소스 직접 분석)
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)라는 방식을 쓴다.
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 과제에서 출발해 프로덕션 패키지로 자라난 것으로 보인다.
토큰화는 이미 최적화가 많이 된 분야다. OpenAI의 tiktoken도, HuggingFace의 tokenizers도 속도 때문에 Rust로 작성됐고 멀티스레드로 돈다. 그런데 gigatoken은 "이미 빠른" 그들보다 다시 100~1300배 빠르다고 주장한다. 보통 이런 주장은 과장이지만, gigatoken은 같은 입력에서 토큰이 한 개도 다르지 않음을 검증하고(공정 비교), 재현 명령까지 제공한다는 점에서 눈길을 끈다.
| 토크나이저 | gigatoken | HF tokenizers | tiktoken | vs HF |
|---|---|---|---|---|
| GPT-2 (EPYC 144코어) | 24.53 GB/s | 24.8 MB/s | 36.0 MB/s | 989× |
| GPT-2 (M4 Max 16코어) | 8.79 GB/s | ~6.9 MB/s | ~63 MB/s | 1268× |
| Llama 3 계열 | 22.15 GB/s | 48.5 MB/s | — | 457× |
| Qwen 3 계열 | 22.16 GB/s | 34.2 MB/s | — | 648× |
| DeepSeek V3/R1 | 19.69 GB/s | 26.2 MB/s | — | 750× |
| Gemma 3 (SentencePiece) | 3.43 GB/s | 357 MB/s | — | 9.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장에서 뜯어본다.
"Hello world"를 ["Hello", " world"]로 나눈다. BPE 병합은 이 조각 안에서만 일어나고 조각 경계를 넘지 않는다. 이렇게 미리 잘라두면 병합 대상이 짧아져 빠르고, 공백·구두점 처리가 일관돼진다. 문제는 이 "자르기" 규칙이 복잡한 유니코드 정규식이라 느리다는 것 — gigatoken이 정조준한 지점이다.빠르기만 하면 안 쓴다. 기존 코드를 다 바꿔야 한다면 아무도 옮겨오지 않기 때문이다. gigatoken은 .as_hf(), .as_tiktoken() 어댑터로 HuggingFace fast tokenizer API와 tiktoken Encoding API 양쪽을 흉내 낸다. 특수 토큰, eot_token, n_vocab까지 맞춘다. 즉 기존 학습 스크립트에서 import 한 줄만 바꾸면 되는 이 "이식 비용 제로" 설계가, 순수 속도만큼이나 사람들이 몰리는 이유다.
gigatoken의 기술 선택은 "속도를 위한 것"과 "쓰기 편하게 하기 위한 것"으로 딱 나뉜다. 무거운 계산은 전부 Rust가 하고, 사용자와 만나는 얇은 표면만 Python이다. 특이하게도 GPU·CUDA·C++이 전혀 없다 — CPU 한 대의 SIMD와 캐시만으로 승부한다.
| 계층 | 핵심 기술 | 역할 |
|---|---|---|
| 언어/툴체인 | Rust edition 2024 · nightly · portable_simd | 안정 버전에 없는 SIMD 기능을 쓰려고 nightly를 씀 |
| Python 다리 | pyo3 0.29 (abi3-py310) · numpy 0.29 | Rust 함수를 import gigatoken으로 노출 |
| 병렬성 | rayon 1.11 | work-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_precompiled | HF Hub 직접 다운로드, 특수토큰 매칭, 유니코드/SP 정규화 |
| Python 출력 | awkward(가변길이 배열) · typer(CLI) | 문서별 길이가 다른 토큰 배열을 효율적으로 담기 |
| 빌드/배포 | maturin · manylinux2014 wheels · criterion(벤치) | Rust+Python 혼합 패키지를 pip 휠로 빌드 |
pip install용 휠(wheel)로 빌드하는 도구다. abi3는 "Python 3.10 이상 어느 버전에서도 도는 하나의 바이너리"를 만드는 안정 ABI인데, gigatoken은 배포 편의를 위해 이걸 쓰되 "그 대가로 Python 반복 루프가 조금 느려진다"고 알려진 이슈에 적어뒀다(향후 2배 개선 여지).Rust 코어 = 공장의 자동화 생산라인, Python = 주문 창구. 손님(사용자)은 창구에서 쉬운 말로 주문만 하고(tokenizer.encode(...)), 실제 대량 생산은 뒤편 Rust 라인이 전 코어를 돌려 처리한다. 가장 빠른 경로(encode_files)에서는 원자재(텍스트 파일)조차 창구를 거치지 않고 라인으로 직행한다 — Python을 거치는 순간 느려지기 때문이다.
gigatoken이 GB/s를 뽑는 비결은 하나의 마법이 아니라 세 개의 축이 곱해진 결과다. 전체 파이프라인을 먼저 그림으로 보자.
일반 토크나이저는 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은 대문자를 소문자로 접는 트릭(비트 하나만 켜면 A→a)이고, wrapping_sub 뒤 범위 비교는 "a~z 사이냐"를 분기 없이 답한다. 분기가 없으면 CPU의 분기 예측 실패가 사라져 파이프라인이 안 멈춘다.
같은 단어(pretoken)는 텍스트에 계속 나온다. the, of, and... 그래서 한 번 계산한 단어의 토큰 결과를 외워두면 BPE를 다시 돌릴 필요가 없다. gigatoken의 ShortPretokenCache는 1GB OpenWebText에서 99.4% 적중한다. 설계가 지독하게 하드웨어 친화적이다:
hashbrown은 2개).pretoken 캐시 = 자주 쓰는 단어를 통째로 외운 속기사. "the"가 나올 때마다 철자를 하나씩 분석하지 않는다. 눈에 들어오는 순간 기호 하나로 적는다. 100번 중 99번은 이렇게 "외운 대로" 처리하고, 처음 보는 낯선 단어(1%)만 실제로 BPE 병합을 돌린다.
마지막 축은 규모다. Rust가 파일을 직접 mmap해 텍스트가 Python 인터프리터를 아예 거치지 않고 rayon 풀로 흩어진다. 거대한 단일 문서조차 단어 경계에서 쪼개 전 코어를 쓰되, 토큰 결과는 바이트 단위로 동일하게 재조립한다. 출력은 문서마다 numpy 배열을 새로 만드는 대신 awkward Array(플랫 버퍼 1개 + 오프셋)로 담아 할당 폭발을 막는다. 불변(immutable) 모델 테이블은 Arc로 공유해 프로세스 fork도 O(1)이다.
이 프로젝트가 반복해서 증명하는 것: 순수 계산보다 계층 경계를 넘는 비용(Python↔Rust 왕복, 캐시 미스, TLB 미스, 분기 예측 실패)이 더 크다는 사실이다. 그래서 세 축 모두 "경계 넘기를 없애는" 방향으로 설계됐다 — 정규식 엔진 호출 제거, 메모리 접근 1회로 축소, Python 우회.
흥미롭게도 저자는 프로파일링으로 "여기가 한계"임을 증명한다. 성능 카운터를 보면 Bad speculation(분기 예측 실패)이 25%인데, 단어 경계는 텍스트 내용에 따라 제멋대로라 CPU가 원리적으로 예측할 수 없다. 즉 이 25%는 튜닝으로 못 없애는 물리적 바닥이다. PGO(프로파일 기반 최적화)를 시도했다가 "데이터 의존 분기라 효과 없음"으로 되돌린 기록도 남아 있다.
구조만 봐도 "어디를 먼저 읽어야 하는지"가 보인다. 속도의 핵심 3개는 src/batch.rs(병렬 엔진), src/pretokenize/fast/mask.rs(SIMD 스캐너), src/bpe/pretoken_cache.rs(캐시)다. 그런데 학습 자료로 가장 값진 파일은 코드가 아니라 문서다 — 루트의 pretokenizer_optimization_log.md와 profiling/ 폴더. 여기엔 "무엇을 시도해서 얼마가 나왔고, 무엇이 실패했으며 왜인지"가 단계별로 적혀 있다.
tokenizer.json의 byte_fallback 플래그를 보고 두 Rust 백엔드(BPETokenizer/SentencePieceTokenizer) 중 알맞은 것을 자동으로 고른다.gigatoken이 특별한 이유는 "빠른 토크나이저"라서가 아니라, 고성능 CPU 코드를 어떻게 짜고 어떻게 증명하는지를 통째로 보여주는 살아있는 교본이라서다. 토큰화에 관심이 없어도 아래 다섯 가지는 어떤 성능 작업에나 그대로 옮겨 쓸 수 있다.
모두가 "BPE 병합이 느리겠지"라 생각할 때, 저자는 프로파일링으로 정규식 사전토큰화가 절반임을 찾아냈다. 최적화의 첫 걸음은 코드를 고치는 게 아니라 어디가 진짜 병목인지 측정하는 것. profiling/report.md의 "walker 49% / BPE miss 15% / cache 14%" 분해가 모든 결정의 출발점이었다.
if가 많은 코드는 CPU 분기 예측이 틀릴 때마다 파이프라인이 비워진다. is_letter를 (b|0x20).wrapping_sub(b'a') < 26 같은 분기 없는 산술로 바꾸는 SWAR, 그리고 조건을 비트마스크로 모아 한 번에 처리하는 기법은 파서·검증기·스캐너 어디에나 통한다.
같은 알고리즘도 메모리 접근 패턴이 속도를 가른다. "키를 u128로 패킹해 비교 1회", "엔트리를 32바이트로 맞춰 조회당 캐시 라인 1개", "값을 인라인 저장해 2차 접근 제거", "huge page로 TLB 미스 방지" — 표준 해시맵을 이기는 이 네 수는 성능이 필요한 모든 조회 구조에 응용된다.
최적화 일지의 백미. 병목이 "일이 많아서"가 아니라 의존 체인이 길어서(레이턴시)임을 발견하고, 독립적인 커서 2개를 번갈아 처리(인터리브)해 CPU의 비순차 실행이 파이프라인 빈틈을 채우게 했다 — 코드량은 그대로인데 25% 공짜 향상. 처리량 최적화와 레이턴시 최적화는 접근이 정반대라는 걸 보여준다.
pretokenizer_optimization_log.md는 성공만 적지 않는다 — "hot/cold 분리는 되돌림(LLVM 인라인 방해)", "2-pass SWAR는 2.5배 느림", "PGO 효과 없음"까지 이유와 함께 남긴다. 게다가 모든 변경은 "출력 토큰이 HF와 완전히 동일한가" 게이트를 통과해야만 채택된다. 빠르지만 틀린 코드는 무가치하다는 원칙이 프로젝트 전체를 관통한다.
gigatoken은 GPU가 필요 없다. 대신 성능을 다 뽑으려면 SIMD를 지원하는 최신 CPU와 몇 가지 시스템 조건이 붙는다.
| 항목 | 요구/권장 | 메모 |
|---|---|---|
| CPU | NEON(ARM/애플) 또는 AVX2/AVX-512(x86) | 없으면 스칼라(SWAR) 폴백 — 느려짐 |
| GPU | 불필요 | 순수 CPU/SIMD 구현, CUDA 없음 |
| OS | Linux 권장 · macOS 지원 · Windows는 WSL | huge page·madvise는 Linux 전용 이득 |
| 메모리 | 입력 파일을 mmap할 여유 | 실제 상주는 페이지 단위라 파일 크기보다 적을 수 있음 |
| Python | 3.10 이상 | abi3-py310 휠 |
| 빌드(소스 시) | Rust nightly + maturin | pip install 휠 쓰면 빌드 불필요 |
| 코어 수 | 많을수록 선형 이득 | 144코어 EPYC에서 24 GB/s, 16코어에서 6~9 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(v0.9.0)이고 저자가 "Windows 테스트가 부족하니 WSL을 권장"한다고 명시했다. 프로덕션 파이프라인에 넣기 전, 내가 쓰는 토크나이저·플랫폼에서 --validate로 토큰 동일성부터 확인하는 게 안전하다.
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"))
배우는 것: 토크나이저의 입출력이 "텍스트 ↔ 정수 배열"이라는 감각, 드롭인 교체의 의미.
텍스트 파일 하나(수백 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 지원이 배수에 미치는 영향.
is_letter를 직접 짜고 벤치Rust(또는 C)로 "바이트가 글자인지"를 ① 평범한 if 버전과 ② SWAR/분기 없는 버전으로 각각 짜서, 1GB 텍스트에 대해 처리 속도를 비교한다. pretokenizer_optimization_log.md의 수치(47 MiB/s → 830 MiB/s 여정)를 참고 삼는다.
배우는 것: 분기 제거가 실제로 얼마나 빠른지, 마이크로벤치 짜는 법(criterion).
"공백으로 단어를 나눈다"는 단순 규칙만이라도 SIMD(또는 SWAR)로 비트마스크 스캐너를 구현해 본다. 64바이트를 읽어 "공백 위치 비트마스크"를 만들고, 그 비트를 pop하며 단어 경계를 뽑는다. gigatoken의 mask.rs가 정답지.
배우는 것: SIMD 인트린식, 비트 연산으로 경계 유도, "바이트당 분기" 없애기.
perf로 "물리적 바닥" 관찰하기Linux perf stat으로 자신의 스캐너를 프로파일해 branch-misses·cache-misses·IPC를 읽는다. gigatoken이 말한 "bad speculation 25%가 예측 불가능한 바닥"을 내 코드에서도 재현·측정하고, 어디까지가 튜닝 가능하고 어디부터가 물리 한계인지 선을 그어본다.
배우는 것: 하드웨어 성능 카운터 해석, 레이턴시 vs 처리량 진단, "그만 최적화할 때"를 아는 감각.
gigatoken을 제대로 씹어먹으려면 여러 층의 배경지식이 필요하다. 밑에서부터 쌓는 6주 로드맵을 제안한다.
| 주차 | 주제 | 할 일 / 재료 |
|---|---|---|
| 1주차 | 토큰화의 원리 | BPE를 손으로 이해하기. Karpathy의 minbpe로 200줄짜리 BPE를 직접 구현. GPT-2 정규식이 무슨 규칙인지 뜯어보기. |
| 2주차 | Rust 시스템 기초 | 소유권·slice·unsafe·#[inline]. criterion으로 마이크로벤치 짜는 법. gigatoken의 bpe/mod.rs 읽기. |
| 3주차 | SIMD & SWAR | SWAR로 바이트 병렬 판정 → 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 소스)는 이미 최고급이니, 순서대로 따라가면 된다.
if 결과를 미리 찍었다가 틀리면 파이프라인을 비운다. 단어 경계처럼 예측 불가능한 분기는 근본적 성능 바닥이 된다.pip install gigatoken)README.md · design_doc.md — 설계 의도와 벤치마크 표pretokenizer_optimization_log.md — ★ 최적화 여정 일지(성공+실패 기록)src/pretokenize/fast/mask.rs — SIMD 마스크 스캐너(속도의 핵심)src/bpe/pretoken_cache.rs — 캐시라인 친화 커스텀 해시테이블src/batch.rs — mmap→청크분할→rayon 병렬 엔진profiling/report.md · profiling/campaign_report.md — 프로파일 분해와 최적화 캠페인