Transformers documentation
Exportadores
Exportadores
Exporte qualquer PreTrainedModel para ONNX, ExecuTorch ou um programa PyTorch independente, independentemente do runtime de destino.
exporter = DynamoExporter() # or OnnxExporter, ExecutorchExporter
config = DynamoConfig(dynamic=True)
exported = exporter.export(model, inputs, config=config)Os exportadores estão localizados dentro do Transformers, em vez de em uma biblioteca de nível inferior, de modo que alterações de arquitetura, novos padrões de atenção e tipos de cache personalizados são suportados no momento da exportação assim que são incorporados ao código de modelagem.
Os exportadores são experimentais. Muitas das correções neste módulo contornam bugs específicos em projetos de origem (Torch, ONNX Script, ONNX Runtime, ExecuTorch) e serão removidas assim que a correção for incorporada à origem. Até que a API se estabilize, considere as correções vinculadas às versões usadas na suíte de testes. Fixe essas versões em ferramentas de produção e espere que novas correções surjam e as antigas desapareçam conforme as alterações de origem forem incorporadas.
| Exportar | Saída | Tempo de execução |
|---|---|---|
DynamoExporter | ExportedProgram | Qualquer runtime do PyTorch, compilação AOT |
OnnxExporter | ONNXProgram | Qualquer runtime ONNX (ORT, TensorRT, OpenVINO) |
ExecutorchExporter | ExecutorchProgramManager | Dispositivos móveis e de borda (ExecuTorch) |
OpenVINOExporter | openvino.Model | Runtime OpenVINO (CPU/GPU/NPU da Intel) |
AutoHfExporter seleciona o exportador adequado a partir de uma configuração, e AutoExportConfig seleciona a
classe de configuração adequada a partir de um dicionário. Ambos seguem o mesmo padrão de classe automática do Transformers, o que
é útil quando o backend é selecionado em tempo de execução em vez de ser fixado no local da chamada.
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)Instalação
Instale as dependências do backend para o qual você planeja exportar.
As versões abaixo são aquelas para as quais a suíte de testes do exportador é fixada. Lançamentos mais novos ou mais antigos geralmente funcionam, mas os patches do exportador visam uma superfície de API específica, portanto, para ferramentas de produção, fixe estas versões e espere que
HfExporterregistre um aviso quando detectar divergência.
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"Exige macOS 14 ou posterior em Apple Silicon. O MLX está incluído nas wheels noturnas do ExecuTorch para macOS ARM64; não é um pacote ou extra separado. O par noturno a seguir foi validado com os testes do exportador MLX. Instale-o a partir do registro noturno padrão do 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 o torch explicitamente, pois as wheels noturnas do ExecuTorch não o declaram como dependência.
Para compilações a partir do código-fonte, consulte o guia de instalação do MLX.
pip install transformers "torch==2.12.0" "openvino==2026.3.1"Exportar um modelo
Todos os exportadores compartilham a mesma interface. Crie um exportador com uma configuração e chame export().
Alterne entre runtimes trocando a classe 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 seleciona o destino: xnnpack (o padrão) para CPU, mlx para GPU Apple Silicon ou cuda para uma GPU com suporte a CUDA. Escolha a opção de instalação correspondente. A execução com MLX requer o delegate MLX e suas bibliotecas Metal;
solicitar CUDA sem um ambiente com suporte a CUDA gera um 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
Passar dynamic=True marca todas as dimensões do tensor como dinâmicas, de modo que o grafo exportado aceita entradas de qualquer tamanho em tempo de execução, sem necessidade de reprocessamento.
Para um controle mais detalhado sobre quais dimensões são dinâmicas, passe explicitamente dynamic_shapes, que é encaminhado diretamente para 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 a geração autoregressiva, o forward do modelo tem formas diferentes na etapa de prefill (prompt completo, sem cache de KV) em relação à etapa de decodificação (token único, cache de KV preenchido). Os exportadores expõem ~HfExporter.export_for_generation, que divide as duas etapas e exporta cada uma.
Para modelos generativos multimodais, o prefill é dividido adicionalmente em um codificador de imagem ou áudio, o modelo de linguagem e lm_head. A descoberta do codificador e do modelo de linguagem usa os acessores get_encoder() (modality="image" ou "audio") e get_decoder(), de modo que qualquer nova arquitetura que utilize esses componentes funciona imediatamente.
Um componente de projetor aparece apenas quando o modelo expõe um
sob um nome de atributo (multi_modal_projector, connector, embed_vision, embed_audio). O Qwen2-VL abaixo incorpora seu projetor na torre de visão, de modo que o dicionário de componentes
não possui uma chave separada multi_modal_projector. Novas arquiteturas devem alinhar o atributo do projetor
a um desses nomes, em vez de expandir a 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}Os componentes exportados são grafos independentes, não um pipeline de inferência pronto para execução. O chamador é responsável por executar cada codificador, projetar os embeddings e orquestrar o loop de geração.
Como o export_for_generation funciona
decompose_for_generation() executa model.generate(**inputs, max_new_tokens=2) uma vez e faz o hook de model.forward para capturar os kwargs reais de prefill e decode (e os kwargs por submódulo, por meio de hooks em cada encoder/projector/language model, caso o modelo seja multimodal). É por isso que ele funciona para qualquer arquitetura, incluindo modelos apenas de decoder, SSM, encoder-decoder e multimodais, sem a necessidade de código de adaptação específico para cada modelo. export_for_generation é uma linha única sobre ele.
A captura executa o modelo de forma imediata em inputs, portanto, passe valores pequenos, porém representativos, como um prompt curto, uma única imagem pequena ou alguns quadros de áudio. O programa exportado não fica vinculado a esses tamanhos (formas dinâmicas ainda fluem através dele), mas entradas de captura menores tornam decompose_for_generation mais barato e mantêm a inferência de formas simbólicas viável.
Chame decompose_for_generation diretamente para agir entre a decomposição e a exportação, como executar um forward eager para verificação, trocar as entradas de um submódulo ou pular uma 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)Decodificação de múltiplos tokens
Por padrão, o componente decode é um passo de token único — um token de consulta contra o cache de KV —
portanto, torch.export especializa seu eixo de sequência de consulta para 1. Passe multi_token_decode=True para capturar decode como um decode multi-token: decompose_for_generation() mescla dois
passos consecutivos de decode (ele captura com max_new_tokens=3) em um único forward, de modo que esse eixo permanece
simbólico. Um único gráfico então atende a todos os comprimentos de consulta — um token (decodificação comum), muitos tokens
de uma vez (continuação a partir do passado, por exemplo, aceitando um bloco de tokens especulativos) e um prefill simples
quando o cache está vazio.
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 tokensO eixo de consulta permanece simbólico apenas em uma exportação com formas dinâmicas (dynamic=True); uma exportação estática o fixa no comprimento capturado, resultando em um gráfico multi-token fixo. Ele se combina com o cache KV estático abaixo — o decodificação mesclada grava os tokens de cada etapa no cache de tamanho fixo no local, e o cache gerencia internamente onde eles são posicionados.
Cache KV estático
Os exemplos de ExecuTorch nesta seção, incluindo atualizações sem cópia e o loop de decodificação, são exclusivos para XNNPACK. O exportador MLX rejeita
StaticCache; useDynamicCachecom MLX.
generate() cresce um DynamicCache por padrão, realocando à medida que a sequência se estende — um alvo móvel para um grafo exportado. Um cache estático é um buffer de tamanho fixo, alocado uma única vez e gravado no local na posição atual a cada etapa. Combinado com um decodificação multi-token, ele reduz a geração a um único grafo exportado: o grafo decode recebe um cache de tamanho fixo e um número variável de tokens de consulta, de modo que um único grafo atende tanto o prompt (cache vazio → prefill) quanto cada token gerado (cache preenchido → decodificação). Exporte-o encaminhando um GenerationConfig com cache_implementation="static" (e um max_cache_len) junto com 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
)O grafo decode agora possui dois eixos simbólicos — o comprimento da consulta (quantos tokens você fornece) e o comprimento do cache (max_cache_len, redimensionável no momento do carregamento). dynamic=True marca esses (e todos os outros eixos) como Dim.AUTO, de modo que o grafo exportado aceita qualquer comprimento de prompt e tamanho de cache no momento do carregamento.
Atualizações in-place sem cópia
O cache estático é passado e mutado no local, de modo que um único buffer carrega o estado entre as etapas de decodificação, sem cópias no host — desde que o runtime vincule os buffers do chamador em vez de copiar através de sua própria arena. O que isso exige é a única parte específica de cada backend que resta:
Dynamo — o programa exportado modela a gravação no cache como uma
USER_INPUT_MUTATION, portanto, chamarcomponents["decode"].module()(...)atualiza diretamente os tensors de cache que você passa. Reutilize os mesmos tensors a cada etapa; não há nada a configurar.ONNX Runtime — o grafo de decodificação expõe o cache como pares correspondentes de
input.<name>/output.<name>. OCudaSession.set_buffer_sharingdo ORT (onnxruntime.transformers.io_binding_helper) vincula cada par a um único buffer de dispositivo, de modo que o cache é lido e atualizado no local ao longo do loop, sem idas e vindas com o host.ExecuTorch (XNNPACK) — desative as alocações de planejamento de memória no
ExecutorchConfigpara que a gravação in-place possa ser feita no próprio tensor do chamador (consulte a referência para saber o que cada flag faz):config = ExecutorchConfig( backend="xnnpack", dynamic=True, alloc_graph_input=False, alloc_graph_output=False, alloc_mutable_buffers=False, )A gravação in-place sem cópia também exige que o chamador vincule os buffers de saída em tempo de execução via
Method::set_output_data_ptr— algo não exposto pelo runtime Python (executorch.runtime.Methodexpõe apenasexecute/set_inputs/get_outputs). As flags acima configuram isso, mas a gravação in-place é um caminho exclusivo de C++ (veja o exemplo de loop de decodificação do ExecuTorch abaixo). A partir do Python, leia o cache atualizado de volta das saídas do método a cada etapa.
Decode-loop inference examples
O loop tem a mesma forma em todos os backends — é o mesmo grafo do início ao fim. Comece com um cache de tamanho fixo vazio, processe o prompt inteiro uma única vez (cache vazio → prefill) e, em seguida, um token de cada vez (cache preenchido → decode). Cada chamada passa input_ids, uma attention_mask causal e position_ids (avançadas pelo número de novos tokens a cada etapa), além do cache, e retorna logits para todas as posições de consulta. A posição em que cada token é armazenado no cache é rastreada internamente pelo cache estático, portanto não há nada extra para passar pela chamada. A configuração do cache varia conforme o runtime (um objeto StaticCache para Dynamo, buffers de dispositivo brutos para ONNX Runtime, arrays do chamador em C++ para ExecuTorch), de modo que cada aba monta a sua própria configuração abaixo. As abas de Dynamo e ONNX Runtime atualizam o cache no local; o caminho no local do ExecuTorch é em C++ (seu runtime em Python não pode, conforme mencionado acima).
torch.export registra a gravação no cache estático como USER_INPUT_MUTATION, de modo que o module() do grafo carregado atualiza o StaticCache que você passa diretamente — um único cache carrega o estado em todo o laço, sem nada para vincular ou devolver. register_pytree_node(StaticCache) permite que o torch.export.load desfaça a aplanamento da entrada StaticCache. O cache precisa ser inicializado previamente (torch.export incorpora o K/V alocado na especificação de entrada, então um cache vazio preguiçoso não corresponderá) — mas o programa salvo carrega seus próprios example_inputs, então reutilize esse modelo StaticCache já inicializado, reiniciado para vazio:
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)O ONNX Runtime executa o grafo como está; a atualização do cache in-place é feita com o CudaSession do ORT (onnxruntime.transformers.io_binding_helper), um wrapper fino sobre o io-binding do ORT. set_buffer_sharing vincula um input.<name> do cache e seu output.<name> correspondente a um único buffer de dispositivo, de modo que os K/V/contador mutados sejam gravados diretamente de volta na entrada; allocate_buffers aloca as saídas restantes (não compartilhadas) — aqui apenas logits; e infer(feed_dict) vincula seus tensores CUDA por ponteiro e executa. Os buffers de cache vêm diretamente dos metadados de entrada do próprio grafo (forma e tipo de get_inputs()), portanto nenhuma configuração de modelo é necessária — o único eixo simbólico (comprimento do cache) se torna 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)O runtime on-device do ExecuTorch é em C++, e a atualização in-place do cache depende de Method::set_output_data_ptr — não exposto pelo runtime Python (executorch.runtime.Method expõe apenas execute/set_inputs/get_outputs), portanto o decodificação sem cópia é um caminho exclusivo de C++. Vincule cada output de cache mutado ao buffer de input de cache correspondente, e os novos K/V/contador serão colocados nos buffers de StaticCache do chamador sem cópias. Assim como nas outras abas, as formas e tamanhos vêm do próprio artefato — aqui, do method_meta do programa (input_tensor_meta/output_tensor_meta → TensorInfo::nbytes()), pois o C++ não possui um runtime Python para 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));
}Quantização
Cada configuração de exportação aceita um quantizer que quantiza o modelo durante a exportação, no grafo FX do torch.export (PT2E ou NNCF, para todos os backends) ou no modelo ONNX ou OpenVINO convertido (ONNX Runtime ou NNCF). Consulte Quantização de exportações para ver os quantizadores, seus backends, a calibração e como adicionar um.
Limitações e soluções alternativas
torch.export, torch.onnx.export e ExecuTorch apresentam limitações em relação a padrões específicos do PyTorch. Os exportadores contornam essas limitações com um pequeno conjunto de patches reversíveis e correções em nível de FX aplicados em pontos bem definidos do fluxo de exportação. Nenhuma dessas etapas é visível a partir da API pública export, mas os pontos mais comuns a saber são:
- FlashAttention e FlexAttention não são exportáveis em nenhum backend.
sdpaé a configuração preferida eeagertambém funciona (mais lento). Defina um deles no modelo antes de chamarexportse ele estiver usando outra coisa. grouped_mmé rastreado sem problemas peloDynamoExportere é traduzido automaticamente para oOnnxExporter. Para oExecutorchExportercom XNNPACK ou MLX, o exportador substitui os especialistas MoE porbatched_mmantes da exportação.
Próximas etapas
- Adicione suporte de exportação para uma nova arquitetura ou backend com os registros de patch e fix em Estendendo os exportadores.
- Quantize um export ou adicione um quantizer em Quantização de exports.