Transformers documentation
Exportateurs
Exportateurs
Exportez n'importe quel PreTrainedModel vers ONNX, ExecuTorch ou un programme PyTorch autonome, quel que soit le runtime cible.
exporter = DynamoExporter() # or OnnxExporter, ExecutorchExporter
config = DynamoConfig(dynamic=True)
exported = exporter.export(model, inputs, config=config)Les exportateurs sont intégrés à Transformers plutôt qu’à une bibliothèque en aval, de sorte que les modifications d’architecture, les nouveaux motifs d’attention et les types de cache personnalisés sont pris en charge lors de l’exportation dès qu’ils sont intégrés au code de modélisation.
Les exportateurs sont expérimentaux. De nombreux correctifs de ce module contournent des bogues spécifiques en amont (Torch, ONNX Script, ONNX Runtime, ExecuTorch) et seront supprimés dès que le correctif sera intégré en amont. Jusqu’à ce que l’API se stabilise, considérez les correctifs comme liés aux versions utilisées dans la suite de tests. Fixez ces versions dans les outils de production, et attendez-vous à ce que de nouveaux correctifs apparaissent et que d’anciens disparaissent au fur et à mesure que les modifications en amont sont intégrées.
| Exporter | Sortie | Runtime |
|---|---|---|
DynamoExporter | ExportedProgram | Tout runtime PyTorch, compilation AOT |
OnnxExporter | ONNXProgram | Tout runtime ONNX (ORT, TensorRT, OpenVINO) |
ExecutorchExporter | ExecutorchProgramManager | Appareils mobiles et périphériques (ExecuTorch) |
OpenVINOExporter | openvino.Model | Runtime OpenVINO (CPU/GPU/NPU Intel) |
AutoHfExporter sélectionne l’exporteur approprié à partir d’une configuration, et AutoExportConfig sélectionne la classe de configuration appropriée à partir d’un dictionnaire. Les deux suivent le même motif de classe automatique que dans Transformers, ce qui est utile lorsque le backend est sélectionné à l’exécution plutôt qu’encodé en dur au point d’appel.
from transformers.exporters import AutoExportConfig, AutoHfExporter
export_config_dict = {"export_format": "onnx", "dynamic": True}
config = AutoExportConfig.from_dict(export_config_dict)
exporter = AutoHfExporter.from_config(config)
onnx_program = exporter.export(model, inputs, config=config)Installation
Installez les dépendances pour le backend vers lequel vous prévoyez d'exporter.
Les versions ci-dessous sont celles auxquelles la suite de tests de l’exportateur est verrouillée. Des versions plus récentes ou plus anciennes fonctionnent souvent, mais les correctifs de l’exportateur ciblent une surface d’API spécifique ; pour les outils de production, verrouillez ces versions et attendez-vous à ce que
HfExporterjournalise un avertissement lorsqu’il détecte un écart.
pip install transformers "torch==2.12.0"pip install transformers "torch==2.12.0" "onnx==1.21.0" "onnxscript==0.7.0" onnxruntimepip install transformers "torch==2.12.0" "executorch==1.3.1"Nécessite macOS 14 ou ultérieur sur Apple Silicon. MLX est inclus dans les roues nocturnes ExecuTorch pour macOS ARM64 ; il ne s'agit pas d'un package ou d'un extra distinct. La paire nocturne suivante a été validée avec les tests de l'exportateur MLX. Installez-la depuis le registre nocturne standard d'ExecuTorch :
pip install transformers
pip install \
"executorch==1.6.0.dev20260924" \
"torch==2.15.0.dev20260924" \
--extra-index-url https://download.pytorch.org/whl/nightly/cpuInstallez torch explicitement, car les paquets ExecuTorch de version nocturne ne le déclarent pas comme dépendance.
Pour les compilations à partir du code source, consultez le guide d’installation MLX.
pip install transformers "torch==2.12.0" "openvino==2026.3.1"Exporter un modèle
Tous les exportateurs partagent la même interface. Créez un exportateur avec une configuration, puis appelez export().
Basculez entre les environnements d’exécution en changeant la classe d’exportation.
from transformers import AutoModelForCausalLM, AutoTokenizer
from transformers.exporters import DynamoExporter, DynamoConfig
model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen3-0.6B")
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-0.6B")
inputs = tokenizer("Hello, world!", return_tensors="pt")
exporter = DynamoExporter()
config = DynamoConfig(dynamic=True)
exported = exporter.export(model, inputs, config=config)
# run the exported graph directly
outputs = exported.module()(**inputs)from transformers import AutoModelForCausalLM, AutoTokenizer
from transformers.exporters import OnnxExporter, OnnxConfig
model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen3-0.6B")
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-0.6B")
inputs = tokenizer("Hello, world!", return_tensors="pt")
exporter = OnnxExporter()
config = OnnxConfig(dynamic=True)
onnx_program = exporter.export(model, inputs, config=config)
# save and load with ONNX Runtime
onnx_program.save("model.onnx")
import onnxruntime as ort
session = ort.InferenceSession("model.onnx")
ort_inputs = {k: v.numpy() for k, v in inputs.items()}
outputs = session.run(None, ort_inputs)backend sélectionne la cible : xnnpack (par défaut) pour le CPU, mlx pour le GPU Apple Silicon, ou cuda pour un GPU compatible CUDA. Choisissez l'option d'installation correspondante. L'exécution MLX nécessite le délégué MLX et ses bibliothèques Metal ;
demander CUDA sans environnement compatible CUDA provoque une RuntimeError.
from transformers import AutoModelForCausalLM, AutoTokenizer
from transformers.exporters import ExecutorchExporter, ExecutorchConfig
model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen3-0.6B")
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-0.6B")
inputs = tokenizer("Hello, world!", return_tensors="pt")
exporter = ExecutorchExporter()
config = ExecutorchConfig(backend="xnnpack", dynamic=True) # Use "mlx" on Apple Silicon.
et_program = exporter.export(model, inputs, config=config)
# save for on-device deployment
et_program.save("model.pte")
# load and run via the ExecuTorch Python runtime
from executorch.runtime import Runtime
program = Runtime.get().load_program("model.pte")
method = program.load_method("forward")
outputs = method.execute(list(inputs.values()))from transformers import AutoModelForCausalLM, AutoTokenizer
from transformers.exporters import OpenVINOExporter, OpenVINOConfig
model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen3-0.6B")
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-0.6B")
inputs = tokenizer("Hello, world!", return_tensors="pt")
exporter = OpenVINOExporter()
config = OpenVINOConfig(dynamic=True)
ov_model = exporter.export(model, inputs, config=config)
ov_model.save("model.xml")
# compile and run on CPU (or "GPU" / "NPU" if available)
import openvino as ov
compiled = ov.Core().compile_model(ov_model, "CPU")
ov_inputs = {k: v.numpy() for k, v in inputs.items()}
outputs = compiled(ov_inputs)Formes dynamiques
Passer dynamic=True marque chaque dimension de tenseur comme dynamique, de sorte que le graphe exporté accepte des entrées de n’importe quelle taille à l’exécution, sans retracage.
Pour un contrôle fin des dimensions dynamiques, passez plutôt des dynamic_shapes explicites, qui sont transmis directement à torch.export.export.
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from transformers.exporters import DynamoExporter, DynamoConfig
model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen3-0.6B")
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-0.6B")
inputs = tokenizer(["Hello, world!", "Hi"], padding=True, return_tensors="pt")
batch = torch.export.Dim("batch", min=1, max=32)
seq = torch.export.Dim("seq", min=1, max=2048)
exporter = DynamoExporter()
config = DynamoConfig(
dynamic_shapes={"input_ids": {0: batch, 1: seq}, "attention_mask": {0: batch, 1: seq}},
# Emit data-dependent shape guards as runtime asserts instead of failing the export when a
# guard wouldn't hold across the explicit symbolic range. Most LLMs need this under fine-grained
# ``Dim(min=, max=)`` bounds. Not needed with ``dynamic=True`` / ``Dim.AUTO``, where torch.export
# infers shape relations instead of verifying them against user-stated bounds.
prefer_deferred_runtime_asserts_over_guards=True,
)
exported = exporter.export(model, inputs, config=config)import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from transformers.exporters import OnnxExporter, OnnxConfig
model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen3-0.6B")
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-0.6B")
inputs = tokenizer(["Hello, world!", "Hi"], padding=True, return_tensors="pt")
batch = torch.export.Dim("batch", min=1, max=32)
seq = torch.export.Dim("seq", min=1, max=2048)
exporter = OnnxExporter()
config = OnnxConfig(
dynamic_shapes={"input_ids": {0: batch, 1: seq}, "attention_mask": {0: batch, 1: seq}},
# Emit data-dependent shape guards as runtime asserts instead of failing the export when a
# guard wouldn't hold across the explicit symbolic range. Most LLMs need this under fine-grained
# ``Dim(min=, max=)`` bounds. Not needed with ``dynamic=True`` / ``Dim.AUTO``, where torch.export
# infers shape relations instead of verifying them against user-stated bounds.
prefer_deferred_runtime_asserts_over_guards=True,
)
onnx_program = exporter.export(model, inputs, config=config)import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from transformers.exporters import ExecutorchExporter, ExecutorchConfig
model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen3-0.6B")
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-0.6B")
inputs = tokenizer(["Hello, world!", "Hi"], padding=True, return_tensors="pt")
batch = torch.export.Dim("batch", min=1, max=32)
seq = torch.export.Dim("seq", min=1, max=2048)
exporter = ExecutorchExporter()
config = ExecutorchConfig(
backend="xnnpack", # Use "mlx" on Apple Silicon.
dynamic_shapes={"input_ids": {0: batch, 1: seq}, "attention_mask": {0: batch, 1: seq}},
# Emit data-dependent shape guards as runtime asserts instead of failing the export when a
# guard wouldn't hold across the explicit symbolic range. Most LLMs need this under fine-grained
# ``Dim(min=, max=)`` bounds. Not needed with ``dynamic=True`` / ``Dim.AUTO``, where torch.export
# infers shape relations instead of verifying them against user-stated bounds.
prefer_deferred_runtime_asserts_over_guards=True,
)
et_program = exporter.export(model, inputs, config=config)import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from transformers.exporters import OpenVINOExporter, OpenVINOConfig
model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen3-0.6B")
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-0.6B")
inputs = tokenizer(["Hello, world!", "Hi"], padding=True, return_tensors="pt")
batch = torch.export.Dim("batch", min=1, max=32)
seq = torch.export.Dim("seq", min=1, max=2048)
exporter = OpenVINOExporter()
config = OpenVINOConfig(
dynamic_shapes={"input_ids": {0: batch, 1: seq}, "attention_mask": {0: batch, 1: seq}},
# Emit data-dependent shape guards as runtime asserts instead of failing the export when a
# guard wouldn't hold across the explicit symbolic range — most LLMs need this under fine-grained
# ``Dim(min=, max=)`` bounds. Not needed with ``dynamic=True`` / ``Dim.AUTO``, where torch.export
# infers shape relations instead of verifying them against user-stated bounds.
prefer_deferred_runtime_asserts_over_guards=True,
)
ov_model = exporter.export(model, inputs, config=config)Modèles génératifs
Pour la génération autorégressive, la forme de la méthode forward du modèle diffère entre l'étape de préremplissage (prompt complet, sans cache KV) et l'étape de décodage (jeton unique, cache KV rempli). Les exportateurs exposent ~HfExporter.export_for_generation, qui sépare les deux étapes et exporte chacune d'elles.
Pour les modèles génératifs multi-modaux, le préremplissage se divise en outre en un encodeur d’image ou d’audio, le modèle de langage et lm_head. La découverte de l’encodeur et du modèle de langage utilise les accesseurs get_encoder() (modality="image" ou "audio") et get_decoder(), de sorte que toute nouvelle architecture utilisant ces composants fonctionne immédiatement.
Un composant de projecteur n’apparaît que lorsque le modèle en expose un sous un nom d’attribut (multi_modal_projector, connector, embed_vision, embed_audio). Qwen2-VL ci-dessous intègre son projecteur dans la tour de vision, de sorte que son dictionnaire de composants ne contient pas de clé multi_modal_projector distincte. Les nouvelles architectures doivent aligner l’attribut de leur projecteur sur l’un de ces noms plutôt que d’allonger la liste.
from transformers import AutoModelForImageTextToText, AutoProcessor
from transformers.exporters import DynamoExporter, DynamoConfig
model = AutoModelForImageTextToText.from_pretrained("Qwen/Qwen2-VL-2B-Instruct")
processor = AutoProcessor.from_pretrained("Qwen/Qwen2-VL-2B-Instruct")
messages = [{"role": "user", "content": [{"type": "image", "url": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/pipeline-cat-chonk.jpeg"}, {"type": "text", "text": "Describe this image."}]}]
text = processor.apply_chat_template(messages, add_generation_prompt=True, tokenize=False)
inputs = processor(text=text, images=messages[0]["content"][0]["url"], return_tensors="pt").to(model.device)
exporter = DynamoExporter()
config = DynamoConfig(dynamic=True)
components = exporter.export_for_generation(model, inputs, config=config)
# components = {"image_encoder": ExportedProgram, "language_model": ExportedProgram, "lm_head": ExportedProgram, "decode": ExportedProgram}from transformers import AutoModelForImageTextToText, AutoProcessor
from transformers.exporters import OnnxExporter, OnnxConfig
model = AutoModelForImageTextToText.from_pretrained("Qwen/Qwen2-VL-2B-Instruct")
processor = AutoProcessor.from_pretrained("Qwen/Qwen2-VL-2B-Instruct")
messages = [{"role": "user", "content": [{"type": "image", "url": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/pipeline-cat-chonk.jpeg"}, {"type": "text", "text": "Describe this image."}]}]
text = processor.apply_chat_template(messages, add_generation_prompt=True, tokenize=False)
inputs = processor(text=text, images=messages[0]["content"][0]["url"], return_tensors="pt").to(model.device)
exporter = OnnxExporter()
config = OnnxConfig(dynamic=True)
components = exporter.export_for_generation(model, inputs, config=config)
# components = {"image_encoder": ONNXProgram, "language_model": ONNXProgram, "lm_head": ONNXProgram, "decode": ONNXProgram}from transformers import AutoModelForImageTextToText, AutoProcessor
from transformers.exporters import ExecutorchExporter, ExecutorchConfig
model = AutoModelForImageTextToText.from_pretrained("Qwen/Qwen2-VL-2B-Instruct")
processor = AutoProcessor.from_pretrained("Qwen/Qwen2-VL-2B-Instruct")
messages = [{"role": "user", "content": [{"type": "image", "url": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/pipeline-cat-chonk.jpeg"}, {"type": "text", "text": "Describe this image."}]}]
text = processor.apply_chat_template(messages, add_generation_prompt=True, tokenize=False)
inputs = processor(text=text, images=messages[0]["content"][0]["url"], return_tensors="pt").to(model.device)
exporter = ExecutorchExporter()
config = ExecutorchConfig(backend="xnnpack", dynamic=True) # Use "mlx" on Apple Silicon.
components = exporter.export_for_generation(model, inputs, config=config)
# components = {"image_encoder": ExecutorchProgramManager, "language_model": ..., "lm_head": ..., "decode": ...}from transformers import AutoModelForImageTextToText, AutoProcessor
from transformers.exporters import OpenVINOExporter, OpenVINOConfig
model = AutoModelForImageTextToText.from_pretrained("Qwen/Qwen2-VL-2B-Instruct")
processor = AutoProcessor.from_pretrained("Qwen/Qwen2-VL-2B-Instruct")
messages = [{"role": "user", "content": [{"type": "image", "url": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/pipeline-cat-chonk.jpeg"}, {"type": "text", "text": "Describe this image."}]}]
text = processor.apply_chat_template(messages, add_generation_prompt=True, tokenize=False)
inputs = processor(text=text, images=messages[0]["content"][0]["url"], return_tensors="pt").to(model.device)
exporter = OpenVINOExporter()
config = OpenVINOConfig(dynamic=True)
components = exporter.export_for_generation(model, inputs, config=config)
# components = {"image_encoder": openvino.Model, "language_model": openvino.Model, "lm_head": openvino.Model, "decode": openvino.Model}Les composants exportés sont des graphes indépendants, et non un pipeline d'inférence prêt à l'emploi. L'appelant est responsable de l'exécution de chaque encodeur, de la projection des embeddings et de l'orchestration de la boucle de génération.
Comment fonctionne export_for_generation
decompose_for_generation() exécute model.generate(**inputs, max_new_tokens=2) une fois et installe des hooks sur model.forward pour capturer les kwargs réels de préfill et de décodage (ainsi que les kwargs par sous-module via des hooks sur chaque encodeur/projecteur/modèle de langage si le modèle est multimodal). C’est pourquoi cela fonctionne pour n’importe quelle architecture, y compris les modèles uniquement décodeur, SSM, encodeur-décodeur et multimodaux, sans code de liaison spécifique à chaque modèle. export_for_generation est un simple appel à cette fonction.
La capture exécute le modèle de manière immédiate sur inputs. Il est donc recommandé de transmettre des valeurs petites mais représentatives, telles qu’un court prompt, une seule petite image ou quelques trames audio. Le programme exporté n’est pas lié à ces tailles (les formes dynamiques continuent de circuler), mais des entrées de capture plus petites rendent decompose_for_generation moins coûteux et maintiennent l’inférence des formes symboliques gérable.
Appel direct de decompose_for_generation pour intervenir entre la décomposition et l’export, par exemple pour exécuter un passage avant en mode eager à des fins de vérification, remplacer les entrées d’un sous-module ou ignorer une étape.
from transformers.exporters.utils import decompose_for_generation
components = decompose_for_generation(model, inputs)
# {"image_encoder": (submodel, fwd_kwargs), "language_model": (...), ..., "decode": (...)}
exported = {}
for name, (submodel, subinputs) in components.items():
eager_outputs = submodel(**subinputs) # sanity-check the eager forward before exporting
exported[name] = exporter.export(submodel, subinputs, config=config)Décodage multi-jetons
Par défaut, le composant decode est une étape mono-jeton — un jeton de requête face au cache KV —
de sorte que torch.export spécialise son axe de séquence de requête à 1. Passez multi_token_decode=True pour capturer decode comme un décodage multi-jetons à la place : decompose_for_generation() fusionne deux
étapes de décodage consécutives (il capture avec max_new_tokens=3) en un seul forward, de sorte que cet axe reste
symbolique. Un seul graphe sert alors toutes les longueurs de requête — un jeton (décodage ordinaire), plusieurs jetons
à la fois (continuation à partir du passé, par exemple l’acceptation d’un bloc de jetons spéculatifs), et un préremplissage
simple lorsque le cache est vide.
from transformers.exporters import DynamoExporter, DynamoConfig
exporter = DynamoExporter()
config = DynamoConfig(dynamic=True)
components = exporter.export_for_generation(model, inputs, config=config, multi_token_decode=True)
# components["decode"] now accepts a variable number of query tokensfrom transformers.exporters import OnnxExporter, OnnxConfig
exporter = OnnxExporter()
config = OnnxConfig(dynamic=True)
components = exporter.export_for_generation(model, inputs, config=config, multi_token_decode=True)
# components["decode"] now accepts a variable number of query tokensfrom transformers.exporters import ExecutorchExporter, ExecutorchConfig
exporter = ExecutorchExporter()
config = ExecutorchConfig(backend="xnnpack", dynamic=True) # Use "mlx" on Apple Silicon.
components = exporter.export_for_generation(model, inputs, config=config, multi_token_decode=True)
# components["decode"] now accepts a variable number of query tokensL’axe de requête ne reste symbolique que lors d’un export à forme dynamique (dynamic=True) ; un export statique le fige à la longueur capturée, produisant un graphe multi-jetons fixe. Il se combine avec le cache KV statique ci-dessous — le décodage fusionné écrit les jetons de chaque étape dans le cache de taille fixe sur place, et le cache gère en interne leur emplacement.
Cache KV statique
Les exemples ExecuTorch de cette section, y compris les mises à jour sans copie et la boucle de décodage, sont exclusifs à XNNPACK. L’exportateur MLX rejette
StaticCache; utilisezDynamicCacheavec MLX.
generate() fait croître un DynamicCache par défaut, en réallouant à mesure que la séquence s’allonge — une cible mouvante pour un graphe exporté. Un cache statique est un tampon de taille fixe, alloué une seule fois et écrit sur place à la position courante à chaque étape. Combiné à un décodage multi-jetons, il réduit la génération à un seul graphe exporté : le graphe decode prend un cache de taille fixe et un nombre variable de jetons de requête, de sorte qu’un seul graphe sert à la fois le prompt (cache vide → préremplissage) et chaque jeton généré (cache rempli → décodage). Exportez-le en transmettant un GenerationConfig avec cache_implementation="static" (et un max_cache_len) aux côtés de multi_token_decode=True :
from transformers import GenerationConfig
from transformers.exporters import DynamoExporter, DynamoConfig
exporter = DynamoExporter()
gen_config = GenerationConfig(cache_implementation="static", max_cache_len=2048)
components = exporter.export_for_generation(
model, inputs, config=DynamoConfig(dynamic=True), generation_config=gen_config, multi_token_decode=True
)from transformers import GenerationConfig
from transformers.exporters import OnnxExporter, OnnxConfig
exporter = OnnxExporter()
gen_config = GenerationConfig(cache_implementation="static", max_cache_len=2048)
components = exporter.export_for_generation(
model, inputs, config=OnnxConfig(dynamic=True), generation_config=gen_config, multi_token_decode=True
)from transformers import GenerationConfig
from transformers.exporters import ExecutorchExporter, ExecutorchConfig
exporter = ExecutorchExporter()
gen_config = GenerationConfig(cache_implementation="static", max_cache_len=2048)
components = exporter.export_for_generation(
model, inputs, config=ExecutorchConfig(backend="xnnpack", dynamic=True), generation_config=gen_config, multi_token_decode=True
)Le graphe decode possède désormais deux axes symboliques — la longueur de la requête (le nombre de jetons fournis) et la longueur du cache (max_cache_len, redimensionnable au chargement). dynamic=True marque ces axes (ainsi que tous les autres) comme Dim.AUTO, de sorte que le graphe exporté accepte n'importe quelle longueur de prompt et n'importe quelle taille de cache au moment du chargement.
Mises à jour sur place sans copie
Le cache statique est transmis et modifié sur place, de sorte qu’un seul tampon conserve l’état entre les étapes de décodage sans copie hôte — à condition que le runtime lie les tampons de l’appelant plutôt que de copier via son propre arène. Ce que cela implique est la seule partie propre à chaque backend restante :
Dynamo — le programme exporté modélise l’écriture du cache comme une
USER_INPUT_MUTATION, de sorte que l’appel decomponents["decode"].module()(...)met à jour directement les tenseurs de cache que vous transmettez. Réutilisez les mêmes tenseurs à chaque étape ; aucune configuration n’est nécessaire.ONNX Runtime — le graphe de décodage expose le cache sous forme de paires appariées
input.<name>/output.<name>. La méthodeCudaSession.set_buffer_sharingd’ORT (onnxruntime.transformers.io_binding_helper) associe chaque paire à un tampon de périphérique unique, de sorte que le cache est lu et mis à jour sur place au fil de la boucle, sans aucun aller-retour avec l’hôte.ExecuTorch (XNNPACK) — désactivez les allocations de planification mémoire sur
ExecutorchConfigafin que l’écriture en place puisse se faire dans le tenseur de l’appelant (voir la référence pour connaître le rôle de chaque drapeau) :config = ExecutorchConfig( backend="xnnpack", dynamic=True, alloc_graph_input=False, alloc_graph_output=False, alloc_mutable_buffers=False, )L'écriture in-place sans copie nécessite également que l'appelant lie les tampons de sortie à l'exécution via
Method::set_output_data_ptr— non exposé par le runtime Python (executorch.runtime.Methodn'expose queexecute/set_inputs/get_outputs). Les drapeaux ci-dessus le configurent, mais l'écriture in-place est un chemin exclusif au C++ (voir l'exemple de boucle de décodage ExecuTorch ci-dessous). Depuis Python, relisez le cache mis à jour depuis les sorties de la méthode à chaque étape.
Decode-loop inference examples
La boucle a la même forme sur chaque backend — c’est le même graphe tout au long. On part d’un cache de taille fixe vide, on alimente le prompt complet une seule fois (cache vide → préremplissage), puis un jeton à la fois (cache rempli → décodage). Chaque appel transmet input_ids, un attention_mask causal et position_ids (avancés du nombre de nouveaux jetons à chaque étape), ainsi que le cache, et renvoie les logits pour chaque position de requête. L’emplacement de chaque jeton dans le cache est suivi en interne par le cache statique, il n’y a donc rien d’autre à passer dans l’appel. La configuration du cache varie selon le runtime (un objet StaticCache pour Dynamo, des tampons bruts de l’appareil pour ONNX Runtime, des tableaux de l’appelant en C++ pour ExecuTorch), chaque onglet construit donc le sien ci-dessous. Les onglets Dynamo et ONNX Runtime mettent à jour le cache sur place ; le chemin sur place d’ExecuTorch est en C++ (son runtime Python ne le peut pas, comme indiqué plus haut).
torch.export enregistre l’écriture du cache statique comme une USER_INPUT_MUTATION, de sorte que la méthode module() du graphe chargé met à jour le StaticCache que vous passez directement — un seul cache porte l’état sur l’ensemble de la boucle, sans rien à lier ni à renvoyer. register_pytree_node(StaticCache) permet à torch.export.load de déplier l’entrée StaticCache. Le cache doit être initialisé à l’avance (torch.export intègre les K/V alloués dans la spécification d’entrée, donc un cache vide initialisé paresseusement ne correspondra pas) — mais le programme enregistré contient ses propres example_inputs, il suffit donc de réutiliser ce modèle de StaticCache déjà initialisé, remis à zéro :
import copy
import torch
from transformers import StaticCache
from transformers.exporters.exporter_dynamo import register_pytree_node
register_pytree_node(StaticCache)
exported = torch.export.load("decode.pt2")
decode = exported.module() # runs on the device its inputs / cache live on (CUDA here)
# the artifact carries an initialized StaticCache template — reuse it (reset to empty)
_, example_kwargs = exported.example_inputs
past_key_values = copy.deepcopy(example_kwargs["past_key_values"])
past_key_values.reset()
def causal_mask(positions, cache_len): # [1, 1, len(positions), cache_len]
return (torch.arange(cache_len, device="cuda")[None, :] <= positions[:, None])[None, None]
# prefill: the whole prompt in one call
positions = torch.arange(prompt_len, device="cuda")
logits = decode(input_ids=prompt_ids, attention_mask=causal_mask(positions, max_cache_len),
position_ids=positions[None], past_key_values=past_key_values).logits
next_token = logits[:, -1:].argmax(-1)
# decode: query=1 buffers reused in place
input_ids = torch.empty((1, 1), dtype=torch.long, device="cuda")
position_ids = torch.empty((1, 1), dtype=torch.long, device="cuda")
attention_mask = torch.empty((1, 1, 1, max_cache_len), dtype=torch.bool, device="cuda")
slots = torch.arange(max_cache_len, device="cuda")
for position in range(prompt_len, max_cache_len):
input_ids.copy_(next_token)
position_ids.fill_(position)
attention_mask[0, 0, 0].copy_(slots <= position)
logits = decode(input_ids=input_ids, attention_mask=attention_mask,
position_ids=position_ids, past_key_values=past_key_values).logits
next_token = logits[:, -1:].argmax(-1)ONNX Runtime exécute le graphe tel quel ; la mise à jour du cache in place est effectuée avec CudaSession de ORT (onnxruntime.transformers.io_binding_helper), un wrapper fin sur l’io-binding de ORT. set_buffer_sharing associe une entrée de cache input.<name> et sa sortie correspondante output.<name> à un seul buffer de périphérique, de sorte que les K/V/compteur modifiés sont écrits directement dans l’entrée ; allocate_buffers alloue les sorties restantes (non partagées) — ici uniquement logits ; et infer(feed_dict) associe vos tenseurs CUDA par pointeur et exécute. Les buffers de cache proviennent directement des métadonnées d’entrée du graphe (forme et type de get_inputs()), de sorte qu’aucune configuration de modèle n’est nécessaire — l’axe symbolique unique (longueur du cache) devient max_cache_len :
import torch
import onnxruntime as ort
from onnxruntime.transformers.io_binding_helper import CudaSession, TypeHelper
def causal_mask(positions, cache_len):
return (torch.arange(cache_len, device="cuda")[None, :] <= positions[:, None])[None, None]
session = ort.InferenceSession("decode.onnx", providers=["CUDAExecutionProvider"])
cuda = CudaSession(session, torch.device("cuda"))
# fresh device cache buffers built from each cache input's own shape/dtype; share each
# input.<name>/output.<name> pair on one buffer so the update lands in place
cache = {}
for i in session.get_inputs():
if not i.name.startswith("input."):
continue
name = i.name[len("input.") :]
dims = [max_cache_len if isinstance(d, str) and not d.isdigit() else int(d) for d in i.shape]
cache[name] = torch.zeros(dims, dtype=TypeHelper.ort_type_to_torch_type(i.type), device="cuda")
cuda.set_buffer_sharing(f"input.{name}", f"output.{name}")
cache_feed = {f"input.{name}": buf for name, buf in cache.items()}
vocab_size = next(o.shape[-1] for o in session.get_outputs() if o.name.endswith("logits"))
# prefill: the whole prompt in one call
positions = torch.arange(prompt_len, device="cuda")
cuda.allocate_buffers({"logits": (1, prompt_len, vocab_size)})
out = cuda.infer({"input_ids": prompt_ids, "attention_mask": causal_mask(positions, max_cache_len),
"position_ids": positions[None], **cache_feed})
next_token = out["logits"][:, -1:].argmax(-1)
# decode: query=1 buffers reused in place
cuda.allocate_buffers({"logits": (1, 1, vocab_size)})
input_ids = torch.empty((1, 1), dtype=torch.long, device="cuda")
position_ids = torch.empty((1, 1), dtype=torch.long, device="cuda")
attention_mask = torch.empty((1, 1, 1, max_cache_len), dtype=torch.bool, device="cuda")
slots = torch.arange(max_cache_len, device="cuda")
for position in range(prompt_len, max_cache_len):
input_ids.copy_(next_token)
position_ids.fill_(position)
attention_mask[0, 0, 0].copy_(slots <= position)
out = cuda.infer({"input_ids": input_ids, "attention_mask": attention_mask,
"position_ids": position_ids, **cache_feed})
next_token = out["logits"][:, -1:].argmax(-1)Le runtime sur appareil d’ExecuTorch est en C++, et la mise à jour du cache in place repose sur Method::set_output_data_ptr — non exposé par le runtime Python (executorch.runtime.Method n’expose que execute/set_inputs/get_outputs), de sorte que le décodage sans copie est un chemin réservé au C++. Liez chaque sortie de cache modifiée à son entrée de cache correspondante, et les nouvelles valeurs K/V/compteur atterrissent dans les tampons StaticCache de l’appelant sans aucune copie. Comme dans les autres onglets, les formes et les tailles proviennent de l’artefact lui-même — ici de la method_meta du programme (input_tensor_meta/output_tensor_meta → TensorInfo::nbytes()), le C++ n’ayant pas de runtime Python à interroger :
#include <executorch/extension/data_loader/file_data_loader.h>
#include <executorch/extension/tensor/tensor_ptr.h>
#include <executorch/runtime/executor/method.h>
#include <executorch/runtime/executor/program.h>
using namespace executorch::runtime;
using executorch::extension::FileDataLoader;
using executorch::extension::make_tensor_ptr;
// load the exported decode and its `forward` method (Result error-checks elided for brevity)
auto loader = FileDataLoader::from("decode.pte");
auto program = Program::load(&loader.get());
// method-execution memory: a fixed arena for bookkeeping + one buffer per the method's memory plan
std::array<uint8_t, 4 * 1024 * 1024> arena;
MemoryAllocator method_allocator(arena.size(), arena.data());
auto meta = program->method_meta("forward");
std::vector<std::vector<uint8_t>> planned(meta->num_memory_planned_buffers());
std::vector<Span<uint8_t>> planned_spans;
for (size_t i = 0; i < planned.size(); ++i) {
planned[i].resize(meta->memory_planned_buffer_size(i).get());
planned_spans.push_back({planned[i].data(), planned[i].size()});
}
HierarchicalAllocator planned_allocator({planned_spans.data(), planned_spans.size()});
MemoryManager memory_manager(&method_allocator, &planned_allocator);
auto decode = std::move(program->load_method("forward", &memory_manager).get());
// shapes and sizes come from method_meta — no external config. Inputs are [ids, mask, position_ids,
// cache×N]; outputs are [returned cache×N, logits, mutated cache×N].
const size_t num_cache_tensors = meta->num_inputs() - 3;
const size_t logits_out_idx = num_cache_tensors;
std::vector<size_t> cache_nbytes(num_cache_tensors), cache_out_idx(num_cache_tensors);
for (size_t i = 0; i < num_cache_tensors; ++i) {
cache_nbytes[i] = meta->input_tensor_meta(3 + i)->nbytes();
cache_out_idx[i] = num_cache_tensors + 1 + i; // mutated cache = the last N outputs
}
const size_t logits_nbytes = meta->output_tensor_meta(logits_out_idx)->nbytes();
// one set of StaticCache buffers (K/V + per-layer counters) is reused across steps, bound in place each call
auto forward = [&](const TensorPtr& input_ids, const TensorPtr& mask, const TensorPtr& position_ids) {
decode.set_input(EValue(*input_ids), 0);
decode.set_input(EValue(*mask), 1);
decode.set_input(EValue(*position_ids), 2);
for (int i = 0; i < num_cache_tensors; ++i)
decode.set_input(EValue(*cache_tensor[i]), 3 + i);
// bind each mutated-cache output onto that same input tensor's data → the write lands in place, zero copies
for (int i = 0; i < num_cache_tensors; ++i)
decode.set_output_data_ptr(cache_tensor[i]->mutable_data_ptr(), cache_nbytes[i], cache_out_idx[i]);
decode.set_output_data_ptr(logits_data, logits_nbytes, logits_out_idx);
decode.execute(); // cache updated in place; logits written to logits_data
return argmax_last(logits_data); // greedy pick
};
// prefill the whole prompt, then decode one token per step — the cache carries in place across all calls
int64_t next_token = forward(prompt_ids, prompt_mask, prompt_positions);
for (int64_t position = prompt_len; position < max_cache_len; ++position) {
next_token = forward(make_tensor_ptr({1, 1}, &next_token, ScalarType::Long),
causal_mask(position), // [1, 1, 1, max_cache_len] bool
make_tensor_ptr({1, 1}, &position, ScalarType::Long));
}Quantification
Chaque configuration d’export prend un quantizer qui quantise le modèle pendant l’export, sur le graphe FX de torch.export
(PT2E ou NNCF, pour chaque backend) ou sur le modèle ONNX ou OpenVINO converti (ONNX Runtime ou NNCF). Consultez Quantifier les exports pour les quantiseurs, leurs backends, la calibration et comment en ajouter un.
Limites et solutions de contournement
torch.export, torch.onnx.export et ExecuTorch présentent chacun des limites concernant certains motifs spécifiques de PyTorch. Les exporteurs contournent ces problèmes à l’aide d’un petit ensemble de correctifs réversibles et de corrections au niveau FX, appliqués à des points bien définis du flux d’exportation. Rien de tout cela n’est visible depuis l’API publique export, mais voici les points les plus courants à connaître :
- FlashAttention et FlexAttention ne sont pas exportables sur aucun backend.
sdpaest le paramètre recommandé eteagerfonctionne également (plus lentement). Définissez l’un d’eux sur le modèle avant d’appelerexports’il utilise autre chose. grouped_mmest tracé sans problème parDynamoExporteret est traduit automatiquement pourOnnxExporter. PourExecutorchExporteravec XNNPACK ou MLX, l’exportateur remplace les experts MoE parbatched_mmavant l’exportation.- Les couches à fenêtre glissante (Mistral, Gemma 3) sont exportées avec un cache clé-valeur complet ; le masque glissant applique toujours la fenêtre.
Étapes suivantes
- Ajoutez la prise en charge de l’exportation pour une nouvelle architecture ou un nouveau backend à l’aide des registres de correctifs et de modifications dans Étendre les exportateurs.
- Quantifier un export, ou ajouter un quantificateur, dans Quantifier les exports.