Skip to content

fix: improve processor loading performance by avoiding redundant tokenizer parsing - #44927

Merged
ydshieh merged 6 commits into
mainfrom
faster_processor_loading
Mar 23, 2026
Merged

ydshieh merged 6 commits into
mainfrom
faster_processor_loading

Conversation

@ydshieh

@ydshieh ydshieh commented Mar 22, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

In TokenizersBackend.convert_to_native_format(), when a tokenizer has a custom __init__ (the elif branch), tokenizer.json was parsed twice:

  1. TokenizerFast.from_file(fast_tokenizer_file) — full Rust parse including the entire vocab (~0.6s for large tokenizers like CohereTokenizer with 256k entries) — used only to extract post_processor, padding, truncation
  2. json.load(tokenizer_handle) — Python parse of the same file — to extract vocab and merges

Fix

Read the file once with json.load, then build a minimal tokenizer (same structure but empty vocab/merges) via TokenizerFast.from_str to cheaply obtain the Rust post_processor/padding/truncation objects — without parsing the full vocab.

Benchmark

Measured over 300 runs of AyaVisionProcessor.from_pretrained("local") (AyaVisionProcessor uses CohereTokenizer which has a custom __init__):

With fix    : avg=1.151s  min=0.987s  max=1.422s
Without fix : avg=1.545s  min=1.311s  max=2.513s
Speedup     : 1.34x  (~0.4s saved per load on average)

@HuggingFaceDocBuilderDev

Copy link
Copy Markdown

The docs for this PR live here. All of your documentation changes will be reflected on that endpoint. The docs are available until 30 days after the last update.

@ydshieh

ydshieh commented Mar 22, 2026 •

Copy link
Copy Markdown
Collaborator Author

Benchmark

Script used (bench.py):

import time
import os
import sys
from transformers import (
    CohereTokenizer,
    LlamaTokenizer,
    GemmaTokenizer,
)

N = 100

TOKENIZERS = {
    "CohereTokenizer":     (CohereTokenizer,      "hf-internal-testing/namespace-CohereForAI-repo_name_aya-vision-8b"),
    "LlamaTokenizer":      (LlamaTokenizer,       "huggyllama/llama-7b"),
    "GemmaTokenizer":      (GemmaTokenizer,       "google/gemma-7b"),
}

if len(sys.argv) < 2:
    print(f"Usage: python bench.py <TokenizerClassName>")
    print(f"Available: {', '.join(TOKENIZERS)}")
    sys.exit(1)

name = sys.argv[1]
if name not in TOKENIZERS:
    print(f"Unknown tokenizer: {name}. Available: {', '.join(TOKENIZERS)}")
    sys.exit(1)

cls, hub_id = TOKENIZERS[name]
local_path = f"local_bench_{name}"

# Download from hub and save locally if not cached
if not os.path.isdir(local_path):
    print(f"Downloading {name} from {hub_id}...")
    obj = cls.from_pretrained(hub_id)
    obj.save_pretrained(local_path)
    print(f"Saved to {local_path}")

# Run benchmark
times = []
for _ in range(N):
    t0 = time.perf_counter()
    cls.from_pretrained(local_path)
    times.append(time.perf_counter() - t0)

total = sum(times)
print(f"{name}  N={N}  total={total:.3f}s  avg={total/N:.3f}s  min={min(times):.3f}s  max={max(times):.3f}s")

Run with: python bench.py <TokenizerClassName> (with fix at HEAD, without fix at 0567834cbc).

Results (N=100)

Tokenizer Vocab size With fix Without fix Speedup
CohereTokenizer 256k • avg=1.318s • min=1.235s • max=1.603s • avg=1.863s • min=1.714s • max=2.172s 1.41x
LlamaTokenizer 32k • avg=0.222s • min=0.180s • max=0.312s • avg=0.305s • min=0.259s • max=0.450s 1.37x
GemmaTokenizer 256k • avg=1.708s • min=1.505s • max=1.905s • avg=2.441s • min=2.204s • max=2.677s 1.43x

Comment thread src/transformers/processing_utils.py Resolved
@ydshieh
ydshieh requested review from ArthurZucker and itazap March 23, 2026 09:09
minimal_model["vocab"] = {}
minimal_model["merges"] = []
else:
minimal_model["vocab"] = {}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

like this one can be initialized out of this if block since it's always done

# "type" field in the "model" section, so we cannot determine the model type from JSON.
# In both cases we fall back to the original from_file path (no performance improvement).
model_type = tokenizer_json.get("model", {}).get("type")
if model_type in ("BPE", "WordPiece", "WordLevel"):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

this list could be a constant alongside MODEL_TO_TRAINER_MAPPING

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I will change it to model_type != "Unigram"

@ArthurZucker ArthurZucker left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Good catch!


local_kwargs["post_processor"] = tok_from_file.post_processor
local_kwargs["tokenizer_padding"] = tok_from_file.padding
local_kwargs["tokenizer_truncation"] = tok_from_file.truncation

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

for me this should be a feature request for tokenizers to allows post_processor.from_str().
Its kinda hacky but I guess should work well!

@ydshieh
ydshieh force-pushed the faster_processor_loading branch from 099e875 to 05b3467 Compare March 23, 2026 10:10
@ydshieh
ydshieh force-pushed the faster_processor_loading branch from 05b3467 to 9bea0f9 Compare March 23, 2026 10:12
Comment on lines +134 to +140
model_type = tokenizer_json.get("model", {}).get("type")
if model_type != "Unigram":
minimal_tokenizer_json = dict(tokenizer_json)
minimal_model = dict(tokenizer_json["model"])
minimal_model["vocab"] = {}
if model_type == "BPE":
minimal_model["merges"] = []

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@tarekziade applied your suggestions, thx (force pushed after git rebase)

@ydshieh
ydshieh enabled auto-merge March 23, 2026 10:13
@ydshieh
ydshieh added this pull request to the merge queue Mar 23, 2026
Merged via the queue into main with commit b1527a3 Mar 23, 2026
29 checks passed
@ydshieh
ydshieh deleted the faster_processor_loading branch March 23, 2026 11:03
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.

4 participants