Transformers documentation
Exporters
Exporters
대상 런타임과 관계없이 모든 PreTrainedModel을 ONNX, ExecuTorch 또는 독립형 PyTorch 프로그램으로 내보낼 수 있습니다.
exporter = DynamoExporter() # or OnnxExporter, ExecutorchExporter
config = DynamoConfig(dynamic=True)
exported = exporter.export(model, inputs, config=config)exporter는 하위 라이브러리가 아닌 Transformers 내부에 위치하므로, modeling 코드에 반영되는 시점부터 아키텍처 변경, 새로운 attention 패턴, 사용자 정의 cache type이 export 시점에 지원됩니다.
exporter는 실험적입니다. 이 모듈의 패치 중 상당수는 특정 업스트림 버그(Torch, ONNX Script, ONNX Runtime, ExecuTorch)를 우회하기 위해 작성되었으며, 해당 버그가 업스트림에서 수정되는 대로 제거됩니다. API가 안정화될 때까지 패치는 테스트 스위트에서 사용된 버전과 묶여 있다고 간주하십시오. 프로덕션 도구에서는 해당 버전을 고정하고, 업스트림 변경 사항이 반영됨에 따라 새로운 패치가 추가되고 기존 패치가 제거될 수 있음을 예상하십시오.
| Exporter | 출력 | 런타임 |
|---|---|---|
DynamoExporter | ExportedProgram | 모든 PyTorch 런타임, AOT 컴파일 |
OnnxExporter | ONNXProgram | 모든 ONNX 런타임(ORT, TensorRT, OpenVINO) |
ExecutorchExporter | ExecutorchProgramManager | 모바일 및 엣지 디바이스 (ExecuTorch) |
OpenVINOExporter | openvino.Model | OpenVINO 런타임 (Intel CPU/GPU/NPU) |
AutoHfExporter는 설정에서 올바른 exporter를 선택하고, AutoExportConfig는 dict에서 올바른 config 클래스를 선택합니다. 둘 다 Transformers의 auto-class 패턴을 따르며, 이는 백엔드가 호출 지점에서 하드코딩되는 대신 런타임에 선택될 때 유용합니다.
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)설치
내보내려는 백엔드에 대한 종속성을 설치합니다.
아래 버전은 exporter 테스트 스위트가 고정된(pinned) 버전입니다. 더 최신이거나 오래된 릴리스도 종종 작동하지만, exporter 패치는 특정 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"Apple Silicon에서는 macOS 14 이상을 요구합니다. MLX는 macOS ARM64 ExecuTorch nightly wheels에 포함되어 있으며, 별도의 패키지나 extra가 아닙니다. 다음 nightly pair는 MLX exporter 테스트로 검증되었습니다. 표준 ExecuTorch nightly registry에서 설치하세요:
pip install transformers
pip install \
"executorch==1.6.0.dev20260924" \
"torch==2.15.0.dev20260924" \
--extra-index-url https://download.pytorch.org/whl/nightly/cputorch는 nightly ExecuTorch wheels이 이를 의존성으로 선언하지 않으므로 명시적으로 설치해야 합니다.
소스 빌드의 경우 MLX 설치 가이드를 참조하세요.
pip install transformers "torch==2.12.0" "openvino==2026.3.1"모델 내보내기
모든 exporter는 동일한 인터페이스를 공유합니다. config를 사용하여 exporter를 생성하고 export()를 호출합니다.
exporter 클래스를 교체하여 런타임 간 전환을 수행합니다.
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는 대상 백엔드를 선택합니다: CPU의 경우 xnnpack(기본값), Apple Silicon GPU의 경우 mlx, 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를 전달하면 모든 텐서 차원이 동적으로 표시되어, 재추적 없이 런타임에 임의 크기의 입력을 받는 내보낸 그래프를 생성합니다.
어떤 차원이 동적인지에 대한 세밀한 제어를 위해, torch.export.export로 직접 전달되는 명시적인 dynamic_shapes를 대신 전달하세요.
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 캐시 있음)에서 서로 다른 형태를 가집니다. Exporter는 ~HfExporter.export_for_generation을 노출하며, 이 메서드는 두 단계를 분리하여 각각 내보냅니다.
멀티모달 생성 모델의 경우, prefill은 이미지 또는 오디오 인코더, 언어 모델, lm_head로 추가로 분리됩니다. 인코더와 언어 모델 탐색에는 get_encoder()(modality="image" 또는 "audio") 및 get_decoder() 접근자가 사용되므로, 이러한 접근자를 사용하는 새로운 아키텍처는 추가 설정 없이 바로 작동합니다.
모델이 특정 속성 이름(multi_modal_projector, connector, embed_vision, embed_audio)으로 projector를 노출할 때만 projector 구성 요소가 표시됩니다. 아래 Qwen2-VL은 projector를 vision tower에 통합하므로, 해당 구성 요소 사전에 별도의 multi_modal_projector 키가 없습니다. 새로운 아키텍처는 목록을 확장하는 대신 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에 훅을 걸어 실제 prefill 및 decode kwargs(모델이 멀티모달인 경우 각 encoder/projector/language model에 대한 훅을 통해 서브모듈별 kwargs 포함)를 캡처합니다. 이 때문에 decoder-only, SSM, encoder-decoder, 멀티모달 모델을 포함한 모든 아키텍처에서 모델별 추가 코딩 없이 작동합니다. export_for_generation는 이를 기반으로 한 한 줄 함수입니다.
캡처는 inputs에 대해 모델을 즉시(eagerly) 실행하므로, 짧은 프롬프트, 작은 단일 이미지, 또는 몇 프레임의 오디오와 같이 작지만 대표성을 갖는 값을 전달하세요. 내보낸 프로그램은 해당 크기에 고정되지 않습니다(동적 shape가 여전히 흐름을 유지하지만), 더 작은 캡처 입력은 decompose_for_generation의 비용을 낮추고 심볼릭 shape 추론을 관리 가능하게 유지합니다.
decompose_for_generation을 직접 호출하여 분해와 내보내기 사이에 동작을 수행할 수 있습니다. 예를 들어, 검증을 위해 eager forward를 실행하거나, 서브모듈의 입력을 교체하거나, 특정 단계를 건너뛰는 등의 작업을 수행할 수 있습니다.
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 컴포넌트는 단일 토큰 단계입니다. 즉, KV 캐시를 대상으로 하나의 쿼리 토큰을 처리하므로 torch.export는 쿼리 시퀀스 축을 1로 특수화합니다. multi_token_decode=True를 전달하면 decode를 멀티 토큰 디코딩으로 캡처합니다. decompose_for_generation()는 두 개의 연속된 디코딩 단계(max_new_tokens=3로 캡처)를 하나의 포워드로 병합하여 해당 축이 심볼릭 상태로 유지되도록 합니다. 이렇게 하나의 그래프가 모든 쿼리 길이를 처리할 수 있습니다. 하나의 토큰(일반 디코딩), 여러 토큰을 한 번에 처리(예: 스펙ulative 토큰 청크를 수락하는 과거로부터의 연속 처리), 그리고 캐시가 비어 있는 경우의 일반 프리필입니다.
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 캐시와 조합하여 사용할 수 있으며, 병합된 디코드는 각 단계의 토큰을 고정 크기 캐시에 인플레이스로 기록하고, 캐시는 내부적으로 토큰이 저장될 위치를 처리합니다.
정적 KV 캐시
이 섹션의 ExecuTorch 예제, 즉 zero-copy 업데이트와 decode 루프는 XNNPACK 전용입니다. MLX exporter는
StaticCache를 거부하므로, MLX에서는DynamicCache를 사용하세요.
generate()는 기본적으로 DynamicCache를 확장하며, 시퀀스가 길어질 때마다 재할당을 수행합니다. 이는 내보낸 그래프에 대해 움직이는 대상이 됩니다. static 캐시는 고정된 크기의 버퍼로, 한 번만 할당되고 각 단계에서 현재 위치에 인플레이스로 기록됩니다. multi-token decode와 결합하면 생성 과정을 단일 내보내기 그래프로 축소할 수 있습니다. decode 그래프는 고정된 크기의 캐시와 변수 수의 쿼리 토큰을 입력으로 받으므로, 하나의 그래프가 프롬프트(빈 캐시 → 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로 표시하므로, 내보낸 그래프는 로드 시점에 임의의 프롬프트 길이와 캐시 크기를 받아들입니다.
Zero-copy in-place 업데이트
정적 캐시는 전달된 후 제자리에서 변경되므로, 런타임이 자체 아레나를 통해 복사하는 대신 호출자의 버퍼를 바인딩하는 한, 호스트 복사 없이 하나의 버퍼가 디코딩 단계 간에 상태를 유지합니다. 이를 위해 필요한 것은 백엔드별로 남은 유일한 부분입니다:
Dynamo — 내보낸 프로그램은 캐시 쓰기를
USER_INPUT_MUTATION으로 모델링하므로,components["decode"].module()(...)를 호출하면 전달한 캐시 텐서를 직접 업데이트합니다. 각 단계마다 동일한 텐서를 재사용하면 되며, 설정할 것이 없습니다.ONNX Runtime — 디코드 그래프는 캐시를 매칭된
input.<name>/output.<name>쌍으로 노출합니다. ORT의CudaSession.set_buffer_sharing(onnxruntime.transformers.io_binding_helper)는 각 쌍을 하나의 디바이스 버퍼에 바인딩하여, 루프 동안 캐시를 호스트 왕복 없이 제자리에서 읽고 업데이트합니다.ExecuTorch (XNNPACK) —
ExecutorchConfig에서 메모리 계획 할당을 비활성화하여 in-place write가 호출자의 자체 텐서에 기록되도록 합니다(각 플래그의 역할은 참고 문서를 확인하세요):config = ExecutorchConfig( backend="xnnpack", dynamic=True, alloc_graph_input=False, alloc_graph_output=False, alloc_mutable_buffers=False, )zero-copy in-place write는 호출자가 런타임에
Method::set_output_data_ptr을 통해 출력 버퍼를 바인딩해야 합니다 — Python 런타임에서는 노출되지 않습니다 (executorch.runtime.Method는execute/set_inputs/get_outputs만 노출합니다). 위의 플래그는 이를 설정하지만, in-place write는 C++ 전용 경로입니다(아래의 ExecuTorch decode-loop 예시 참조). Python에서는 각 단계마다 메서드 출력에서 업데이트된 캐시를 다시 읽어 와야 합니다.
Decode-loop inference examples
루프는 모든 백엔드에서 동일한 형태이며, 전체적으로 동일한 그래프를 따릅니다. 빈 고정 크기 캐시에서 시작하여 프롬프트 전체를 한 번에 입력합니다(빈 캐시 → prefill), 이후에는 토큰을 하나씩 처리합니다(채워진 캐시 → decode). 각 호출에서는 input_ids, 인과적 attention_mask, position_ids(각 단계마다 새 토큰 수만큼 증가)와 캐시를 전달하고, 모든 쿼리 위치의 logits를 반환받습니다. 각 토큰이 캐시의 어디에 배치되는지는 정적 캐시가 내부적으로 추적하므로, 호출을 통해 별도로 전달할 것이 없습니다. 캐시 설정 방식은 런타임마다 다릅니다(Dynamo의 경우 StaticCache 객체, ONNX Runtime의 경우 원시 디바이스 버퍼, ExecuTorch의 경우 C++에서 호출자 배열), 따라서 각 탭은 아래에서 자체적으로 구성합니다. Dynamo와 ONNX Runtime 탭은 캐시를 제자리(in-place)로 업데이트하며, 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은 그래프를 그대로 실행하며, 인플레이스 캐시 업데이트는 ORT의 CudaSession(onnxruntime.transformers.io_binding_helper)를 통해 수행됩니다. 이는 ORT io-binding의 얇은 래퍼입니다. set_buffer_sharing는 캐시 input.<name>와 그에 대응하는 output.<name>을 하나의 디바이스 버퍼에 바인딩하여, 변경된 K/V/카운터가 입력으로 바로 다시 기록되도록 합니다. allocate_buffers는 나머지(공유되지 않는) 출력, 여기서는 logits만 할당합니다. 그리고 infer(feed_dict)는 포인터로 CUDA 텐서를 바인딩하고 실행합니다. 캐시 버퍼는 그래프 자체의 입력 메타데이터(get_inputs()의 shape와 type)에서 직접 가져오므로 모델 설정이 필요하지 않습니다. 유일한 심볼릭 축(캐시 길이)은 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)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));
}양자화
모든 export config는 quantizer를 받으며, 이 quantizer는 export 과정에서 모델을 양자화합니다. 대상은 torch.export FX 그래프(모든 백엔드에서 PT2E 또는 NNCF)이거나 변환된 ONNX 또는 OpenVINO 모델(ONNX Runtime 또는 NNCF)입니다. quantizer, 해당 백엔드, calibration, 그리고 추가 방법에 대해서는 Quantizing exports를 참조하세요.
한계 및 우회 방법
torch.export, torch.onnx.export, ExecuTorch는 각각 특정 PyTorch 패턴 주변에서 미흡한 부분이 있습니다. 익스포터는 이러한 문제를 해결하기 위해, 내보내기 플로우의 잘 정의된 지점에서 적용되는 소수의 가역적 패치와 FX 레벨 수정을 사용합니다. 이 과정은 공개된 export API에서는 보이지 않지만, 가장 일반적으로 알아야 할 사항은 다음과 같습니다:
- FlashAttention과 FlexAttention은 어떤 백엔드에서도 내보낼 수 없습니다.
sdpa가 권장 설정이며eager도 작동합니다(속도가 느림). 다른 설정을 사용 중이라면export를 호출하기 전에 모델에 이 중 하나를 설정하세요. grouped_mm은DynamoExporter를 통해 정상적으로 추적되며,OnnxExporter에는 자동으로 변환됩니다. XNNPACK 또는 MLX를 사용하는ExecutorchExporter의 경우, 익스포터는 내보내기 전에 MoE 전문가를batched_mm로 교체합니다.
다음 단계
- 내보내기 확장의 패치 및 수정 레지스트리를 사용하여 새로운 아키텍처 또는 백엔드에 대한 내보내기 지원을 추가합니다.
- Quantizing exports에서 내보내기를 양자화하거나 양자화기를 추가합니다.