# Ekspor

Ekspor [PreTrainedModel](/docs/transformers/main/id/main_classes/model#transformers.PreTrainedModel) apa pun ke ONNX, ExecuTorch, atau program PyTorch mandiri, terlepas dari runtime target.

```python
exporter = DynamoExporter()  # or OnnxExporter, ExecutorchExporter
config = DynamoConfig(dynamic=True)
exported = exporter.export(model, inputs, config=config)
```

Ekspor ini 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.

> [!WARNING]
> 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.

```python
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.

> [!TIP]
> Versi di bawah ini adalah versi yang digunakan oleh suite pengujian exporter. Rilis yang lebih baru atau lebih lama sering kali tetap berfungsi, tetapi patch exporter menargetkan permukaan API tertentu, sehingga untuk alat produksi, kunci versi ini dan harapkan `HfExporter` untuk mencatat peringatan ketika mendeteksi penyimpangan.

```bash
pip install transformers "torch==2.12.0"
```

```bash
pip install transformers "torch==2.12.0" "onnx==1.21.0" "onnxscript==0.7.0" onnxruntime
```

```bash
pip install transformers "torch==2.12.0" "executorch==1.3.1"
```

Wajib menggunakan macOS 14 atau lebih baru pada Apple Silicon. MLX disertakan dalam wheel nightly ExecuTorch untuk macOS ARM64; ini bukan paket atau extra terpisah. Pasangan nightly berikut telah divalidasi dengan tes exporter MLX. Instal dari registry nightly ExecuTorch standar:

```bash
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
```

Instal `torch` secara eksplisit karena wheel ExecuTorch nightly tidak mendeklarasikannya sebagai dependensi. Untuk build dari source, lihat [panduan instalasi MLX](https://docs.pytorch.org/executorch/main/backends/mlx/mlx-overview.html).

```bash
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()](/docs/transformers/main/id/main_classes/exporters#transformers.exporters.HfExporter.export).

Beralih antar runtime dengan mengganti kelas exporter.

```python
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)
```

```python
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](/docs/transformers/main/id/main_classes/exporters#transformers.exporters.ExecutorchConfig.backend) memilih target: `xnnpack` (default) untuk CPU, `mlx` untuk GPU Apple Silicon, atau `cuda` untuk GPU yang mendukung CUDA. Pilih [opsi instalasi](#installation) yang sesuai. Eksekusi MLX memerlukan MLX delegate dan pustaka Metal-nya; permintaan CUDA tanpa lingkungan yang mendukung CUDA akan memicu `RuntimeError`.

```python
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()))
```

```python
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](https://pytorch.org/docs/stable/export.html).

```python
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)
```

```python
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)
```

```python
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)
```

```python
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 menyediakan `~HfExporter.export_for_generation`, yang memisahkan kedua tahap tersebut 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()](/docs/transformers/main/id/main_classes/model#transformers.PreTrainedModel.get_encoder) (`modality="image"` atau `"audio"`) dan [get_decoder()](/docs/transformers/main/id/main_classes/model#transformers.PreTrainedModel.get_decoder), sehingga arsitektur baru apa pun yang menggunakan ini langsung berfungsi tanpa perlu konfigurasi tambahan.

Komponen projector hanya muncul ketika model mengeksposnya 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 komponen tidak memiliki kunci `multi_modal_projector` yang terpisah. Arsitektur baru harus menyelaraskan atribut projector mereka dengan salah satu nama ini, alih-alih memperpanjang daftar.

```python
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}
```

```python
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}
```

```python
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": ...}
```

```python
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}
```

> [!WARNING]
> 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()](/docs/transformers/main/id/main_classes/exporters#transformers.exporters.utils.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.

Proses capture menjalankan model secara eager 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 tahap dekomposisi dan ekspor, seperti menjalankan eager forward untuk verifikasi, mengganti input dari submodule, atau melewati suatu tahap.

```python
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. Sampaikan `multi_token_decode=True` untuk menangkap `decode` sebagai decode **multi-token** alih-alih: [decompose_for_generation()](/docs/transformers/main/id/main_classes/exporters#transformers.exporters.utils.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 (decoding biasa), banyak token sekaligus (kelanjutan-dari-masa-lalu, misalnya menerima sepotong token spekulatif), dan prefill biasa ketika cache kosong.

```python
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 tokens
```

```python
from 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 tokens
```

```python
from 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
```

Sumbu query hanya tetap simbolik pada ekspor bentuk dinamis (`dynamic=True`); pada ekspor statis, sumbu ini dibekukan pada panjang yang ditangkap, sehingga menghasilkan graf multi-token yang tetap. Hal ini dapat dikombinasikan dengan KV cache statis di bawah — decode yang digabungkan menuliskan 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

> [!NOTE]
> Contoh ExecuTorch pada bagian ini, termasuk pembaruan zero-copy dan loop decode, bersifat **hanya untuk XNNPACK**. Ekspor MLX menolak `StaticCache`; gunakan `DynamicCache` dengan MLX.

`generate()` secara default membangun `DynamicCache` dengan melakukan alokasi ulang saat sekuens bertambah — sebuah target yang bergerak untuk graf yang diekspor. Cache **static** adalah buffer berukuran tetap, dialokasikan sekali dan ditulis di tempat pada posisi saat ini di setiap langkah. Digabungkan dengan [multi-token decode](#multi-token-decode), hal ini menyederhanakan proses generasi menjadi satu graf ekspor tunggal: graf `decode` menerima cache berukuran tetap dan jumlah token query yang *variable*, sehingga satu graf melayani baik prompt (cache kosong → prefill) maupun setiap token yang dihasilkan (cache terisi → decode). Ekspor dengan meneruskan `GenerationConfig` yang memiliki `cache_implementation="static"` (dan `max_cache_len`) bersama dengan `multi_token_decode=True`:

```python
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
)
```

```python
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
)
```

```python
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` kini memiliki dua sumbu simbolik — panjang query (jumlah token yang Anda masukkan) dan panjang cache (`max_cache_len`, yang 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 ke 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 pemanggilan `components["decode"].module()(...)` akan memperbarui tensor cache yang Anda berikan secara langsung. Gunakan 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_sharing` dari ORT (`onnxruntime.transformers.io_binding_helper`) mengikat setiap pasangan ke satu buffer perangkat, sehingga cache dibaca dan diperbarui di tempatnya selama loop tanpa perlu bolak-balik ke host.

- **ExecuTorch (XNNPACK)** — matikan alokasi perencanaan memori pada `ExecutorchConfig` agar penulisan in-place dapat dilakukan pada tensor milik pemanggil (lihat referensi untuk penjelasan fungsi masing-masing flag):

  ```python
  config = ExecutorchConfig(
      backend="xnnpack",
      dynamic=True,
      alloc_graph_input=False,
      alloc_graph_output=False,
      alloc_mutable_buffers=False,
  )
  ```

  > [!NOTE]
  > Penulisan in-place tanpa salinan (zero-copy) juga mengharuskan pemanggil untuk mengikat buffer output pada saat runtime melalui `Method::set_output_data_ptr` — **yang tidak dipaparkan oleh runtime Python** (`executorch.runtime.Method` hanya mengekspos `execute`/`set_inputs`/`get_outputs`). Flag di atas menyiapkan hal tersebut, tetapi penulisan in-place adalah jalur **hanya C++** (lihat contoh loop decode ExecuTorch di bawah). Dari Python, baca kembali cache yang diperbarui dari output method pada setiap langkah.

Contoh inferensi loop decode

Loop-nya memiliki bentuk yang sama di setiap backend — ini adalah *grafik yang sama* di sepanjang proses. Mulai dari cache berukuran tetap yang kosong, berikan 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. Posisi setiap token 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 secara in-place; jalur in-place ExecuTorch menggunakan C++ (runtime Python-nya tidak bisa, seperti yang disebutkan di atas).

torch.export mencatat penulisan static-cache sebagai `USER_INPUT_MUTATION`, sehingga `module()` dari graph yang dimuat akan memperbarui `StaticCache` yang Anda berikan **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 input spec, sehingga cache kosong yang dibuat secara lazy tidak akan cocok) — tetapi program yang disimpan membawa `example_inputs` miliknya sendiri, jadi gunakan kembali template `StaticCache` yang sudah diinisialisasi tersebut, dan reset ke keadaan kosong:

```python
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`), yaitu wrapper tipis di atas io-binding ORT. `set_buffer_sharing` mengikat `input.<name>` cache dan `output.<name>` yang bersesuaian 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 lalu menjalankan inferensi. Buffer cache berasal langsung dari metadata input graf itu sendiri (shape dan type dari `get_inputs()`), sehingga tidak diperlukan konfigurasi model — satu sumbu simbolik (panjang cache) menjadi `max_cache_len`:

```python
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 ditulis dalam C++, dan pembaruan cache in-place bergantung pada `Method::set_output_data_ptr` — **yang tidak dipaparkan oleh runtime Python** (`executorch.runtime.Method` hanya mengekspos `execute`/`set_inputs`/`get_outputs`), sehingga decode zero-copy merupakan jalur khusus C++. Ikaitkan 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 kode C++ tidak memiliki runtime Python untuk ditanyakan:

```cpp
#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 proses ekspor, baik pada grafik FX `torch.export` (PT2E atau NNCF, untuk setiap backend) maupun pada model ONNX atau OpenVINO yang telah dikonversi (ONNX Runtime atau NNCF). Lihat [Mengkuantisasi ekspor](./exporters_quantization) untuk daftar quantizer, backend-nya, kalibrasi, dan cara menambahkannya.

## Keterbatasan dan solusi

`torch.export`, `torch.onnx.export`, dan ExecuTorch masing-masing memiliki keterbatasan pada pola PyTorch tertentu. Ekspor ini mengatasi keterbatasan tersebut 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. `sdpa` adalah yang lebih disarankan
pengaturan `eager` juga berfungsi (lebih lambat). Atur salah satunya pada model sebelum memanggil `export` jika model tersebut menggunakan sesuatu yang lain.
- `grouped_mm` dapat ditelusuri dengan baik melalui `DynamoExporter` dan diterjemahkan secara otomatis untuk `OnnxExporter`.
Untuk `ExecutorchExporter` dengan XNNPACK atau MLX, exporter mengganti pakar MoE menjadi `batched_mm` sebelum 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](./exporters_extend).
- Kuantisasi sebuah ekspor, atau tambahkan kuantizer, di [Kuantisasi ekspor](./exporters_quantization).

