Skip to content

Fix added-token prefix space in SentencePiece fast tokenizers#45822

Open
qflen wants to merge 6 commits into
huggingface:mainfrom
qflen:fix-added-token-prefix-space
Open

Fix added-token prefix space in SentencePiece fast tokenizers#45822
qflen wants to merge 6 commits into
huggingface:mainfrom
qflen:fix-added-token-prefix-space

Conversation

@qflen

@qflen qflen commented May 7, 2026

Copy link
Copy Markdown
Contributor

CI

Fixes #28218

When a user adds a token to a SentencePiece-style fast tokenizer (e.g. NLLB), the tokens-trie splits the input
at the new token and runs the Metaspace pre_tokenizer on each chunk.

The default prepend_scheme="always" then prefixes every chunk with , so "abcdgym" tokenizes to ["abcd", "▁gym"] and decode(encode("abcdgym")) returns "abcd gym".

This PR mirrors what SpmConverter.pre_tokenizer already does for non-legacy spm tokenizers:
when a post-init, non-special add_tokens call lands on a standalone Metaspace(prepend_scheme="always"), the pre_tokenizer is swapped for one with prepend_scheme="first" so only the first chunk receives the prefix.

Sequence pipelines (WhitespaceSplit + Metaspace, used by T5, Pegasus, UDOP, LASR) are left untouched because they rely on "always" for whitespace handling.

With tokenizer.add_tokens(["abcd"]) applied to a fresh NLLB tokenizer:

Input Before After
"abcdgym" ["abcd", "▁gym"]"abcd gym" ["abcd", "gym"]"abcdgym"
"I like to walk abcdgym along the beach" [..., "▁walk", "▁", "abcd", "▁gym", "▁along", ...]"I like to walk abcd gym along the beach" [..., "▁walk", "▁", "abcd", "gym", "▁along", ...]"I like to walk abcdgym along the beach"
"abcd gym" (literal space already there) ["abcd", "▁gym"] ["abcd", "▁gym"] (unchanged)

Inputs without any added tokens are unchanged (e.g. "Hello world"["▁Hello", "▁world"] either way).

Code Agent Policy

  • I confirm that this is not a pure code agent PR.

Before submitting

  • This PR fixes a typo or improves the docs (you can dismiss the other checks if that's the case).
  • Did you read the contributor guideline, Pull Request section?
  • Was this discussed or approved via a Github issue or the forum? Please add a link to it if that is the case.
    Issue: Tokenizer adds an additional space after the added token #28218
    (@ArthurZucker proposed mirroring the SpmConverter approach)
  • Did you make sure to update the documentation with your changes?
  • Did you write any new necessary tests?

Who can review?

@ArthurZucker @itazap


AI assistance disclosure

I've used Opus 4.7 Max Effort for debugging and reviewed / double-checked every line.

qflen added 2 commits May 7, 2026 12:06
Fixes huggingface#28218.

When a user calls add_tokens on a SentencePiece-style fast tokenizer
(NLLB, Reformer, MLuke, ...), the tokens-trie splits the input at the
new token and runs the Metaspace pre_tokenizer on each chunk. With the
default prepend_scheme="always", every chunk is given a leading "▁",
so "abcdgym" tokenizes to ["abcd", "▁gym"] and decodes back to
"abcd gym".

Mirror what SpmConverter.pre_tokenizer already does for non-legacy spm
tokenizers: in TokenizersBackend._add_tokens, when a post-init,
non-special add_tokens call lands on a standalone Metaspace
pre_tokenizer with prepend_scheme="always", swap it for one with
prepend_scheme="first" so only the first chunk receives the prefix.
Sequence pipelines (WhitespaceSplit + Metaspace) are left untouched
because they depend on "always" semantics for whitespace handling.

Init-time additions of persisted tokens (e.g. BigBird's <unused_*>
slots) are excluded via an _init_complete guard so existing
integration tests for those tokenizers stay green.
…fix-space

# Conflicts:
#	tests/tokenization/test_tokenization_fast.py
@Rocketknight1

Copy link
Copy Markdown
Member

cc @ArthurZucker @itazap

@itazap

itazap commented May 12, 2026

Copy link
Copy Markdown
Collaborator

okay but since "always" and "first" are equivalent for normal text (without added tokens), the root fix is just: in _get_prepend_scheme, return "first" instead of "always" when add_prefix_space=True. That would fix new conversions to fast ! we just need to handle it in _add_tokens for the cases when loading a .json from the hub but should be a bit simpler without new flags

Comment thread src/transformers/tokenization_utils_tokenizers.py Outdated
Comment thread tests/tokenization/test_tokenization_fast.py
Fold the fix into `_get_prepend_scheme`: it now returns "first" instead
of "always" when `add_prefix_space=True`, so standalone Metaspace
pre_tokenizers minted via `convert_slow_tokenizer` no longer leave a
spurious "▁" prefix on the chunk after an added token (huggingface#28218). "first"
and "always" are identical on a standalone Metaspace for text without
added tokens.

WhitespaceSplit + Metaspace genuinely needs "always" so each split word
keeps its "▁" SentencePiece marker; PegasusConverter and XLNetTokenizer
now hardcode that scheme.

Drop the `_init_complete` guard and `_align_metaspace_for_added_tokens`
helper: the swap to "first" is inlined in `_add_tokens` and triggers
when a non-special token is registered (covers .json loads that still
bake in "always", whether the token comes from persisted state or a
user `add_tokens` call).

BigBird's persisted `<sep_*>`/`<length_*>` slots are non-special, so
the swap now runs at load time and the chunk after a literal `<s>`
loses its prefix; integration fixtures updated to match.
@qflen qflen force-pushed the fix-added-token-prefix-space branch from f16c094 to 637e2cf Compare May 12, 2026 14:21
@qflen

qflen commented May 12, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the feedback. Pushed changes.

@qflen qflen requested a review from itazap May 12, 2026 14:45
qflen added 2 commits July 4, 2026 17:43
The `_add_tokens` override flipped a standalone Metaspace("always") to
"first" on any non-special add, including checkpoint restore. A tokenizer
that loads as a standalone Metaspace (e.g. kosmos-2) re-registers its own
added tokens both inside and after `__init__`, so the flip rewrote the
saved scheme and dropped the "▁" after in-text special tokens, breaking
Kosmos2ProcessorTest::test_full_processor.

Now only realign when a genuinely new (not-yet-present) non-special token
is added to a finished tokenizer: restoring a checkpoint's own vocab is
left alone, while a real user `add_tokens` still gets the huggingface#28218 fix. Add
a checkpoint-restore regression test.

Also restore big_bird's integration expectations. main refactored it into
a self-contained tokenizer that hardcodes prepend_scheme="always", so it
no longer takes the converter's "first" path; it still gets the fix via
`_add_tokens` on a real user add.
@qflen qflen force-pushed the fix-added-token-prefix-space branch from b4e25ec to d9f40a4 Compare July 4, 2026 17:41
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

[For maintainers] Suggested jobs to run (before merge)

run-slow: xlnet

@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.

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

CI recap

Dashboard: View test results in Grafana
Latest run: 28796948160:2
Result: failure | Jobs: 15 | Tests: 171,542 | Failures: 6 | Duration: 21h 34m

@itazap

itazap commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Thanks for working on this!

One concern: _tokenizer_json_is_byte_level fetches and fully parses tokenizer.json on every AutoTokenizer.from_pretrained where model_type is "llama" - this same file gets fetched and parsed again immediately after by convert_to_native_format. .

I'm wondering if we can instead do an existence check instead, like _has_tekken_tokenizer_file above it?

Llama1/2 (aentencepiece) always ships tokenizer.model I believe and Llama3+ never do

@qflen

qflen commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

@itazap thanks, this is the #28218 metaspace prepend_scheme fix, but I can add the byte-level detection here too.

@qflen

qflen commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

Actually, was your comment meant for #47017? I think this can be merged as is.

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.

Tokenizer adds an additional space after the added token

5 participants