Transformers documentation
Trình xuất
Trình xuất
Xuất bất kỳ PreTrainedModel nào sang ONNX, ExecuTorch hoặc một chương trình PyTorch độc lập, bất kể runtime đích.
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.
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.
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.
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
HfExportersẽ ghi nhật ký một cảnh báo khi phát hiện sự lệch lạc.
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"Yêu cầu macOS 14 trở lên trên Apple Silicon. MLX được bao gồm trong các bánh xe nightly macOS ARM64 ExecuTorch; đây không phải là một gói hoặc phần bổ sung riêng biệt. Cặp nightly sau đã được xác thực với các bài kiểm tra bộ xuất MLX. Cài đặt nó từ registry nightly ExecuTorch tiêu chuẩn:
pip install transformers
pip install \
"executorch==1.6.0.dev20260924" \
"torch==2.15.0.dev20260924" \
--extra-index-url https://download.pytorch.org/whl/nightly/cpuCà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.
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().
Chuyển đổi giữa các runtime bằng cách thay thế lớp 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 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 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.
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)Hình dạng động
Việc truyền dynamic=True đánh dấu mọi chiều 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.
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)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 (đề bài đầy đủ, không có bộ nhớ đệm KV) so với bước decode (một token duy nhất, bộ nhớ đệm KV đã đượ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() (modality="image" hoặc "audio") và get_decoder(), do đó bất kỳ kiến trúc mới nào sử dụng các thành phần này đề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, do đó 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ì mở rộng danh sách.
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}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() 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 như 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 duy nhất dựa trên nó.
The capture chạy model một cách trực tiếp 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 bở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.
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() 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
mang tính 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 prefill thông thường
khi bộ nhớ đệm trống.
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 tokensTrục query chỉ giữ ở dạng biểu tượng khi xuất với hình dạng động (dynamic=True); một bản xuất tĩnh sẽ đóng băng nó ở độ dài đã bắt được, tạo ra một đồ 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 sẽ 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
Các ví dụ ExecuTorch trong phần này, bao gồm cập nhật zero-copy và vòng lặp giải mã, chỉ dành cho XNNPACK. Trình xuất MLX từ chối
StaticCache; hãy sử dụngDynamicCachevớ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, 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 truyền một GenerationConfig với cache_implementation="static" (và một max_cache_len) cùng với 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
)Đồ thị decode hiện có hai trục biểu tượng — độ dài truy vấn (số lượng token bạn đưa vào) 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ộ đệ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ọicomponents["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_sharingcủ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 các lượt đi về phía host.ExecuTorch (XNNPACK) — tắt các cấp phát bộ nhớ lập kế hoạch trên
ExecutorchConfigđể thao tác ghi tại chỗ có thể thực hiện trên chính tensor của người gọi (xem tài liệu tham khảo để biết ý nghĩa của từng cờ):config = ExecutorchConfig( backend="xnnpack", dynamic=True, alloc_graph_input=False, alloc_graph_output=False, alloc_mutable_buffers=False, )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.Methodchỉ cung cấpexecute/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.
Decode-loop inference examples
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 nằm 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:
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 đầu vào; allocate_buffers cấp phát các đầu ra 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 đầu vào của chính đồ thị (hình dạng và kiểu 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 (độ dài cache) trở thành 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 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 đó giải mã 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à K/V/counter mới sẽ nằm trong các bộ đệm StaticCache của người gọi mà không cần sao chép. Giống như 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:
#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 để 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.
sdpalà cài đặt được ưu tiên vàeagercũng hoạt động (chậm hơn). Hãy đặt một trong hai trên model trước khi gọiexportnếu model đang sử dụng cài đặt khác. grouped_mmđược trace tốt quaDynamoExportervà được tự động chuyển đổi choOnnxExporter. VớiExecutorchExportersử dụng XNNPACK hoặc MLX, exporter sẽ thay thế các chuyên gia MoE bằngbatched_mmtrướ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.
- 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.