Skip to content

Trimming a model

What the trimmed tokenizer keeps

trim_tokenizer drives the vocabulary surgery and then round-trips the result through save_pretrained and AutoTokenizer.from_pretrained, so everything outside tokenizer.json survives: the chat template, model_max_length, the special-tokens map and the rest of tokenizer_config.json.

Added tokens keep their id-linked entries in added_tokens_decoder, and their matching flags (single_word, lstrip, rstrip, normalized, special) are preserved, which is what keeps a chat template tokenizing to exactly the ids it did before.

Added tokens are ordinary vocabulary entries here: skeletoken folds them in on load, so there is no separate id space above the base vocab and no Qwen-style vocab_size versus len() trap.

The embedding table and the head

With trim_model: true, trim_model trims the embedding table and the output head if there is one, by selecting the relevant rows of the table. The row order comes from an IdRemap, built from the two vocabularies and matched on token strings, which is what keeps every added token's trained embedding row.

Covered shapes: tied and untied heads, a head bias (a masked-LM head has one even when its weights are tied, and tying does not carry it along), encoder-decoders, and encoders with no head at all. Token ids stored on the config and the generation config follow the remap, including the ones a multimodal checkpoint keeps on config.text_config rather than at the top level.

Note

Multimodal models have not been thoroughly tested. Please open an issue if you run into problems.

resolve_model_class picks the class to load with, reading the checkpoint's own architectures rather than reaching for AutoModel. That matters: AutoModel returns the base model, so a ...ForCausalLM or ...ForMaskedLM checkpoint would lose its head, silently discarding trained weights when the head is untied and demoting config.architectures on save.

Alignment padding

As is relatively well known and common, pad_to_multiple_of rounds the matrix up past the end of the vocabulary to a value that keeps tensor cores efficient. Those extra rows are reachable output: transformers fills them from the mean of the existing embeddings, so their logits compete with the real ones and generate can emit an id past the end of the vocabulary. trimbed zeroes them, and the head rows and bias entries with them.

Sidecar files

sentence-transformers keeps its pooling and dense modules in separate files that save_pretrained knows nothing about. Without them, SentenceTransformer reopens the output with default mean pooling and no error at all. copy_sidecar_files carries those vocabulary-independent files across, matching DEFAULT_SIDECAR_PATTERNS unless sidecar_patterns says otherwise. Nothing the trim itself writes is ever copied.

Verification

A trim that quietly changes how text tokenizes is worse than one that fails, so the pipeline proves its work.

verify_tokenizer re-encodes sampled corpus texts with both tokenizers and reports how many came out identical, how many decode to the same string, and the token-count ratio. A run with no corpus verifies against its keep_texts and chat-template literals instead.

verify_model goes further and runs both models on the same texts, comparing hidden states and logits against verify_tolerance. For an encoder-decoder it primes one decoder step at the model's own decoder_start_token_id, which is also what puts the trimmed output head into the comparison.

Sample texts are whole corpus documents, which are regularly longer than the context the model can take, so both encodings are cut to the same budget: the smaller of the model's position table and the tokenizer's model_max_length. The report's max_length says what that came to, and is null for a model that has no such ceiling (a T5, say). Nothing is cut for the tokenizer comparison, which never runs a forward pass and is happy with documents of any length.

Both return the report models the run serialises: VerificationReport and ModelVerificationReport.