tokenizers v1: 인코딩, 디코딩 및 확장성 측정 결과
요약
tokenizers v1의 출시로 토크나이저 성능이 대폭 개선되었습니다. 이 버전은 이전 버전에 비해 수십 배 빨라졌으며, 단일/멀티 스레드 및 다양한 하드웨어에서의 확장성을 측정했습니다. 이는 모델 워크로드 증가에 맞춰 토큰화 과정의 효율성과 속도를 극대화하는 데 중점을 둡니다.
핵심 포인트
- v1은 이전 버전 대비 수십 배 빠른 성능을 달성했습니다.
- 단일 스레드, 멀티 스레드 등 다양한 환경에서 확장성을 검증했습니다.
- BPE를 포함한 토크나이저 전반에 걸쳐 일반적인 개선을 이루었습니다.
- IBM, NVIDIA 등의 협력으로 플랫폼 지원 범위를 넓혔습니다.
모델이 더 빨라지고 워크로드가 확장됨에 따라 그 균형은 변화하기 시작합니다. 방대한 데이터셋으로 학습시키거나, 많은 동시 요청을 처리하거나, 긴 입력을 반복적으로 처리하는 것은 토크나이저(tokenizer)에게 충분한 압박을 가하여 모델의 데이터를 고갈시킬 수 있습니다.
이것이 바로 우리가 다가오는 tokenizers v1 버전에 성능에 중점을 두기로 결정한 이유입니다. 토큰화는 가벼워야 하며 워크플로우와 함께 확장되어야 합니다. 여러분의 GPU는 CPU가 토큰화를 완료하기를 기다리며 절대 유휴 상태여서는 안 됩니다.
이 글에서는 v1이 어떻게 v0.23보다 수십 배나 더 빨라졌는지 살펴봅니다.
이 작업은 생태계 전체 덕분에 완전히 가능했습니다. 토큰화는 오픈 소스 작업에서 매우 활발한 분야이며, gigatoken, tiktoken, kitoken, tokie, fastokens, wordchipper, ai-tokenizer 등과 같은 라이브러리뿐만 아니라 수많은 다른 라이브러리들이 빠를 수 있는 토크나이저의 가능성을 계속해서 밀어붙여 왔습니다. 우리는 이러한 작업들을 읽었고, 아래 몇 가지 아이디어들은 다른 프로젝트가 시도해 볼 가치가 있다는 것을 보여주었기 때문에 우리에게 도달할 수 있었습니다.
이 리팩토링(refactor) 이전에는 tokenizers가 가질 수 있는 성능에 한참 미치지 못했기 때문에, 여기에 기여하는 것이 가치가 없어 보일 수도 있었습니다. 이 리팩토링을 통해 우리는 tokenizers가 기여할 가치가 있는 라이브러리라는 것을 명확히 하고자 합니다.
또한 플랫폼 지원 범위를 넓히기 위해 패치를 제공하고 다양한 하드웨어에서 테스트를 도와준 IBM, NVIDIA, 그리고 ExecuTorch 팀에게 감사드립니다.
우리는 tokenizers v1의 릴리스 후보(release candidate) 결과를 다른 널리 사용되는 대안들과 비교하여 보여줍니다. 단일 스레드, 멀티스레드, 스레드를 통한 확장성, 모델별 비교, 언어별 비교, 지연 시간(latency), 디코딩 처리량(decoding throughput), 메모리 히프(memory heap), 그리고 크레이트 크기(crate size)에 대해 자세히 다룹니다.
이것은 tokbench 리포지토리에서 실행되었으며, 원하시면 여러분의 하드웨어에서 벤치마크를 다시 실행할 수 있는 명령어를 추가했습니다.
v1은 v0.23과 동일한 토큰 ID를 생성합니다. 목표는 출력을 보존하고, API와 어휘(vocabulary), 병합 순위(merge ranks)를 유지하는 동시에 개선할 수 있는 모든 것을 개선하는 것이었습니다. 여기에는 폭(breadth)도 포함됩니다. 이 라이브러리는 BPE에 특화되기보다는 토크나이저 계열 전반에 걸쳐 일반성을 유지하므로, v1은 v0.23이 로드했던 모든 것을 로드합니다.
토크나이저는 텍스트를 모델이 읽을 수 있는 정수 목록으로 변환합니다. tokenizers는 이 변환을 네 단계에서 실행합니다. 정규화(Normalization)는 원본 텍스트에 소문자 처리(lowercasing) 또는 유니코드 정규화와 같은 연산을 적용합니다. 사전 토큰화(Pre-tokenization)는 텍스트를 사전 토큰(pre-tokens)이라는 더 작은 조각들로 분할합니다. 모델은 각 사전 토큰을 토큰으로 변환하고 이를 어휘에 있는 ID에 매핑합니다. 후처리(Post-processing)는 모델이 예상하는 모든 특수 토큰을 추가합니다.
모델 단계(model stage)에서 여기에 설명된 대부분의 작업이 발생합니다. 이 기사에서 측정된 10개의 모델 계열 중 8개는 바이트 쌍 인코딩(byte pair encoding), 즉 BPE를 사용합니다. BPE는 사전 토큰의 바이트부터 시작하여 순위가 가장 높은 인접한 쌍을 반복적으로 결합할 때까지 진행하며, 더 이상 순위가 매겨진 쌍이 남아있지 않을 때 멈춥니다. 이 순위는 토크나이저가 훈련될 때 학습되며 함께 제공되므로, 동일한 텍스트는 항상 동일한 ID를 생성합니다. 병합은 사전 토큰 경계를 넘지 않습니다. 나머지 두 계열은 라이브러리가 지원하는 다른 두 가지 모델 유형인 WordPiece와 Unigram을 사용합니다.
토큰화 파이프라인 페이지에서는 네 단계를 문서화합니다. Tokenization algorithms는 BPE, WordPiece, 그리고 Unigram에 대해 설명합니다.
각 단계별로 작업이 이루어졌습니다. 중요했던 변경 사항들은 다음과 같습니다:
| change | what it does |
|---|---|
| workspace split | 하나의 크레이트(crate)가 워크스페이스(workspace)가 되었습니다: tk-encode는 필수 런타임이며, tk-serialize, tk-convert, 그리고 tk-train은 애플리케이션이 필요할 때만 연결됩니다 |
| ... | |
| BPE 모델은 정규 표현식(regular expression)을 사용하여 입력 텍스트를 더 작고 처리하기 쉬운 청크인 사전 토큰으로 분할합니다. 병합은 사전 토큰 내부에서 발생하며, 두 사전 토큰 사이의 경계를 넘지 않기 때문에 이 분할이 파이프라인의 나머지 부분이 보는 것을 결정합니다. |
해당 정규 표현식은 모델의 고정 파라미터입니다. 토크나이저와 함께 제공되며 런타임에 절대 변경되지 않기 때문에, 매번 인코딩할 때 일반 목적의 정규 표현식 엔진을 해석할 필요가 없습니다. 주어진 모델이 실제로 사용하는 패턴에 대해서는 수동으로 한 번 분할 함수를 작성할 수 있습니다.
이렇게 손으로 작성한 함수는 최신 CPU의 SIMD 명령어(Single Instruction, Multiple Data)를 사용할 수 있습니다. 이는 하나의 연산을 여러 바이트에 동시에 적용하며 UTF-8 텍스트에 적합합니다. bitcannon은 입력의 바이트를 비트의 병렬 스트림으로 간주하므로, 한 글자씩 전진하는 스캔 방식 대신 전체 레지스터에 걸쳐 부울 연산으로 경계를 찾아냅니다. 이는 레지스터당 64바이트를 결정합니다. 같은 아이디어가 텍스트 처리를 위한 Parabix와 JSON을 위한 simdjson을 구동합니다.
이것은 패턴 인식에 달려 있습니다. 소수의 문법(grammar)만 대부분의 바이트 수준 BPE 모델을 커버하며, 그 패턴이 아닌 토크나이저는 정규 표현식 경로를 유지하고 이러한 속도 향상을 얻지 못합니다. 이것이 위에서 언급된 이득들이 크게 달라지는 이유입니다.
실제 텍스트에는 반복되는 단어가 많습니다. BPE는 주어진 사전 토큰에 대해 항상 동일한 토큰 ID를 생성하므로, v1은 결과를 한 번 처리한 후 저장할 수 있습니다. 스레드 로컬 캐시(thread-local cache)가 각 사전 토큰의 바이트를 해당 토큰 ID에 매핑하여, 이후 발생되는 경우들이 병합 과정을 건너뛸 수 있게 합니다.
자연스럽게도 입력이 증가함에 따라 고유한 단어의 수는 전체 단어 수보다 느리게 증가할 수 있습니다. 반복되는 단어는 시간이 지남에 따라 입력에서 더 큰 비중을 차지하게 됩니다. 여전히 새로운 단어가 나타나기 때문에, 아래 애니메이션에서 간헐적인 누락이 발생합니다.
다음과 같이 공유 접두사(shared-prefix) 결과를 재현할 수 있습니다:
tokbench measure prefix-sharing \
--engine pipeline \
--engine hf-tokenizers \
...
캐싱은 입력에 반복되는 사전 토큰이 포함될 때 가장 효과적입니다. 반복되는 사전 토큰이 적은 입력의 경우, 많은 히트(hit)를 받지 못하면서 조회 비용을 지불할 수 있습니다.
다음 주요 비용은 BPE 병합 루프(BPE merge loop)에서 발생합니다. 각 사전 토큰(pre-token)에 대해, 이 루프는 가장 높은 우선순위의 인접 쌍을 반복적으로 찾아 병합합니다. 이전 구현 방식에서는 호출할 때마다 새로운 메모리를 할당하고 모든 사전 토큰에 대해 새로운 우선순위 큐를 구축했습니다.
v1은 호출자가 소유한 스크래치 버퍼(scratch buffer)를 재사용하여 이러한 반복적인 할당을 제거했습니다. 이 버전은 심볼들을 평면 배열(flat array)에 저장하고, 인접한 심볼들은 해당 배열 내의 위치로 연결함으로써 병합 중 업데이트 비용을 절감합니다. 또한 단일 모델 호출(single model call)에서 사전 토큰 배치(batch of pre-tokens)를 처리합니다.
각 후보 쌍(candidate pair) 역시 하나의 64비트 값으로 패킹되며, 여기에 병합 순위(merge rank)가 상위 비트(high bits)에 포함됩니다. 따라서 두 후보를 비교하는 것은 단순히 두 정수(integer)를 비교하는 것만으로 충분하며, '여기서 병합 없음(no merge here)'은 가능한 가장 큰 값이기 때문에 루프는 분기(branch) 없이 다음 병합을 찾습니다.
벤치마크 설계의 사소한 차이가 토크나이저 성능에 큰 차이를 만들 수 있습니다. 우리는 비교를 일관되게 유지하기 위해 다음과 같은 규칙을 사용했습니다.
| 규칙 | 이유 |
|---|---|
| 단일 타이밍 루프(one timing loop) | 모든 엔진이 동일한 루프를 실행하며, 엔진별 빠른 경로가 없습니다 |
| ... | |
| 반복적으로 하나의 문서를 인코딩하는 것이 같은 빌드에서 개별 문서 스트림을 인코딩하는 것보다 빠를 수 있습니다. 첫 번째 접근 방식은 전체 문서가 이미 캐시(cache)에 표현되어 있을 때의 성능을 측정합니다. 두 번째는 이전에 본 사전 토큰이 캐시에 남아 있도록 허용하면서 새로운 입력에 대한 성능을 측정합니다. |
두 조건 모두 서로 다른 워크로드(workload)를 측정함에도 불구하고 때때로 '웜(warm)'하다고 설명됩니다. 저희의 주요 결과는 개별 문서를 사용하며, 전체 코퍼스(corpus)가 캐시에 담기에는 너무 큽니다. 토크나이저 벤치마크는 어떤 워크로드를 사용하는지 식별해야 하는데, 그 선택이 결과에 지배적일 수 있기 때문입니다.
10개 모델 패밀리 v1의 인코딩 경로를 통틀어 볼 때, Apple M4 Max에서 단일 스레드로 실행했을 때 v0.23보다 텍스트를 3배에서 30배까지 더 빠르게 인코딩합니다. 최저 성능은 t5-base이고 최고 성능은 gpt2입니다. 8개의 워커에 걸쳐 선형적으로 **76%**만큼 확장됩니다. 이러한 변경 사항 전반에 걸쳐 v1은 출시된 라이브러리와 정확히 동일한 토큰 ID를 생성합니다.
전반적인 개선은 여러 변경 사항들이 함께 작동하면서 이루어졌습니다: 정규 표현식 엔진을 대체하는 수동 작성(hand-written) 스플리터, 반복되는 단어를 다시 병합하지 않고 처리하는 캐시, 할당자(allocator)를 전혀 건드리지 않는 병합 루프(merge loop), 그리고 사전 토큰(pre-token)당 하나가 아닌 배치(batch)당 모델 호출을 사용하는 것입니다. 각각은 파이프라인의 서로 다른 지점에서 수행되는 작업을 줄여줍니다.
다음 우선순위는 더 많은 모델 패밀리에 대한 지원입니다. 1.0.0 이전에 새로운 병합 루프로 추가 모델들을 옮길 예정입니다.
릴리스 후보(release candidates)가 안정화되면, 다음 단계는 트랜스포머스 라이브러리(transformers library)와 토크나이저 라이브러리에 의존하는 나머지 생태계 전반에 걸쳐 개선 사항을 가져오는 것이 될 것입니다.
본 게시물은 tokbench 결과를 바탕으로 작성되었으며 지원 범위가 확장됨에 따라 업데이트될 예정입니다.
v1의 릴리스 후보는 crates.io에서 확인할 수 있습니다. 호출하는 API는 기존과 동일하므로, 변경되는 유일한 부분은 설치할 빌드(build)입니다.
설치 방법은 일반적인 방식대로 진행합니다:
cargo add tokenizers --pre
학습 기능은 기본적으로 활성화되어 있어 C++ 종속성까지 함께 가져옵니다. 인코딩만 필요하다면, 학습 구현을 제외하기 위해 이를 비활성화하세요:
cargo add tokenizers --pre --no-default-features --features http
인코딩 방식은 변경되지 않았습니다: 동일한 호출로 동일한 ID를 얻습니다.
use tokenizers::tokenizer::{Result, Tokenizer};
fn main() -> Result<()> {
let tokenizer = Tokenizer::from_pretrained(
이 게시물에 있는 모든 그림은 이 크레이트(crate)를 기준으로 측정되었습니다. Python 바인딩은 동일한 코드를 래핑하며 `bindings/python`에서 빌드되지만, 이러한 측정에는 포함되지 않는 호출당 오버헤드가 추가됩니다.
본 게시물의 벤치마크는 가장 먼저 나열된 완성된 릴리스 후보(release-candidate) 작업을 다룹니다. 나머지 섹션들은 아직 `1.0.0`에 필요한 것과 우리가 이후 탐색할 계획을 보여줍니다.
이 작업은 crates.io의 Rust 프리릴리즈 버전에서 확인할 수 있습니다:
cargo add tokenizers --pre
- 워크스페이스 분리(workspace split): 단일 크레이트를 `tk-encode`, `tk-serialize`, `tk-convert`, 그리고 `tk-train`으로 나누어, 애플리케이션이 사용하는 부분만 연결하도록 합니다. - 비트캐논(bitcannon): 인코딩 경로의 정규식 분할을 GPT-2, cl100k, o200k, Tekken 및 DeepSeek을 포괄하는 비트스트림 작업으로 대체했습니다. 이는 처음에 배포된 유한 상태 기계(finite-state machines)를 대체했습니다 (#2201 #2317)
- WordCache: 이전에 처리된 프리토큰(pre-token)의 토큰 ID를 재사용합니다 (#2262),
`af5a3e3`
- 더 빠른 조회 및 병합 구조체: FlatCache, MPHF RankStore, 증분 병합(incremental merging), 그리고 BucketVocabStore를 추가했습니다 (#2190 #2188)
- 재사용 가능한 모델 메모리: 임시 모델 상태를 스크래치 버퍼(scratch buffers)로 이동시켜 토큰화가 호출마다 할당하지 않도록 합니다 (#2175 #2183)
- 파이프라인 후처리(pipeline post-processing): 후처리를 `STAGE_POST` 파이프라인 단계(#2182)로 노출합니다. - 배치 모델 호출(batched model calls): 여러 프리토큰 스팬을 한 번의 호출로 처리합니다 (#2304)
- 더 빠른 디코딩: 디코딩된 바이트를 재사용 가능한 버퍼에 직접 작성하여 중간 문자열 및 복사본을 피하고, 토큰 조회를 가속하며, 버퍼링 스트리밍(buffered streaming)을 지원하고 배치를 병렬로 디코드합니다
`role_to_token`
지원(#2343)- Node.js 바인딩 (#2281)
- 단일 인코딩 구현: `tk-encode`를 사용합니다
훈련(training) 중 검증(validation) 과정에서 훈련과 추론(inference)이 다른 토큰화 결과를 생성하지 않도록 합니다. - 선택적 오프셋 및 마스크: 이 메타데이터는 요청 시에만 계산하여, 토큰 ID 전용 경로에서는 사용하지 않습니다.
- 정규화기(normalizers) 재작업
- atomnorm(#2209)을 기반으로 bitnorm 지원
- spm 사전 컴파일
- 더 단순해진 Python 바인딩: 서브클래싱, 직렬화, 사용자 정의 디코더, 변이 동작 및 free-threaded CPython에 대한 지원은 유지하면서 락킹(locking), 래퍼 타입(wrapper types), 수동 작성 분배 코드(handwritten dispatch code)를 줄입니다.
- ExecuTorch와 llama.cpp용 추론 전용 C 및 C++ 바인딩, 이후 JVM, Swift, Go 바인딩이 가능할 수 있습니다.
- tok-devices: 텍스트와 토큰 ID를 장치(device)에 유지하면서 GPU 인코딩 및 배치 디코딩을 탐색합니다. 디코더는 어휘집(vocabulary)을 한 번 업로드하고, 출력 위치를 병렬로 계산한 다음, 해당 바이트들을 GPU에서 수집할 것입니다. 이것은 대규모 배치를 위한 선택적 구성 요소이며, 추가적인 프로토타이핑 및 측정이 필요합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Hugging Face Blog의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기