記事のサマリー(TL;DR)
- Hugging Faceは、トークナイザーライブラリ「tokenizers」の次期バージョンv1のリリース候補(release candidate)をcrates.ioで公開しました。v1はv0.23と同じトークンID・API・語彙・マージランクを維持しつつ、性能面を改善したものです。
- 提供元の説明によれば、v1は測定対象の10種類のモデルファミリーにおいて、Apple M4 Max上での単一スレッド実行時にv0.23比で3〜30倍高速化し、最も遅いのはt5-base、最も速いのはgpt2としています。また8ワーカーでのスケーリングは線形の76%に達するとしています。
- 高速化は、正規表現エンジンに代わるビットストリーム処理(bitcannon)、繰り返し出現する単語をキャッシュする仕組み(word cache)、アロケータを使わないマージループの書き換え、複数プレトークンをまとめて処理するバッチ化など、複数の変更の組み合わせによるものです。
利用条件・導入方法
- v1は現時点でリリース候補(pre-release)であり、通常のインストールコマンドに
--preフラグを付けて導入します。
cargo add tokenizers --pre
- トレーニング機能はデフォルトで有効になっており、C++依存関係を伴います。エンコードのみが必要な場合は、この機能を無効化してトレーニング実装を除外できます。
cargo add tokenizers --pre --no-default-features --features http
- エンコード呼び出し自体(
encode/encode_batch)はv0.23から変更がなく、同じ呼び出しで同じトークンIDが得られるとしています。複数コアでのスケーリングを測定する際に使われるのはencode_batchです。 - Python バインディングは同じRustコードをラップして構築されていますが、呼び出しごとのオーバーヘッドが加わるため、本記事の測定値には含まれていません。
詳細
背景:なぜトークナイザーの性能を重視するか
これまでトークナイザーはMLワークフローのボトルネックとされてきませんでした。計算量の観点では、トークン化はパイプライン中の重いモデリング処理と比べて軽量です。しかし、モデルが高速化しワークロードが拡大するにつれてこのバランスは変わりつつあります。大規模データセットでの学習、多数の同時リクエストの処理、長い入力の繰り返し処理などは、トークナイザーに十分な負荷をかけ、モデルへのデータ供給を止めてしまう場合があるとしています。こうした背景から、次期バージョンであるv1では性能に重点を置いたとしています。
この開発は、gigatoken、tiktoken、kitoken、tokie、fastokens、wordchipper、ai-tokenizerなど、オープンソースのトークナイザー関連プロジェクトの取り組みを参考にした部分があるとしています。また、IBM、NVIDIA、ExecuTorchチームがパッチの提供や幅広いハードウェアでのテストに協力したことにも謝意が示されています。
ベンチマークの内容
ベンチマークは、単一スレッド、マルチスレッド、スレッド数に応じたスケーリング、モデル別比較、言語別比較、レイテンシ、デコードスループット、メモリヒープ、クレートサイズなど多岐にわたって実施されています。これらはtokbenchリポジトリから実行され、自分のハードウェアでベンチマークを再実行するためのコマンドも用意されているとしています。
v1が実現していること
v1はv0.23と同じトークンIDを生成します。目標は、出力・API・語彙・マージランクを維持したまま、改善できる部分を改善することでした。これには対応範囲の広さも含まれ、ライブラリはBPEに特化せず、v0.23が読み込めたものはすべてv1でも読み込めるとしています。
トークナイザーはテキストをモデルが読み取る整数列に変換する処理を4段階で行います。
- 正規化(Normalization):小文字化やUnicode正規化などの処理を生テキストに適用
- プレトークン化(Pre-tokenization):テキストを「プレトークン」と呼ばれる小さな単位に分割
- モデル段階:各プレトークンをトークンに変換し、語彙内のIDにマッピング
- 後処理(Post-processing):モデルが必要とする特殊トークンを追加
本記事で説明される作業の大部分はモデル段階で行われています。今回測定した10種類のモデルファミリーのうち8つがバイトペアエンコーディング(BPE)を使用しています。BPEはプレトークンのバイト列から出発し、ランクが最も高い隣接ペアを、対象となるペアがなくなるまで繰り返し結合します。このランクはトークナイザーの学習時に決定され、トークナイザーとともに配布されるため、同じテキストは常に同じIDを生成します。マージがプレトークンの境界を越えることはありません。残る2つのモデルファミリーはWordPieceとUnigramで、いずれもこのライブラリがサポートするモデルタイプです。
主な変更点
各段階に対して行われた変更のうち、重要なものは次の通りです。
- workspace split(ワークスペース分割):単一のクレートを複数に分割。
tk-encodeは実行時に必須で、tk-serialize・tk-convert・tk-trainはアプリケーションが必要とする場合にのみリンクされます。 - no-alloc model(アロケーションなしのモデル処理):マージ処理の作業領域が呼び出し側が所有するスクラッチバッファ上に置かれ、ループがアロケータに触れなくなりました。
- bitcannon:分割パターンを、正規表現エンジンではなくSIMD命令を用いたビットストリーム上のブール演算に置き換えました。
- merge-loop rewrite(マージループの書き換え):結合対象の要素を、1つの事前確保されたバッファ内のイントルーシブな双方向連結リストとして扱い、マージ時にはデータ移動ではなく2つのインデックス更新のみで済むようにしました。
- word cache(単語キャッシュ):プレトークンのバイト列から完成済みIDへのスレッドローカルなメモ化により、繰り返し出現する単語は一度だけマージされます。
- native parallelism(ネイティブな並列処理):1つの共有トークナイザーが複数スレッドから同時にエンコードを行えるようにし、各スレッドが自身のサブプールからスクラッチバッファと単語キャッシュを取得することで、単一ロックへの待機が発生しないようにしました(#2365)。
分割処理:正規表現ではなくビットストリーム
BPEモデルは、入力テキストを「プレトークン」と呼ばれる小さな単位に分割するために正規表現を使用します。マージはプレトークン内でのみ行われ、プレトークン間の境界を越えることはないため、この分割処理がパイプラインの以降の挙動を左右します。
この正規表現はモデルの固定パラメータであり、トークナイザーとともに配布され、実行時に変わることはありません。そのため、エンコードのたびに汎用の正規表現エンジンで解釈する必要はなく、特定モデルが実際に使うパターンに対応する分割関数を、一度だけ手書きすることが可能だとしています。手書きの関数であれば、現代のCPUのSIMD命令(単一命令・複数データ)を利用でき、これは多数のバイトに一度に同じ操作を適用するもので、UTF-8テキストの処理に適しているとしています。
bitcannonは、入力のバイト列を並列なビットストリームとして扱い、1文字ずつ進めるスキャンではなく、レジスタ全体にわたるブール演算から境界を導き出します。1回のレジスタ演算で64バイトを処理するとしています。同様の発想は、テキスト処理向けのParabixやJSON向けのsimdjsonでも採用されているとしています。
ただしこの手法はパターンの認識に依存します。バイトレベルのBPEモデルの大半は少数の文法パターンでカバーされますが、パターンがそれらに含まれないトークナイザーは従来の正規表現パスのままとなり、この高速化の恩恵を受けません。これが、高速化の度合いにばらつきがある理由だとしています。
単語キャッシュ
実際のテキストには繰り返し出現する単語が多く含まれます。BPEは同じプレトークンに対して常に同じトークンIDを生成するため、v1は一度処理した結果を保存できます。スレッドローカルなキャッシュが各プレトークンのバイト列をトークンIDにマッピングし、以降の出現ではマージ処理をスキップできるとしています。
入力が長くなるにつれ、ユニークな単語数は総単語数よりも緩やかに増加する傾向があり、繰り返し出現する単語が入力に占める割合は増加します。新しい単語は依然として出現するため、記事内のアニメーションで示されている「時折発生するキャッシュミス」の要因になっているとしています。
共有プレフィックスの結果は、以下のコマンドで再現できるとしています。
tokbench measure prefix-sharing \
--engine pipeline \
--engine hf-tokenizers \
--compare-to pipeline-no-cache \
--corpus agentic_swe
キャッシュは、入力に繰り返しのプレトークンが多く含まれる場合に最も効果を発揮するとしています。繰り返しの少ない入力では、ルックアップのコストがかかるものの、ヒット数はあまり得られないとしています。
マージループ
次に大きなコストとなるのがBPEのマージループです。各プレトークンについて、ループは優先度が最も高い隣接ペアを繰り返し探して結合します。従来の実装では、呼び出しごとに新しいメモリを確保し、プレトークンごとに新しい優先度付きキューを構築していました。v1は呼び出し側が所有するスクラッチバッファを再利用することで、こうした繰り返しのメモリ確保をなくしています。シンボルはフラットな配列に格納され、隣接するシンボルはその配列内の位置によってリンクされるため、マージ中の更新が軽くなっています。また、複数のプレトークンをまとめて1回のモデル呼び出しで処理します。
さらに、各候補ペアは1つの64ビット値にパックされ、マージランクが上位ビットに格納されます。これにより2つの候補を比較する処理は単なる整数比較となり、「ここではマージしない」という状態は取りうる最大値として表現されるため、ループは分岐なしに次のマージ対象を見つけられるとしています。
ベンチマーク手法
ベンチマーク設計のわずかな違いが、トークナイザー性能に大きな差を生むことがあります。比較の一貫性を保つために、以下のルールが用いられたとしています。
| ルール | 理由 |
|---|---|
| 単一のタイミングループ | すべてのエンジンが同一のループで実行され、エンジン固有の高速パスを使わない |
| ロードの除外 | 語彙の読み込みは別途計測し、エンコード時間には含めない |
| IDハッシュの検証 | 出力IDに対するFNV-1aがベースラインと完全一致することを確認 |
| 共通セルのみ | 中央値は、すべてのエンジンが実行し検証を完了したセルについてのみ算出 |
| プロセスごとの完全な掃引 | 各繰り返しは新しいプロセスで開始し、すべてのセルを保持 |
| 物理コア固定 | ワーカーは8つの異なる物理コアに固定され、SMTの兄弟スレッドは使用しない |
| 独立したジョブ | 別個のジョブによってホスト間のばらつきを測定 |
同じビルド上で、1つの文書を繰り返しエンコードする方が、異なる文書群をエンコードするより速くなる場合があるとしています。前者は文書全体が既にキャッシュに乗っている状態の性能を測定し、後者は、以前見たプレトークンがキャッシュに残った状態を維持しつつ新しい入力を処理する性能を測定します。どちらも「ウォーム」な状態と呼ばれることがありますが、実際には異なるワークロードを測定しているとしています。本記事の主要な結果は異なる文書群を用いたものであり、対象コーパス全体はキャッシュに収まりきらないほど大きいとしています。トークナイザーのベンチマークは、どちらのワークロードを使用しているかを明示すべきであるとし、その選択が結果を左右しうると述べています。
総合結果
v1のエンコード処理が対応する10種類のモデルファミリー全体で、Apple M4 Max上の単一スレッド実行においてv0.23比で3〜30倍高速だとしています。最も低い倍率はt5-base、最も高い倍率はgpt2です。8ワーカーでは線形の76%までスケールするとしています。これらの変更を通じて、v1は公開版ライブラリと全く同じトークンIDを生成するとしています。
全体的な改善は、複数の変更が組み合わさった結果だとしています。正規表現エンジンに代わる手書きの分割処理、繰り返し単語を再マージせずに応答するキャッシュ、アロケータに触れないマージループ、そしてプレトークンごとではなくバッチごとに1回のモデル呼び出しを行う方式です。それぞれがパイプライン内の異なる箇所での処理量を削減しているとしています。
次の優先事項は、対応モデルファミリーの拡大です。1.0.0までに、追加のモデルを新しいマージループに移行するとしています。リリース候補が安定した後は、tokenizersライブラリに依存するtransformersライブラリおよびその他のエコシステムへ、この改善を波及させることが次のステップだとしています。この投稿はtokbenchの結果をもとに生成されており、対応範囲の拡大に応じて更新されるとしています。
導入方法
v1のリリース候補はcrates.ioで公開されています。呼び出すAPIは従来と同じであり、変わるのはインストールするビルドのみです。通常のインストールコマンドは以下の通りです。
cargo add tokenizers --pre
トレーニング機能はデフォルトで有効な機能(feature)の内側にあり、C++依存関係を伴います。エンコードのみが必要な場合は、これを無効化してトレーニング実装を除外できます。
cargo add tokenizers --pre --no-default-features --features http
エンコード処理自体は変更されておらず、呼び出し方法もIDも従来と同じです。
use tokenizers::tokenizer::{Result, Tokenizer};
fn main() -> Result<()> {
let tokenizer = Tokenizer::from_pretrained("deepseek-ai/DeepSeek-V4-Flash", None)?;
let encoding = tokenizer.encode("The tokenizer is no longer the bottleneck.", false)?;
println!("{:?}", encoding.get_ids());
// [671, 17840, 9160, 344, 1119, 5827, 270, 111127, 16]
println!("{:?}", encoding.get_tokens());
// ["The", "Ġtoken", "izer", "Ġis", "Ġno", "Ġlonger", "Ġthe", "Ġbottleneck", "."]
Ok(())
}
バッチ処理では encode_batch を使うと複数コアにわたってスケールするとしており、これが前述のスケーリング測定で使われている呼び出しです。
let encodings = tokenizer.encode_batch(documents, false)?;
本記事で示されたすべての数値はこのクレートに対して測定されたものです。Pythonバインディングは同じコードをラップしてbindings/pythonから構築されていますが、呼び出しごとのオーバーヘッドが加わるため、本記事の測定にはPythonバインディングは含まれていないとしています。
v1に向けた進捗状況
本記事で紹介したベンチマークは、既に完了しているリリース候補版の作業を対象としています。以下は1.0.0に向けて残っている作業と、その後に検討予定の項目です。
リリース候補で実装済み(Rustのプレリリース版としてcrates.ioで公開、cargo add tokenizers --preで利用可能)
- workspace split:単一クレートを
tk-encode・tk-serialize・tk-convert・tk-trainに分割し、アプリケーションは使用する部分のみをリンク - bitcannon:エンコードパスの正規表現による分割をビットストリーム演算に置き換え。GPT-2、cl100k、o200k、Tekken、DeepSeekに対応。これは最初に導入された有限状態機械を置き換えたもの(#2201、#2317)
- WordCache:既に処理済みのプレトークンのトークンIDを再利用(#2262、af5a3e3)
- より高速なルックアップ・マージ構造:FlatCache、MPHF RankStore、インクリメンタルマージ、BucketVocabStoreを追加(#2190、#2188)
- モデルメモリの再利用:一時的なモデル状態をスクラッチバッファに移し、トークン化のたびにアロケーションが発生しないようにする(#2175、#2183)
- パイプラインの後処理:後処理をSTAGE_POSTパイプラインステージとして公開(#2182)
- バッチ化されたモデル呼び出し:複数のプレトークンの範囲を1回の呼び出しで処理(#2304)
- より高速なデコード:デコードしたバイト列を再利用可能なバッファへ直接書き込み、中間文字列やコピーを回避し、トークンルックアップを高速化、バッファ付きストリーミングに対応、バッチのデコードを並列化
- role_to_tokenのサポート(#2343)
- Node.jsバインディング(#2281)
1.0.0で予定されている作業
- 単一のエンコード実装:学習時の検証に
tk-encodeを使用し、学習と推論で異なるトークン化結果が生じないようにする - オプションのオフセット・マスク:このメタデータを要求された場合にのみ計算し、トークンIDのみのパスを軽量に保つ
- ノーマライザーの見直し:atomnormを基盤としたbitnormのサポート(#2209)、SentencePieceのprecompiled対応
- よりシンプルなPythonバインディング:サブクラス化、シリアライズ、カスタムデコーダー、変更操作、フリースレッドCPythonのサポートを維持しつつ、ロック、ラッパー型、手書きのディスパッチコードを削減
- ExecuTorchおよびllama.cpp向けの推論専用のC/C++バインディング(JVM、Swift、Goバインディングも今後追加される可能性がある)
1.0.0以降に検討予定
- tok-devices:テキストとトークンIDをデバイス上に保持したまま、GPUによるエンコードとバッチデコードを検討する取り組み。デコーダーは語彙を一度だけアップロードし、出力位置を並列に計算し、対応するバイト列をGPU上で収集する構想です。これは大規模バッチ向けのオプション機能として位置づけられており、今後のプロトタイピングと計測の対象になるとしています。