Transformers documentation
エクスポート
エクスポート
ターゲットランタイムを問わず、任意のPreTrainedModelを ONNX、ExecuTorch、またはスタンドアロンの PyTorch プログラムにエクスポートできます。
exporter = DynamoExporter() # or OnnxExporter, ExecutorchExporter
config = DynamoConfig(dynamic=True)
exported = exporter.export(model, inputs, config=config)エクスポート機能は下流のライブラリではなく Transformers 本体に実装されているため、アーキテクチャの変更、新しいアテンションパターン、カスタムキャッシュタイプは、モデリングコードに反映された時点でエクスポート時にサポートされます。
エクスポート機能は実験的なものです。このモジュール内の多くのパッチは、特定のアップストリームのバグ(Torch、ONNX Script、ONNX Runtime、ExecuTorch)を回避するために用意されており、アップストリームで修正が反映され次第、削除されます。APIが安定するまで、これらのパッチはテストスイートで使用するバージョンに依存しているものとして扱ってください。本番環境のツールでは、これらのバージョンを固定してください。また、アップストリームの変更が反映されるにつれて、新しいパッチが追加され、古いパッチが削除されることを想定してください。
| エクスポート | 出力 | ランタイム |
|---|---|---|
DynamoExporter | ExportedProgram | PyTorch ランタイム全般、AOT コンパイル |
OnnxExporter | ONNXProgram | 任意の ONNX ランタイム(ORT、TensorRT、OpenVINO) |
ExecutorchExporter | ExecutorchProgramManager | モバイルおよびエッジデバイス(ExecuTorch) |
OpenVINOExporter | openvino.Model | OpenVINO ランタイム(Intel CPU/GPU/NPU) |
AutoHfExporter は設定から適切なエクスポートクラスを選択し、AutoExportConfig は辞書から適切な設定クラスを選択します。どちらも Transformers の auto クラスパターンに従っており、バックエンドをハードコードせず実行時に選択する場合に便利です。
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)インストール
エクスポート先のバックエンドの依存関係をインストールします。
以下のバージョンは、エクスポート用のテストスイートが固定されているものです。新しいリリースや古いリリースでも動作する場合がありますが、エクスポート用のパッチは特定の API 表面をターゲットにしているため、本番環境のツールではこれらのバージョンを固定し、
HfExporterが乖離を検出すると警告をログに出力することを前提としてください。
pip install transformers "torch==2.12.0"pip install transformers "torch==2.12.0" "onnx==1.21.0" "onnxscript==0.7.0" onnxruntimepip install transformers "torch==2.12.0" "executorch==1.3.1"Apple Silicon 搭載の macOS 14 以降が必要です。MLX は macOS ARM64 向けの ExecuTorch ナイトリー版ホイールに同梱されており、独立したパッケージや extra ではありません。以下のナイトリー版ペアは、MLX エクスポートテストで検証済みです。標準の ExecuTorch ナイトリー版レジストリからインストールしてください。
pip install transformers
pip install \
"executorch==1.6.0.dev20260924" \
"torch==2.15.0.dev20260924" \
--extra-index-url https://download.pytorch.org/whl/nightly/cpunightly ExecuTorch の wheel は torch を依存関係として宣言していないため、torch を明示的にインストールしてください。
ソースからのビルドについては、MLX インストールガイドを参照してください。
pip install transformers "torch==2.12.0" "openvino==2026.3.1"モデルのエクスポート
すべてのエクスポートツールは同じインターフェースを共有しています。設定を使用してエクスポートツールを作成し、export()を呼び出します。
エクスポートークラスを切り替えることで、ランタイムを切り替えます。
from transformers import AutoModelForCausalLM, AutoTokenizer
from transformers.exporters import DynamoExporter, DynamoConfig
model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen3-0.6B")
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-0.6B")
inputs = tokenizer("Hello, world!", return_tensors="pt")
exporter = DynamoExporter()
config = DynamoConfig(dynamic=True)
exported = exporter.export(model, inputs, config=config)
# run the exported graph directly
outputs = exported.module()(**inputs)from transformers import AutoModelForCausalLM, AutoTokenizer
from transformers.exporters import OnnxExporter, OnnxConfig
model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen3-0.6B")
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-0.6B")
inputs = tokenizer("Hello, world!", return_tensors="pt")
exporter = OnnxExporter()
config = OnnxConfig(dynamic=True)
onnx_program = exporter.export(model, inputs, config=config)
# save and load with ONNX Runtime
onnx_program.save("model.onnx")
import onnxruntime as ort
session = ort.InferenceSession("model.onnx")
ort_inputs = {k: v.numpy() for k, v in inputs.items()}
outputs = session.run(None, ort_inputs)backend でターゲットを選択します: CPU には xnnpack (デフォルト)、Apple Silicon GPU には mlx、CUDA 対応 GPU には cuda を指定します。それぞれに対応する インストールオプション を選択してください。MLX の実行には MLX デリゲートとその Metal ライブラリが必要です。CUDA 対応環境でない状態で CUDA を要求すると、RuntimeError が発生します。
from transformers import AutoModelForCausalLM, AutoTokenizer
from transformers.exporters import ExecutorchExporter, ExecutorchConfig
model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen3-0.6B")
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-0.6B")
inputs = tokenizer("Hello, world!", return_tensors="pt")
exporter = ExecutorchExporter()
config = ExecutorchConfig(backend="xnnpack", dynamic=True) # Use "mlx" on Apple Silicon.
et_program = exporter.export(model, inputs, config=config)
# save for on-device deployment
et_program.save("model.pte")
# load and run via the ExecuTorch Python runtime
from executorch.runtime import Runtime
program = Runtime.get().load_program("model.pte")
method = program.load_method("forward")
outputs = method.execute(list(inputs.values()))from transformers import AutoModelForCausalLM, AutoTokenizer
from transformers.exporters import OpenVINOExporter, OpenVINOConfig
model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen3-0.6B")
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-0.6B")
inputs = tokenizer("Hello, world!", return_tensors="pt")
exporter = OpenVINOExporter()
config = OpenVINOConfig(dynamic=True)
ov_model = exporter.export(model, inputs, config=config)
ov_model.save("model.xml")
# compile and run on CPU (or "GPU" / "NPU" if available)
import openvino as ov
compiled = ov.Core().compile_model(ov_model, "CPU")
ov_inputs = {k: v.numpy() for k, v in inputs.items()}
outputs = compiled(ov_inputs)動的な形状
dynamic=True を渡すと、すべてのテンソル次元が動的としてマークされ、エクスポートされたグラフが実行時に任意サイズの入力を、再トレースなしで受け入れられるようになります。
どの次元を動的にするかを細かく制御するには、代わりに明示的なdynamic_shapesを渡し、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)生成モデル
自己回帰生成では、モデルのforwardは、プリフィルステップ(プロンプト全体、KVキャッシュなし)とデコードステップ(単一トークン、KVキャッシュが設定済み)で異なる形状を持ちます。エクスポートツールは~HfExporter.export_for_generationを公開しており、これにより両方のステージを分割してそれぞれをエクスポートします。
マルチモーダル生成モデルでは、prefill はさらに画像または音声エンコーダー、言語モデル、および lm_head に分割されます。エンコーダーと言語モデルの検出には get_encoder()(modality="image" または "audio")および get_decoder() アクセサーが使用されるため、これらのアクセサーを利用する新しいアーキテクチャは、追加の対応なしでそのまま動作します。
モデルが特定の属性名(multi_modal_projector、connector、embed_vision、embed_audio)で projector を公開している場合のみ、projector コンポーネントが表示されます。以下の Qwen2-VL は projector を vision tower に組み込んでいるため、そのコンポーネント辞書には独立した multi_modal_projector キーがありません。新しいアーキテクチャでは、リストを増やすのではなく、projector の属性をこれらの名前のいずれかに合わせる必要があります。
from transformers import AutoModelForImageTextToText, AutoProcessor
from transformers.exporters import DynamoExporter, DynamoConfig
model = AutoModelForImageTextToText.from_pretrained("Qwen/Qwen2-VL-2B-Instruct")
processor = AutoProcessor.from_pretrained("Qwen/Qwen2-VL-2B-Instruct")
messages = [{"role": "user", "content": [{"type": "image", "url": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/pipeline-cat-chonk.jpeg"}, {"type": "text", "text": "Describe this image."}]}]
text = processor.apply_chat_template(messages, add_generation_prompt=True, tokenize=False)
inputs = processor(text=text, images=messages[0]["content"][0]["url"], return_tensors="pt").to(model.device)
exporter = DynamoExporter()
config = DynamoConfig(dynamic=True)
components = exporter.export_for_generation(model, inputs, config=config)
# components = {"image_encoder": ExportedProgram, "language_model": ExportedProgram, "lm_head": ExportedProgram, "decode": ExportedProgram}from transformers import AutoModelForImageTextToText, AutoProcessor
from transformers.exporters import OnnxExporter, OnnxConfig
model = AutoModelForImageTextToText.from_pretrained("Qwen/Qwen2-VL-2B-Instruct")
processor = AutoProcessor.from_pretrained("Qwen/Qwen2-VL-2B-Instruct")
messages = [{"role": "user", "content": [{"type": "image", "url": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/pipeline-cat-chonk.jpeg"}, {"type": "text", "text": "Describe this image."}]}]
text = processor.apply_chat_template(messages, add_generation_prompt=True, tokenize=False)
inputs = processor(text=text, images=messages[0]["content"][0]["url"], return_tensors="pt").to(model.device)
exporter = OnnxExporter()
config = OnnxConfig(dynamic=True)
components = exporter.export_for_generation(model, inputs, config=config)
# components = {"image_encoder": ONNXProgram, "language_model": ONNXProgram, "lm_head": ONNXProgram, "decode": ONNXProgram}from transformers import AutoModelForImageTextToText, AutoProcessor
from transformers.exporters import ExecutorchExporter, ExecutorchConfig
model = AutoModelForImageTextToText.from_pretrained("Qwen/Qwen2-VL-2B-Instruct")
processor = AutoProcessor.from_pretrained("Qwen/Qwen2-VL-2B-Instruct")
messages = [{"role": "user", "content": [{"type": "image", "url": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/pipeline-cat-chonk.jpeg"}, {"type": "text", "text": "Describe this image."}]}]
text = processor.apply_chat_template(messages, add_generation_prompt=True, tokenize=False)
inputs = processor(text=text, images=messages[0]["content"][0]["url"], return_tensors="pt").to(model.device)
exporter = ExecutorchExporter()
config = ExecutorchConfig(backend="xnnpack", dynamic=True) # Use "mlx" on Apple Silicon.
components = exporter.export_for_generation(model, inputs, config=config)
# components = {"image_encoder": ExecutorchProgramManager, "language_model": ..., "lm_head": ..., "decode": ...}from transformers import AutoModelForImageTextToText, AutoProcessor
from transformers.exporters import OpenVINOExporter, OpenVINOConfig
model = AutoModelForImageTextToText.from_pretrained("Qwen/Qwen2-VL-2B-Instruct")
processor = AutoProcessor.from_pretrained("Qwen/Qwen2-VL-2B-Instruct")
messages = [{"role": "user", "content": [{"type": "image", "url": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/pipeline-cat-chonk.jpeg"}, {"type": "text", "text": "Describe this image."}]}]
text = processor.apply_chat_template(messages, add_generation_prompt=True, tokenize=False)
inputs = processor(text=text, images=messages[0]["content"][0]["url"], return_tensors="pt").to(model.device)
exporter = OpenVINOExporter()
config = OpenVINOConfig(dynamic=True)
components = exporter.export_for_generation(model, inputs, config=config)
# components = {"image_encoder": openvino.Model, "language_model": openvino.Model, "lm_head": openvino.Model, "decode": openvino.Model}エクスポートされたコンポーネントは独立したグラフであり、すぐに実行できる推論パイプラインではありません。 呼び出し元は、各エンコーダーの実行、埋め込みの射影、および生成ループのオーケストレーションを担当する必要があります。
export_for_generation の動作
decompose_for_generation() は model.generate(**inputs, max_new_tokens=2) を一度実行し、model.forward にフックを設定して、実際の prefill と decode の kwargs(マルチモーダルモデルの場合には、各エンコーダー/プロジェクター/言語モデルへのフックを通じて、サブモジュールごとの kwargs も)を取得します。そのため、モデルごとの個別対応なしに、decoder-only、SSM、encoder-decoder、マルチモーダルモデルを含むあらゆるアーキテクチャで動作します。export_for_generation はこれに対する一行のラッパーです。
キャプチャはinputsに対してモデルをイージリーモードで実行するため、短いプロンプト、1枚の小さな画像、数フレームの音声など、小さくても代表性のある値を渡してください。エクスポートされたプログラムはこれらのサイズに固定されるわけではありません(動的な形状は引き続きフローします)が、キャプチャ時の入力を小さくすることでdecompose_for_generationのコストを下げ、シンボリック形状の推論を扱いやすく保つことができます。
decompose_for_generationを直接呼び出すことで、分解とエクスポートの間に介入できます。例えば、検証用のeager forwardを実行したり、サブモジュールの入力を差し替えたり、特定のステージをスキップしたりできます。
from transformers.exporters.utils import decompose_for_generation
components = decompose_for_generation(model, inputs)
# {"image_encoder": (submodel, fwd_kwargs), "language_model": (...), ..., "decode": (...)}
exported = {}
for name, (submodel, subinputs) in components.items():
eager_outputs = submodel(**subinputs) # sanity-check the eager forward before exporting
exported[name] = exporter.export(submodel, subinputs, config=config)マルチトークンデコード
デフォルトでは、decode コンポーネントは単一トークンのステップです。つまり、KV キャッシュに対して 1 つのクエリトークンを処理します。そのため、torch.export はクエリシーケンスの軸を 1 に特化します。multi_token_decode=True を渡すと、代わりに decode をマルチトークンデコードとしてキャプチャできます。decompose_for_generation() は、連続する 2 つのデコードステップ(max_new_tokens=3 でキャプチャします)を 1 つのフォワードにマージするため、その軸はシンボリックのままになります。これにより、単一のグラフがすべてのクエリ長に対応できます。1 トークン(通常のデコーディング)、複数トークン同時(過去からの継続、例えば投機的トークンのチャンクを承認する場合)、そしてキャッシュが空の場合の通常のプリフィルです。
from transformers.exporters import DynamoExporter, DynamoConfig
exporter = DynamoExporter()
config = DynamoConfig(dynamic=True)
components = exporter.export_for_generation(model, inputs, config=config, multi_token_decode=True)
# components["decode"] now accepts a variable number of query tokensfrom transformers.exporters import OnnxExporter, OnnxConfig
exporter = OnnxExporter()
config = OnnxConfig(dynamic=True)
components = exporter.export_for_generation(model, inputs, config=config, multi_token_decode=True)
# components["decode"] now accepts a variable number of query tokensfrom transformers.exporters import ExecutorchExporter, ExecutorchConfig
exporter = ExecutorchExporter()
config = ExecutorchConfig(backend="xnnpack", dynamic=True) # Use "mlx" on Apple Silicon.
components = exporter.export_for_generation(model, inputs, config=config, multi_token_decode=True)
# components["decode"] now accepts a variable number of query tokensクエリ軸は、動的形状でのエクスポート(dynamic=True)の場合のみシンボリックなまま保持されます。静的エクスポートでは、キャプチャされた長さに固定され、固定長のマルチトークングラフが生成されます。これは以下の静的 KV キャッシュと組み合わせることができ、マージされたデコードは各ステップのトークンを固定サイズのキャッシュにインプレースで書き込み、キャッシュが内部でその配置先を管理します。
静的 KV キャッシュ
このセクションの ExecuTorch の例(ゼロコピー更新およびデコードループを含む)は、XNNPACK 専用です。MLX エクスポートでは
StaticCacheは拒否されるため、MLX ではDynamicCacheを使用してください。
generate() はデフォルトで DynamicCache を構築し、シーケンスが伸びるたびに再割り当てを行います。これはエクスポートされたグラフにとって移動するターゲットです。static キャッシュは固定サイズのバッファであり、一度だけ割り当てられ、各ステップで現在の位置にインプレースで書き込まれます。multi-token decode と組み合わせることで、生成を単一のエクスポート済みグラフに集約できます。decode グラフは固定サイズのキャッシュと 可変 な数のクエリトークンを受け取るため、1つのグラフでプロンプト(空のキャッシュ → prefill)と各生成トークン(埋められたキャッシュ → decode)の両方を処理できます。GenerationConfig に cache_implementation="static"(および max_cache_len)を指定し、multi_token_decode=True とともに転送することでエクスポートします。
from transformers import GenerationConfig
from transformers.exporters import DynamoExporter, DynamoConfig
exporter = DynamoExporter()
gen_config = GenerationConfig(cache_implementation="static", max_cache_len=2048)
components = exporter.export_for_generation(
model, inputs, config=DynamoConfig(dynamic=True), generation_config=gen_config, multi_token_decode=True
)from transformers import GenerationConfig
from transformers.exporters import OnnxExporter, OnnxConfig
exporter = OnnxExporter()
gen_config = GenerationConfig(cache_implementation="static", max_cache_len=2048)
components = exporter.export_for_generation(
model, inputs, config=OnnxConfig(dynamic=True), generation_config=gen_config, multi_token_decode=True
)from transformers import GenerationConfig
from transformers.exporters import ExecutorchExporter, ExecutorchConfig
exporter = ExecutorchExporter()
gen_config = GenerationConfig(cache_implementation="static", max_cache_len=2048)
components = exporter.export_for_generation(
model, inputs, config=ExecutorchConfig(backend="xnnpack", dynamic=True), generation_config=gen_config, multi_token_decode=True
)decode グラフには現在、2 つのシンボリック軸があります。クエリ長(入力するトークン数)と、キャッシュ長(max_cache_len、ロード時にリサイズ可能)です。dynamic=True を指定すると、これら(およびその他のすべての軸)が Dim.AUTO としてマークされ、エクスポートされたグラフはロード時に任意のプロンプト長とキャッシュサイズを受け入れるようになります。
ゼロコピーによるインプレース更新
静的キャッシュは渡され、その場で変更されるため、1つのバッファがデコードステップ間で状態を保持し、ホストへのコピーは発生しません。ただし、ランタイムが呼び出し元のバッファをバインドし、独自のアレーナを介してコピーしないことが条件です。これを実現するために必要な唯一のバックエンド固有の部分は次のとおりです。
Dynamo — エクスポートされたプログラムは、キャッシュの書き込みを
USER_INPUT_MUTATIONとしてモデル化します。そのため、components["decode"].module()(...)を呼び出すと、直接渡したキャッシュテンソルが更新されます。各ステップで同じテンソルを再利用してください。設定は不要です。ONNX Runtime — デコードグラフは、キャッシュを
input.<name>/output.<name>の対応するペアとして公開します。ORT のCudaSession.set_buffer_sharing(onnxruntime.transformers.io_binding_helper)は、各ペアを 1 つのデバイスバッファにバインドするため、ループ内でホストへの往復なしに、キャッシュをその場で読み取り・更新できます。ExecuTorch (XNNPACK) —
ExecutorchConfigのメモリプランニング割り当てを無効にし、インプレース書き込みが呼び出し元のテンソルに直接反映されるようにします(各フラグの詳細はリファレンスを参照してください):config = ExecutorchConfig( backend="xnnpack", dynamic=True, alloc_graph_input=False, alloc_graph_output=False, alloc_mutable_buffers=False, )ゼロコピーのインプレース書き込みでは、呼び出し側が
Method::set_output_data_ptrを介して実行時に出力バッファをバインドする必要があります — Python ランタイムからは公開されていません(executorch.runtime.Methodが公開するのはexecute/set_inputs/get_outputsのみです)。上記のフラグで設定は行われますが、インプレース書き込みは C++ 専用のパスです(以下の ExecuTorch のデコードループの例を参照)。Python からでは、各ステップでメソッドの出力から更新されたキャッシュを読み戻してください。
Decode-loop inference examples
ループはすべてのバックエンドで同じ形状です — 一貫して同じグラフです。空の固定サイズキャッシュから開始し、プロンプト全体を一度に投入します(空のキャッシュ → prefill)。その後、1トークンずつ処理します(埋まったキャッシュ → decode)。各呼び出しでは、input_ids、causalなattention_mask、position_ids(各ステップで新しいトークン数だけ進める)に加えてキャッシュを渡し、すべてのクエリ位置に対するlogitsが返されます。各トークンがキャッシュ内のどこに配置されるかは、静的キャッシュが内部で追跡するため、呼び出しに追加で引き回すものは何もありません。キャッシュのセットアップ方法はランタイムによって異なります(DynamoではStaticCacheオブジェクト、ONNX Runtimeでは生のデバイスバッファ、ExecuTorchではC++の呼び出し元配列)。そのため、各タブは以下でそれぞれ独自の構築を行います。DynamoとONNX Runtimeのタブはキャッシュをインプレースで更新します。ExecuTorchのインプレースパスはC++です(上記の通り、そのPythonランタイムではインプレース更新ができません)。
torch.export records the static-cache write as a USER_INPUT_MUTATION, so the loaded graph’s module() updates the StaticCache you pass in directly — one cache carries state across the whole loop with
nothing to bind or thread back out. register_pytree_node(StaticCache) lets torch.export.load unflatten
the StaticCache input. The cache has to be initialized up front (torch.export bakes the allocated K/V
into the input spec, so a lazy blank cache won’t match) — but the saved program carries its own example_inputs, so reuse that already-initialized StaticCache template, reset to empty:
import copy
import torch
from transformers import StaticCache
from transformers.exporters.exporter_dynamo import register_pytree_node
register_pytree_node(StaticCache)
exported = torch.export.load("decode.pt2")
decode = exported.module() # runs on the device its inputs / cache live on (CUDA here)
# the artifact carries an initialized StaticCache template — reuse it (reset to empty)
_, example_kwargs = exported.example_inputs
past_key_values = copy.deepcopy(example_kwargs["past_key_values"])
past_key_values.reset()
def causal_mask(positions, cache_len): # [1, 1, len(positions), cache_len]
return (torch.arange(cache_len, device="cuda")[None, :] <= positions[:, None])[None, None]
# prefill: the whole prompt in one call
positions = torch.arange(prompt_len, device="cuda")
logits = decode(input_ids=prompt_ids, attention_mask=causal_mask(positions, max_cache_len),
position_ids=positions[None], past_key_values=past_key_values).logits
next_token = logits[:, -1:].argmax(-1)
# decode: query=1 buffers reused in place
input_ids = torch.empty((1, 1), dtype=torch.long, device="cuda")
position_ids = torch.empty((1, 1), dtype=torch.long, device="cuda")
attention_mask = torch.empty((1, 1, 1, max_cache_len), dtype=torch.bool, device="cuda")
slots = torch.arange(max_cache_len, device="cuda")
for position in range(prompt_len, max_cache_len):
input_ids.copy_(next_token)
position_ids.fill_(position)
attention_mask[0, 0, 0].copy_(slots <= position)
logits = decode(input_ids=input_ids, attention_mask=attention_mask,
position_ids=position_ids, past_key_values=past_key_values).logits
next_token = logits[:, -1:].argmax(-1)ONNX Runtime はグラフをそのまま実行します。インプレースなキャッシュの更新は、ORT の CudaSession(onnxruntime.transformers.io_binding_helper)によって行われます。これは ORT の io-binding の薄いラッパーです。set_buffer_sharing は、キャッシュの input.<name> とそれに対応する output.<name> を1 つのデバイスバッファにバインドするため、変更された K/V/カウンタはそのまま入力に書き戻されます。allocate_buffers は、残りの(共有されない)出力を割り当てます。ここでは logits のみです。そして infer(feed_dict) は、CUDA テンソルをポインタでバインドして実行します。キャッシュバッファは、グラフ自身の入力メタデータ(get_inputs() の shape と type)から直接取得されるため、モデル設定は不要です。唯一のシンボリックな軸(キャッシュ長)は max_cache_len になります。
import torch
import onnxruntime as ort
from onnxruntime.transformers.io_binding_helper import CudaSession, TypeHelper
def causal_mask(positions, cache_len):
return (torch.arange(cache_len, device="cuda")[None, :] <= positions[:, None])[None, None]
session = ort.InferenceSession("decode.onnx", providers=["CUDAExecutionProvider"])
cuda = CudaSession(session, torch.device("cuda"))
# fresh device cache buffers built from each cache input's own shape/dtype; share each
# input.<name>/output.<name> pair on one buffer so the update lands in place
cache = {}
for i in session.get_inputs():
if not i.name.startswith("input."):
continue
name = i.name[len("input.") :]
dims = [max_cache_len if isinstance(d, str) and not d.isdigit() else int(d) for d in i.shape]
cache[name] = torch.zeros(dims, dtype=TypeHelper.ort_type_to_torch_type(i.type), device="cuda")
cuda.set_buffer_sharing(f"input.{name}", f"output.{name}")
cache_feed = {f"input.{name}": buf for name, buf in cache.items()}
vocab_size = next(o.shape[-1] for o in session.get_outputs() if o.name.endswith("logits"))
# prefill: the whole prompt in one call
positions = torch.arange(prompt_len, device="cuda")
cuda.allocate_buffers({"logits": (1, prompt_len, vocab_size)})
out = cuda.infer({"input_ids": prompt_ids, "attention_mask": causal_mask(positions, max_cache_len),
"position_ids": positions[None], **cache_feed})
next_token = out["logits"][:, -1:].argmax(-1)
# decode: query=1 buffers reused in place
cuda.allocate_buffers({"logits": (1, 1, vocab_size)})
input_ids = torch.empty((1, 1), dtype=torch.long, device="cuda")
position_ids = torch.empty((1, 1), dtype=torch.long, device="cuda")
attention_mask = torch.empty((1, 1, 1, max_cache_len), dtype=torch.bool, device="cuda")
slots = torch.arange(max_cache_len, device="cuda")
for position in range(prompt_len, max_cache_len):
input_ids.copy_(next_token)
position_ids.fill_(position)
attention_mask[0, 0, 0].copy_(slots <= position)
out = cuda.infer({"input_ids": input_ids, "attention_mask": attention_mask,
"position_ids": position_ids, **cache_feed})
next_token = out["logits"][:, -1:].argmax(-1)ExecuTorch のオンデバイスランタイムは C++ で実装されており、インプレースなキャッシュ更新は Method::set_output_data_ptr に依存しています — Python ランタイムからは公開されていません(executorch.runtime.Method が公開するのは execute/set_inputs/get_outputs のみです)。そのため、ゼロコピーのデコードは C++ 専用のパスとなります。変更されたキャッシュの output を、対応するキャッシュの input バッファにバインドすると、新しい K/V/カウンタはコピーなしで呼び出し元の StaticCache バッファに格納されます。他のタブと同様に、形状とサイズはアーティファクト自体から取得します — ここではプログラムの method_meta(input_tensor_meta/output_tensor_meta → TensorInfo::nbytes())です。C++ にはクエリできる Python ランタイムが存在しないためです:
#include <executorch/extension/data_loader/file_data_loader.h>
#include <executorch/extension/tensor/tensor_ptr.h>
#include <executorch/runtime/executor/method.h>
#include <executorch/runtime/executor/program.h>
using namespace executorch::runtime;
using executorch::extension::FileDataLoader;
using executorch::extension::make_tensor_ptr;
// load the exported decode and its `forward` method (Result error-checks elided for brevity)
auto loader = FileDataLoader::from("decode.pte");
auto program = Program::load(&loader.get());
// method-execution memory: a fixed arena for bookkeeping + one buffer per the method's memory plan
std::array<uint8_t, 4 * 1024 * 1024> arena;
MemoryAllocator method_allocator(arena.size(), arena.data());
auto meta = program->method_meta("forward");
std::vector<std::vector<uint8_t>> planned(meta->num_memory_planned_buffers());
std::vector<Span<uint8_t>> planned_spans;
for (size_t i = 0; i < planned.size(); ++i) {
planned[i].resize(meta->memory_planned_buffer_size(i).get());
planned_spans.push_back({planned[i].data(), planned[i].size()});
}
HierarchicalAllocator planned_allocator({planned_spans.data(), planned_spans.size()});
MemoryManager memory_manager(&method_allocator, &planned_allocator);
auto decode = std::move(program->load_method("forward", &memory_manager).get());
// shapes and sizes come from method_meta — no external config. Inputs are [ids, mask, position_ids,
// cache×N]; outputs are [returned cache×N, logits, mutated cache×N].
const size_t num_cache_tensors = meta->num_inputs() - 3;
const size_t logits_out_idx = num_cache_tensors;
std::vector<size_t> cache_nbytes(num_cache_tensors), cache_out_idx(num_cache_tensors);
for (size_t i = 0; i < num_cache_tensors; ++i) {
cache_nbytes[i] = meta->input_tensor_meta(3 + i)->nbytes();
cache_out_idx[i] = num_cache_tensors + 1 + i; // mutated cache = the last N outputs
}
const size_t logits_nbytes = meta->output_tensor_meta(logits_out_idx)->nbytes();
// one set of StaticCache buffers (K/V + per-layer counters) is reused across steps, bound in place each call
auto forward = [&](const TensorPtr& input_ids, const TensorPtr& mask, const TensorPtr& position_ids) {
decode.set_input(EValue(*input_ids), 0);
decode.set_input(EValue(*mask), 1);
decode.set_input(EValue(*position_ids), 2);
for (int i = 0; i < num_cache_tensors; ++i)
decode.set_input(EValue(*cache_tensor[i]), 3 + i);
// bind each mutated-cache output onto that same input tensor's data → the write lands in place, zero copies
for (int i = 0; i < num_cache_tensors; ++i)
decode.set_output_data_ptr(cache_tensor[i]->mutable_data_ptr(), cache_nbytes[i], cache_out_idx[i]);
decode.set_output_data_ptr(logits_data, logits_nbytes, logits_out_idx);
decode.execute(); // cache updated in place; logits written to logits_data
return argmax_last(logits_data); // greedy pick
};
// prefill the whole prompt, then decode one token per step — the cache carries in place across all calls
int64_t next_token = forward(prompt_ids, prompt_mask, prompt_positions);
for (int64_t position = prompt_len; position < max_cache_len; ++position) {
next_token = forward(make_tensor_ptr({1, 1}, &next_token, ScalarType::Long),
causal_mask(position), // [1, 1, 1, max_cache_len] bool
make_tensor_ptr({1, 1}, &position, ScalarType::Long));
}量子化
すべてのエクスポート設定は、エクスポート時にモデルを量子化するquantizerを受け取ります。量子化は、torch.exportのFXグラフ上(すべてのバックエンドでPT2EまたはNNCF)または変換後のONNXまたはOpenVINOモデル上(ONNX RuntimeまたはNNCF)で実行されます。量子化器、そのバックエンド、キャリブレーション、および追加方法については、エクスポートの量子化を参照してください。
制限事項と回避策
torch.export、torch.onnx.export、ExecuTorch はそれぞれ、特定の PyTorch パターンまわりに課題があります。エクスポートツールは、エクスポートフロー内の明確に定義されたポイントで、少数の可逆的なパッチと FX レベルの修正を適用してこれらを回避しています。これらはパブリックな export API から見えませんが、よく知っておくべき点として:
- FlashAttention と FlexAttention は、どのバックエンドでもエクスポートできません。
sdpaが推奨される設定であり、eagerも動作します(遅くなります)。モデルがそれ以外の設定を使用している場合は、exportを呼び出す前に、いずれかの設定をモデルに設定してください。 grouped_mmはDynamoExporterによるトレースを正常に追跡でき、OnnxExporter向けには自動的に変換されます。ExecutorchExporterを XNNPACK または MLX と組み合わせて使用する場合は、エクスポート前に MoE エキスパートがbatched_mmに置き換えられます。
次のステップ
- エクスポート機能の拡張の「パッチとレジストリの修正」を参照して、新しいアーキテクチャまたはバックエンドのエクスポートサポートを追加します。
- エクスポートの量子化で、エクスポートを量子化するか、量子化器を追加します。