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 | Salida | Tiempo de ejecución |
|---|---|---|
DynamoExporter | ExportedProgram | Cualquier entorno de ejecución de PyTorch, compilación AOT |
OnnxExporter | ONNXProgram | Cualquier runtime de ONNX (ORT, TensorRT, OpenVINO) |
ExecutorchExporter | ExecutorchProgramManager | Dispositivos móviles y de borde (ExecuTorch) |
OpenVINOExporter | openvino.Model | Runtime OpenVINO (CPU/GPU/NPU de Intel) |
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)
onnx_program = 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.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 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.
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)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,
)
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)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)
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}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)
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 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)
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
)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 llamar acomponents["decode"].module()(...)actualiza 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 module() del grafo cargado actualiza la StaticCache que pasas directamente: una sola caché lleva el estado a lo largo de todo el bucle, sin necesidad de enlazar nada ni devolverlo. register_pytree_node(StaticCache) permite que torch.export.load descompacte la entrada StaticCache. 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 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 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));
}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.
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.