Skip to content

Improve throughput with gigatoken and batching - #312

Open
jopetty wants to merge 5 commits into
allenai:mainfrom
jopetty:main
Open

Improve throughput with gigatoken and batching#312
jopetty wants to merge 5 commits into
allenai:mainfrom
jopetty:main

Conversation

@jopetty

@jopetty jopetty commented Jul 31, 2026

Copy link
Copy Markdown

This PR improves the speed of the dolma toolkit by (a) using gigatoken as a drop-in alternative for the huggingface backend and (b) adds document batching. For large datasets, this can give a substantial improvement in processing time; in my use cases, it improves from ~100kt/s to ~500Mt/s.

Changes

  1. Tokenizer backend (gt support): Tokenizer now wraps both gigatoken.Tokenizer and huggingface's tokenizers.Tokenizer. Selectable with a new --tokenizer.backend flag. When --tokenizer.fast is False, this has no effect.
  2. Batching: tokenize_file now accumulates documents into batches (bounded by a new --tokenizer.batch_size / --tokenizer.batch_max_bytes CLI config, defaulting to 64 docs / 8 MiB) and calls encode_batch instead of encoding per-document. Per-record error handling is preserved, so if a batch fails to encode, it falls back to encoding records individually so one bad document doesn't drop its neighbors.
  3. Dependency: added gigatoken>=0.10.0,<0.11.0 to pyproject.toml. Pinned with an upper bound since the wrapper relies on gigatoken's private _hf_config() method, and the package is still young/moving fast.

Testing

On dolma's test data, using the gt backend over the hf backend yields a ~10x speedup; when adding source batching, this extends to a ~18x speedup:

Mode hf backend gt backend Speedup
singleton (encode per doc) 864,291 tok/s (107.3s) 8,434,734 tok/s (11.0s) ~9.8x
batched (encode_batch, size 64) 2,818,331 tok/s (32.9s) 50,165,011 tok/s (1.8s) ~17.8x

jopetty added 5 commits July 28, 2026 17:18
flush_batch's fallback path appended None into a list mypy inferred as
list[list[int]], which suppressed the type error but made the
downstream `is None` check look unreachable. Explicitly type the list
as list[list[int] | None] and narrow through a fresh binding instead
of relying on a type: ignore.

Also caps gigatoken to <0.11.0 since the wrapper depends on its
private _hf_config() method, which isn't covered by semver.
Reintroduces the HuggingFace tokenizers Rust bindings as a selectable
backend (now the default) alongside gigatoken, via
--tokenizer.backend hf|huggingface|gt|gigatoken. This makes it easy to
profile the two against each other and de-risks adopting gigatoken,
since it's a very new library, by keeping the previously-shipped
implementation one flag away.

Threads a TokenizerBackend enum through Tokenizer.from_file/
from_pretrained, the tokenizer executor, tokenize_in_parallel, and the
CLI config, with early validation so an unknown backend name fails
fast instead of deep inside a worker process. Adds gigatoken-backend
test coverage alongside the existing default-backend tests, and a
--backend flag to the batch-throughput benchmark script.

Also fixes a real bug this surfaced: tokenizers.Tokenizer.from_file
requires a str, not a PathLike, unlike gigatoken.Tokenizer.
Two lines from the batching/backend-selection work were a few
characters over the line-length limit; black wraps them differently
than my editor did. No behavior change.
@AkshitaB
AkshitaB requested a review from undfined August 25, 2026 19:34

@undfined undfined left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good overall. Two things to confirm before merging please.

# Gigatoken recognizes added special tokens by default, which matches
# HuggingFace when ``encode_special_tokens`` is false. The opt-in flag
# instead encodes the special-token text as ordinary text.
if self.encode_special_tokens and any(token in text for text in inputs for token in self.special_tokens):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The literal scan misses normalized special-token matches

token in text does not reproduce Hugging Face’s added-token matching rules. Special tokens marked normalized=True can match only after the tokenizer normalizer runs, even when their literal content is absent from the original input.
I reproduced this with a Llama-compatible special token ▁zzzzunique and input zzzzunique: Hugging Face encoded ordinary token pieces with encode_special_tokens=True, whereas this gigatoken path emitted special-token ID 32003.
For correctness, either route every encode_special_tokens=True call through the Hugging Face fallback or implement matching equivalent to the tokenizer’s normalization and added-token rules.

if self.encode_special_tokens and any(token in text for text in inputs for token in self.special_tokens):
fallback = self._hf_special_token_fallback
fallback.encode_special_tokens = True
return [encoding.ids for encoding in fallback.encode_batch(inputs)]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fallback unexpectedly applies the tokenizer post-processor

fallback.encode_batch(inputs) defaults to add_special_tokens=True, unlike the Hugging Face path above, which explicitly disables it. Consequently, if any input contains recognized special-token text, tokenizers with a post-processor can inject BOS/EOS tokens into every document in the batch.
With the Llama fixture, I reproduced Hugging Face producing [8656, 1426, 2] while the gigatoken path produced [1, 8656, 1426, 2]. With paragraph segmentation, the post-processor may also run once per paragraph. Double check, but I think we always want this here:

fallback.encode_batch(inputs, add_special_tokens=False)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants