Skip to content

Commit f16c094

Browse files
qflenclaude
andcommitted
Simplify added-token prefix-space fix
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 (#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. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent d775585 commit f16c094

6 files changed

Lines changed: 25 additions & 42 deletions

File tree

src/transformers/convert_slow_tokenizer.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -110,13 +110,8 @@ def import_protobuf(error_message=""):
110110

111111

112112
def _get_prepend_scheme(add_prefix_space: bool, original_tokenizer) -> str:
113-
if add_prefix_space:
114-
prepend_scheme = "always"
115-
if not getattr(original_tokenizer, "legacy", True):
116-
prepend_scheme = "first"
117-
else:
118-
prepend_scheme = "never"
119-
return prepend_scheme
113+
# "first" avoids the spurious "▁" prefix on chunks following a user-added token (#28218).
114+
return "first" if add_prefix_space else "never"
120115

121116

122117
def generate_merges(vocab, vocab_scores, skip_tokens: Collection[str] | None = None):
@@ -1316,7 +1311,8 @@ def unk_id(self, proto):
13161311
return proto.trainer_spec.unk_id + self.original_tokenizer.offset
13171312

13181313
def pre_tokenizer(self, replacement, add_prefix_space):
1319-
prepend_scheme = _get_prepend_scheme(add_prefix_space, self.original_tokenizer)
1314+
# WhitespaceSplit + Metaspace needs "always" so each split word gets the "▁" marker.
1315+
prepend_scheme = "always" if add_prefix_space else "never"
13201316
return pre_tokenizers.Sequence(
13211317
[
13221318
pre_tokenizers.WhitespaceSplit(),

src/transformers/models/xlnet/tokenization_xlnet.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
from tokenizers import AddedToken, Regex, Tokenizer, decoders, normalizers, pre_tokenizers, processors
1717
from tokenizers.models import Unigram
1818

19-
from ...tokenization_utils_base import _get_prepend_scheme
2019
from ...tokenization_utils_tokenizers import TokenizersBackend
2120
from ...utils import logging
2221

@@ -144,7 +143,8 @@ def __init__(
144143
self._tokenizer.normalizer = normalizers.Sequence(list_normalizers)
145144

146145
add_prefix_space = True
147-
prepend_scheme = _get_prepend_scheme(add_prefix_space, self)
146+
# WhitespaceSplit + Metaspace needs "always" so each split word gets the "▁" marker.
147+
prepend_scheme = "always" if add_prefix_space else "never"
148148
self._tokenizer.pre_tokenizer = pre_tokenizers.Sequence(
149149
[
150150
pre_tokenizers.WhitespaceSplit(),

src/transformers/tokenization_utils_base.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3547,13 +3547,8 @@ def load_vocab_and_merges(pretrained_model_name_or_path, **kwargs):
35473547

35483548

35493549
def _get_prepend_scheme(add_prefix_space: bool, original_tokenizer) -> str:
3550-
if add_prefix_space:
3551-
prepend_scheme = "always"
3552-
if not getattr(original_tokenizer, "legacy", True):
3553-
prepend_scheme = "first"
3554-
else:
3555-
prepend_scheme = "never"
3556-
return prepend_scheme
3550+
# "first" avoids the spurious "▁" prefix on chunks following a user-added token (#28218).
3551+
return "first" if add_prefix_space else "never"
35573552

35583553

35593554
def generate_merges(vocab, vocab_scores: dict[str, float] | None = None, skip_tokens: Collection[str] | None = None):

src/transformers/tokenization_utils_tokenizers.py

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

491-
self._init_complete = True
492-
493491
@property
494492
def is_fast(self) -> bool:
495493
return True
@@ -729,24 +727,19 @@ def _add_tokens(self, new_tokens: list[str | AddedToken], special_tokens=False)
729727
if special_tokens:
730728
return self._tokenizer.add_special_tokens(new_tokens)
731729

732-
if getattr(self, "_init_complete", False) and any(isinstance(t, str) or not t.special for t in new_tokens):
733-
self._align_metaspace_for_added_tokens()
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+
pre_tokenizer = self._tokenizer.pre_tokenizer
735+
if isinstance(pre_tokenizer, pre_tokenizers_fast.Metaspace) and pre_tokenizer.prepend_scheme == "always":
736+
self._tokenizer.pre_tokenizer = pre_tokenizers_fast.Metaspace(
737+
replacement=pre_tokenizer.replacement,
738+
prepend_scheme="first",
739+
split=pre_tokenizer.split,
740+
)
734741
return self._tokenizer.add_tokens(new_tokens)
735742

736-
def _align_metaspace_for_added_tokens(self) -> None:
737-
# Mirror `SpmConverter.pre_tokenizer` for non-legacy spm tokenizers: switch a
738-
# standalone Metaspace pre_tokenizer from prepend_scheme="always" to "first"
739-
# so chunks following a user-added token are not given a spurious "▁" prefix.
740-
# Sequence pipelines (eg. WhitespaceSplit + Metaspace) rely on "always"
741-
# semantics for whitespace handling and are intentionally skipped.
742-
pre_tokenizer = self._tokenizer.pre_tokenizer
743-
if isinstance(pre_tokenizer, pre_tokenizers_fast.Metaspace) and pre_tokenizer.prepend_scheme == "always":
744-
self._tokenizer.pre_tokenizer = pre_tokenizers_fast.Metaspace(
745-
replacement=pre_tokenizer.replacement,
746-
prepend_scheme="first",
747-
split=pre_tokenizer.split,
748-
)
749-
750743
def num_special_tokens_to_add(self, pair: bool = False) -> int:
751744
"""
752745
Returns the number of added tokens when encoding a sequence with special tokens.

tests/models/big_bird/test_tokenization_big_bird.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ class BigBirdTokenizationTest(TokenizerTesterMixin, unittest.TestCase):
3131
from_pretrained_id = "google/bigbird-roberta-base"
3232
tokenizer_class = BigBirdTokenizer
3333

34-
integration_expected_tokens = ['▁This', '▁is', '▁a', '▁test', '▁', '😊\n', 'I', '▁was', '▁born', '▁in', '▁9', '2000', ',', '▁and', '▁this', '▁is', '▁fals', 'é', '.', '\n生活的真谛是\n', 'Hi', '▁Hello', '\n', 'Hi', '▁Hello', '\n\n', '▁', '\n', '▁', '\n', '▁Hello', '\n', '<s>', '▁', '\n', 'hi', '<s>', 'there', '\n', 'The', '▁following', '▁string', '▁should', '▁be', '▁properly', '▁encoded', ':', '▁Hello', '.', '\n', 'But', '▁', 'ird', '▁and', '▁', 'ปี', '▁', 'ird', '▁', 'ด\n', 'Hey', '▁how', '▁are', '▁you', '▁doing'] # fmt: skip
35-
integration_expected_token_ids = [871, 419, 358, 1433, 321, 100, 141, 474, 4743, 388, 961, 11125, 112, 391, 529, 419, 27908, 266, 114, 100, 17351, 18536, 100, 17351, 18536, 100, 321, 100, 321, 100, 18536, 100, 2, 321, 100, 5404, 2, 713, 100, 565, 1809, 4832, 916, 408, 6206, 30341, 126, 18536, 114, 100, 1638, 321, 1548, 391, 321, 100, 321, 1548, 321, 100, 10915, 804, 490, 446, 1905] # fmt: skip
36-
expected_tokens_from_ids = ['▁This', '▁is', '▁a', '▁test', '▁', '<unk>', 'I', '▁was', '▁born', '▁in', '▁9', '2000', ',', '▁and', '▁this', '▁is', '▁fals', 'é', '.', '<unk>', 'Hi', '▁Hello', '<unk>', 'Hi', '▁Hello', '<unk>', '▁', '<unk>', '▁', '<unk>', '▁Hello', '<unk>', '<s>', '▁', '<unk>', 'hi', '<s>', 'there', '<unk>', 'The', '▁following', '▁string', '▁should', '▁be', '▁properly', '▁encoded', ':', '▁Hello', '.', '<unk>', 'But', '▁', 'ird', '▁and', '▁', '<unk>', '▁', 'ird', '▁', '<unk>', 'Hey', '▁how', '▁are', '▁you', '▁doing'] # fmt: skip
37-
integration_expected_decoded_text = "This is a test <unk>I was born in 92000, and this is falsé.<unk>Hi Hello<unk>Hi Hello<unk> <unk> <unk> Hello<unk><s> <unk>hi<s> there<unk>The following string should be properly encoded: Hello.<unk>But ird and <unk> ird <unk>Hey how are you doing"
34+
integration_expected_tokens = ['▁This', '▁is', '▁a', '▁test', '▁', '😊\n', 'I', '▁was', '▁born', '▁in', '▁9', '2000', ',', '▁and', '▁this', '▁is', '▁fals', 'é', '.', '\n生活的真谛是\n', 'Hi', '▁Hello', '\n', 'Hi', '▁Hello', '\n\n', '▁', '\n', '▁', '\n', '▁Hello', '\n', '<s>', '\n', 'hi', '<s>', 'there', '\n', 'The', '▁following', '▁string', '▁should', '▁be', '▁properly', '▁encoded', ':', '▁Hello', '.', '\n', 'But', '▁', 'ird', '▁and', '▁', 'ปี', '▁', 'ird', '▁', 'ด\n', 'Hey', '▁how', '▁are', '▁you', '▁doing'] # fmt: skip
35+
integration_expected_token_ids = [871, 419, 358, 1433, 321, 100, 141, 474, 4743, 388, 961, 11125, 112, 391, 529, 419, 27908, 266, 114, 100, 17351, 18536, 100, 17351, 18536, 100, 321, 100, 321, 100, 18536, 100, 2, 100, 5404, 2, 8218, 100, 565, 1809, 4832, 916, 408, 6206, 30341, 126, 18536, 114, 100, 1638, 321, 1548, 391, 321, 100, 321, 1548, 321, 100, 10915, 804, 490, 446, 1905] # fmt: skip
36+
expected_tokens_from_ids = ['▁This', '▁is', '▁a', '▁test', '▁', '<unk>', 'I', '▁was', '▁born', '▁in', '▁9', '2000', ',', '▁and', '▁this', '▁is', '▁fals', 'é', '.', '<unk>', 'Hi', '▁Hello', '<unk>', 'Hi', '▁Hello', '<unk>', '▁', '<unk>', '▁', '<unk>', '▁Hello', '<unk>', '<s>', '<unk>', 'hi', '<s>', 'there', '<unk>', 'The', '▁following', '▁string', '▁should', '▁be', '▁properly', '▁encoded', ':', '▁Hello', '.', '<unk>', 'But', '▁', 'ird', '▁and', '▁', '<unk>', '▁', 'ird', '▁', '<unk>', 'Hey', '▁how', '▁are', '▁you', '▁doing'] # fmt: skip
37+
integration_expected_decoded_text = "This is a test <unk>I was born in 92000, and this is falsé.<unk>Hi Hello<unk>Hi Hello<unk> <unk> <unk> Hello<unk><s><unk>hi<s>there<unk>The following string should be properly encoded: Hello.<unk>But ird and <unk> ird <unk>Hey how are you doing"

tests/tokenization/test_tokenization_fast.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -218,9 +218,8 @@ def test_class_after_save_and_reload(self):
218218
self.assertIsInstance(tokenizer, PreTrainedTokenizerFast)
219219

220220
def test_added_token_does_not_get_metaspace_prefix(self):
221-
# Issue #28218: when a non-special token is added to a SentencePiece-style fast
222-
# tokenizer, the chunk that follows the added token must not be given a
223-
# spurious "▁" prefix from the Metaspace pre_tokenizer.
221+
# #28218: a non-special add_tokens call on a SentencePiece-style fast tokenizer
222+
# must not leave a spurious "▁" prefix on the chunk after the added token.
224223
vocab = [
225224
("<unk>", 0.0),
226225
("▁", -10.0),

0 commit comments

Comments
 (0)