Skip to content

Commit b4e25ec

Browse files
committed
Scope prepend_scheme realignment to post-init add_tokens
The `_add_tokens` override flipped a standalone Metaspace("always") to "first" on every non-special add, including a tokenizer's own added tokens restored during `from_pretrained`. For a checkpoint that loads as a standalone Metaspace with non-special added tokens (e.g. kosmos-2, which restores 1035 of them), this rewrote the saved scheme at load time and dropped the metaspace prefix after in-text special tokens, breaking Kosmos2ProcessorTest::test_full_processor. Gate the flip on a `_prepend_scheme_can_flip` flag set once construction finishes, so only genuine post-init user `add_tokens` calls realign the scheme. Add a regression test covering checkpoint restore.
1 parent c0cce5b commit b4e25ec

2 files changed

Lines changed: 31 additions & 4 deletions

File tree

src/transformers/tokenization_utils_tokenizers.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,10 @@ def __init__(self, *args, **kwargs):
488488
if self._should_update_post_processor:
489489
self.update_post_processor()
490490

491+
# Construction (incl. checkpoint-restore of added tokens) is done; later non-special adds are
492+
# genuine user edits and may realign the Metaspace prepend_scheme in `_add_tokens` (#28218).
493+
self._prepend_scheme_can_flip = True
494+
491495
@property
492496
def is_fast(self) -> bool:
493497
return True
@@ -727,10 +731,12 @@ def _add_tokens(self, new_tokens: list[str | AddedToken], special_tokens=False)
727731
if special_tokens:
728732
return self._tokenizer.add_special_tokens(new_tokens)
729733

730-
# For tokenizers loaded from a .json that bakes in prepend_scheme="always",
731-
# swap a standalone Metaspace to "first" once a non-special token enters so
732-
# the chunk after the added token does not pick up a spurious "▁" (#28218).
733-
if any(isinstance(t, str) or not t.special for t in new_tokens):
734+
# #28218: a non-special token added to a standalone Metaspace(prepend_scheme="always") leaves a
735+
# spurious "▁" on the chunk after it; realign to "first" so only the first chunk is prefixed. Gated
736+
# on post-init so restoring a checkpoint's own added tokens does not silently change its scheme.
737+
if getattr(self, "_prepend_scheme_can_flip", False) and any(
738+
isinstance(t, str) or not t.special for t in new_tokens
739+
):
734740
pre_tokenizer = self._tokenizer.pre_tokenizer
735741
if isinstance(pre_tokenizer, pre_tokenizers_fast.Metaspace) and pre_tokenizer.prepend_scheme == "always":
736742
self._tokenizer.pre_tokenizer = pre_tokenizers_fast.Metaspace(

tests/tokenization/test_tokenization_fast.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,27 @@ def test_added_token_does_not_get_metaspace_prefix(self):
243243
["▁Hi", "▁", "abcd", "gym", "▁world"],
244244
)
245245

246+
def test_checkpoint_restore_keeps_metaspace_prepend_scheme(self):
247+
# #28218 follow-up: restoring a checkpoint that legitimately baked prepend_scheme="always"
248+
# together with its own non-special added tokens must NOT flip the scheme to "first". The
249+
# realignment only applies to genuine post-init user add_tokens, never to load-time restore.
250+
vocab = [("<unk>", 0.0), ("▁", -10.0), ("gym", 0.0), ("▁gym", 0.0)]
251+
backend = Tokenizer(Unigram(vocab=vocab, unk_id=0))
252+
backend.pre_tokenizer = pre_tokenizers.Metaspace(replacement="▁", prepend_scheme="always", split=True)
253+
backend.add_tokens(["abcd"]) # a non-special token baked into the checkpoint
254+
fast = PreTrainedTokenizerFast(tokenizer_object=backend, unk_token="<unk>")
255+
self.assertEqual(fast._tokenizer.pre_tokenizer.prepend_scheme, "always")
256+
257+
with tempfile.TemporaryDirectory() as tmp_dir:
258+
fast.save_pretrained(tmp_dir)
259+
reloaded = PreTrainedTokenizerFast.from_pretrained(tmp_dir)
260+
261+
# Restoring "abcd" while loading must leave the scheme untouched...
262+
self.assertEqual(reloaded._tokenizer.pre_tokenizer.prepend_scheme, "always")
263+
# ...while a genuine post-load user add still realigns it.
264+
reloaded.add_tokens(["efgh"])
265+
self.assertEqual(reloaded._tokenizer.pre_tokenizer.prepend_scheme, "first")
266+
246267
def test_bpe_tokenizer_skips_clean_up_tokenization_spaces(self):
247268
"""BPE tokenizers should not apply clean_up_tokenization even when the flag is True.
248269

0 commit comments

Comments
 (0)