Skip to content

rm slow tokenizers - #40936

Merged
ArthurZucker merged 277 commits into
mainfrom
one_tokenizer
Nov 27, 2025
Merged

ArthurZucker merged 277 commits into
mainfrom
one_tokenizer

Conversation

@itazap

@itazap itazap commented Sep 17, 2025 •

Copy link
Copy Markdown
Collaborator

Tokenization

Just as we moved towards a single backend library for model definition, we want Tokenizer to be a lot more intuitive.
With v5, you can now initialize an empty LlamaTokenizer and train it directly on your new task!

Defining a new tokenizer object should be as simple as this:

from transformers import TokenizersBackend, generate_merges
from tokenizers import pre_tokenizers, Tokenizer
from tokenizers.model import BPE

class Llama5Tokenizer(TokenizersBackend):
    def __init__(self,        unk_token="<unk>",bos_token="<s>", eos_token="</s>", vocab=None, merges=None ):
        if vocab is None:
            self._vocab = {
                str(unk_token): 0,
                str(bos_token): 1,
                str(eos_token): 2,
            }

        else:
            self._vocab = vocab

        if merges is not None:
            self._merges = merges
        else:
            self._merges = generate_merges(filtered_vocab)

        self._tokenizer = Tokenizer(
            BPE(vocab=self._vocab, merges=self._merges, fuse_unk=True)
        )
        self._tokenizer.pre_tokenizer = pre_tokenizers.Metaspace(
            replacement="▁", prepend_scheme=_get_prepend_scheme(self.add_prefix_space, self), split=False
        )
        super().__init__(
            tokenizer_object=self._tokenizer,
            unk_token=unk_token,
            bos_token=bos_token,
            eos_token=eos_token,
        )

And now if you call Llama5Tokenizer() you just get an empty, trainable tokenizer that follows the definition of the authors of Llama5 (it does not exist yet 😉).

The above is the main motivation towards refactoring tokenization: we want people to just instantiate a tokenizer like they would a model, empty or not and with exactly what they defined.

Non-tokenizers

If you tokenizers is not common, or you just don't want to rely on sentencepiece nor tokenizers you can just import the PythonBackend (previousl PreTrainedTokenzier) which has all the API and logic for added tokens, encoding and decoding wieht them etc.

If you want to have en less features, you can use the common PreTrainedTokenizerBase mixin, which mostly defines transformers tokenizer API: encode, decode, vocab_size, get_vocab, convert_tokens_to_ids, convert_ids_to_tokens, from_pretrained, save_pretrained, etc.

Backend Architecture Changes

Moving away from "slow" vs "fast" tokenizers:

Previously, transformers maintained two parallel implementations for many tokenizers:

  • "Slow" tokenizers (tokenization_<model>.py) - Python-based implementations, often using SentencePiece as the backend.
  • "Fast" tokenizers (tokenization_<model>_fast.py) - Rust-based implementations using the 🤗 tokenizers library.

In v5, we consolidate to a single tokenizer file per model: tokenization_<model>.py. This file will use the most appropriate backend available:

  1. TokenizersBackend (preferred): Rust-based tokenizers from the 🤗 tokenizers library. In general its performances are better, but it also offers a lot more features that are comonly adopted across the ecosystem, like handling additional tokens, easily update the state of the tokenizer, automatic parallelisation etc.
  2. SentencePieceBackend: For models requiring SentencePiece
  3. PythonBackend: Pure Python implementations
  4. MistralCommonBackend: Relies on MistralCommon's toknenization library. (Previously MistralCommonTokenizer)

The AutoTokenizer automatically selects the appropriate backend based on available files and dependencies. This is transparent, you continue to use AutoTokenizer.from_pretrained() as before. This allows transformers to be future-proof and modular to easily support future backends.

API Changes

1. Direct tokenizer initialization with vocab and merges:

In v5, you can now initialize tokenizers directly with vocabulary and merges, enabling training custom tokenizers from scratch:

# v5: Initialize a blank tokenizer for training
from transformers import LlamaTokenizer

# Create a tokenizer with custom vocabulary and merges
vocab = {"<unk>": 0, "<s>": 1, "</s>": 2, "hello": 3, "world": 4}
merges = [("h", "e"), ("l", "l"), ("o", " ")]

tokenizer = LlamaTokenizer(vocab=vocab, merges=merges)

# Or initialize a blank tokenizer to train on your own dataset
tokenizer = LlamaTokenizer()  # Creates a blank Llama-like tokenizer

But you can no longer pass a vocab file. As this accounts for from_pretrained use-case.

2. Simplified decoding API:

The batch_decode method has been unified with decode. Both single and batch decoding now use the same method:

from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("t5-small") 
inputs = ["hey how are you?", "fine"]
tokenizer.decode(tokenizer.encode(inputs))

Gives:

- 'hey how are you?</s> fine</s>'
+ ['hey how are you?</s>', 'fine</s>']

This is mostly because people get list[list[int]] out of generate, but then they would use decode because they use encode and would get:

   ...: tokenizer.decode([[1,2], [1,4]])
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[2], line 4
      2 tokenizer = AutoTokenizer.from_pretrained("t5-small") 
      3 inputs = ["hey how are you?", "fine"]
----> 4 tokenizer.decode([[1,2], [1,4]])

File /raid/arthur/transformers/src/transformers/tokenization_utils_base.py:3948, in PreTrainedTokenizerBase.decode(self, token_ids, skip_special_tokens, clean_up_tokenization_spaces, **kwargs)
   3945 # Convert inputs to python lists
   3946 token_ids = to_py_obj(token_ids)
-> 3948 return self._decode(
   3949     token_ids=token_ids,
   3950     skip_special_tokens=skip_special_tokens,
   3951     clean_up_tokenization_spaces=clean_up_tokenization_spaces,
   3952     **kwargs,
   3953 )

File /raid/arthur/transformers/src/transformers/tokenization_utils_fast.py:682, in PreTrainedTokenizerFast._decode(self, token_ids, skip_special_tokens, clean_up_tokenization_spaces, **kwargs)
    680 if isinstance(token_ids, int):
    681     token_ids = [token_ids]
--> 682 text = self._tokenizer.decode(token_ids, skip_special_tokens=skip_special_tokens)
    684 clean_up_tokenization_spaces = (
    685     clean_up_tokenization_spaces
    686     if clean_up_tokenization_spaces is not None
    687     else self.clean_up_tokenization_spaces
    688 )
    689 if clean_up_tokenization_spaces:

TypeError: argument 'ids': 'list' object cannot be interpreted as an integer

3. Unified encoding API:

The encode_plus is deprecated → call directly with __call__

3. apply_chat_template returns BatchEncoding:

Previously, apply_chat_template returned input_ids for backward compatibility. In v5, it now consistently returns a BatchEncoding dict like other tokenizer methods:

# v5
messages = [
    {"role": "user", "content": "Hello!"},
    {"role": "assistant", "content": "Hi there!"}
]

# Now returns BatchEncoding with input_ids, attention_mask, etc.
outputs = tokenizer.apply_chat_template(messages, return_tensors="pt")
print(outputs.keys())  # dict_keys(['input_ids', 'attention_mask'])

Removed legacy configuration file saving:

  • special_tokens_map.json - special tokens are now stored in tokenizer_config.json.
  • added_tokens.json - added tokens are now stored in tokenizer.json.
  • added_tokens_decoder is only stored when there is no tokenizer.json.

When loading older tokenizers, these files are still read for backward compatibility, but new saves use the consolidated format.

@itazap itazap changed the title rm slow tokenizer llama rm slow tokenizers Sep 19, 2025
@itazap
itazap force-pushed the one_tokenizer branch 4 times, most recently from af77c18 to dc0611f Compare September 25, 2025 11:31
@itazap
itazap marked this pull request as draft September 30, 2025 09:03

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

Nice!

Comment on lines -2452 to -2457
@require_tokenizers
def test_added_token_are_matched_longest_first(self):
if not self.test_slow_tokenizer:
self.skipTest(reason="This test is only for slow tokenizers")

tokenizers = self.get_tokenizers(fast=False)

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.

should be moved to sentencepiece as well

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.

yes it's in test_sentencepiece_backend_mixin.py

Comment on lines -2831 to -2883
words = ["Wonderful", "no", "inspiration", "example", "with", "subtoken"]
text = " ".join(words)
batch_size = 3

encoding = tokenizer_r.encode_plus(text, add_special_tokens=False)

batch_encoding = tokenizer_r([text] * batch_size, add_special_tokens=False)
num_tokens = len(encoding["input_ids"])

last_word_index = len(words) - 1
last_token_index = num_tokens - 1
last_batch_index = batch_size - 1
last_char_index = len(text) - 1

# words, tokens
self.assertEqual(len(encoding.words(0)), num_tokens)
self.assertEqual(max(encoding.words(0)), last_word_index)
self.assertEqual(min(encoding.words(0)), 0)
self.assertEqual(len(batch_encoding.words(last_batch_index)), num_tokens)
self.assertEqual(max(batch_encoding.words(last_batch_index)), last_word_index)
self.assertEqual(min(batch_encoding.words(last_batch_index)), 0)
self.assertEqual(len(encoding.tokens(0)), num_tokens)

# Assert token_to_word
self.assertEqual(encoding.token_to_word(0), 0)
self.assertEqual(encoding.token_to_word(0, 0), 0)
self.assertEqual(encoding.token_to_word(last_token_index), last_word_index)
self.assertEqual(encoding.token_to_word(0, last_token_index), last_word_index)
self.assertEqual(batch_encoding.token_to_word(1, 0), 0)
self.assertEqual(batch_encoding.token_to_word(0, last_token_index), last_word_index)
self.assertEqual(batch_encoding.token_to_word(last_batch_index, last_token_index), last_word_index)

# Assert word_to_tokens
self.assertEqual(encoding.word_to_tokens(0).start, 0)
self.assertEqual(encoding.word_to_tokens(0, 0).start, 0)
self.assertEqual(encoding.word_to_tokens(last_word_index).end, last_token_index + 1)
self.assertEqual(encoding.word_to_tokens(0, last_word_index).end, last_token_index + 1)
self.assertEqual(batch_encoding.word_to_tokens(1, 0).start, 0)
self.assertEqual(batch_encoding.word_to_tokens(0, last_word_index).end, last_token_index + 1)
self.assertEqual(
batch_encoding.word_to_tokens(last_batch_index, last_word_index).end, last_token_index + 1
)

# Assert token_to_chars
self.assertEqual(encoding.token_to_chars(0).start, 0)
self.assertEqual(encoding.token_to_chars(0, 0).start, 0)
self.assertEqual(encoding.token_to_chars(last_token_index).end, last_char_index + 1)
self.assertEqual(encoding.token_to_chars(0, last_token_index).end, last_char_index + 1)
self.assertEqual(batch_encoding.token_to_chars(1, 0).start, 0)
self.assertEqual(batch_encoding.token_to_chars(0, last_token_index).end, last_char_index + 1)
self.assertEqual(
batch_encoding.token_to_chars(last_batch_index, last_token_index).end, last_char_index + 1
)

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.

indeed rust takes care of these for himself, the other part can be tested in sentencepiece file

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.

seems like we only tested tokenizer_r aka rust here, and spiece / slow never supported the tokens_to_chars, word_to_tokens, etc.

Comment thread tests/test_tokenization_common.py Outdated
Comment on lines +2720 to +2721
self.skipTest(
reason="This test is now in TokenizersBackendTesterMixin - it tests tokenizers-backend API, not transformers code"

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.

indeed thats for tokenizers overlay

Comment on lines -3757 to -3758
# Check the changes
for token in special_tokens_list:

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.

and this can go to trash as well

@itazap itazap mentioned this pull request Oct 10, 2025
@itazap
itazap requested a review from ArthurZucker October 14, 2025 08:16

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

A good start:

    def __init__(self, vocab, merges):
    self.tokenizer = Tokenizer(
            BPE(
                vocab=vocab,
                merges=merges,
                dropout=None,
                unk_token=None,
                continuing_subword_prefix="",
                end_of_word_suffix="",
                fuse_unk=False,
                byte_fallback=False,
            )
        )

        tokenizer.normalizer = normalizers.NFC()

        tokenizer.pre_tokenizer = pre_tokenizers.Sequence(
            [
                pre_tokenizers.Split(
                    Regex(
                        r"""(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\r\n\p{L}\p{N}]?\p{L}+|\p{N}| ?[^\s\p{L}\p{N}]+[\r\n]*|\s*[\r\n]+|\s+(?!\S)|\s+"""
                    ),
                    behavior="isolated",
                    invert=False,
                ),
                pre_tokenizers.ByteLevel(
                    add_prefix_space=getattr(self.original_tokenizer, "add_prefix_space", False),
                    use_regex=False,
                ),
            ]
        )

        tokenizer.decoder = decoders.ByteLevel()
        tokenizer.post_processor = processors.ByteLevel(trim_offsets=False)

        return tokenizer

Ideally I think we can even just do this, without defining the functions separately.
The only upside would have been that we can use modular for less copy pasting, but its so small that I want to have this explicit, without extra abstraction!

Comment thread src/transformers/__init__.py Resolved
Comment on lines +1149 to +1152
logger.info(
"Falling back to PreTrainedSentencePieceTokenizer since tokenizer.model file was found "
"but no config or tokenizer class could be determined."
)

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.

IDK if we want to fallback here! I think if tokenizer.json is not found -> we convert tokenizer.model to tokenizer.json, unless user enforces sentencepiece

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.

enforce by passing like. tokenizer_backend="sentencepiece" for ex?

Comment on lines +125 to +126
def _tokenizer(self) -> Tokenizer:
return Tokenizer(Unigram(self._vocab_scores, unk_id=self._unk_id(), byte_fallback=True))

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.

yep that's good, tho I think we might want to abstract

def _model(self) -> Model:
     return Unigram(...)

return output
def _decoder(self, replacement=None, add_prefix_space=None):
return decoders.Sequence([decoders.Replace("▁", " "), decoders.ByteFallback(), decoders.Fuse()])

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.

and then finally a function that shows how we build the final tokenizer. I think we want __init__ to make self.tokenizer = Tokenizer(model=self._model(), decoder=self._decoder, etc)

"""Tokenizer configuration for this tokenizer."""
return Tokenizer(BPE(vocab=self._vocab, merges=self._merges, fuse_unk=True, byte_fallback=True, dropout=None))

def _vocab(self):

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.

should be Initial vocab or something


def _normalizer(self):
"""Normalizer configuration for this tokenizer."""
if self.legacy:

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.

no legacy in general! (we want to hide non good default most probably) so the super class will support changing this, but the real llama tokenizer is not with legacy

Comment on lines +168 to +170
def _normalizer(self):
"""Normalizer configuration for this tokenizer."""
return normalizers.NFC()

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.

nice

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

Second part of the review, very nice work on ubloating already

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.

very nice

self._special_tokens_map["additional_special_tokens"] = [] # BC default to empty list

# Directly set hidden values to allow init with tokens not yet in vocab
for key in list(kwargs.keys()):

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.

we can keep this as a TODO, but with the new logic that was added, we already have the self.xxx_token and self.xxx_token_id so IDK if additional_special_tokens is even useful. Let's leave it for later anyhways

Comment on lines +1121 to +1128
if not isinstance(value, (list, tuple)) or not all(isinstance(t, (str, AddedToken)) for t in value):
raise ValueError(f"Tokens {value} for key {key} should all be str or AddedToken instances")
new_tokens = [
(AddedToken(t, rstrip=False, lstrip=False, normalized=False, special=True) if isinstance(t, str) else t)
for t in value
if replace_additional_special_tokens or str(t) not in self.additional_special_tokens
]
if replace_additional_special_tokens and new_tokens:

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.

I would kind of want to get rid of this and put it only in spm, because tokenizers just supports tokenizer.special_tokens which gives all special tokens -> duplicated info with the additional special tokens

return all_toks
seen = set()
all_toks = []
for value in self.special_tokens_map.values():

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.

same here, would leave as abstract and rely on tokenizers's special_tokens attr if we can!

Comment on lines +1875 to +1887
@classmethod
def convert_added_tokens(cls, obj: Union[AddedToken, Any], save=False, add_type_field=True):
if isinstance(obj, dict) and "__type" in obj and obj["__type"] == "AddedToken":
obj.pop("__type")
return AddedToken(**obj)
if isinstance(obj, AddedToken) and save:
obj = obj.__getstate__()
if add_type_field:
obj["__type"] = "AddedToken"
else:
# Don't save "special" for previous tokenizers
obj.pop("special")
return obj

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.

IDRemember why we use this one? Only for SPM no?

) -> BatchEncoding:
# Input validation (from _call_one)
def _is_valid_text_input(t):

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.

I think (but I might be wrong here) that tokenizers does the typechecking itself as well

self.assertEqual(tokens, EXPECTED_TOKENS)
def test_integration_expected_token_ids(self):
for tok in self.tokenizers:
self.assertEqual(tok.encode(input_string), expected_token_ids)

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 is just missing a decode test

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.

overall LGTM!

@itazap
itazap requested a review from ArthurZucker October 14, 2025 13:56
str(unk_token): 3,
}

self._merges = merges if merges is not None else generate_merges(self._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.

you actually should never generate merges out of the bos pad eos unk ! so the merge generation should happen before

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.

is it all special tokens or just these 4? in convert_slow_tokenizer it currently indexes the vocab[3:]

Comment on lines +112 to +113
self.add_tokens(list(self.all_special_tokens), special_tokens=True)
self.update_post_processor()

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.

both can probably be called from the TokenizerBackend class wdyt? As in we are adding the post processor thing to all of them, and that already by default special tokens need to be added?

sub_texts = "".join(sub_texts)

return sub_texts.replace(SPIECE_UNDERLINE, " ")
self._post_init()

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.

you can also just call

self.add_tokens(list(self.all_special_tokens), special_tokens=True)

but adding token has historically been done in the super call!

brittlewis12 pushed a commit to brittlewis12/llama.cpp that referenced this pull request Aug 17, 2026
…rg#26217)

bytes_to_unicode was removed from transformers.models.gpt2.tokenization_gpt2
in huggingface/transformers#40936, but it had already been copied into
transformers.convert_slow_tokenizer in huggingface/transformers#30334
(transformers 4.54.1), so import it directly from there.

Applies the same fix to chatglm.py.
@blipbyte blipbyte mentioned this pull request Aug 22, 2026
1 of 5 tasks
tarekziade added a commit that referenced this pull request Aug 24, 2026
bart, mobilebert and squeezebert started firing when mlinter 0.1.5 extended
TRF038 to tokenizer files. Their tokenization_*.py define no classes at all,
each being a two-line back-compat alias left by "rm slow tokenizers" (#40936),
which deleted their test files on purpose. A test file here could only assert
alias identity, and the aliased tokenizers are already covered by
tests/models/roberta and tests/models/bert.

Allowlisted as a placeholder: the better fix is upstream, where TRF038 should
skip a source file that defines no classes.

TRF038 now reports no findings.
tarekziade added a commit that referenced this pull request Aug 28, 2026
bart, mobilebert and squeezebert started firing when mlinter 0.1.5 extended
TRF038 to tokenizer files. Their tokenization_*.py define no classes at all,
each being a two-line back-compat alias left by "rm slow tokenizers" (#40936),
which deleted their test files on purpose. A test file here could only assert
alias identity, and the aliased tokenizers are already covered by
tests/models/roberta and tests/models/bert.

Allowlisted as a placeholder: the better fix is upstream, where TRF038 should
skip a source file that defines no classes.

TRF038 now reports no findings.
sywangyi pushed a commit to sywangyi/transformers that referenced this pull request Aug 28, 2026
…gface#48259)

* future mlinter 0.1.5

* fix rule 58

* Add tokenizer tests for TRF038

Adds the missing tokenizer test files that mlinter's TRF038 reports, now that
the rule covers tokenization_*.py files.

* fix roberta

* Allowlist the three alias-only tokenizer modules for TRF038

bart, mobilebert and squeezebert started firing when mlinter 0.1.5 extended
TRF038 to tokenizer files. Their tokenization_*.py define no classes at all,
each being a two-line back-compat alias left by "rm slow tokenizers" (huggingface#40936),
which deleted their test files on purpose. A test file here could only assert
alias identity, and the aliased tokenizers are already covered by
tests/models/roberta and tests/models/bert.

Allowlisted as a placeholder: the better fix is upstream, where TRF038 should
skip a source file that defines no classes.

TRF038 now reports no findings.

* Route composite sub-configs through CONFIG_MAPPING for TRF009

mlinter 0.1.5 extended TRF009 from modeling files to every model file, which
surfaced six configs importing a sibling model's config class directly. Each now
resolves it through CONFIG_MAPPING with AutoConfig as the sub_configs marker,
which is what the rule prescribes and what 99 other composite configs already
do:

  cohere_asr               ParakeetEncoderConfig    -> parakeet_encoder
  esmfold2                 EsmcConfig               -> esmc
  gemma4_assistant         Gemma4TextConfig         -> gemma4_text
  gemma4_unified_assistant Gemma4UnifiedTextConfig  -> gemma4_unified_text
  ovis2                    Qwen2Config              -> qwen2
  sam3                     CLIPTextConfig           -> clip_text_model

cohere_asr already used CONFIG_MAPPING for instantiation, so only its
sub_configs value and import changed. Where the imported class also appeared in
a type annotation it is widened to PreTrainedConfig, and esmfold2's _init_nested
helper takes the class from CONFIG_MAPPING since it calls cls() directly.

Verified: default construction and to_dict/from_dict round-trip resolve the same
concrete config class as before for all six, and tests/models for the six models
pass (552 passed, 551 skipped).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Backport mlinter rule-prose compression and allowlist the rest of TRF009

Two changes to utils/rules.toml.

Backports transformers-mlinter d0ac218 ("compress the rule explanation prose",
before: upstream's file as the base, with this repo's licence header and its
allowlist_models entries and their justification comments re-applied on top, so
the only remaining difference from the installed mlinter/rules.toml is the header
and those allowlists. make fix-repo regenerated
docs/source/en/modeling_rules.md.

Allowlists the ten TRF009 findings left after the CONFIG_MAPPING fixes, in three
groups, each with its reasoning recorded in the file:

- Back-compat tokenizer shims (bart, convbert, distilbert, fnet, mobilebert,
  squeezebert): the cross-model import is the module's whole purpose.
- generation_*.py (nemotron3_5_asr, nemotron_asr_streaming): the rule points at
  modular files, but the converter has no generation_*.py support, so the
  prescribed remedy does not exist yet.
- sam3: CLIPTextModelWithProjection is needed for checkpoint key compatibility.
  shieldgemma2: fixable with a modular file, left as follow-up.

make typing and make check-repo now pass.

* Fix vibevoice for mlinter 0.1.5 after rebase

VibeVoice (huggingface#40546) landed on main after this branch forked, so it was never
checked against the 0.1.5 rules and brought in two findings.

TRF058: VibeVoiceDiffusionHeadSinusoidalEmbedding created `freq` via
`self.register_buffer(...)`, which cannot be inherited and tweaked from a
modular file. Assign `nn.Buffer(..., persistent=False)` instead, which also
makes the `freq: torch.Tensor` linting workaround unnecessary. Same
non-persistent buffer and same forward output.

TRF009: allowlist `vibevoice`. generation_vibevoice.py imports a runtime cache
class from its own sub-model package; the family already routes its sub-configs
through CONFIG_MAPPING, and a cache class cannot come from CONFIG_MAPPING. It
also sits in a generation_*.py file, which the modular converter does not
support, the same blocker as the nemotron entries.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Adopt transformers-mlinter main: sync rule prose, drop dead allowlist entries

Re-ran the checker against transformers-mlinter git main (e6533da). Green, and
the bump makes a chunk of this branch's curation obsolete.

Synced the rule prose the repo copy mirrors, which had gone stale because the
described behavior changed upstream:
  TRF029 -- an optional `None`-default parameter is now an override, not a
            second source of truth, so it is exempt
  TRF035 -- F401/F821/F822 are now accepted in modular files
  TRF041 -- guards (an `if` with no `else` that only raises or only logs) and a
            `DEFAULT_EXEMPT_ATTRIBUTES` list of framework plumbing are exempt

Dropped 39 allowlist entries that no longer fire, mostly mlinter#27 (the rule
engine now resolves a class's base chain across models instead of only inside
the linted file), which this branch had flagged upstream rather than worked
around:
  TRF018  16 -> 1   only `radio`, the one real case, is left
  TRF020   1 -> 0   the `axk2` false positive is gone
  TRF034  17 -> 5   the 12 cross-file false positives are gone; the 5 real
                    findings stay as follow-ups
  TRF029   2 -> 1   TRF035  5 -> 0   TRF041  14 -> 9
Each rule's comment block now describes what is left and why. Verified minimal:
emptying any of these six allowlists reproduces exactly the kept entries.

24 further entries (TRF002, TRF004, TRF005, TRF006, TRF009 `maskformer`,
TRF010, TRF019, TRF056) are also dead, but they were already dead before this
bump, so they are left alone here. TRF037's `x_clip` stays: the rule is opt-in,
so it never fires and the entry documents what would.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Regenerate modeling_rules.md for the synced rule prose

Generated from utils/rules.toml by utils/check_modeling_rules_doc.py; picks up
the TRF029, TRF035 and TRF041 prose synced from transformers-mlinter main.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Drop 19 more allowlist entries that no longer fire

Swept every rule the way the earlier mlinter#27 pass was done: emptied each
allowlist, re-ran, and checked what actually fires. 213 entries -> 194.

"Does not fire" has four causes, so each candidate was classified rather than
deleted on the strength of a green run:
  - the model is compliant now
  - the pattern survives only in a generated file, copied from a parent
  - the model is grandfathered by cutoff_date, so it is never checked
  - the rule is opt-in and never runs at all
Only the first two are safe to drop. To separate them, each candidate rule was
run directly against the model's own source with grandfathering off.

Removed as compliant, verified rule-by-rule against the source (14):
  TRF005  9 -> 0   the rule accepts an explicit `_no_split_modules = None`, a
                   modular `AttributeError` sentinel, and raw-string elements
  TRF010  2 -> 0   both configs carry @strict(accept_kwargs=True)
  TRF006  4 -> 3   TRF009 15 -> 14 (maskformer)   TRF056 2 -> 1 (kimi_k25)

Removed as generated-only copies (5):
  TRF004  9 -> 4   hubert, sew, unispeech, unispeech_sat and wavlm define no
                   `tie_weights` in their modular files; the override exists
                   only in their generated modeling_*.py, copied from
                   Wav2Vec2ForCTC. wav2vec2 still fires, so the root cause stays
                   tracked -- listing all five implied five separate bugs.

Kept deliberately, and now documented in the file (5):
  TRF002  lighton_ocr -- `base_model_prefix = ""` is real and authored in its
          own modular file; the rule misses it only because it resolves base
          chains within a single file, so `LightOnOcrModel(Mistral3Model)` is
          not recognized as a pretrained class
  TRF019  blip_2, grounding_dino, omdet_turbo -- grandfathered by cutoff_date;
          they still set `_defaults` and fire once grandfathering is off
  TRF037  x_clip -- the rule is opt-in, so the entry records what would fire

Verified: emptying every allowlist now reproduces exactly the kept entries,
except those 5, each of which has a stated reason.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Adopt the TRF034 trunk scoping from mlinter main

mlinter#65 scoped TRF034 to the model's checkpointable token-mixing trunk. Sync
the rule prose and cut the allowlist from 5 models to 1.

`dinov3_convnext`, `openai` and `xcodec2` never set
`supports_gradient_checkpointing`, so `gradient_checkpointing_enable()` raises
for them rather than skipping a layer and there was nothing to report.
`tipsv2_dpt`'s three entries are DPT head layers, not a token-mixing stack.
`x_clip`'s `PromptGeneratorLayer` stays: an attention stack on a model that does
enable checkpointing, so it is a real finding and a follow-up for that model.

Over this checkout with every cutoff neutralised, TRF034 goes from 103 findings
over 76 models to 20 over 19, and every survivor is a transformer stack in a
checkpointable model.

* Pin transformers-mlinter 0.1.5

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Qubitium added a commit to ModelCloud/OMY that referenced this pull request Aug 31, 2026
* Retry get_daily_ci_runs on stale GitHub API cache (huggingface#48374)

* [debug] Poll GitHub API 10x to observe cache inconsistency in schedule runs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Retry get_daily_ci_runs when GitHub API returns stale cache results

The GitHub Actions search index (used for event=/branch= filters) can lag
behind the database — different backend nodes return wildly different
total_count values (190, 238, 311, 413 observed for the same URL within
minutes), and the most-recent runs are missing from stale responses.

Detect staleness by checking whether the current GITHUB_RUN_ID appears in
the returned list; if absent, retry up to 3 times with a 30s delay.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [debug] Use get_daily_ci_runs in debug job; add current_run_id param

Add optional `current_run_id` parameter to `get_daily_ci_runs` so callers
can supply a known schedule run ID for stale-cache detection when
GITHUB_RUN_ID belongs to a non-schedule-triggered job (e.g. push).

Update the debug workflow job to call the Python function directly
(two quick back-to-back calls) instead of raw curl, so the retry logic
is exercised end-to-end and visible in the CI logs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [debug] Force retry path with fake run ID to verify stale-detection logging

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Revert "[debug] Force retry path with fake run ID to verify stale-detection logging"

This reverts commit e49340f.

* [debug] Run get_daily_ci_runs 20 times to catch stale cache hit

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [debug] Sleep 60s between get_daily_ci_runs calls to hit different cache nodes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Increase get_daily_ci_runs max_attempts from 3 to 5

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Add unit tests for get_daily_ci_runs stale-cache retry logic

- tests/utils/test_get_previous_daily_ci.py: 5 tests covering fresh hit
  (no retry), stale→fresh (one retry), all stale (max_attempts exhausted),
  no current_run_id (stale check skipped), and empty schedule fallback to
  workflow_run event
- utils/get_previous_daily_ci.py: ruff formatting fix (long print line)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Infer stale-check eligibility from current run's workflow_id

Instead of an explicit `current_run_id` argument, `get_daily_ci_runs` now
always fetches the current run's metadata (GITHUB_RUN_ID) upfront and
compares its `workflow_id` to the queried one:

- Same workflow → stale-cache detection applies (max_attempts=5, 30s sleep)
- Different workflow → skip (e.g. AMD CI querying Nvidia CI runs); left for
  a follow-up PR once the same-workflow case is confirmed stable

Also adds a TODO comment on the event=workflow_run fallback path (AMD CI)
and updates the 6 unit tests accordingly (new test for the different-workflow
case; all tests account for the upfront current-run lookup call).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Gate stale-cache check on GITHUB_EVENT_NAME == 'schedule'

Push- and dispatch-triggered runs won't appear in event=schedule results
even when the API is fresh, causing spurious retries. Only enable the
stale-cache retry loop when the current run is itself schedule-triggered.

Adds a corresponding unit test (test_non_schedule_event_skips_stale_check).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Revert debug changes to self-scheduled-caller.yml

Restore the workflow to its main-branch state; the debug polling job was
only needed to verify the stale-cache behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Condense get_daily_ci_runs comments and refer to huggingface#48374 for rationale

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: ydshieh <ydshieh@users.noreply.github.com>

* Fix `safe_open` mmap memory exhaustion on Windows by using `pread` backend (huggingface#48341)

* Fix `safe_open` mmap memory exhaustion on Windows by using `pread` backend

On Windows, memory-mapping safetensors files reserves copy-on-write commit
charge for the entire file. For large multi-shard checkpoints this exhausts
virtual memory. Switch to the `pread` backend on `win32` in both
`_load_pretrained_model` and `MtpModel` loading paths.

* Address review: add MPS backend fix to MtpModel, hoist backend selection before loop, use targeted sys.platform patch in test

* Address review: remove tests

* [ONNX] Skip affected models on torch 2.13 (two dynamo regressions) (huggingface#48191)

* [ONNX] Skip affected models on torch >= 2.13 (two dynamo ONNX regressions)

torch 2.13.0 introduced two regressions in dynamo ONNX export:
- pytorch/pytorch#194381: aten.sub type-promotion failure for scalar - int_tensor
- pytorch/pytorch#194382: aten.mul.Scalar missing ONNX decomposition

Skip all 35 affected model classes under `EXPORT_SKIPS["onnx"]` when
torch >= 2.13, using the existing skip infrastructure. Will be removed
once the upstream PyTorch fixes land.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Scope ONNX skip to torch == 2.13.x only (auto-runs on 2.14+)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Add missing BigBird subclasses to torch 2.13 ONNX skip list

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [ONNX] Clarify skip guard is torch == 2.13.x only (not >=)

Update comment and skip reason strings to say `torch == 2.13` instead of
`torch >= 2.13`, making it clear the guard auto-lifts on 2.14+.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [ONNX] Fix the torch 2.13 scalar regressions instead of skipping the models

Both regressions are a Python float meeting an integral tensor, whose promotion
torch 2.13 mishandles: `1.0 - int_mask` crashes the decomposition pass
(pytorch/pytorch#194381) and `int_mask * 2.0` reaches translation with no
registered ONNX decomposition (pytorch/pytorch#194382). On the same torch,
`1.0 - float_tensor` and `float_tensor * 2.0` export fine — so promoting the
tensor operand up front, to the dtype the op already produces, is enough. The
op and its overload are left alone, and the inserted cast carries the op's own
`meta` because for these elementwise cases it is the same value.

Decomposition also emits `mul.Scalar` with a *symbolic* second operand (a
division result, not a literal). There is no constant to promote there, so that
one is rewritten to `mul.Tensor`, which has the two-operand translation — the
same rewrite `_fix_remainder_scalar` makes for the same reason. Reachable
because the FX fixes run again right after `run_decompositions`.

This drops the 39 skip entries: the 15 affected families export again on 2.13,
including the vision models (Sam, SamHQ, GotOcr2, GroundingDino, SegGpt,
EfficientLoFTR, DeepseekOcr2).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Resolve the promoted-op set on first use, not at import

`exporter_onnx` is importable without torch — the CI job that imports transformers
with PIL only proved this the hard way — and naming `torch.ops.aten.*` overloads in
a module-level frozenset broke that with a `NameError` before anything ran.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Say what the `mul.Scalar` operand is, and check it

Review raised two things. The `Node` check assumed a tensor operand: it is never one. Across the affected
families all 33 sites are an `operator.truediv` result, i.e. a `SymFloat`, which `mul.Tensor`'s translation
does take — so the rewrite stands, but the guard now names the forms that op accepts instead of trusting
the node type, and anything else keeps the `mul.Scalar` overload and fails visibly in translation.

The fixes are also deliberately not version-gated, which the docstring implied they were. Both rewrites
are semantics-preserving on any torch — a cast to the dtype the op already produces, and an overload swap
with the same meaning — so gating would only decide which torch exercises the path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: ydshieh <ydshieh@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: IlyasMoutawwakil <moutawwakil.ilyas.tsi@gmail.com>
Co-authored-by: Ilyas Moutawwakil <57442720+IlyasMoutawwakil@users.noreply.github.com>

* [Qwen3VLMoe] Update `test_small_model_integration_test_batch` expected output (value drift) (huggingface#48376)

Update Qwen3VLMoe batch integration test expected output (value drift)

Co-authored-by: ydshieh <ydshieh@users.noreply.github.com>

* [LongcatFlash] Fix test_longcat_generation_cpu: use device_map="cpu" to avoid MoE disk offload issue (huggingface#48377)

* [LongcatFlash] Fix test_longcat_generation_cpu by using device_map="cpu"

`device_map="auto"` causes accelerate to offload MoE expert weights to disk,
which then fails to reload them due to an internal weight format incompatibility.
Since the test already requires large CPU RAM, use `device_map="cpu"` to keep
all weights in memory and avoid disk offloading entirely.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [LongcatFlash] Update golden string and skip test_longcat_generation_cpu on small runners

- `test_shortcat_generation`: update expected output to current model output (value drift)
- `test_longcat_generation_cpu`: replace `@require_large_cpu_ram` with
  `@require_torch_accelerator_memory(memory=1100)` — the 562B parameter model requires
  ~1,047 GiB of bfloat16 weights, far exceeding the CI runner budget (84 GiB single /
  168 GiB dual), and disk offloading fails due to MoE weight format incompatibility
  with accelerate

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* remove unused require_large_cpu_ram import

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: ydshieh <ydshieh@users.noreply.github.com>

* fix: flash-attn fallback failing on torch2.13 (huggingface#48388)

fix: probe the same flash-attn kernel version that the loader resolves

* [docs] Pass ContinuousBatchingConfig and sliding window models  (huggingface#48381)

docs

* Raise when a paged attention forward is called with no cache (huggingface#48297)

* Fall back to the base attention implementation when a paged attention forward has no cache

A standard forward on a model switched to a paged attention implementation (as done by
ContinuousBatchingManager) silently computed non-causal attention: sdpa_attention_paged_forward
treats cache as optional and mask creation early-exits to None for paged implementations.
Now, when the continuous batching kwargs are absent, the paged sdpa/flash forwards delegate to
their base implementation, the eager paged forward materializes a causal mask, and mask creation
resolves 2D padding masks with the base implementation's mask function.

* Raise instead of falling back when a paged attention forward has no cache

Drops the redirect to the base implementation and the paged-aware mask
creation, per review. `paged|sdpa` and `paged|eager` now refuse a call with
no paged cache rather than silently attending bidirectionally.

---------

Co-authored-by: Rémi Ouazan <83456801+remi-or@users.noreply.github.com>

* Bump transformers-mlinter to 0.1.5 and clear the new findings (huggingface#48259)

* future mlinter 0.1.5

* fix rule 58

* Add tokenizer tests for TRF038

Adds the missing tokenizer test files that mlinter's TRF038 reports, now that
the rule covers tokenization_*.py files.

* fix roberta

* Allowlist the three alias-only tokenizer modules for TRF038

bart, mobilebert and squeezebert started firing when mlinter 0.1.5 extended
TRF038 to tokenizer files. Their tokenization_*.py define no classes at all,
each being a two-line back-compat alias left by "rm slow tokenizers" (huggingface#40936),
which deleted their test files on purpose. A test file here could only assert
alias identity, and the aliased tokenizers are already covered by
tests/models/roberta and tests/models/bert.

Allowlisted as a placeholder: the better fix is upstream, where TRF038 should
skip a source file that defines no classes.

TRF038 now reports no findings.

* Route composite sub-configs through CONFIG_MAPPING for TRF009

mlinter 0.1.5 extended TRF009 from modeling files to every model file, which
surfaced six configs importing a sibling model's config class directly. Each now
resolves it through CONFIG_MAPPING with AutoConfig as the sub_configs marker,
which is what the rule prescribes and what 99 other composite configs already
do:

  cohere_asr               ParakeetEncoderConfig    -> parakeet_encoder
  esmfold2                 EsmcConfig               -> esmc
  gemma4_assistant         Gemma4TextConfig         -> gemma4_text
  gemma4_unified_assistant Gemma4UnifiedTextConfig  -> gemma4_unified_text
  ovis2                    Qwen2Config              -> qwen2
  sam3                     CLIPTextConfig           -> clip_text_model

cohere_asr already used CONFIG_MAPPING for instantiation, so only its
sub_configs value and import changed. Where the imported class also appeared in
a type annotation it is widened to PreTrainedConfig, and esmfold2's _init_nested
helper takes the class from CONFIG_MAPPING since it calls cls() directly.

Verified: default construction and to_dict/from_dict round-trip resolve the same
concrete config class as before for all six, and tests/models for the six models
pass (552 passed, 551 skipped).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Backport mlinter rule-prose compression and allowlist the rest of TRF009

Two changes to utils/rules.toml.

Backports transformers-mlinter d0ac218 ("compress the rule explanation prose",
before: upstream's file as the base, with this repo's licence header and its
allowlist_models entries and their justification comments re-applied on top, so
the only remaining difference from the installed mlinter/rules.toml is the header
and those allowlists. make fix-repo regenerated
docs/source/en/modeling_rules.md.

Allowlists the ten TRF009 findings left after the CONFIG_MAPPING fixes, in three
groups, each with its reasoning recorded in the file:

- Back-compat tokenizer shims (bart, convbert, distilbert, fnet, mobilebert,
  squeezebert): the cross-model import is the module's whole purpose.
- generation_*.py (nemotron3_5_asr, nemotron_asr_streaming): the rule points at
  modular files, but the converter has no generation_*.py support, so the
  prescribed remedy does not exist yet.
- sam3: CLIPTextModelWithProjection is needed for checkpoint key compatibility.
  shieldgemma2: fixable with a modular file, left as follow-up.

make typing and make check-repo now pass.

* Fix vibevoice for mlinter 0.1.5 after rebase

VibeVoice (huggingface#40546) landed on main after this branch forked, so it was never
checked against the 0.1.5 rules and brought in two findings.

TRF058: VibeVoiceDiffusionHeadSinusoidalEmbedding created `freq` via
`self.register_buffer(...)`, which cannot be inherited and tweaked from a
modular file. Assign `nn.Buffer(..., persistent=False)` instead, which also
makes the `freq: torch.Tensor` linting workaround unnecessary. Same
non-persistent buffer and same forward output.

TRF009: allowlist `vibevoice`. generation_vibevoice.py imports a runtime cache
class from its own sub-model package; the family already routes its sub-configs
through CONFIG_MAPPING, and a cache class cannot come from CONFIG_MAPPING. It
also sits in a generation_*.py file, which the modular converter does not
support, the same blocker as the nemotron entries.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Adopt transformers-mlinter main: sync rule prose, drop dead allowlist entries

Re-ran the checker against transformers-mlinter git main (e6533da). Green, and
the bump makes a chunk of this branch's curation obsolete.

Synced the rule prose the repo copy mirrors, which had gone stale because the
described behavior changed upstream:
  TRF029 -- an optional `None`-default parameter is now an override, not a
            second source of truth, so it is exempt
  TRF035 -- F401/F821/F822 are now accepted in modular files
  TRF041 -- guards (an `if` with no `else` that only raises or only logs) and a
            `DEFAULT_EXEMPT_ATTRIBUTES` list of framework plumbing are exempt

Dropped 39 allowlist entries that no longer fire, mostly mlinter#27 (the rule
engine now resolves a class's base chain across models instead of only inside
the linted file), which this branch had flagged upstream rather than worked
around:
  TRF018  16 -> 1   only `radio`, the one real case, is left
  TRF020   1 -> 0   the `axk2` false positive is gone
  TRF034  17 -> 5   the 12 cross-file false positives are gone; the 5 real
                    findings stay as follow-ups
  TRF029   2 -> 1   TRF035  5 -> 0   TRF041  14 -> 9
Each rule's comment block now describes what is left and why. Verified minimal:
emptying any of these six allowlists reproduces exactly the kept entries.

24 further entries (TRF002, TRF004, TRF005, TRF006, TRF009 `maskformer`,
TRF010, TRF019, TRF056) are also dead, but they were already dead before this
bump, so they are left alone here. TRF037's `x_clip` stays: the rule is opt-in,
so it never fires and the entry documents what would.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Regenerate modeling_rules.md for the synced rule prose

Generated from utils/rules.toml by utils/check_modeling_rules_doc.py; picks up
the TRF029, TRF035 and TRF041 prose synced from transformers-mlinter main.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Drop 19 more allowlist entries that no longer fire

Swept every rule the way the earlier mlinter#27 pass was done: emptied each
allowlist, re-ran, and checked what actually fires. 213 entries -> 194.

"Does not fire" has four causes, so each candidate was classified rather than
deleted on the strength of a green run:
  - the model is compliant now
  - the pattern survives only in a generated file, copied from a parent
  - the model is grandfathered by cutoff_date, so it is never checked
  - the rule is opt-in and never runs at all
Only the first two are safe to drop. To separate them, each candidate rule was
run directly against the model's own source with grandfathering off.

Removed as compliant, verified rule-by-rule against the source (14):
  TRF005  9 -> 0   the rule accepts an explicit `_no_split_modules = None`, a
                   modular `AttributeError` sentinel, and raw-string elements
  TRF010  2 -> 0   both configs carry @strict(accept_kwargs=True)
  TRF006  4 -> 3   TRF009 15 -> 14 (maskformer)   TRF056 2 -> 1 (kimi_k25)

Removed as generated-only copies (5):
  TRF004  9 -> 4   hubert, sew, unispeech, unispeech_sat and wavlm define no
                   `tie_weights` in their modular files; the override exists
                   only in their generated modeling_*.py, copied from
                   Wav2Vec2ForCTC. wav2vec2 still fires, so the root cause stays
                   tracked -- listing all five implied five separate bugs.

Kept deliberately, and now documented in the file (5):
  TRF002  lighton_ocr -- `base_model_prefix = ""` is real and authored in its
          own modular file; the rule misses it only because it resolves base
          chains within a single file, so `LightOnOcrModel(Mistral3Model)` is
          not recognized as a pretrained class
  TRF019  blip_2, grounding_dino, omdet_turbo -- grandfathered by cutoff_date;
          they still set `_defaults` and fire once grandfathering is off
  TRF037  x_clip -- the rule is opt-in, so the entry records what would fire

Verified: emptying every allowlist now reproduces exactly the kept entries,
except those 5, each of which has a stated reason.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Adopt the TRF034 trunk scoping from mlinter main

mlinter#65 scoped TRF034 to the model's checkpointable token-mixing trunk. Sync
the rule prose and cut the allowlist from 5 models to 1.

`dinov3_convnext`, `openai` and `xcodec2` never set
`supports_gradient_checkpointing`, so `gradient_checkpointing_enable()` raises
for them rather than skipping a layer and there was nothing to report.
`tipsv2_dpt`'s three entries are DPT head layers, not a token-mixing stack.
`x_clip`'s `PromptGeneratorLayer` stays: an attention stack on a model that does
enable checkpointing, so it is a real finding and a follow-up for that model.

Over this checkout with every cutoff neutralised, TRF034 goes from 103 findings
over 76 models to 20 over 19, and every survivor is a transformer stack in a
checkpointable model.

* Pin transformers-mlinter 0.1.5

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

* [CB] Fix wrong device scoping (huggingface#48370)

* Add device scoping

* Added a regression test

* [Improvement] Make gated delta rule more explicit  (huggingface#47625)

* MAke gdr more explicit and support per-channel decay

* Nits and tests

* regenerated

* Delete the KDA mode of GDN

* Bake in the UT transform

* Review compliance 1/n

* Apply suggestions from code review

Co-authored-by: Anton Vlasjuk <73884904+vasqu@users.noreply.github.com>

* Readability

* Review 2/n

* Memory and speed optims

* Added test file

* Fix export

* Merge mess up fix

* fix2

* Fix in place identity

* Review compliance

* second review 1/n

* second review 2/n

* second review 3/n

* second review 4/n

* second review 5/n

* Fix repo

* Last review

* Remove test class

* Make fix repo

* Fix BPE tokenizer test

---------

Co-authored-by: Anton Vlasjuk <73884904+vasqu@users.noreply.github.com>

* fix some failure in xpu (huggingface#48252)

* fix some failure in xpu

Signed-off-by: Wang, Yi <yi.a.wang@intel.com>

* fmt

Signed-off-by: Wang, Yi <yi.a.wang@intel.com>

* update

Signed-off-by: Wang, Yi <yi.a.wang@intel.com>

* update

Signed-off-by: Wang, Yi A <yi.a.wang@intel.com>

---------

Signed-off-by: Wang, Yi <yi.a.wang@intel.com>
Signed-off-by: Wang, Yi A <yi.a.wang@intel.com>

* Update Tailscale action version in workflow (huggingface#48394)

* skip mtp slow tests for now (huggingface#48328) (huggingface#48329)

skip mtp slow tests for now

* [VibeVoice] Skip generate export tests (flaky) (huggingface#48396)

* [VibeVoice] Skip generate export tests (flaky, decompose_prefill_decode incompatible)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [VibeVoice] Skip flaky static-cache compile tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: ydshieh <ydshieh@users.noreply.github.com>

* Fix interval merge invariant in _find_disjoint (huggingface#47860)

Fix interval merge invariant in `_find_disjoint`

`_find_disjoint` implements a sweep-line merge over tensor memory ranges,
but assigns `last_stop = stop` instead of tracking the running maximum.
When a tensor is strictly contained inside an earlier, larger one,
`last_stop` shrinks to the contained tensor's end pointer, so a subsequent
tensor that genuinely overlaps the larger one is classified as disjoint.

Use `last_stop = max(last_stop, stop)`, the standard sweep-line invariant.

Note on impact: this is a latent correctness fix, not a user-facing bug fix.
Triggering the mis-classification requires strict containment, which in turn
guarantees the resulting group is not "identical" in `_find_identical`, so
`remove_tied_weights_from_state_dict` raises the same RuntimeError either way.
Verified exhaustively over all 3- and 4-interval configurations (54k cases):
the corrected version matches ground-truth connected components in 100% of
cases, and the raise-vs-succeed outcome never differs. The observable
improvement is that the error message now reports the complete set of
overlapping tensors instead of a truncated one.

* Fix incorrect tuple return annotations on forward methods returning a Tensor (huggingface#48359)

A number of `forward` methods declare a `tuple[...]` return annotation but
return a bare `torch.Tensor`. Same class of issue as huggingface#45208, fixed for
`Qwen3MoeSparseMoeBlock` in huggingface#45352.

Each of the 29 classes touched here was instantiated and called with a minimal
config, and the returned object's type observed to be `torch.Tensor`. Annotation
only: no runtime behaviour is affected.

Eleven annotations were edited in `modular_*.py` sources and the corresponding
`modeling_*.py` files regenerated with `utils/modular_model_converter.py`;
`utils/check_modular_conversion.py` and `utils/check_copies.py` both pass.

Co-authored-by: Gustavo <gustavo.vp350@gmail.com>

* Avoid print to stdout that fails the job `check_failed_tests` job (huggingface#48391)

fix

Co-authored-by: ydshieh <ydshieh@users.noreply.github.com>

* [Docs]: Update GLM 5.3 (huggingface#48401)

* docs: update GLM 5.3

Signed-off-by: Shijin Zhang <75300765+Dovis01@users.noreply.github.com>

* docs: update toc

Signed-off-by: Shijin Zhang <75300765+Dovis01@users.noreply.github.com>

* fix

Signed-off-by: Shijin Zhang <75300765+Dovis01@users.noreply.github.com>

---------

Signed-off-by: Shijin Zhang <75300765+Dovis01@users.noreply.github.com>

---------

Signed-off-by: Wang, Yi <yi.a.wang@intel.com>
Signed-off-by: Wang, Yi A <yi.a.wang@intel.com>
Signed-off-by: Shijin Zhang <75300765+Dovis01@users.noreply.github.com>
Co-authored-by: Yih-Dar <2521628+ydshieh@users.noreply.github.com>
Co-authored-by: ydshieh <ydshieh@users.noreply.github.com>
Co-authored-by: eryk-roch <eryk.roch.rybak@intel.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: IlyasMoutawwakil <moutawwakil.ilyas.tsi@gmail.com>
Co-authored-by: Ilyas Moutawwakil <57442720+IlyasMoutawwakil@users.noreply.github.com>
Co-authored-by: NanoCode012 <nano@axolotl.ai>
Co-authored-by: Steven Liu <59462357+stevhliu@users.noreply.github.com>
Co-authored-by: Quentin Gallouédec <45557362+qgallouedec@users.noreply.github.com>
Co-authored-by: Rémi Ouazan <83456801+remi-or@users.noreply.github.com>
Co-authored-by: Tarek Ziade <tarek@ziade.org>
Co-authored-by: Anton Vlasjuk <73884904+vasqu@users.noreply.github.com>
Co-authored-by: Wang, Yi <yi.a.wang@intel.com>
Co-authored-by: Guillaume LEGENDRE <glegendre01@gmail.com>
Co-authored-by: Vikas Sharma <vikasharmadev8@gmail.com>
Co-authored-by: Gustavo Dias Apolinário <100954782+Gronoxx@users.noreply.github.com>
Co-authored-by: Gustavo <gustavo.vp350@gmail.com>
Co-authored-by: Shijin Zhang <75300765+Dovis01@users.noreply.github.com>
thecodacus pushed a commit to thecodacus/llama.cpp that referenced this pull request Sep 7, 2026
…rg#26217)

bytes_to_unicode was removed from transformers.models.gpt2.tokenization_gpt2
in huggingface/transformers#40936, but it had already been copied into
transformers.convert_slow_tokenizer in huggingface/transformers#30334
(transformers 4.54.1), so import it directly from there.

Applies the same fix to chatglm.py.
zbrad pushed a commit to zbrad/llama.cpp that referenced this pull request Sep 10, 2026
…rg#26217)

bytes_to_unicode was removed from transformers.models.gpt2.tokenization_gpt2
in huggingface/transformers#40936, but it had already been copied into
transformers.convert_slow_tokenizer in huggingface/transformers#30334
(transformers 4.54.1), so import it directly from there.

Applies the same fix to chatglm.py.
sbucaille pushed a commit to sbucaille/transformers that referenced this pull request Sep 16, 2026
…gface#48259)

* future mlinter 0.1.5

* fix rule 58

* Add tokenizer tests for TRF038

Adds the missing tokenizer test files that mlinter's TRF038 reports, now that
the rule covers tokenization_*.py files.

* fix roberta

* Allowlist the three alias-only tokenizer modules for TRF038

bart, mobilebert and squeezebert started firing when mlinter 0.1.5 extended
TRF038 to tokenizer files. Their tokenization_*.py define no classes at all,
each being a two-line back-compat alias left by "rm slow tokenizers" (huggingface#40936),
which deleted their test files on purpose. A test file here could only assert
alias identity, and the aliased tokenizers are already covered by
tests/models/roberta and tests/models/bert.

Allowlisted as a placeholder: the better fix is upstream, where TRF038 should
skip a source file that defines no classes.

TRF038 now reports no findings.

* Route composite sub-configs through CONFIG_MAPPING for TRF009

mlinter 0.1.5 extended TRF009 from modeling files to every model file, which
surfaced six configs importing a sibling model's config class directly. Each now
resolves it through CONFIG_MAPPING with AutoConfig as the sub_configs marker,
which is what the rule prescribes and what 99 other composite configs already
do:

  cohere_asr               ParakeetEncoderConfig    -> parakeet_encoder
  esmfold2                 EsmcConfig               -> esmc
  gemma4_assistant         Gemma4TextConfig         -> gemma4_text
  gemma4_unified_assistant Gemma4UnifiedTextConfig  -> gemma4_unified_text
  ovis2                    Qwen2Config              -> qwen2
  sam3                     CLIPTextConfig           -> clip_text_model

cohere_asr already used CONFIG_MAPPING for instantiation, so only its
sub_configs value and import changed. Where the imported class also appeared in
a type annotation it is widened to PreTrainedConfig, and esmfold2's _init_nested
helper takes the class from CONFIG_MAPPING since it calls cls() directly.

Verified: default construction and to_dict/from_dict round-trip resolve the same
concrete config class as before for all six, and tests/models for the six models
pass (552 passed, 551 skipped).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Backport mlinter rule-prose compression and allowlist the rest of TRF009

Two changes to utils/rules.toml.

Backports transformers-mlinter d0ac218 ("compress the rule explanation prose",
before: upstream's file as the base, with this repo's licence header and its
allowlist_models entries and their justification comments re-applied on top, so
the only remaining difference from the installed mlinter/rules.toml is the header
and those allowlists. make fix-repo regenerated
docs/source/en/modeling_rules.md.

Allowlists the ten TRF009 findings left after the CONFIG_MAPPING fixes, in three
groups, each with its reasoning recorded in the file:

- Back-compat tokenizer shims (bart, convbert, distilbert, fnet, mobilebert,
  squeezebert): the cross-model import is the module's whole purpose.
- generation_*.py (nemotron3_5_asr, nemotron_asr_streaming): the rule points at
  modular files, but the converter has no generation_*.py support, so the
  prescribed remedy does not exist yet.
- sam3: CLIPTextModelWithProjection is needed for checkpoint key compatibility.
  shieldgemma2: fixable with a modular file, left as follow-up.

make typing and make check-repo now pass.

* Fix vibevoice for mlinter 0.1.5 after rebase

VibeVoice (huggingface#40546) landed on main after this branch forked, so it was never
checked against the 0.1.5 rules and brought in two findings.

TRF058: VibeVoiceDiffusionHeadSinusoidalEmbedding created `freq` via
`self.register_buffer(...)`, which cannot be inherited and tweaked from a
modular file. Assign `nn.Buffer(..., persistent=False)` instead, which also
makes the `freq: torch.Tensor` linting workaround unnecessary. Same
non-persistent buffer and same forward output.

TRF009: allowlist `vibevoice`. generation_vibevoice.py imports a runtime cache
class from its own sub-model package; the family already routes its sub-configs
through CONFIG_MAPPING, and a cache class cannot come from CONFIG_MAPPING. It
also sits in a generation_*.py file, which the modular converter does not
support, the same blocker as the nemotron entries.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Adopt transformers-mlinter main: sync rule prose, drop dead allowlist entries

Re-ran the checker against transformers-mlinter git main (e6533da). Green, and
the bump makes a chunk of this branch's curation obsolete.

Synced the rule prose the repo copy mirrors, which had gone stale because the
described behavior changed upstream:
  TRF029 -- an optional `None`-default parameter is now an override, not a
            second source of truth, so it is exempt
  TRF035 -- F401/F821/F822 are now accepted in modular files
  TRF041 -- guards (an `if` with no `else` that only raises or only logs) and a
            `DEFAULT_EXEMPT_ATTRIBUTES` list of framework plumbing are exempt

Dropped 39 allowlist entries that no longer fire, mostly mlinter#27 (the rule
engine now resolves a class's base chain across models instead of only inside
the linted file), which this branch had flagged upstream rather than worked
around:
  TRF018  16 -> 1   only `radio`, the one real case, is left
  TRF020   1 -> 0   the `axk2` false positive is gone
  TRF034  17 -> 5   the 12 cross-file false positives are gone; the 5 real
                    findings stay as follow-ups
  TRF029   2 -> 1   TRF035  5 -> 0   TRF041  14 -> 9
Each rule's comment block now describes what is left and why. Verified minimal:
emptying any of these six allowlists reproduces exactly the kept entries.

24 further entries (TRF002, TRF004, TRF005, TRF006, TRF009 `maskformer`,
TRF010, TRF019, TRF056) are also dead, but they were already dead before this
bump, so they are left alone here. TRF037's `x_clip` stays: the rule is opt-in,
so it never fires and the entry documents what would.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Regenerate modeling_rules.md for the synced rule prose

Generated from utils/rules.toml by utils/check_modeling_rules_doc.py; picks up
the TRF029, TRF035 and TRF041 prose synced from transformers-mlinter main.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Drop 19 more allowlist entries that no longer fire

Swept every rule the way the earlier mlinter#27 pass was done: emptied each
allowlist, re-ran, and checked what actually fires. 213 entries -> 194.

"Does not fire" has four causes, so each candidate was classified rather than
deleted on the strength of a green run:
  - the model is compliant now
  - the pattern survives only in a generated file, copied from a parent
  - the model is grandfathered by cutoff_date, so it is never checked
  - the rule is opt-in and never runs at all
Only the first two are safe to drop. To separate them, each candidate rule was
run directly against the model's own source with grandfathering off.

Removed as compliant, verified rule-by-rule against the source (14):
  TRF005  9 -> 0   the rule accepts an explicit `_no_split_modules = None`, a
                   modular `AttributeError` sentinel, and raw-string elements
  TRF010  2 -> 0   both configs carry @strict(accept_kwargs=True)
  TRF006  4 -> 3   TRF009 15 -> 14 (maskformer)   TRF056 2 -> 1 (kimi_k25)

Removed as generated-only copies (5):
  TRF004  9 -> 4   hubert, sew, unispeech, unispeech_sat and wavlm define no
                   `tie_weights` in their modular files; the override exists
                   only in their generated modeling_*.py, copied from
                   Wav2Vec2ForCTC. wav2vec2 still fires, so the root cause stays
                   tracked -- listing all five implied five separate bugs.

Kept deliberately, and now documented in the file (5):
  TRF002  lighton_ocr -- `base_model_prefix = ""` is real and authored in its
          own modular file; the rule misses it only because it resolves base
          chains within a single file, so `LightOnOcrModel(Mistral3Model)` is
          not recognized as a pretrained class
  TRF019  blip_2, grounding_dino, omdet_turbo -- grandfathered by cutoff_date;
          they still set `_defaults` and fire once grandfathering is off
  TRF037  x_clip -- the rule is opt-in, so the entry records what would fire

Verified: emptying every allowlist now reproduces exactly the kept entries,
except those 5, each of which has a stated reason.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Adopt the TRF034 trunk scoping from mlinter main

mlinter#65 scoped TRF034 to the model's checkpointable token-mixing trunk. Sync
the rule prose and cut the allowlist from 5 models to 1.

`dinov3_convnext`, `openai` and `xcodec2` never set
`supports_gradient_checkpointing`, so `gradient_checkpointing_enable()` raises
for them rather than skipping a layer and there was nothing to report.
`tipsv2_dpt`'s three entries are DPT head layers, not a token-mixing stack.
`x_clip`'s `PromptGeneratorLayer` stays: an attention stack on a model that does
enable checkpointing, so it is a real finding and a follow-up for that model.

Over this checkout with every cutoff neutralised, TRF034 goes from 103 findings
over 76 models to 20 over 19, and every survivor is a transformer stack in a
checkpointable model.

* Pin transformers-mlinter 0.1.5

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
pull Bot pushed a commit to A-Archives-and-Forks/transformers that referenced this pull request Sep 28, 2026
…ace#48912)

* Restore the Unicode whitespace set in the GPT-SW3 tokenizer

`GPTSw3Tokenizer.whitespaces` is used by `preprocess_text` to map unusual
Unicode spaces to a plain space, a step the docstring describes as identical
to what was used when training the tokenizer.

In 05c0e1d ("rm slow tokenizers", huggingface#40936) nine of the eleven characters in
that set were rewritten to U+0020 and U+0084 became an empty string, so the
set now holds only {U+0020, U+3000, U+FFFC} and seven Unicode spaces are no
longer normalized: U+2002, U+2003, U+2005, U+2008, U+2009, U+200A, U+202F.

With AI-Sweden-Models' released spiece.model, "en text" now tokenizes as
['en', '<0xE2>', '<0x80>', '<0x89>', 'text'] instead of ['en', 'text'].

The characters are restored as explicit escapes so a future source rewrite
cannot silently lose them again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Drop the comment explaining the escapes

Applies the review suggestion.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Core: Tokenization Internals of the library; Tokenization. for_v5?

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants