Transformers documentation
Экспортеры
Экспортеры
Экспортируйте любую модель PreTrainedModel в формат ONNX, ExecuTorch или автономную программу PyTorch, независимо от целевого рантайма.
exporter = DynamoExporter() # or OnnxExporter, ExecutorchExporter
config = DynamoConfig(dynamic=True)
exported = exporter.export(model, inputs, config=config)Экспортеры находятся внутри Transformers, а не в сторонней библиотеке, поэтому изменения архитектуры, новые схемы внимания и пользовательские типы кэша поддерживаются на этапе экспорта сразу после их появления в коде моделирования.
Экспортеры являются экспериментальными. Многие патчи в этом модуле обходят конкретные ошибки в исходных проектах (Torch, ONNX Script, ONNX Runtime, ExecuTorch) и будут удалены, как только исправления появятся в этих проектах. До стабилизации API рассматривайте патчи как привязанные к версиям, используемым в тестовом наборе. Зафиксируйте эти версии в производственных инструментах и ожидайте, что по мере поступления изменений в исходных проектах будут появляться новые патчи, а старые исчезать.
| Экспортер | Вывод | Время выполнения |
|---|---|---|
DynamoExporter | ExportedProgram | Любой рантайм PyTorch, AOT-компиляция |
OnnxExporter | ONNXProgram | Любой ONNX runtime (ORT, TensorRT, OpenVINO) |
ExecutorchExporter | ExecutorchProgramManager | Мобильные и периферийные устройства (ExecuTorch) |
OpenVINOExporter | openvino.Model | OpenVINO runtime (Intel CPU/GPU/NPU) |
AutoHfExporter выбирает подходящий экспортер на основе конфигурации, а AutoExportConfig выбирает правильный класс конфигурации из словаря. Оба следуют тому же паттерну авто-классов, что и в Transformers, что полезно, когда бэкенд выбирается во время выполнения, а не задан жёстко в месте вызова.
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)Установка
Установите зависимости для бэкенда, в который вы планируете экспортировать модель.
Перечисленные ниже версии зафиксированы в тестовом наборе экспортера. Более новые или старые релизы часто также работают, но патчи экспортера нацелены на конкретный интерфейс API, поэтому для производственных инструментов фиксируйте эти версии и ожидайте, что
HfExporterбудет выводить предупреждение при обнаружении расхождений.
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"Требуется macOS 14 или более поздняя версия на Apple Silicon. MLX входит в ночные сборки ExecuTorch для macOS ARM64; это не отдельный пакет или дополнительный компонент. Следующая пара ночных сборок была проверена с помощью тестов экспортера MLX. Установите её из стандартного реестра ночных сборок 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/cpuЯвно установите torch, так как nightly-сборки ExecuTorch не указывают его в качестве зависимости.
Для сборки из исходного кода см. руководство по установке MLX.
pip install transformers "torch==2.12.0" "openvino==2026.3.1"Экспорт модели
Все экспортеры имеют одинаковый интерфейс. Создайте экспортер с конфигурацией и вызовите export().
Переключайтесь между рантаймами, заменяя класс экспортера.
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 выбирает целевую платформу: xnnpack (по умолчанию) для CPU, mlx для GPU Apple Silicon или cuda для GPU с поддержкой CUDA. Выберите соответствующий вариант установки. Для выполнения MLX необходим MLX delegate и его библиотеки Metal;
запрос CUDA в среде без поддержки CUDA вызывает 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)Динамические формы
Передача dynamic=True помечает каждое измерение тензора как динамическое, благодаря чему экспортированный граф принимает входные данные любого размера во время выполнения без повторного трассирования.
Для точного контроля над тем, какие измерения являются динамическими, вместо этого передавайте явный dynamic_shapes, который напрямую передаётся в 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)Генеративные модели
Для авторегрессионной генерации у модели forward разные формы на шаге prefill (полный промпт, без KV-кэша) и на шаге decode (один токен, заполненный KV-кэш). Экспортеры предоставляют ~HfExporter.export_for_generation, который разделяет оба этапа и экспортирует каждый из них.
Для мультимодальных генеративных моделей префилл дополнительно разделяется на энкодер изображений или аудио, языковую модель и lm_head. Обнаружение энкодера и языковой модели выполняется с помощью аксессоров get_encoder() (modality="image" или "audio") и get_decoder(), поэтому любая новая архитектура, использующая их, работает без дополнительной настройки.
Компонент проектора появляется только в том случае, если модель предоставляет его под одним из следующих имён атрибутов: multi_modal_projector, connector, embed_vision, embed_audio. В модели Qwen2-VL, приведённой ниже, проектор объединён с визуальной башней, поэтому в её словаре компонентов нет отдельного ключа multi_modal_projector. Новые архитектуры должны привязывать атрибут проектора к одному из этих имён, а не расширять список.
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}Экспортируемые компоненты представляют собой независимые графы, а не готовый к запуску конвейер вывода. Вызывающая сторона отвечает за запуск каждого энкодера, проекцию эмбеддингов и организацию цикла генерации.
Как работает export_for_generation
decompose_for_generation() один раз выполняет model.generate(**inputs, max_new_tokens=2) и подключает хук к model.forward, чтобы перехватить фактические аргументы kwargs для prefill и decode (а также аргументы kwargs для каждого подмодуля через хуки на каждом энкодере/проекторе/языковой модели, если модель мультимодальная). Именно поэтому метод работает с любой архитектурой, включая модели только с декодером, SSM, encoder-decoder и мультимодальные модели, без необходимости писать отдельный код для каждой модели. export_for_generation представляет собой однострочную обёртку над этим методом.
Захват выполняет модель в режиме eager на inputs, поэтому передавайте небольшие, но репрезентативные значения, например короткий промпт, одно маленькое изображение или несколько аудиокадров. Экспортированная программа не привязана к этим размерам (динамические формы по-прежнему передаются), но меньшие входные данные для захвата делают decompose_for_generation дешевле и сохраняют вывод символьных форм управляемым.
Вызовите decompose_for_generation напрямую, чтобы выполнить действия между декомпозицией и экспортом, например, выполнить прямой проход в режиме eager для проверки, заменить входные данные подмодуля или пропустить этап.
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)Декодирование нескольких токенов
По умолчанию компонент decode представляет собой шаг single-token — один токен запроса против KV-кэша —
поэтому torch.export специализирует ось последовательности запросов на значение 1. Передайте multi_token_decode=True, чтобы захватить decode как multi-token декодирование: decompose_for_generation() объединяет два
последовательных шага декодирования (захват выполняется с max_new_tokens=3) в один forward, благодаря чему эта ось остаётся
символьной. Тогда один граф обслуживает любую длину запроса — один токен (обычное декодирование), несколько токенов
одновременно (продолжение из прошлого, например, приём блока спекулятивных токенов) и обычный prefill,
когда кэш пуст.
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 tokensОсь запроса остаётся символьной только при экспорте с динамической формой (dynamic=True); при статическом экспорте она фиксируется на длине, зафиксированной при захвате, что приводит к графу с фиксированным количеством токенов. Это сочетается со статическим кэшем KV, описанным ниже: объединённый decode на каждом шаге записывает токены в кэш фиксированного размера на месте, а кэш самостоятельно определяет, куда они попадают.
Статический кэш KV
Примеры ExecuTorch в этом разделе, включая обновления без копирования и цикл декодирования, поддерживаются только в XNNPACK. Экспортировщик MLX отклоняет
StaticCache; для MLX используйтеDynamicCache.
generate() по умолчанию наращивает DynamicCache, перераспределяя память по мере расширения последовательности, — это движущаяся цель для экспортированного графа. Статический кэш представляет собой буфер фиксированного размера, выделяемый один раз и заполняемый на месте в текущей позиции на каждом шаге. В сочетании с декодированием нескольких токенов это сводит генерацию к одному экспортированному графу: граф decode принимает кэш фиксированного размера и переменное количество query-токенов, поэтому один граф обслуживает и промпт (пустой кэш → prefill), и каждый сгенерированный токен (заполненный кэш → decode). Экспортируйте его, передавая GenerationConfig с cache_implementation="static" (и max_cache_len) вместе с 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
)Граф decode теперь имеет две символические оси — длину запроса (количество токенов, которые вы передаёте) и длину кэша (max_cache_len, изменяемую при загрузке). dynamic=True помечает эти (и все остальные оси) как Dim.AUTO, поэтому экспортированный граф принимает любую длину промпта и размер кэша при загрузке.
Обновления на месте без копирования
Статический кэш передаётся и изменяется на месте, поэтому один буфер несёт состояние между шагами декодирования без копирования на хост — при условии, что рантайм привязывает буферы вызывающего кода, а не копирует их через собственную арену. Единственная оставшаяся часть, специфичная для каждого бэкенда, — это то, что для этого требуется:
Dynamo — экспортированная программа моделирует запись в кэш как
USER_INPUT_MUTATION, поэтому вызовcomponents["decode"].module()(...)напрямую обновляет переданные вами тензоры кэша. Повторно используйте одни и те же тензоры на каждом шаге; настраивать ничего не нужно.ONNX Runtime — граф декодирования представляет кэш в виде пар
input.<name>/output.<name>. МетодCudaSession.set_buffer_sharingв ORT (onnxruntime.transformers.io_binding_helper) привязывает каждую пару к одному буферу на устройстве, благодаря чему кэш читается и обновляется на месте в течение цикла без передачи данных на хост.ExecuTorch (XNNPACK) — отключите выделение памяти для планирования в
ExecutorchConfig, чтобы запись на месте могла выполняться в тензоре вызывающей стороны (см. справочную информацию о том, что делает каждый флаг):config = ExecutorchConfig( backend="xnnpack", dynamic=True, alloc_graph_input=False, alloc_graph_output=False, alloc_mutable_buffers=False, )Запись на месте без копирования (zero-copy) также требует, чтобы вызывающая сторона привязывала выходные буферы во время выполнения через
Method::set_output_data_ptr— это не доступно в Python-рантайме (executorch.runtime.Methodпредоставляет толькоexecute/set_inputs/get_outputs). Флаги выше настраивают этот механизм, однако запись на месте — это путь, доступный только в C++ (см. пример цикла декодирования ExecuTorch ниже). Из Python обновлённый кэш следует читать обратно из выходов метода на каждом шаге.
Decode-loop inference examples
Цикл имеет одинаковую структуру на всех бэкендах — это один и тот же граф на протяжении всего процесса. Начните с пустого кэша фиксированного размера, передайте весь промпт один раз (пустой кэш → prefill), а затем по одному токену (заполненный кэш → decode). Каждый вызов принимает input_ids, каузальную attention_mask и position_ids (сдвигается на количество новых токенов на каждом шаге), а также кэш, и возвращает логиты для каждой позиции запроса. То, где каждый токен размещается в кэше, отслеживается внутри статического кэша, поэтому нет необходимости дополнительно передавать что-либо через вызов. Настройка кэша различается в зависимости от рантайма (объект StaticCache для Dynamo, сырые буферы устройства для ONNX Runtime, массивы вызывающей стороны на C++ для ExecuTorch), поэтому каждая вкладка ниже строит свой вариант. Вкладки Dynamo и ONNX Runtime обновляют кэш на месте; путь обновления на месте в ExecuTorch реализован на C++ (его Python-рантайм не может этого сделать, как отмечено выше).
torch.export фиксирует запись в статический кэш как USER_INPUT_MUTATION, поэтому module() загруженного графа обновляет StaticCache, который вы передаёте напрямую — один кэш несёт состояние на протяжении всего цикла, и ничего не нужно привязывать или возвращать обратно. register_pytree_node(StaticCache) позволяет torch.export.load развернуть вход StaticCache. Кэш должен быть инициализирован заранее (torch.export встраивает выделенные K/V в спецификацию входа, поэтому ленивый пустой кэш не совпадёт) — но сохранённая программа содержит собственные example_inputs, поэтому переиспользуйте этот уже инициализированный шаблон StaticCache, сбросив его в пустое состояние:
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 выполняет граф как есть; обновление кэша на месте выполняется с помощью CudaSession (onnxruntime.transformers.io_binding_helper), тонкой обёртки над io-binding ORT. set_buffer_sharing связывает вход кэша input.<name> и соответствующий выход output.<name> с одним буфером на устройстве, так что изменённые K/V/счётчик записываются напрямую обратно во вход; allocate_buffers выделяет оставшиеся (необщие) выходы — здесь это только logits; а infer(feed_dict) связывает ваши CUDA-тензоры по указателю и запускает выполнение. Буферы кэша берутся напрямую из метаданных входов самого графа (форма и тип из get_inputs()), поэтому конфигурация модели не требуется — единственная символьная ось (длина кэша) становится 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)On-device runtime ExecuTorch написан на C++, и обновление кэша на месте опирается на Method::set_output_data_ptr — недоступное в Python-рантайме (executorch.runtime.Method предоставляет только execute/set_inputs/get_outputs), поэтому декодирование без копирования — это путь, доступный только в C++. Привяжите каждый изменённый output к соответствующему буферу input кэша, и новые K/V/счётчики попадут в буферы StaticCache вызывающего кода без копирования. Как и в других вкладках, формы и размеры берутся из самого артефакта — здесь это method_meta программы (input_tensor_meta/output_tensor_meta → TensorInfo::nbytes()), так как в C++ нет Python-рантайма для запроса:
#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));
}Квантование
Каждая конфигурация экспорта принимает quantizer, который квантует модель во время экспорта, на графе FX torch.export
(PT2E или NNCF, для каждого бэкенда) или на сконвертированной модели ONNX или OpenVINO (ONNX Runtime или NNCF). См. Квантование экспортов для информации о квантизаторах, их бэкендах, калибровке и о том, как добавить один из них.
Ограничения и обходные решения
torch.export, torch.onnx.export и ExecuTorch имеют ряд ограничений, связанных с конкретными паттернами PyTorch. Экспортеры обходят эти проблемы с помощью небольшого набора обратимых патчей и исправлений на уровне FX, применяемых в определённых точках процесса экспорта. Ничего из этого не видно из публичного API export, но вот наиболее важные моменты, о которых стоит знать:
- FlashAttention и FlexAttention не поддерживают экспорт ни на одном бэкенде.
sdpa— рекомендуемый вариант,eagerтакже работает (медленнее). Если модель использует другой вариант, установите один из них перед вызовомexport. grouped_mmкорректно отслеживается черезDynamoExporterи автоматически переводится дляOnnxExporter. ДляExecutorchExporterс XNNPACK или MLX экспортер заменяет MoE-эксперты наbatched_mmперед экспортом.- Слои со скользящим окном (Mistral, Gemma 3) экспортируются с полным кэшем ключей и значений; скользящая маска по-прежнему применяет окно.
Дальнейшие шаги
- Добавьте поддержку экспорта для новой архитектуры или бэкенда с помощью патча и исправьте реестры в Расширение экспортеров.
- Квантизация экспорта или добавление квантизатора описаны в разделе Квантизация экспорта.