Transformers documentation
Ekspor
Ekspor
Ekspor PreTrainedModel apa pun ke ONNX, ExecuTorch, atau program PyTorch mandiri, terlepas dari runtime target.
exporter = DynamoExporter() # or OnnxExporter, ExecutorchExporter
config = DynamoConfig(dynamic=True)
exported = exporter.export(model, inputs, config=config)Ekspor berada di dalam Transformers, bukan di pustaka hilir, sehingga perubahan arsitektur, pola perhatian baru, dan tipe cache kustom didukung saat ekspor begitu tersedia di kode pemodelan.
Ekspor ini bersifat eksperimental. Banyak patch di modul ini bekerja untuk menghindari bug spesifik di hulu (Torch, ONNX Script, ONNX Runtime, ExecuTorch) dan akan dihapus begitu perbaikan diterapkan di hulu. Hingga API stabil, anggap patch terikat pada versi yang digunakan dalam suite pengujian. Kunci versi tersebut dalam tooling produksi, dan antisipasi munculnya patch baru serta hilangnya patch lama seiring penerapan perubahan di hulu.
| Exporter | Output | Runtime |
|---|---|---|
DynamoExporter | ExportedProgram | Runtime PyTorch apa pun, kompilasi AOT |
OnnxExporter | ONNXProgram | Semua runtime ONNX (ORT, TensorRT, OpenVINO) |
ExecutorchExporter | ExecutorchProgramManager | Perangkat mobile dan edge (ExecuTorch) |
OpenVINOExporter | openvino.Model | Runtime OpenVINO (CPU/GPU/NPU Intel) |
AutoHfExporter memilih exporter yang tepat dari sebuah config, dan AutoExportConfig memilih kelas config yang tepat dari sebuah dict. Keduanya mengikuti pola auto-class yang sama di Transformers, yang berguna ketika backend dipilih pada saat runtime alih-alih di-hardcode di titik pemanggilan.
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)Instalasi
Install dependensi untuk backend yang Anda rencanakan untuk diekspor.
Versi di bawah ini adalah versi yang digunakan oleh suite pengujian exporter. Rilis yang lebih baru atau lebih lama sering kali berfungsi, tetapi patch exporter menargetkan permukaan API tertentu, sehingga untuk tooling produksi, kunci versi ini dan harapkan
HfExporteruntuk mencatat peringatan ketika mendeteksi penyimpangan.
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"Memerlukan macOS 14 atau lebih baru pada Apple Silicon. MLX disertakan dalam roda (wheels) nightly ExecuTorch macOS ARM64; ini bukan paket atau extra terpisah. Pasangan nightly berikut telah divalidasi dengan pengujian exporter MLX. Instal dari registry nightly ExecuTorch standar:
pip install transformers
pip install \
"executorch==1.6.0.dev20260924" \
"torch==2.15.0.dev20260924" \
--extra-index-url https://download.pytorch.org/whl/nightly/cpuInstal torch secara eksplisit karena wheel ExecuTorch nightly tidak mendeklarasikannya sebagai dependensi.
Untuk build dari source, lihat panduan instalasi MLX.
pip install transformers "torch==2.12.0" "openvino==2026.3.1"Ekspor model
Semua exporter berbagi antarmuka yang sama. Buat sebuah exporter dengan konfigurasi, lalu panggil export().
Beralih antar runtime dengan mengganti kelas 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 memilih target: xnnpack (default) untuk CPU, mlx untuk GPU Apple Silicon, atau cuda untuk GPU yang mendukung CUDA. Pilih opsi instalasi yang sesuai. Eksekusi MLX memerlukan MLX delegate dan pustaka Metal-nya;
meminta CUDA tanpa lingkungan yang mendukung CUDA akan memicu 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)Bentuk dinamis
Menyampaikan dynamic=True menandai setiap dimensi tensor sebagai dinamis sehingga graf yang diekspor menerima input berukuran apa pun saat runtime tanpa perlu melacak ulang.
Untuk kontrol yang lebih rinci mengenai dimensi mana yang dinamis, berikan dynamic_shapes secara eksplisit sebagai gantinya, yang diteruskan langsung ke 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)Model generatif
Untuk generasi autoregresif, forward model memiliki bentuk yang berbeda pada langkah prefill (prompt penuh, tanpa KV cache) dibandingkan langkah decode (satu token, KV cache terisi). Ekspor menampilkan ~HfExporter.export_for_generation, yang memisahkan kedua tahap dan mengekspor masing-masing.
Untuk model generatif multi-modal, prefill juga dipecah menjadi encoder gambar atau audio, model bahasa, dan lm_head. Penemuan encoder dan model bahasa menggunakan accessor get_encoder() (modality="image" atau "audio") dan get_decoder(), sehingga arsitektur baru apa pun yang menggunakan ini langsung berfungsi.
Komponen projector hanya muncul ketika model mengekspos satu di bawah nama atribut (multi_modal_projector, connector, embed_vision, embed_audio). Qwen2-VL di bawah ini menggabungkan projectornya ke dalam vision tower, sehingga dict komponennya tidak memiliki kunci multi_modal_projector yang terpisah. Arsitektur baru harus menyelaraskan atribut projector mereka dengan salah satu nama ini, bukan memperpanjang daftar.
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}Komponen yang diekspor adalah graf-graf independen, bukan pipeline inferensi yang siap dijalankan. Pemanggil bertanggung jawab untuk menjalankan setiap encoder, memproyeksikan embedding, dan mengorkestrasikan loop generasi.
Cara kerja export_for_generation
decompose_for_generation() menjalankan model.generate(**inputs, max_new_tokens=2) sekali dan mengaitkan model.forward untuk menangkap kwargs prefill dan decode yang sebenarnya (serta kwargs per-submodul melalui hook pada setiap encoder/projector/language model jika modelnya multi-modal). Itulah mengapa cara ini berfungsi untuk arsitektur apa pun, termasuk decoder-only, SSM, encoder-decoder, dan model multi-modal, tanpa perlu kode perekat per-model. export_for_generation adalah satu baris di atasnya.
The capture menjalankan model secara eagerly pada inputs, sehingga berikan nilai yang kecil namun representatif, seperti prompt pendek, satu gambar kecil, atau beberapa frame audio. Program yang diekspor tidak terikat pada ukuran tersebut (bentuk dinamis tetap mengalir), tetapi input capture yang lebih kecil membuat decompose_for_generation lebih murah dan menjaga inferensi bentuk simbolik tetap dapat ditangani.
Panggil decompose_for_generation secara langsung untuk bertindak antara dekomposisi dan ekspor, seperti menjalankan eager forward untuk verifikasi, mengganti input dari submodule, atau melewati tahap tertentu.
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 multi-token
Secara default, komponen decode adalah langkah single-token — satu token query terhadap KV cache —
sehingga torch.export menspesialisasikan sumbu query-sequence-nya menjadi 1. Lewati multi_token_decode=True untuk menangkap decode sebagai decode multi-token alih-alih: decompose_for_generation() menggabungkan dua
langkah decode yang berurutan (ditangkap dengan max_new_tokens=3) menjadi satu forward, sehingga sumbu tersebut tetap
simbolik. Satu graf kemudian melayani setiap panjang query — satu token (dekoding biasa), banyak token
sekaligus (kelanjutan-dari-masa-lalu, misalnya menerima sepotong token spekulatif), dan prefill biasa
ketika cache kosong.
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 tokensSumbu query hanya tetap simbolik pada ekspor bentuk dinamis (dynamic=True); ekspor statis membekukannya pada panjang yang ditangkap, menghasilkan graf multi-token yang tetap. Ini berpadu dengan KV cache statis di bawah — decode yang digabungkan menulis token dari setiap langkah ke dalam cache berukuran tetap secara in-place, dan cache menangani di mana token tersebut ditempatkan secara internal.
Cache KV statis
Contoh ExecuTorch pada bagian ini, termasuk pembaruan zero-copy dan loop decode, bersifat hanya untuk XNNPACK. Ekspor MLX menolak
StaticCache; gunakanDynamicCachedengan MLX.
generate() secara default membangun DynamicCache, yang dialokasikan ulang saat urutan diperpanjang — sebuah target yang bergerak untuk graf yang diekspor. Cache statis adalah buffer berukuran tetap, yang dialokasikan sekali dan ditulis di tempat pada posisi saat ini di setiap langkah. Digabungkan dengan multi-token decode, hal ini meruntuhkan proses generasi menjadi satu graf yang diekspor: graf decode menerima cache berukuran tetap dan jumlah token query yang variabel, sehingga satu graf melayani baik prompt (cache kosong → prefill) maupun setiap token yang dihasilkan (cache terisi → decode). Ekspor dengan meneruskan GenerationConfig dengan cache_implementation="static" (dan max_cache_len) bersama 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
)Graf decode sekarang memiliki dua sumbu simbolik — panjang query (jumlah token yang Anda masukkan) dan panjang cache (max_cache_len, dapat diubah ukurannya saat pemuatan). dynamic=True menandai sumbu-sumbu ini (dan setiap sumbu lainnya) sebagai Dim.AUTO, sehingga graf yang diekspor menerima panjang prompt dan ukuran cache apa pun saat pemuatan.
Pembaruan di tempat tanpa salinan
Cache statis diteruskan dan dimutasi di tempat, sehingga satu buffer membawa state melintasi langkah-langkah decode tanpa salinan host — selama runtime mengikat buffer pemanggil alih-alih menyalin melalui arena miliknya sendiri. Yang dibutuhkan adalah satu-satunya bagian per-backend yang tersisa:
Dynamo — program yang diekspor memodelkan penulisan cache sebagai
USER_INPUT_MUTATION, sehingga memanggilcomponents["decode"].module()(...)akan memperbarui tensor cache yang Anda berikan secara langsung. Gunakan kembali tensor yang sama di setiap langkah; tidak ada yang perlu dikonfigurasi.ONNX Runtime — graf decode mengekspos cache sebagai pasangan
input.<name>/output.<name>yang cocok.CudaSession.set_buffer_sharingdari ORT (onnxruntime.transformers.io_binding_helper) mengikat setiap pasangan ke satu buffer perangkat, sehingga cache dibaca dan diperbarui di tempatnya selama loop tanpa perjalanan bolak-balik ke host.ExecuTorch (XNNPACK) — matikan alokasi perencanaan memori pada
ExecutorchConfigagar penulisan in-place dapat dilakukan pada tensor milik pemanggil (lihat referensi untuk mengetahui fungsi masing-masing flag):config = ExecutorchConfig( backend="xnnpack", dynamic=True, alloc_graph_input=False, alloc_graph_output=False, alloc_mutable_buffers=False, )Penulisan in-place tanpa salinan juga memerlukan pemanggil untuk mengikat buffer output pada waktu runtime melalui
Method::set_output_data_ptr— tidak ditampilkan oleh runtime Python (executorch.runtime.Methodhanya mengeksposexecute/set_inputs/get_outputs). Flag di atas mengonfigurasinya, tetapi penulisan in-place adalah jalur hanya C++ (lihat contoh loop decode ExecuTorch di bawah). Dari Python, baca kembali cache yang diperbarui dari output metode pada setiap langkah.
Decode-loop inference examples
Loop-nya memiliki bentuk yang sama di setiap backend — ini adalah grafik yang sama di sepanjang proses. Mulai dari cache berukuran tetap yang kosong, masukkan seluruh prompt sekali (cache kosong → prefill), lalu satu token pada satu waktu (cache terisi → decode). Setiap panggilan meneruskan input_ids, attention_mask kausal, dan position_ids (diperbarui sesuai jumlah token baru pada setiap langkah), ditambah cache, dan mengembalikan logits untuk setiap posisi query. Di mana setiap token ditempatkan dalam cache dilacak secara internal oleh cache statis, sehingga tidak ada hal tambahan yang perlu diteruskan melalui panggilan. Cara cache disiapkan berbeda di setiap runtime (objek StaticCache untuk Dynamo, buffer perangkat mentah untuk ONNX Runtime, array pemanggil di C++ untuk ExecuTorch), sehingga setiap tab membangunnya sendiri di bawah. Tab Dynamo dan ONNX Runtime memperbarui cache di tempat; jalur di tempat ExecuTorch adalah C++ (runtime Python-nya tidak bisa, seperti yang disebutkan di atas).
torch.export mencatat penulisan cache statis sebagai USER_INPUT_MUTATION, sehingga module() dari graf yang dimuat memperbarui StaticCache yang Anda lewati secara langsung — satu cache membawa state di seluruh loop tanpa perlu mengikat atau mengembalikan apa pun. register_pytree_node(StaticCache) memungkinkan torch.export.load untuk unflatten input StaticCache. Cache harus diinisialisasi di awal (torch.export membakar K/V yang dialokasikan ke dalam spesifikasi input, sehingga cache kosong yang dimuat secara lazy tidak akan cocok) — tetapi program yang disimpan membawa example_inputs miliknya sendiri, jadi gunakan kembali template StaticCache yang sudah diinisialisasi tersebut, direset menjadi kosong:
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 menjalankan graf apa adanya; pembaruan cache in-place dilakukan dengan CudaSession (onnxruntime.transformers.io_binding_helper), sebuah wrapper tipis di atas io-binding ORT. set_buffer_sharing mengikat input.<name> cache dan output.<name> yang sesuai ke satu buffer perangkat, sehingga K/V/counter yang dimutasi ditulis langsung kembali ke input; allocate_buffers mengalokasikan output lainnya (non-shared) — di sini hanya logits; dan infer(feed_dict) mengikat tensor CUDA Anda melalui pointer dan menjalankan. Buffer cache berasal langsung dari metadata input graf itu sendiri (shape dan tipe dari get_inputs()), sehingga tidak diperlukan konfigurasi model — satu sumbu simbolik (panjang cache) menjadi 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)Runtime on-device ExecuTorch menggunakan C++, dan pembaruan cache in-place bergantung pada Method::set_output_data_ptr — tidak dipaparkan oleh runtime Python (executorch.runtime.Method hanya mengekspos execute/set_inputs/get_outputs), sehingga decode zero-copy adalah jalur khusus C++. Bind setiap output cache yang dimutasi ke buffer input cache yang sesuai, dan K/V/penghitung baru akan masuk ke buffer StaticCache pemanggil tanpa salinan. Seperti pada tab lain, bentuk dan ukuran berasal dari artefak itu sendiri — di sini dari method_meta program (input_tensor_meta/output_tensor_meta → TensorInfo::nbytes()), karena C++ tidak memiliki runtime Python untuk ditanyakan:
#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));
}Kuantisasi
Setiap konfigurasi ekspor menerima quantizer yang mengkuantisasi model selama ekspor, pada grafik FX torch.export (PT2E atau NNCF, untuk setiap backend) atau pada model ONNX atau OpenVINO yang telah dikonversi (ONNX Runtime atau NNCF). Lihat Mengkuantisasi ekspor untuk daftar quantizer, backend mereka, kalibrasi, dan cara menambahkannya.
Keterbatasan dan solusi
torch.export, torch.onnx.export, dan ExecuTorch masing-masing memiliki keterbatasan di sekitar pola PyTorch tertentu. Eksporernya mengatasi hal ini dengan serangkaian kecil patch yang dapat dibalikkan dan perbaikan pada level FX yang diterapkan pada titik-titik yang terdefinisi dengan baik dalam alur ekspor. Tidak ada dari hal ini yang terlihat dari API export publik, tetapi hal-hal paling umum yang perlu diketahui:
- FlashAttention dan FlexAttention tidak dapat diekspor di backend mana pun.
sdpaadalah pengaturan yang disarankan daneagerjuga berfungsi (lebih lambat). Atur salah satunya pada model sebelum memanggilexportjika model tersebut menggunakan pengaturan lain. grouped_mmdapat ditelusuri dengan baik melaluiDynamoExporterdan diterjemahkan secara otomatis untukOnnxExporter. UntukExecutorchExporterdengan XNNPACK atau MLX, exporter mengganti pakar MoE menjadibatched_mmsebelum ekspor.- Lapisan sliding-window (Mistral, Gemma 3) diekspor dengan cache key-value penuh; sliding mask tetap menerapkan jendela tersebut.
Langkah selanjutnya
- Tambahkan dukungan ekspor untuk arsitektur atau backend baru dengan patch dan perbaiki registri di Memperluas exporter.
- Kuantisasi sebuah ekspor, atau tambahkan kuantizer, di Kuantisasi ekspor.