# Trình xuất

Xuất bất kỳ [PreTrainedModel](/docs/transformers/main/vi/main_classes/model#transformers.PreTrainedModel) nào sang ONNX, ExecuTorch hoặc một chương trình PyTorch độc lập, bất kể runtime đích.

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

Các trình xuất nằm trong Transformers thay vì một thư viện hạ nguồn, do đó các thay đổi kiến trúc, các mẫu chú ý mới và các kiểu bộ nhớ đệm tùy chỉnh được hỗ trợ tại thời điểm xuất ngay khi chúng được đưa vào mã mô hình hóa.

> [!WARNING]
> Các trình xuất (exporter) đang ở giai đoạn thử nghiệm. Nhiều bản vá trong module này được tạo ra để khắc phục các lỗi cụ thể từ các dự án thượng nguồn (Torch, ONNX Script, ONNX Runtime, ExecuTorch) và sẽ được loại bỏ ngay khi bản sửa lỗi được tích hợp lên thượng nguồn. Cho đến khi API ổn định, hãy coi các bản vá này là gắn liền với các phiên bản được sử dụng trong bộ kiểm thử. Hãy cố định (pin) các phiên bản đó trong công cụ sản xuất, và dự kiến các bản vá mới sẽ xuất hiện trong khi các bản vá cũ sẽ biến mất khi các thay đổi từ thượng nguồn được áp dụng.

| Exporter               | Đầu ra                     | Thời gian chạy                                    |
| ---------------------- | -------------------------- | ------------------------------------------ |
| `DynamoExporter`     | `ExportedProgram`          | Bất kỳ runtime PyTorch nào, biên dịch AOT       |
| `OnnxExporter`       | `ONNXProgram`              | Bất kỳ runtime ONNX nào (ORT, TensorRT, OpenVINO) |
| `ExecutorchExporter` | `ExecutorchProgramManager` | Thiết bị di động và biên (ExecuTorch)       |
| `OpenVINOExporter`   | `openvino.Model`           | Trình chạy OpenVINO (CPU/GPU/NPU của Intel)       |

`AutoHfExporter` chọn exporter phù hợp từ một config, và `AutoExportConfig` chọn lớp config phù hợp từ một dict. Cả hai đều tuân theo cùng một mẫu auto-class trong Transformers, điều này hữu ích khi backend được chọn vào lúc runtime thay vì được hardcode tại vị trí gọi.

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

## Cài đặt

Cài đặt các phụ thuộc cho backend mà bạn dự định xuất ra.

> [!TIP]
> Các phiên bản bên dưới là những phiên bản mà bộ kiểm thử exporter được cố định. Các bản phát hành mới hơn hoặc cũ hơn thường vẫn hoạt động, nhưng các bản vá của exporter nhắm đến một bề mặt API cụ thể, vì vậy đối với công cụ sản xuất, hãy cố định các phiên bản này và kỳ vọng `HfExporter` sẽ ghi nhật ký một cảnh báo khi phát hiện sự lệch lạc.

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

Yêu cầu macOS 14 trở lên trên Apple Silicon. MLX được bao gồm trong các wheel nightly macOS ARM64 của ExecuTorch; đây không phải là một gói hoặc extra riêng biệt. Cặp nightly sau đã được xác thực với các bài kiểm tra exporter MLX. Cài đặt nó từ registry nightly ExecuTorch tiêu chuẩn:

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

Cài đặt `torch` một cách tường minh vì các wheel ExecuTorch nightly không khai báo nó như một phụ thuộc. Đối với việc build từ mã nguồn, xem [hướng dẫn cài đặt MLX](https://docs.pytorch.org/executorch/main/backends/mlx/mlx-overview.html).

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

## Xuất mô hình

Tất cả các exporter chia sẻ cùng một giao diện. Tạo một exporter với một config, và gọi [export()](/docs/transformers/main/vi/main_classes/exporters#transformers.exporters.HfExporter.export).

Chuyển đổi giữa các runtime bằng cách thay thế lớp 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/vi/main_classes/exporters#transformers.exporters.ExecutorchConfig.backend) chọn đích: `xnnpack` (mặc định) cho CPU, `mlx` cho GPU Apple Silicon, hoặc `cuda` cho GPU hỗ trợ CUDA. Chọn [tùy chọn cài đặt](#installation) tương ứng. Việc thực thi MLX yêu cầu MLX delegate và các thư viện Metal của nó; yêu cầu CUDA trong môi trường không hỗ trợ CUDA sẽ gây ra `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)
```

## Hình dạng động

Việc truyền `dynamic=True` đánh dấu mọi chiều của tensor là động, cho phép đồ thị đã xuất nhận đầu vào có kích thước bất kỳ lúc runtime mà không cần trace lại.

Để kiểm soát chi tiết hơn các chiều nào là động, hãy truyền `dynamic_shapes` rõ ràng thay vào đó, giá trị này sẽ được chuyển trực tiếp đến [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)
```

## Mô hình sinh

Đối với sinh tự hồi quy, `forward` của mô hình có các hình dạng khác nhau ở bước prefill (prompt đầy đủ, không có KV cache) so với bước decode (một token duy nhất, KV cache đã được nạp). Các trình xuất (exporter) cung cấp `~HfExporter.export_for_generation`, tách cả hai giai đoạn và xuất từng giai đoạn.

Đối với các mô hình sinh đa phương thức, quá trình prefill được tách thêm thành bộ mã hóa hình ảnh hoặc âm thanh, mô hình ngôn ngữ và `lm_head`. Việc phát hiện bộ mã hóa và mô hình ngôn ngữ sử dụng các bộ truy cập [get_encoder()](/docs/transformers/main/vi/main_classes/model#transformers.PreTrainedModel.get_encoder) (`modality="image"` hoặc `"audio"`) và [get_decoder()](/docs/transformers/main/vi/main_classes/model#transformers.PreTrainedModel.get_decoder), do đó bất kỳ kiến trúc mới nào sử dụng chúng đều hoạt động ngay lập tức.

Một thành phần projector chỉ xuất hiện khi model công bố một projector dưới tên thuộc tính (`multi_modal_projector`, `connector`, `embed_vision`, `embed_audio`). Qwen2-VL bên dưới gộp projector của nó vào vision tower, vì vậy dict thành phần của nó không có khóa `multi_modal_projector` riêng biệt. Các kiến trúc mới phải căn chỉnh thuộc tính projector của chúng với một trong những tên này thay vì làm tăng danh sách.

```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]
> Các thành phần được xuất ra là các đồ thị độc lập, không phải một pipeline suy luận sẵn sàng để chạy. Người gọi chịu trách nhiệm chạy từng encoder, chiếu các embedding và điều phối vòng lặp sinh.

### Cách `export_for_generation` hoạt động

[decompose_for_generation()](/docs/transformers/main/vi/main_classes/exporters#transformers.exporters.utils.decompose_for_generation) chạy `model.generate(**inputs, max_new_tokens=2)` một lần và gắn hook vào `model.forward` để bắt các kwargs thực tế của prefill và decode (cùng với kwargs cho từng submodule thông qua các hook trên mỗi encoder/projector/language model nếu mô hình là đa phương thức). Đó là lý do nó hoạt động với mọi kiến trúc, bao gồm decoder-only, SSM, encoder-decoder và các mô hình đa phương thức, mà không cần code kết nối riêng cho từng mô hình. `export_for_generation` là một dòng lệnh duy nhất dựa trên nó.

Việc capture chạy model theo cách eager trên `inputs`, vì vậy hãy truyền các giá trị nhỏ nhưng đại diện, chẳng hạn như một prompt ngắn, một hình ảnh nhỏ duy nhất, hoặc một vài khung âm thanh. Chương trình được xuất ra không bị ràng buộc với các kích thước đó (các hình dạng động vẫn được truyền qua), nhưng các đầu vào capture nhỏ hơn sẽ giúp `decompose_for_generation` rẻ hơn và giữ cho việc suy luận hình dạng biểu tượng ở mức khả thi.

Gọi trực tiếp `decompose_for_generation` để thực hiện các thao tác giữa bước phân rã và xuất, chẳng hạn như chạy một lần forward kiểu eager để xác minh, thay đổi đầu vào của một submodule, hoặc bỏ qua một giai đoạn.

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

### Giải mã đa token

Mặc định, thành phần `decode` là một bước **single-token** — một token truy vấn đối với bộ nhớ đệm KV — do đó `torch.export` đặc biệt hóa trục chuỗi truy vấn của nó thành 1. Truyền `multi_token_decode=True` để bắt `decode` như một bước giải mã **multi-token** thay thế: [decompose_for_generation()](/docs/transformers/main/vi/main_classes/exporters#transformers.exporters.utils.decompose_for_generation) gộp hai bước decode liên tiếp (nó bắt với `max_new_tokens=3`) thành một lần forward, nhờ đó trục đó vẫn ở dạng biểu tượng. Một đồ thị duy nhất sau đó phục vụ mọi độ dài truy vấn — một token (giải mã thông thường), nhiều token cùng lúc (tiếp nối từ quá khứ, ví dụ chấp nhận một khối token suy đoán), và một lần prefill thông thường khi bộ nhớ đệm trống.

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

Trục query chỉ giữ ở dạng biểu tượng khi xuất với hình dạng động (`dynamic=True`); xuất tĩnh sẽ đóng băng nó ở độ dài đã bắt, tạo ra đồ thị đa token cố định. Nó kết hợp với bộ nhớ đệm KV tĩnh ở bên dưới — bước decode hợp nhất ghi các token của từng bước vào bộ nhớ đệm có kích thước cố định tại chỗ, và bộ nhớ đệm tự xử lý vị trí chúng được đặt vào.

### Bộ đệm KV tĩnh

> [!NOTE]
> Các ví dụ ExecuTorch trong phần này, bao gồm cập nhật zero-copy và vòng lặp decode, chỉ dành cho **XNNPACK**. Trình xuất MLX từ chối `StaticCache`; hãy sử dụng `DynamicCache` với MLX.

`generate()` mặc định tạo ra một `DynamicCache`, cấp phát lại bộ nhớ khi chuỗi dài thêm — một mục tiêu di chuyển đối với đồ thị đã xuất. Một cache **static** là một bộ đệm có kích thước cố định, được cấp phát một lần và ghi tại chỗ ở vị trí hiện tại ở mỗi bước. Kết hợp với [multi-token decode](#multi-token-decode), nó thu gọn quá trình sinh thành một đồ thị xuất duy nhất: đồ thị `decode` nhận một cache có kích thước cố định và một số lượng token truy vấn *biến đổi*, do đó một đồ thị phục vụ cả prompt (cache trống → prefill) và mỗi token được sinh (cache đã có dữ liệu → decode). Hãy xuất nó bằng cách chuyển tiếp một `GenerationConfig` với `cache_implementation="static"` (và một `max_cache_len`) cùng với `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
)
```

Đồ thị `decode` hiện có hai trục biểu tượng — độ dài truy vấn (số lượng token bạn cung cấp) và độ dài bộ nhớ đệm (`max_cache_len`, có thể điều chỉnh kích thước khi tải). `dynamic=True` đánh dấu các trục này (và mọi trục khác) là `Dim.AUTO`, do đó đồ thị xuất ra sẽ chấp nhận bất kỳ độ dài prompt và kích thước bộ nhớ đệm nào khi tải.

#### Cập nhật tại chỗ không sao chép

Bộ nhớ đệm tĩnh được truyền vào và thay đổi tại chỗ, do đó một bộ đệm duy nhất mang trạng thái qua các bước giải mã mà không cần sao chép trên host — miễn là thời gian thực thi liên kết các bộ đệm của người gọi thay vì sao chép thông qua vùng nhớ riêng của nó. Điều cần thiết cho việc này là phần duy nhất còn lại cho từng backend:

- **Dynamo** — chương trình đã xuất mô hình hóa thao tác ghi cache dưới dạng `USER_INPUT_MUTATION`, do đó việc gọi `components["decode"].module()(...)` sẽ cập nhật trực tiếp các tensor cache mà bạn truyền vào. Hãy tái sử dụng cùng các tensor đó ở mỗi bước; không cần cấu hình gì thêm.

- **ONNX Runtime** — đồ thị decode phơi bày cache dưới dạng các cặp `input.<name>` / `output.<name>` khớp nhau. `CudaSession.set_buffer_sharing` của ORT (`onnxruntime.transformers.io_binding_helper`) gắn mỗi cặp với một bộ đệm thiết bị, nhờ đó cache được đọc và cập nhật tại chỗ qua vòng lặp mà không cần quay về host.

- **ExecuTorch (XNNPACK)** — tắt các cấp phát bộ nhớ lập kế hoạch trên `ExecutorchConfig` để phép ghi tại chỗ có thể được thực hiện trên tensor của chính người gọi (xem tài liệu tham khảo để biết ý nghĩa của từng cờ):

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

  > [!NOTE]
  > Việc ghi tại chỗ không sao chép (zero-copy in-place write) cũng yêu cầu người gọi phải liên kết các bộ đệm đầu ra vào thời điểm chạy thông qua `Method::set_output_data_ptr` — **không được hiển thị bởi môi trường chạy Python** (`executorch.runtime.Method` chỉ cung cấp `execute`/`set_inputs`/`get_outputs`). Các cờ ở trên thiết lập điều này, nhưng việc ghi tại chỗ là một đường dẫn **chỉ dành cho C++** (xem ví dụ vòng lặp giải mã ExecuTorch bên dưới). Từ Python, hãy đọc lại bộ nhớ đệm đã cập nhật từ các đầu ra của phương pháp ở mỗi bước.

Ví dụ suy luận vòng lặp giải mã

Vòng lặp có cùng hình dạng trên mọi backend — đó là *cùng một* đồ thị xuyên suốt. Bắt đầu từ một bộ đệm tĩnh có kích thước cố định, nạp toàn bộ prompt một lần (bộ đệm trống → prefill), sau đó lần lượt từng token (bộ đệm đã có dữ liệu → decode). Mỗi lần gọi truyền vào `input_ids`, một `attention_mask` nhân quả, và `position_ids` (được tăng lên theo số lượng token mới ở mỗi bước), cùng với bộ đệm, và nhận lại logits cho mọi vị trí truy vấn. Vị trí mỗi token được đặt trong bộ đệm được theo dõi bên trong bởi bộ đệm tĩnh, nên không có gì thêm để truyền qua các lần gọi. Cách thiết lập bộ đệm khác nhau tùy theo runtime (một đối tượng `StaticCache` cho Dynamo, các bộ đệm thiết bị thô cho ONNX Runtime, các mảng do người gọi cung cấp trong C++ cho ExecuTorch), vì vậy mỗi tab sẽ tự xây dựng phần của mình bên dưới. Các tab Dynamo và ONNX Runtime cập nhật bộ đệm tại chỗ; đường dẫn tại chỗ của ExecuTorch là C++ (runtime Python của nó không thể làm điều này, như đã đề cập ở trên).

torch.export ghi nhận thao tác ghi vào static-cache dưới dạng `USER_INPUT_MUTATION`, do đó `module()` của đồ thị đã tải sẽ cập nhật trực tiếp `StaticCache` mà bạn truyền vào **trực tiếp** — một cache duy nhất mang trạng thái xuyên suốt toàn bộ vòng lặp mà không cần phải ràng buộc hay truyền ngược ra. `register_pytree_node(StaticCache)` cho phép `torch.export.load` unflatten đầu vào `StaticCache`. Cache phải được **khởi tạo trước** (torch.export đóng gói K/V đã cấp phát vào input spec, nên một cache trống khởi tạo theo kiểu lazy sẽ không khớp) — nhưng chương trình đã lưu chứa sẵn `example_inputs` của riêng nó, vì vậy hãy tái sử dụng mẫu `StaticCache` đã khởi tạo đó, đặt lại về trống:

```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 thực thi đồ thị theo đúng cách đã có; việc cập nhật cache tại chỗ được thực hiện bằng `CudaSession` của ORT (`onnxruntime.transformers.io_binding_helper`), một lớp bọc mỏng trên ORT io-binding. `set_buffer_sharing` liên kết một `input.<name>` của cache với `output.<name>` tương ứng của nó vào **một** bộ đệm thiết bị, nhờ đó K/V/counter đã bị thay đổi được ghi trực tiếp trở lại vào input; `allocate_buffers` cấp phát các output còn lại (không dùng chung) — ở đây chỉ có `logits`; và `infer(feed_dict)` liên kết các tensor CUDA của bạn theo con trỏ rồi chạy. Các bộ đệm cache được lấy trực tiếp từ metadata input của chính đồ thị (shape và type của `get_inputs()`), nên không cần cấu hình mô hình — trục biểu tượng duy nhất (chiều dài cache) trở thành `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 trên thiết bị của ExecuTorch được viết bằng C++, và việc cập nhật cache tại chỗ dựa vào `Method::set_output_data_ptr` — **không được hiển thị bởi runtime Python** (`executorch.runtime.Method` chỉ cung cấp `execute`/`set_inputs`/`get_outputs`), do đó quá trình giải mã không sao chép (zero-copy) là một đường dẫn chỉ dành cho C++. Gắn mỗi **output** của cache đã bị thay đổi vào bộ đệm **input** cache tương ứng, và các K/V/đếm mới sẽ được đặt vào các bộ đệm `StaticCache` của người gọi mà không cần sao chép. Giống như trong các tab khác, hình dạng và kích thước được lấy từ chính artifact — ở đây là `method_meta` của chương trình (`input_tensor_meta`/`output_tensor_meta` → `TensorInfo::nbytes()`), vì C++ không có runtime Python để truy vấn:

```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));
}
```

## Lượng tử hóa

Mọi cấu hình xuất đều nhận một `quantizer` để lượng tử hóa mô hình trong quá trình xuất, trên đồ thị FX của `torch.export` (PT2E hoặc NNCF, cho mọi backend) hoặc trên mô hình ONNX hoặc OpenVINO đã chuyển đổi (ONNX Runtime hoặc NNCF). Xem [Lượng tử hóa các bản xuất](./exporters_quantization) để biết về các `quantizer`, backend của chúng, hiệu chuẩn và cách thêm một `quantizer`.

## Hạn chế và cách khắc phục

`torch.export`, `torch.onnx.export` và ExecuTorch đều có những điểm chưa hoàn thiện xung quanh các mẫu PyTorch cụ thể. Các trình xuất xử lý những điểm này bằng một tập hợp nhỏ các bản vá có thể đảo ngược và các sửa chữa ở cấp FX được áp dụng tại các điểm xác định rõ ràng trong luồng xuất. Không có gì trong số này hiển thị từ API `export` công khai, nhưng những điều phổ biến nhất cần biết:

- FlashAttention và FlexAttention không thể xuất trên bất kỳ backend nào. `sdpa` là lựa chọn được ưu tiên
setting và `eager` cũng hoạt động (chậm hơn). Đặt một trong hai trên model trước khi gọi `export` nếu nó đang dùng thứ khác.
- `grouped_mm` được theo dõi tốt qua `DynamoExporter` và được tự động chuyển đổi cho `OnnxExporter`.
Với `ExecutorchExporter` sử dụng XNNPACK hoặc MLX, trình xuất sẽ thay thế các chuyên gia MoE bằng `batched_mm` trước khi xuất.
- Các lớp sliding-window (Mistral, Gemma 3) được xuất với bộ nhớ đệm key-value đầy đủ; mặt nạ trượt vẫn áp dụng cửa sổ.

## Các bước tiếp theo

- Thêm hỗ trợ xuất cho một kiến trúc hoặc backend mới bằng cách vá và sửa các registry trong
[Mở rộng các trình xuất](./exporters_extend).
- Thực hiện lượng tử hóa một bản xuất, hoặc thêm bộ lượng tử hóa, trong [Lượng tử hóa các bản xuất](./exporters_quantization).

