Transformers documentation
Exportadores
Exportadores
Exporta cualquier PreTrainedModel a ONNX, ExecuTorch o un programa de PyTorch independiente, sin importar el tiempo de ejecución objetivo.
exporter = DynamoExporter() # or OnnxExporter, ExecutorchExporter
config = DynamoConfig(dynamic=True)
exported = exporter.export(model, inputs, config=config)Los exportadores se encuentran dentro de Transformers en lugar de en una biblioteca derivada, por lo que los cambios de arquitectura, los nuevos patrones de atención y los tipos de caché personalizados se admiten en el momento de la exportación en cuanto se incorporan al código de modelado.
Los exportadores son experimentales. Muchas de las correcciones de este módulo resuelven errores específicos de las bibliotecas de origen (Torch, ONNX Script, ONNX Runtime, ExecuTorch) y se eliminarán en cuanto se incorpore la solución en el código de origen. Hasta que la API se estabilice, considere las correcciones como vinculadas a las versiones utilizadas en el conjunto de pruebas. Fije esas versiones en las herramientas de producción y espere que aparezcan nuevas correcciones y desaparezcan las antiguas a medida que se incorporen los cambios en el código de origen.
| Exportador | Programar (.program) | Guardado como | Tiempo de ejecución |
|---|---|---|---|
DynamoExporter | ExportedProgram | model.pt2 | Cualquier entorno de ejecución de PyTorch, compilación AOT |
OnnxExporter | onnx_ir.Model | model.onnx | Cualquier runtime de ONNX (ORT, TensorRT, OpenVINO) |
ExecutorchExporter | ExecutorchProgramManager | model.pte | Dispositivos móviles y de borde (ExecuTorch) |
OpenVINOExporter | openvino.Model | openvino_model.xml + .bin | Runtime OpenVINO (CPU/GPU/NPU de Intel) |
Cada exportación devuelve un ExportedModel, cuyo mapa components asocia nombres con ExportedComponents. Un
componente contiene el programa del backend (.program) y los ExportMetadata registrados durante su trazado
(nombres de entrada y salida, precisión, formas de entrada trazadas, configuración de exportación). ~HfExporter.export produce un único componente "model", por lo que su programa es directamente exported.program. ~ExportedModel.save_pretrained escribe un
archivo por componente, un manifiesto export.json con el archivo y los metadatos de cada componente, y la configuración del modelo.
~ExportedModel.from_pretrained lo vuelve a cargar, desde un directorio local o un repositorio del Hub, con los mismos kwargs de descarga que from_pretrained(). Pasa subfolder para una exportación guardada junto a los pesos del modelo; solo se descargan los archivos propios de la exportación. Los programas de Dynamo, ONNX y OpenVINO se cargan como el tipo que devolvió la exportación. Un grafo ONNX cargado deja sus pesos en model.onnx.data hasta que se leen. Un .pte de ExecuTorch se carga como un executorch.runtime.Program, ya que el ExecutorchProgramManager no puede reconstruirse a partir del archivo.
from transformers.exporters import ExportedModel
exported.program # the backend's program
exported.save_pretrained("exported-model") # model.<suffix>, export.json, config.json
loaded = ExportedModel.from_pretrained("exported-model")Un directorio contiene una sola exportación, ya que export.json y config.json tienen nombres fijos. Mantenga las exportaciones a varios formatos en sus propios subdirectorios (onnx/, openvino/) y cárguelas con subfolder="onnx".
AutoHfExporter selecciona el exportador adecuado a partir de una configuración, y AutoExportConfig selecciona la clase de configuración adecuada a partir de un diccionario. Ambos siguen el mismo patrón de clase automática en Transformers, lo cual es útil cuando el backend se selecciona en tiempo de ejecución en lugar de estar codificado de forma fija en el punto de llamada.
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)
exported = exporter.export(model, inputs, config=config)Instalación
Instale las dependencias del backend al que planea exportar.
Las versiones siguientes son las que tiene fijada la suite de pruebas del exportador. Las versiones más nuevas o más antiguas suelen funcionar, pero los parches del exportador apuntan a una superficie de API específica, por lo que para herramientas de producción fije estas versiones y espere que
HfExporterregistre una advertencia cuando detecte una desviación.
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"Requiere macOS 14 o posterior en Apple Silicon. MLX está incluido en las ruedas nocturnas de ExecuTorch para macOS ARM64; no es un paquete ni una extensión independiente. El siguiente par de versiones nocturnas se validó con las pruebas del exportador de MLX. Instálalo desde el registro nocturno estándar de 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/cpuInstale torch de forma explícita, ya que las ruedas de ExecuTorch de compilación nocturna no lo declaran como dependencia.
Para las compilaciones desde el código fuente, consulte la guía de instalación de MLX.
pip install transformers "torch==2.12.0" "openvino==2026.3.1"Exportar un modelo
Todos los exportadores comparten la misma interfaz. Crea un exportador con una configuración y llama a export().
Cambie entre runtimes cambiando la clase de exportador.
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(**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)
exported = exporter.export(model, inputs, config=config)
# save, then load and run on ONNX Runtime
exported.save_pretrained("qwen3-onnx")
from transformers.exporters import ExportedModel
outputs = ExportedModel.from_pretrained("qwen3-onnx")(**inputs)backend selecciona el destino: xnnpack (el valor predeterminado) para CPU, mlx para GPU de Apple Silicon o cuda para una GPU habilitada para CUDA. Elija la opción de instalación correspondiente. La ejecución de MLX requiere el delegado de MLX y sus bibliotecas Metal;
solicitar CUDA sin un entorno habilitado para CUDA genera un 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.
exported = exporter.export(model, inputs, config=config)
# save for on-device deployment
exported.save_pretrained("qwen3-executorch")
# load and run on the ExecuTorch runtime
from transformers.exporters import ExportedModel
outputs = ExportedModel.from_pretrained("qwen3-executorch")(**inputs)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)
exported = exporter.export(model, inputs, config=config)
exported.save_pretrained("qwen3-openvino")
# run on CPU (or "xpu" for an Intel GPU)
outputs = exported.to("cpu")(**inputs)Ejecutar una exportación
Un ExportedModel ejecuta una exportación en el runtime de su backend (el módulo de torch.export, ONNX Runtime, OpenVINO o el runtime de ExecuTorch) y se invoca de la misma forma que el modelo del que proviene: mismos kwargs, misma clase ModelOutput. El cómputo se realiza en el backend, lo cual suele ser más rápido que PyTorch eager (OpenVINO en CPU, ONNX Runtime en CUDA), y las exportaciones cuantizadas se ejecutan de la misma manera. Las entradas con las que no se trazó la exportación se descartan, por lo que se puede pasar la salida completa de un procesador.
Una exportación nueva se ejecuta en la primera llamada (en el dispositivo predeterminado del backend, o en el que elige ~ExportedModel.to), y ExportedModel.from_pretrained carga una guardada desde un directorio local o el Hub, lista para ejecutarse.
from transformers.exporters import ExportedModel
outputs = exported(**inputs)
exported.save_pretrained("qwen3-onnx")
model = ExportedModel.from_pretrained("qwen3-onnx", device="cuda")
logits = model(**inputs).logitsdevice define dónde se ejecuta: un proveedor de ONNX Runtime ("cpu", "cuda"), un dispositivo de OpenVINO ("cpu", "xpu" para una GPU de Intel, o AUTO de forma predeterminada), o dónde se mueve un módulo de torch.export. Un programa de ExecuTorch se ejecuta en el lugar para el que fue reducido. Las exportaciones con varios componentes (generación) aún no se ejecutan.
Formas dinámicas
Pasar dynamic=True marca cada dimensión de tensor como dinámica, de modo que el grafo exportado acepte entradas de cualquier tamaño en tiempo de ejecución sin volver a trazar.
Para un control más detallado sobre qué dimensiones son dinámicas, pase explícitamente dynamic_shapes en su lugar, que se reenvía directamente a 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,
)
exported = 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,
)
exported = 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,
)
exported = exporter.export(model, inputs, config=config)Modelos generativos
Para la generación autoregresiva, el forward del modelo tiene formas diferentes en el paso de prellenado (prompt completo, sin caché KV) frente al paso de decodificación (un solo token, caché KV poblada). Los exportadores exponen ~HfExporter.export_for_generation, que divide ambas etapas y exporta cada una.
Para los modelos generativos multimodales, el prefill se divide además en un codificador de imagen o audio, el modelo de lenguaje y lm_head. La detección del codificador y del modelo de lenguaje utiliza los accesores get_encoder() (modality="image" o "audio") y get_decoder(), de modo que cualquier nueva arquitectura que utilice estos funciona sin necesidad de configuración adicional.
Un componente de proyector solo aparece cuando el modelo expone uno bajo un nombre de atributo (multi_modal_projector, connector, embed_vision, embed_audio). Qwen2-VL, a continuación, integra su proyector en la torre de visión, por lo que su diccionario de componentes no tiene una clave separada multi_modal_projector. Las nuevas arquitecturas deben alinear su atributo de proyector con uno de estos nombres en lugar de ampliar la lista.
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)
exported = exporter.export_for_generation(model, inputs, config=config)
# exported: ExportedModel with "image_encoder", "language_model", "lm_head" and "decode",
# each an ExportedComponent whose `.program` is an ExportedProgramfrom 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)
exported = exporter.export_for_generation(model, inputs, config=config)
# exported: ExportedModel with "image_encoder", "language_model", "lm_head" and "decode",
# each an ExportedComponent whose `.program` is an onnx_ir.Modelfrom 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.
exported = exporter.export_for_generation(model, inputs, config=config)
# exported: ExportedModel with "image_encoder", "language_model", "lm_head" and "decode",
# each an ExportedComponent whose `.program` is an ExecutorchProgramManagerfrom 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)
exported = exporter.export_for_generation(model, inputs, config=config)
# exported: ExportedModel with "image_encoder", "language_model", "lm_head" and "decode",
# each an ExportedComponent whose `.program` is an openvino.Modelexported.save_pretrained(directory) escribe un archivo por componente (decode.onnx, lm_head.onnx, …), el manifiesto, la configuración del modelo y la configuración de generación pasada a export_for_generation.
Los componentes exportados son grafos independientes, no un pipeline de inferencia listo para ejecutarse. El responsable de ejecutar cada codificador, proyectar los embeddings y orquestar el bucle de generación es el llamador.
Cómo funciona export_for_generation
decompose_for_generation() ejecuta model.generate(**inputs, max_new_tokens=2) una vez y engancha model.forward para capturar los kwargs reales de prefill y decode (y los kwargs por submódulo mediante hooks en cada codificador/proyector/modelo de lenguaje si el modelo es multimodal). Por eso funciona con cualquier arquitectura, incluidos modelos solo decodificador, SSM, codificador-decodificador y multimodales, sin código de pegamento por modelo. export_for_generation es una línea sobre él.
La captura ejecuta el modelo de forma inmediata sobre inputs, por lo que se deben pasar valores pequeños pero representativos, como un prompt corto, una sola imagen pequeña o unos pocos fotogramas de audio. El programa exportado no está atado a esos tamaños (las formas dinámicas siguen fluyendo), pero las entradas de captura más pequeñas hacen que decompose_for_generation sea más barato y mantienen la inferencia de formas simbólicas manejable.
Llama a decompose_for_generation directamente para actuar entre la descomposición y la exportación, como ejecutar un forward eager para verificación, intercambiar las entradas de un submódulo o omitir una etapa.
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)Decodificación de múltiples tokens
Por defecto, el componente decode es un paso de un solo token: un token de consulta contra la caché KV. Por lo tanto, torch.export especializa su eje de secuencia de consulta en 1. Pasa multi_token_decode=True para capturar decode como una decodificación de múltiples tokens: decompose_for_generation() fusiona dos pasos de decodificación consecutivos (se captura con max_new_tokens=3) en una sola pasada, de modo que ese eje se mantiene simbólico. Un único grafo sirve entonces para cualquier longitud de consulta: un token (decodificación ordinaria), muchos tokens a la vez (continuación desde el pasado, por ejemplo, al aceptar un bloque de tokens especulativos) y un prefill normal cuando la caché está vacía.
from transformers.exporters import DynamoExporter, DynamoConfig
exporter = DynamoExporter()
config = DynamoConfig(dynamic=True)
exported = exporter.export_for_generation(model, inputs, config=config, multi_token_decode=True)
# exported.components["decode"] now accepts a variable number of query tokensfrom transformers.exporters import OnnxExporter, OnnxConfig
exporter = OnnxExporter()
config = OnnxConfig(dynamic=True)
exported = exporter.export_for_generation(model, inputs, config=config, multi_token_decode=True)
# exported.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.
exported = exporter.export_for_generation(model, inputs, config=config, multi_token_decode=True)
# exported.components["decode"] now accepts a variable number of query tokensEl eje de consulta solo permanece simbólico en una exportación con formas dinámicas (dynamic=True); una exportación estática lo fija en la longitud capturada, generando un grafo multi-token fijo. Se compone con la caché KV estática descrita a continuación: la decodificación fusionada escribe los tokens de cada paso en la caché de tamaño fijo in situ, y la caché gestiona internamente dónde se ubican.
Caché KV estática
Los ejemplos de ExecuTorch de esta sección, incluidas las actualizaciones sin copia y el bucle de decodificación, son exclusivos de XNNPACK. El exportador de MLX rechaza
StaticCache; usaDynamicCachecon MLX.
generate() crea por defecto una DynamicCache, realocando a medida que la secuencia se extiende: un objetivo en movimiento para un grafo exportado. Una caché estática es un búfer de tamaño fijo, asignado una sola vez y escrito in situ en la posición actual en cada paso. Combinado con un decodificado de múltiples tokens, colapsa la generación en un único grafo exportado: el grafo de decode toma una caché de tamaño fijo y un número variable de tokens de consulta, de modo que un solo grafo sirve tanto para el prompt (caché vacía → prefill) como para cada token generado (caché poblada → decode). Exporte esto reenviando una GenerationConfig con cache_implementation="static" (y un max_cache_len) junto a 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)
exported = exporter.export_for_generation(
model, inputs, config=DynamoConfig(dynamic=True), generation_config=gen_config, multi_token_decode=True
)
exported.save_pretrained("qwen3-static")from transformers import GenerationConfig
from transformers.exporters import OnnxExporter, OnnxConfig
exporter = OnnxExporter()
gen_config = GenerationConfig(cache_implementation="static", max_cache_len=2048)
exported = 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)
exported = exporter.export_for_generation(
model, inputs, config=ExecutorchConfig(backend="xnnpack", dynamic=True), generation_config=gen_config, multi_token_decode=True
)El grafo de decode ahora tiene dos ejes simbólicos: la longitud de la consulta (cuántos tokens se introducen) y la longitud de la caché (max_cache_len, redimensionable en el momento de la carga). dynamic=True marca estos (y todos los demás ejes) como Dim.AUTO, de modo que el grafo exportado acepta cualquier longitud de prompt y tamaño de caché en el momento de la carga.
Actualizaciones in situ sin copia
La caché estática se pasa y se modifica in situ, por lo que un solo búfer lleva el estado a través de los pasos de decodificación sin copias en el host, siempre que el tiempo de ejecución enlace los búferes del llamador en lugar de copiarlos a través de su propia área. Lo que eso requiere es la única parte específica de cada backend que queda:
Dynamo — el programa exportado modela la escritura en la caché como una
USER_INPUT_MUTATION, por lo que al llamar aexported.runners["decode"](...)se actualizan directamente los tensores de caché que se pasan. Reutiliza los mismos tensores en cada paso; no hay nada que configurar.ONNX Runtime — el grafo de decodificación expone la caché como pares correspondientes de
input.<name>/output.<name>. El métodoCudaSession.set_buffer_sharingde ORT (onnxruntime.transformers.io_binding_helper) vincula cada par a un búfer de dispositivo, de modo que la caché se lee y actualiza in situ a lo largo del bucle, sin transiciones al host.ExecuTorch (XNNPACK) — desactive las asignaciones de planificación de memoria en
ExecutorchConfigpara que la escritura in situ se realice en el tensor propio del llamador (consulte la referencia para ver qué hace cada flag):config = ExecutorchConfig( backend="xnnpack", dynamic=True, alloc_graph_input=False, alloc_graph_output=False, alloc_mutable_buffers=False, )La escritura in-place sin copia también requiere que el llamador asigne los búferes de salida en tiempo de ejecución mediante
Method::set_output_data_ptr— no expuesto por el runtime de Python (executorch.runtime.Methodsolo exponeexecute/set_inputs/get_outputs). Las banderas anteriores lo configuran, pero la escritura in-place es una ruta exclusiva de C++ (vea el ejemplo de bucle de decodificación de ExecuTorch a continuación). Desde Python, lea la caché actualizada desde las salidas del método en cada paso.
Decode-loop inference examples
El bucle tiene la misma forma en cada backend: es el mismo grafo en todo momento. Se parte de una caché de tamaño fijo vacía, se introduce el prompt completo una sola vez (caché vacía → prefill) y luego se procesa un token a la vez (caché poblada → decodificación). Cada llamada pasa input_ids, una attention_mask causal y position_ids (avanzadas en el número de tokens nuevos en cada paso), además de la caché, y devuelve logits para cada posición de consulta. La posición en la que cada token se ubica en la caché se rastrea internamente mediante la caché estática, por lo que no hay nada adicional que pasar a través de la llamada. La configuración de la caché varía según el runtime (un objeto StaticCache para Dynamo, buffers de dispositivo crudos para ONNX Runtime, matrices del llamador en C++ para ExecuTorch), por lo que cada pestaña construye la suya a continuación. Las pestañas de Dynamo y ONNX Runtime actualizan la caché in situ; la ruta in situ de ExecuTorch está en C++ (su runtime de Python no puede hacerlo, como se mencionó anteriormente).
torch.export registra la escritura en la caché estática como USER_INPUT_MUTATION, por lo que el ejecutor de decode actualiza la StaticCache que pasas directamente: una sola caché lleva el estado a lo largo de todo el bucle, sin necesidad de vincular nada ni devolverlo. La caché debe inicializarse de antemano (torch.export integra los K/V asignados en la especificación de entrada, por lo que una caché vacía perezosa no coincidirá), pero el programa guardado incluye sus propios example_inputs, así que reutiliza esa plantilla de StaticCache ya inicializada, restableciéndola a vacía:
import copy
import torch
from transformers.exporters import ExportedModel
exported = ExportedModel.from_pretrained("qwen3-static", device="cuda")
decode = exported.runners["decode"]
# the program carries an initialized StaticCache template — reuse it (reset to empty)
_, example_kwargs = exported.components["decode"].program.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 ejecuta el grafo tal cual; la actualización de la caché in situ se realiza con CudaSession de ORT (onnxruntime.transformers.io_binding_helper), un envoltorio fino sobre io-binding de ORT. set_buffer_sharing vincula una entrada de caché input.<name> y su salida correspondiente output.<name> a un búfer de dispositivo, de modo que los K/V/contador modificados se escriben directamente de vuelta en la entrada; allocate_buffers asigna las salidas restantes (no compartidas), en este caso solo logits; y infer(feed_dict) vincula tus tensores CUDA por puntero y ejecuta. Los búferes de caché provienen directamente de los metadatos de entrada del propio grafo (forma y tipo de get_inputs()), por lo que no se necesita ninguna configuración del modelo: el único eje simbólico (longitud de la caché) se convierte en 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)El runtime en el dispositivo de ExecuTorch está escrito en C++, y la actualización de la caché in situ depende de Method::set_output_data_ptr — no expuesto por el runtime de Python (executorch.runtime.Method solo expone execute/set_inputs/get_outputs), por lo que la decodificación sin copia es una ruta exclusiva de C++. Enlaza cada output de la caché modificada con su correspondiente búfer de input de la caché, y los nuevos K/V/contador se colocan en los búferes de StaticCache del llamador sin realizar copias. Al igual que en las otras pestañas, las formas y tamaños provienen del propio artefacto: aquí, del method_meta del programa (input_tensor_meta/output_tensor_meta → TensorInfo::nbytes()), ya que el C++ no tiene un runtime de Python al que consultar:
#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));
}Fusiones de ONNX Runtime
Establezca ort_fusions para reescribir una exportación de ONNX en operaciones de transformador fusionadas de ONNX Runtime antes de que se guarde. ONNX Runtime
ejecuta el grafo fusionado mucho más rápido que el grafo simple. Las fusiones cubren las normalizaciones RMS y de capa, la atención
(MultiHeadAttention, o GroupQueryAttention cuando hay una caché de clave-valor), los embeddings rotatorios, GELU y
las proyecciones que comparten una entrada. Cada fusión produce el mismo resultado que las operaciones que reemplaza.
Establezca ort_fusions en el proveedor de ejecución sobre el que se ejecutará el grafo. Cada proveedor recibe las operaciones fusionadas que admite.
config = OnnxConfig(ort_fusions="cpu")
config = OnnxConfig(ort_fusions="cuda")
config = OnnxConfig(ort_fusions="webgpu")
config = OnnxConfig(ort_fusions="dml")"cpu" | "cuda" | "webgpu" | "dml" | |
|---|---|---|---|---|
| Atención con una caché de clave-valor | GroupQueryAttention, con rotaciones incluidas | GroupQueryAttention con sus normas de consulta y clave; el prefill mantiene MultiHeadAttention por ahora (más lento) | GroupQueryAttention, con rotary como su propio RotaryEmbedding | MultiHeadAttention, con el giro como su propio RotaryEmbedding |
| Otras atenciones | operaciones simples por ahora (más lento) | MultiHeadAttention | MultiHeadAttention | MultiHeadAttention |
| Mezcla de expertos | unfused | MoE | unfused | unfused |
| Gráficos de CUDA | — | las exportaciones estáticas pueden reproducirse | — | — |
Un grafo "cuda" con MoE no se ejecuta en CPU, ya que el MoE de ONNX Runtime solo existe para CUDA. Un grafo "webgpu" se ejecuta
en el navegador con onnxruntime-web, o de forma nativa con onnxruntime-webgpu. Un grafo "dml" se ejecuta en Windows con
onnxruntime-directml.
Las operaciones fusionadas no son ONNX estándar, por lo que otros consumidores de ONNX, como TensorRT o OpenVINO, no pueden leerlas. El valor predeterminado es ort_fusions=None, lo que mantiene el gráfico estándar. Si necesitas el modelo en varios runtime, expórtalo una vez para cada uno.
Las fusiones se ejecutan antes de que OnnxRuntimeQuantizer cuantice el grafo, tal como recomienda ONNX Runtime. Se omiten cuando el modelo se cuantiza antes de la exportación y requieren optimize=True (el valor predeterminado). Las fusiones de "cuda" necesitan onnxruntime 1.28 o posterior.
Cuantización
Cada configuración de exportación toma un quantizer que cuantiza el modelo durante la exportación, en el grafo FX de torch.export (PT2E o NNCF, para cada backend) o en el modelo ONNX u OpenVINO convertido (ONNX Runtime o NNCF). Consulte Cuantización de exportaciones para ver los cuantizadores, sus backends, la calibración y cómo añadir uno.
Limitaciones y soluciones
torch.export, torch.onnx.export y ExecuTorch presentan limitaciones con ciertos patrones específicos de PyTorch. Los exportadores resuelven estas limitaciones mediante un conjunto reducido de parches reversibles y correcciones a nivel de FX aplicados en puntos bien definidos del flujo de exportación. Nada de esto es visible desde la API pública export, pero lo más común que conviene saber es lo siguiente:
- FlashAttention y FlexAttention no son exportables en ningún backend.
sdpaes la configuración preferida yeagertambién funciona (más lento). Establece uno de ellos en el modelo antes de llamar aexportsi está usando otro. grouped_mmse traza sin problemas a través deDynamoExportery se traduce automáticamente paraOnnxExporter. ParaExecutorchExportercon XNNPACK o MLX, el exportador cambia los expertos MoE abatched_mmantes de la exportación.- Las capas de ventana deslizante (Mistral, Gemma 3) se exportan con una caché clave-valor completa; la máscara deslizante sigue aplicando la ventana.
Próximos pasos
- Agregue soporte de exportación para una nueva arquitectura o backend con los registros de parche y corrección en Extending the exporters.
- Cuantiza una exportación o añade un cuantizador en Cuantización de exportaciones.