Transformers documentation
Ajouter un modèle avec des transformers modulaires
Ajouter un modèle avec des transformers modulaires
Modular transformers réduit le code nécessaire pour ajouter un modèle en permettant les imports et l'héritage, contrairement à la politique un modèle, un seul fichier. Au lieu de répéter les composants du modèle dans plusieurs fichiers, ajoutez un fichier modulaire à votre dossier de modèle et héritez des classes existantes.
Un convertisseur génère des fichiers autonomes à partir du fichier modulaire. Les utilisateurs obtiennent la même interface monofichier qu’ils connaissent déjà.
Modular transformers n’est pas destiné à remplacer le code de modélisation legacy. Si votre modèle n’est pas basé sur un modèle existant, ajoutez manuellement un fichier
modeling.py. Il en va de même pour les fichiers de configuration, de tokenisation ou de traitement qui ne peuvent pas hériter proprement d’un fichier similaire.Il n’existe pas non plus d’ordre unique à respecter. Certains contributeurs rédigent d’abord le fichier modulaire et le génèrent à partir de celui-ci. D’autres commencent par un
modeling.pyécrit à la main, puis le refactorisent en fichier modulaire par la suite. Les deux approches fonctionnent.
Mise en œuvre d'un fichier modulaire
Commencez par trouver un modèle dans Transformers similaire au vôtre. De bons points de départ sont Mistral, Qwen2, Cohere et Cohere2, ainsi que Llama. Le tableau ci-dessous associe les composants courants aux modèles dont vous pouvez hériter.
| Composant | Modèle |
|---|---|
| Mélange d'experts | Mixtral ou Qwen2-MoE |
| Incrustation rotative entrelacée (et/ou partielle) | GLM, Phi |
| Modèles d'espace d'état | Jamba, Bamba, Zamba, Mamba2 |
| États cachés récurrents | Gemma2 |
| Motifs d'attention par fenêtre glissante / attention complète par couche | Gemma2, Cohere2 |
| Clipping QKV | Olmo |
| Normalisation QK | Olmo2, Cohere |
| QKV fusionné (non recommandé) | Phi3 |
Utilisez l’outil modular-detector-v2 pour trouver des implémentations existantes dont vous pouvez hériter. Collez un extrait de code et il renvoie les méthodes les plus similaires déjà présentes dans Transformers, afin que vous puissiez identifier la meilleure classe parente avant de commencer à écrire.
Ne modifiez pas un modèle existant uniquement pour que l’héritage fonctionne avec votre nouveau modèle. Si le renommage ou la sous-classement d’une classe parente est trop contraignant, copiez directement le code pertinent à la place.
Créez src/transformers/models/<name>/modular_<name>.py, où <name> correspond au nom du répertoire du modèle en snake_case. Cette section vous guide dans l’implémentation de Olmo2 à partir de Olmo avec l’approche modulaire (reportez-vous au fichier modular_olmo2.py d’origine).
Configuration
Il y a deux points où Olmo2Config diffère de OlmoConfig.
- Un nouvel argument,
rms_norm_eps, a été ajouté. - L'argument
clip_qkvn'est plus utilisé.
Déclarez les nouveaux arguments sous forme d’annotations de type au niveau de la classe, avec une valeur par défaut. Pour les arguments supprimés, affectez AttributeError() afin de supprimer l’attribut hérité dans le fichier généré (voir Suppression d’attributs).
- @auto_docstring(checkpoint="allenai/OLMo-7B-hf")
+ @auto_docstring(checkpoint="allenai/Olmo2-7B-1124-hf")
+ @strict
- class OlmoConfig(PreTrainedConfig):
+ class Olmo2Config(OlmoConfig):
...
- model_type = "olmo"
+ model_type = "olmo2"
...
+ rms_norm_eps: float = 1e-5
- clip_qkv: float | None = None
+ clip_qkv = AttributeError()@auto_docstring génère automatiquement la documentation standard des arguments (voir le guide @auto_docstring). @strict rejette les mots-clés inconnus lors de l'instantiation, ce qui permet de détecter précocement les fautes de frappe et les arguments obsolètes. Ajoutez les deux à chaque classe de configuration, car les décorateurs ne sont pas hérités de la classe parente. Déclarez-les explicitement, même si la configuration parente les possède déjà.
Pour définir un attribut dérivé ou gérer la logique de rétrocompatibilité, utilisez __post_init__ plutôt que __init__. Par exemple, Cohere2 calcule head_dim et dérive layer_types lors de l’initialisation.
def __post_init__(self, **kwargs):
if self.num_key_value_heads is None:
self.num_key_value_heads = self.num_attention_heads
self.head_dim = self.hidden_size // self.num_attention_heads
super().__post_init__(**kwargs)Pour les modèles prenant en charge le parallélisme de tenseurs ou de pipeline, définissez base_model_tp_plan et base_model_pp_plan sous forme de dictionnaires au niveau de la classe dans la configuration. Les deux dictionnaires définissent comment fractionner le modèle sur les différents appareils. Consultez les configurations existantes telles que Olmo2 ou Cohere2 pour des exemples.
class MyNewModelConfig(PreTrainedConfig):
model_type = "my_new_model"
# Tensor parallelism: maps layer name patterns to sharding strategies.
# Use "colwise" / "rowwise" for standard sharding, or the "gather_output" /
# "split_input" variants when an extra op (e.g. a QK norm) prevents fusing.
base_model_tp_plan = {
"layers.*.self_attn.q_proj": "colwise",
"layers.*.self_attn.k_proj": "colwise",
"layers.*.self_attn.v_proj": "colwise",
"layers.*.self_attn.o_proj": "rowwise",
"layers.*.mlp.gate_proj": "colwise",
"layers.*.mlp.up_proj": "colwise",
"layers.*.mlp.down_proj": "rowwise",
}
# Pipeline parallelism: maps submodule names to their (input, output) tensor names.
base_model_pp_plan = {
"embed_tokens": (["input_ids"], ["inputs_embeds"]),
"layers": (["hidden_states", "attention_mask"], ["hidden_states"]),
"norm": (["hidden_states"], ["hidden_states"]),
}Norme
Pour copier une classe parente sans modification, héritez avec pass. L’outil de linter copie le contenu de la classe parente et renomme toutes les références pour correspondre au nouveau modèle.
from ..olmo.modeling_olmo import OlmoRotaryEmbedding
class Olmo2RotaryEmbedding(OlmoRotaryEmbedding):
passPour modifier un comportement spécifique, héritez et n'écrasez que ce qui diffère. Olmo2RMSNorm diffère de LlamaRMSNorm sur une seule ligne. La multiplication a lieu avant le retour au type de données d'entrée, et non après.
from ..llama.modeling_llama import LlamaRMSNorm
class Olmo2RMSNorm(LlamaRMSNorm):
def forward(self, hidden_states):
input_dtype = hidden_states.dtype
hidden_states = hidden_states.to(torch.float32)
variance = hidden_states.pow(2).mean(-1, keepdim=True)
hidden_states = hidden_states * torch.rsqrt(variance + self.variance_epsilon)
- return self.weight * hidden_states.to(input_dtype)
+ return (self.weight * hidden_states).to(input_dtype)Attention
L’attention d’Olmo2 est identique à celle d’Olmo, sauf qu’elle applique RMSNorm aux requêtes et aux clés, et supprime le clipping de qkv. super().__init__(...) copie le corps de la classe parente et ajoute les deux nouvelles lignes de normalisation. La méthode forward est entièrement redéfinie car les requêtes et les clés passent désormais par des normalisations avant la projection. L’outil de vérification de code intègre également dans le fichier généré toutes les fonctions importées, y compris apply_rotary_pos_emb, eager_attention_forward et leurs dépendances.
class Olmo2Attention(OlmoAttention):
def __init__(self, config: Olmo2Config, layer_idx: int | None = None):
super().__init__(config, layer_idx=layer_idx)
+ self.q_norm = Olmo2RMSNorm(config.num_attention_heads * self.head_dim, config.rms_norm_eps)
+ self.k_norm = Olmo2RMSNorm(config.num_key_value_heads * self.head_dim, config.rms_norm_eps)
def forward(self, ...):
...
- query_states = self.q_proj(hidden_states)
- key_states = self.k_proj(hidden_states)
+ query_states = self.q_norm(self.q_proj(hidden_states))
+ key_states = self.k_norm(self.k_proj(hidden_states))
value_states = self.v_proj(hidden_states)
- if self.config.clip_qkv is not None:
- query_states.clamp_(min=-self.config.clip_qkv, max=self.config.clip_qkv)
- key_states.clamp_(min=-self.config.clip_qkv, max=self.config.clip_qkv)
- value_states.clamp_(min=-self.config.clip_qkv, max=self.config.clip_qkv)
-
...DecoderLayer
Après super().__init__(...), écrasez les attributs de normalisation avec des instances de Olmo2RMSNorm et réaffectez self.self_attn à la nouvelle classe Olmo2Attention. La ligne del self.input_layernorm supprime l’assignation de input_layernorm de la classe parente, car Olmo2 applique la normalisation après l’attention plutôt qu’avant. Consultez Suppression d’attributs pour plus de détails sur ce que del supprime et ce qu’il ne supprime pas.
Le forward est réécrit pour refléter le placement de la normalisation post-attention. Une réécriture du forward n'est nécessaire que lorsqu'un attribut est renommé, et non lorsque seul son type change.
class Olmo2DecoderLayer(OlmoDecoderLayer):
def __init__(self, config: Olmo2Config, layer_idx: int):
super().__init__(config, layer_idx=layer_idx)
- self.self_attn = OlmoAttention(config=config, layer_idx=layer_idx)
- self.input_layernorm = OlmoLayerNorm(config.hidden_size)
- self.post_attention_layernorm = OlmoLayerNorm(config.hidden_size)
+ self.self_attn = Olmo2Attention(config=config, layer_idx=layer_idx)
+ del self.input_layernorm
+ self.post_attention_layernorm = Olmo2RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
+ self.post_feedforward_layernorm = Olmo2RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
def forward(self, ...):
residual = hidden_states
- hidden_states = self.input_layernorm(hidden_states)
# Self Attention
hidden_states, _ = self.self_attn(...)
- hidden_states = residual + hidden_states
+ hidden_states = self.post_attention_layernorm(hidden_states)
+ hidden_states = residual + hidden_states
# Fully Connected
residual = hidden_states
- hidden_states = self.post_attention_layernorm(hidden_states)
hidden_states = self.mlp(hidden_states)
- hidden_states = residual + hidden_states
+ hidden_states = self.post_feedforward_layernorm(hidden_states)
+ hidden_states = residual + hidden_states
return hidden_statesModèle
Seul le type de self.norm change ici. La méthode forward est identique à celle de la classe parente, le linter la reprend donc automatiquement.
class Olmo2Model(OlmoModel):
def __init__(self, config: Olmo2Config):
super().__init__(config)
- self.layers = nn.ModuleList(
- [OlmoDecoderLayer(config, layer_idx) for layer_idx in range(config.num_hidden_layers)]
- )
- self.norm = OlmoLayerNorm(config.hidden_size)
+ self.norm = Olmo2RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
+ self.layers = nn.ModuleList(
+ [Olmo2DecoderLayer(config, layer_idx) for layer_idx in range(config.num_hidden_layers)]
+ )Tête de modèle
La logique est identique à celle de OlmoForCausalLM, aucune modification n'est donc nécessaire.
from ..olmo.modeling_olmo import OlmoForCausalLM
class Olmo2ForCausalLM(OlmoForCausalLM):
passAutres classes
Le fichier modeling_olmo2.py généré par le linter contient également des classes (Olmo2MLP, Olmo2RotaryEmbedding, Olmo2PreTrainedModel) qui n’étaient pas explicitement définies dans modular_olmo2.py.
Le linter importe toute classe dont une classe héritée dépend, à moins que vous ne la redéfinissiez explicitement. Les fonctions importées comme apply_rotary_pos_emb suivent la même règle.
Par exemple, OlmoDecoderLayer possède self.mlp = OlmoMLP(config). Olmo2MLP n’étant jamais défini dans le fichier modulaire, le linter le crée automatiquement, ce qui équivaut à utiliser pass.
from ..olmo.modeling_olmo import OlmoMLP
class Olmo2MLP(OlmoMLP):
passSi vous souhaitez que Olmo2MLP hérite d'un autre modèle, soyez explicite.
# switch to Mistral definition
from ..mistral.modeling_mistral import MistralMLP
class Olmo2MLP(MistralMLP):
passFinaliser le fichier
Chaque fichier modulaire doit déclarer un logger et une liste __all__ au niveau du module.
logger = logging.get_logger(__name__)
__all__ = [
"Olmo2Config",
"Olmo2ForCausalLM",
"Olmo2Model",
"Olmo2PreTrainedModel",
]__all__ doit lister chaque classe publique du fichier. Le convertisseur et les imports en aval en dépendent. Une classe absente de __all__ ne sera pas exportée correctement.
Générer les fichiers de modélisation
Le script modular_model_converter.py génère des fichiers autonomes modeling.py, configuration.py et autres à partir de votre fichier modulaire. Pour chaque classe héritée, il copie le corps de la classe parente dans la classe enfant, renomme toutes les références pour correspondre au nouveau modèle et intègre les fonctions ou classes auxiliaires dont ces classes parentes dépendent.
Les fichiers de sortie ne contiennent ni importation inter-modèles ni héritage depuis d’autres répertoires de modèles. L’outil de vérification aplatit l’héritage sur un seul niveau. Si Olmo2Attention hérite de OlmoAttention, la classe Olmo2Attention générée est entièrement autonome. En revanche, si OlmoAttention héritait elle-même d’une autre classe, l’outil de vérification n’intègre pas ce grand-parent.
Exécutez la commande ci-dessous pour générer des fichiers à partir d'un fichier modulaire.
python utils/modular_model_converter.py your_model
Ne modifiez jamais directement les fichiers générés, car toute modification sera écrasée lors de l'exécution suivante.
Modèles pour les fichiers modulaires
Les sections ci-dessous documentent les schémas d’utilisation courants, tels que la suppression d’attributs ou la réécriture de méthodes décorées, lors du travail avec un fichier modulaire.
Suppression des attributs
La suppression d'un attribut hérité dépend de savoir si vous travaillez avec une classe de configuration ou un sous-ensemble de nn.Module.
Pour une classe de configuration, affectez AttributeError() à l'attribut au niveau de la classe.
class MyNewConfig(ParentConfig):
removed_attr = AttributeError()Le linter supprime entièrement la déclaration de l’attribut du fichier de configuration généré. Les classes de configuration utilisent une structure de type dataclass sans __init__, de sorte qu’assigner AttributeError() au niveau de la classe est la bonne approche.
Pour une sous-classe de nn.Module, utilisez del self.attribute après super().__init__(...).
class MyNewModel(ParentModel):
def __init__(self, config: MyNewConfig):
super().__init__(config)
del self.attributedel self.attribute supprime uniquement la ligne d’assignation self.attribute = ... du corps du parent copié. Elle ne supprime pas les autres lignes qui référencent self.attribute. Si la méthode forward du parent ou d’autres méthodes référencent également cet attribut, il faut surcharger ces méthodes aussi.
class DummyModel(nn.Module):
def __init__(self, config: DummyConfig):
super().__init__()
self.attribute = config.attribute
if self.attribute:
# do more stuff with `self.attribute` here
...
class MyNewDummyModel(DummyModel):
def __init__(self, config: MyNewDummyConfig):
super().__init__(config)
del self.attribute
# 'self.attribute = config.attribute' is removed, but the 'if self.attribute:' block remains.
# Override forward() or any other method that references self.attribute.Travailler avec super()
super().__init__(config) indique au convertisseur de copier le corps de la classe parente dans la classe enfant. Deux modèles permettent de remplacer ce comportement.
- Appelez directement une classe parente spécifique lorsque vous avez besoin que la sortie générée appelle un grand-parent (
nn.Module.__init__) plutôt que la classe parente modulaire. - Utilisez
**super_kwargspour hériter de la signature complète d’une méthode parente tout en ajoutant une docstring personnalisée ou en remplaçant un décorateur.
Appeler directement une classe grand-mère
Soyez explicite sur la classe que vous appelez lorsque vous avez besoin de super() pour cibler le parent de la classe générée plutôt que le parent modulaire. L’exemple ci-dessous appelle directement nn.Module.__init__(self). DummyModule est lui-même un nn.Module, de sorte que le convertisseur l’écrit sous la forme super().__init__() dans la classe générée MyNewDummyModule.
class MyNewDummyModule(DummyModule): | class MyNewDummyModule(nn.Module):
|
def __init__(self): | def __init__(self):
nn.Module.__init__(self) | super().__init__()
self.foo = config.foo | self.foo = config.foo
... | ...super_kwargs
Utilisez **super_kwargs pour hériter de la signature complète d’une méthode parente tout en ajoutant une docstring personnalisée ou en remplaçant un décorateur. Dans la signature redéfinie, il indique au linter de déployer tous les arguments de la méthode parente dans la sortie générée.
L'utilisation la plus courante consiste à ajouter une docstring spécifique au modèle, par exemple pour documenter l'argument labels, sans réécrire la signature complète.
# modular_gemma.py
class GemmaForCausalLM(LlamaForCausalLM):
def forward(**super_kwargs):
r"""
Example:
```python
>>> from transformers import AutoTokenizer, GemmaForCausalLM
>>> model = GemmaForCausalLM.from_pretrained("google/gemma-7b")
...
```"""
return super().forward(**super_kwargs)La méthode GemmaForCausalLM.forward générée possède la signature complète de LlamaForCausalLM, sans copie manuelle nécessaire.
**super_kwargs est un raccourci destiné aux cas de niche. Si vous modifiez le comportement, écrivez plutôt la signature complète.
Suppression des méthodes inutilisées
Supprimez une méthode parente en la surchargeant avec une instruction raise AttributeError(""). L'outil de linting supprime la méthode du fichier généré.
class GemmaTokenizer(LlamaTokenizer):
...
def get_spm_processor(self):
raise AttributeError("Not needed for Gemma")
def unk_token_length(self):
raise AttributeError("Not needed for Gemma")Écraser des méthodes décorées
Lorsque vous écrasez une méthode parente décorée, le décorateur de la méthode parente est automatiquement repris. Si vous ajoutez votre propre décorateur, il remplace celui de la méthode parente.
Deux décorateurs apparaissent dans toute la bibliothèque, l'un pour capturer les sorties intermédiaires du modèle et l'autre pour générer automatiquement les docstrings.
Dans l’exemple ci-dessous, une sous-classe redéfinit une méthode parente décorée sans ajouter son propre décorateur. Le décorateur de la classe parente est conservé.
class NewModel(DummyModel): | class NewModel(nn.Module):
... | ...
|
def forward(...): | @decorator(...)
... | def forward(...):
| ...Si vous ajoutez un nouveau décorateur, votre décorateur remplace celui du parent.
class NewModel(DummyModel): | class NewModel(nn.Module):
... | ...
|
@my_new_decorator(...) | @my_new_decorator(...)
def forward(...): | def forward(...):
... | ...Nommage spécial
Le linter renomme automatiquement tout lors de l'héritage d'une classe. Utilisez le même préfixe de nom de classe pour toutes les classes d'un même fichier.
Évitez de mélanger des préfixes comme dans l’exemple ci-dessous. MyModelIncredibleMLP viole les conventions de nommage, et le linter ne saura pas s’il doit utiliser MyModelIncredible ou MyModel lors du renommage des dépendances de plus haut niveau.
class MyModelIncredibleMLP(LlamaMLP):
...
class MyModelDecoderLayer(LlamaDecoderLayer):
...En l'absence de dépendances implicites, vous pouvez renommer une seule classe localement. Redéfinissez explicitement chaque autre mention de cette classe avec le nouveau motif de nom. Sinon, le linter ajoute une classe MyModelMLP indésirable à côté de MyModelIncredibleMLP.
Le linter émet un avertissement lorsqu'il détecte un préfixe ambigu.
We detected multiple prefix names when inheriting from transformers.models.llama.modeling_llama: ('Emu3Text', 'Emu3'). We will only use the most used 'Emu3' prefix when grabbing args and dependencies. Make sure to subclass the intermediate classes with the prefix you want (if different from 'Emu3') or use a single prefix in all the modular (best).Les préfixes ambigus sont les plus fréquents dans les modèles multimodaux, où les noms de classes incluent un qualificatif de modalité tel que Text. Pour attribuer un préfixe spécifique à une dépendance, renommez-la explicitement à l'aide d'un pass.
class Emu3TextMLP(LlamaMLP):
passDocstrings de configuration
Le linter ne prend pas encore en charge l’héritage partiel des docstrings. Lors de l’ajout ou de la suppression d’attributs de configuration, ajoutez la docstring complète directement dans le fichier modulaire, sous la définition de la classe.
Conversion de points de contrôle
Une fois que vous avez généré vos fichiers de modélisation, vérifiez que les poids réels se chargent correctement. Rédigez un script de conversion pour traduire le format de point de contrôle amont en un format compatible avec Transformers, puis enregistrez-le sur le Hub.
Écrire un script de conversion
Ajoutez un fichier convert_<model>_to_hf.py dans src/transformers/models/<model>/. Le script charge les poids en amont, renomme et redimensionne les clés pour qu’elles correspondent aux noms des paramètres de votre module, puis enregistre le résultat avec save_pretrained().
Cherchez un script existant à copier et à adapter. Les modèles situés dans
src/transformers/models/incluent unconvert_*_to_hf.pyque vous pouvez utiliser comme point de départ.
Après avoir exécuté le script, chargez le point de contrôle enregistré avec from_pretrained() et vérifiez que chaque poids attendu a bien été chargé correctement. Les clés du point de contrôle non utilisées indiquent des noms non correspondants ; affichez-les donc pour détecter les problèmes rapidement.
model = YourModelForTask.from_pretrained("path/to/output/")Vérifiez que les formes et les noms correspondent lors de l'itération sur les clés. Les incohérences de forme indiquent généralement qu'un paramètre de votre configuration est incorrect, que l'architecture diffère de l'originale, ou qu'un poids doit être transposé.
for key, tensor in original_state_dict.items():
hf_tensor = hf_model.state_dict().get(mapped_key)
assert hf_tensor.shape == tensor.shape, (
f"Shape mismatch for {key}: expected {tensor.shape}, got {hf_tensor.shape}"
)Corrigez les problèmes en itérant entre votre fichier modulaire, le fichier de modélisation généré et le script de conversion, jusqu’à ce que tous les poids se chargent correctement.
Une fois que le point de contrôle est chargé sans erreur, poussez-le sur le Hub à l'aide de push_to_hub(). Consultez le guide sur le partage de modèles pour plus de détails.
model.push_to_hub("username/your-model-name")Cartographie de conversion au moment de l'exécution
Ajoutez une correspondance d’exécution dans src/transformers/conversion_mapping.py lorsque les poids publiés ne correspondent pas à la disposition des paramètres de votre module. Les cas courants incluent les poids fusionnés stockés séparément et les tenseurs d’experts MoE qui nécessitent un empilement. Cette correspondance permet à from_pretrained() de charger le point de contrôle du Hub sans étape d’exportation distincte.
Reportez-vous au guide sur le chargement dynamique des poids pour savoir comment écrire des règles WeightRenaming et WeightConverter et les enregistrer pour votre model_type.
Étapes suivantes
- Les règles de structure des modèles sont des règles statiques appliquées à tous les fichiers
modeling_*.py,modular_*.pyetconfiguration_*.py. Exécutezmake typingpour les vérifier avant d'ouvrir une PR. - Ajouter des composants de traitement visuel détaille l’ajout d’un processeur d’images, d’un processeur de vidéos et d’un processeur pour un modèle multimodal.
- Génération automatique des docstrings montre comment utiliser
@auto_docstringafin de ne pas avoir à rédiger manuellement la documentation des arguments pour les API de modèles partagées. - Rédiger des tests de modèle explique comment rédiger des tests d'intégration pour votre nouveau modèle et l'exécuter localement.
- Contrôles des pull requests explique les contrôles CI que votre PR doit passer avant de pouvoir être fusionnée, et comment les reproduire et les corriger localement.