Export π€ Transformers models to ONNX by observing a real inference pass instead of relying on hand-written, per-architecture ONNX configurations.
Standard Optimum ONNX export requires
a model-specific OnnxConfig that declares every input/output and which tensor
dimensions are dynamic. This project removes that requirement: it runs the model
on your actual inputs, traces the tensor shapes that flow through it, determines
which dimensions are dynamic empirically, and exports the result β all behind
the familiar from_pretrained(...) interface.
It is built entirely on top of an unmodified optimum / optimum-onnx
installation.
pip install torch transformers onnx onnxruntime
pip install "optimum @ git+https://github.com/huggingface/optimum"
# the ONNX exporter/runtime now lives in the separate optimum-onnx package
pip install "optimum-onnx[onnxruntime] @ git+https://github.com/huggingface/optimum-onnx"
git clone https://github.com/naomili0924/inference_driven_model_compiler.git
# Put the *repo directory itself* on PYTHONPATH. This activates the shadow
# `optimum` package, which (a) makes `optimum-cli` use the inference-driven
# exporter and (b) auto-registers the new export flags onto
# `optimum-cli export onnx` (see optimum/commands/register/register_idmc.py).
# Add the parent dir too if you also want `import inference_driven_model_compiler`
# or the `idmc` CLI.
export PYTHONPATH=/path/to/inference_driven_model_compiler:$PYTHONPATH
With this repo off PYTHONPATH, optimum-cli behaves exactly as stock β the
integration is inert unless the shadow optimum is active.
When this repo is on PYTHONPATH (see Installation), the standard
optimum-cli export onnx command gains four extra flags β no separate tool or
launcher needed. They are registered automatically via
optimum/commands/register/register_idmc.py, which optimum-cli auto-discovers:
| Flag | Description |
|---|---|
--export_by_inference | Enable inference-driven export (traces the model instead of using a hand-written OnnxConfig). |
--module_fixed_axis_fields | JSON dict mapping submodule names to config field names whose values should be treated as static tensor dimensions. |
--inference_kwargs | Inputs used to trace the model (overrides the auto-generated dummy inputs). Accepts inline JSON or a file path. |
--fixed_inputs | JSON list of input names whose traced values must be replayed verbatim instead of being randomly regenerated from their shape. Accepts inline JSON or a file path. |
--fixed_inputsβ value-exact inputs: by default the tracer records only the shape of each traced input and regenerates a random tensor of that shape at export/validation time (which also lets it discover dynamic axes). That is fine when only the shape matters, but breaks for inputs whose value drives graph construction β sizes, counts, indices, or anything read via.item()/.tolist()/ a reshape target β or inputs coupled to another (e.g. a vision encoder'simage_grid_thw, whose product must equalpixel_values' patch count). List those names here to replay their real traced values verbatim:--fixed_inputs='["image_grid_thw", "cache_position"]'(For image-text-to-text models the vision encoder's
pixel_values/image_grid_thware pinned automatically; use--fixed_inputsfor any additional value-exact inputs.)
JSON-or-file:
--inference_kwargs,--module_fixed_axis_fieldsand--fixed_inputsaccept either inline JSON or a path to a.jsonfile β handy when the inputs are large (e.g. a multimodalinference_kwargswith a bigpixel_valuesarray). Three accepted forms, resolved in this order:--inference_kwargs=@inputs.json # '@' prefix: read from file (curl-style) --inference_kwargs=inputs.json # bare path to an existing file: read from file --inference_kwargs='{"input_ids": []}' # inline JSON stringThe
@prefix only forces file interpretation; a bare path works too as long as the file exists at parse time.
optimum-cli export onnx \
--model sentence-transformers/paraphrase-MiniLM-L12-v2 \
/dev/shm/paraphrase-MiniLM \
--export_by_inference=true \
--module_fixed_axis_fields='{"transformer": ["hidden_size","intermediate_size","type_vocab_size","vocab_size"]}'
optimum-cli export onnx \
--model Qwen/Qwen3-4B-Thinking-2507 \
/dev/shm/qwen3-4b-thinking-onnx \
--task text-generation-with-past \
--export_by_inference=true \
--dtype fp16 --device cpu
The inference-driven tracer builds its dummy inputs on CPU, so export decoder models with
--device cpu(a--device cudamodel would mismatch the CPU-resident traced inputs).
--module_fixed_axis_fields is optional for decoder models β the dynamic-axis
inference step figures out num_heads, head_dim, etc. automatically.
Tip for large models: if your disk is limited, export to
/dev/shm(a RAM-backed tmpfs typically >= 80 GB on GPU instances) and copy the result elsewhere afterwards.
from transformers import AutoTokenizer
from inference_driven_model_compiler.optimum.onnxruntime import (
OnTheFlyORTModelForFeatureExtraction,
)
ckpt = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(ckpt)
encoded = tokenizer("ONNX Runtime accelerates inference.", return_tensors="pt")
model = OnTheFlyORTModelForFeatureExtraction.from_pretrained(
ckpt,
inference_kwargs=dict(encoded),
export_by_inference=True,
export=True,
module_fixed_axis_fields={"transformer": ["hidden_size", "num_attention_heads"]},
)
out = model(**encoded)
print(out.last_hidden_state.shape) # (1, seq_len, 768)
from transformers import GPT2Tokenizer
from inference_driven_model_compiler.optimum.onnxruntime import OnTheFlyORTModelForCausalLM
ckpt = "gpt2"
tokenizer = GPT2Tokenizer.from_pretrained(ckpt)
encoded = tokenizer("Replace me by any text you'd like.", return_tensors="pt")
model = OnTheFlyORTModelForCausalLM.from_pretrained(
ckpt,
inference_kwargs=dict(encoded),
export_by_inference=True,
export=True,
module_fixed_axis_fields={"transformer": ["n_ctx", "n_embd"]},
)
output_ids = model.generate(**encoded)
print(tokenizer.decode(output_ids[0]))
Pass export_by_inference=True together with inference_kwargs (the same kwargs
you would pass to the pipeline __call__). The pipeline runs once in PyTorch to
capture real tensor shapes for every submodule, then exports each one to ONNX
automatically β no hand-written OnnxConfig required.
import torch
from inference_driven_model_compiler.optimum.onnxruntime import OnTheFlyORTDiffusionPipeline
inf_kwargs = {
"prompt": "A cat walks on the grass, realistic",
"negative_prompt": "low quality, blurred",
"height": 240,
"width": 416,
"num_frames": 21,
"guidance_scale": 5.0,
}
pipe = OnTheFlyORTDiffusionPipeline.from_pretrained(
"Wan-AI/Wan2.1-T2V-1.3B-Diffusers",
provider="CUDAExecutionProvider",
torch_dtype=torch.float16,
export_by_inference=True,
inference_kwargs=inf_kwargs,
module_fixed_axis_fields={
"text_encoder": ["d_model", "vocab_size"],
"transformer": ["in_channels", "text_dim"],
"vae_decoder": ["base_dim", "z_dim"],
},
)
output = pipe(**inf_kwargs).frames[0]
This exports three ONNX files to a temporary directory and immediately loads them
into ORT sessions β all in one from_pretrained call:
| Submodule | ONNX file | Typical size |
|---|---|---|
text_encoder | text_encoder/model.onnx | ~13 GB (fp16) |
transformer | transformer/model.onnx | ~3 GB (fp16) |
vae_decoder | vae_decoder/model.onnx | ~137 MB (fp16) |
The same export_by_inference=True path handles UNet-based text-to-image
pipelines such as SDXL. The compiler traces the two CLIP text encoders, the
UNet (including the pooled added_cond_kwargs micro-conditioning), and the VAE
decoder, then runs the whole pipeline through ONNX Runtime.
import torch
from inference_driven_model_compiler.optimum.onnxruntime import OnTheFlyORTDiffusionPipeline
inf_kwargs = {
"prompt": "A cinematic photo of a red panda astronaut on the moon",
"num_inference_steps": 1, # SDXL-Turbo is a one-step distilled model
"guidance_scale": 0.0, # no classifier-free guidance
}
pipe = OnTheFlyORTDiffusionPipeline.from_pretrained(
"stabilityai/sdxl-turbo",
provider="CUDAExecutionProvider",
torch_dtype=torch.float16,
export_by_inference=True,
inference_kwargs=inf_kwargs,
)
image = pipe(**inf_kwargs).images[0]
image.save("sdxl_turbo_output.png")
This exports four ONNX submodules and immediately loads them into ORT sessions:
| Submodule | ONNX file | Typical size |
|---|---|---|
text_encoder | text_encoder/model.onnx | ~236 MB (fp16) |
text_encoder_2 | text_encoder_2/model.onnx | ~1.3 GB (fp16) |
unet | unet/model.onnx (+ model.onnx_data) | ~4.8 GB (fp16) |
vae_decoder | vae_decoder/model.onnx | ~190 MB (fp32) |
The SDXL VAE is numerically unstable in fp16 (the diffusers pipeline upcasts it to fp32 via
force_upcast), so the VAE decoder is exported in fp32 even when the rest of the pipeline is fp16. The fp16 latents are auto-cast at inference.
Inference-driven diffusion export traces and exports each submodule on the pipeline's device. Use a CUDA provider for large UNet-based models β exporting an fp16 UNet/VAE on CPU is extremely slow.
Image-editing models β e.g. InstructPix2Pix (timbrooks/instruct-pix2pix) β
take an input image plus a text instruction and produce an edited image.
OnTheFlyORTImageEditPipeline handles the two things that make them different from
text-to-image:
vae.encode(image).latent_dist), which text-to-image never touches, andin_channels == 8).Pass an image (and a prompt) in inference_kwargs so the single tracing
pass exercises the VAE encoder and the wide UNet β then every submodule is
exported to ONNX at the traced resolution.
import torch
from PIL import Image
from inference_driven_model_compiler.optimum.onnxruntime import OnTheFlyORTImageEditPipeline
inf_kwargs = {
"prompt": "turn him into a cyborg",
"image": Image.open("input.png").convert("RGB"), # any RGB image
"num_inference_steps": 10,
"image_guidance_scale": 1.5,
"guidance_scale": 7.5,
}
pipe = OnTheFlyORTImageEditPipeline.from_pretrained(
"timbrooks/instruct-pix2pix",
provider="CUDAExecutionProvider",
torch_dtype=torch.float32,
export_by_inference=True,
inference_kwargs=inf_kwargs,
)
edited = pipe(**inf_kwargs).images[0]
edited.save("edited.png")
This exports four ONNX submodules (note the vae_encoder, exported as the
full encode graph β encoder + quant_conv β latent_parameters β so the input
image becomes a proper latent distribution):
| Submodule | ONNX file | Notes |
|---|---|---|
text_encoder | text_encoder/model.onnx | single CLIP encoder (SD-1.5) |
unet | unet/model.onnx (+ model.onnx_data) | 8-channel input (latents β image latents) |
vae_encoder | vae_encoder/model.onnx | encodes the input image β latent_dist |
vae_decoder | vae_decoder/model.onnx | decodes the final latents β image |
The concrete diffusers pipeline is resolved from the checkpoint's _class_name
and mixed into OnTheFlyORTImageEditPipeline on the fly, so other image-conditioned
pipelines (img2img, SDXL InstructPix2Pix, upscaling, β¦) export and run through
the same class without a model-specific subclass. For pure text-to-image, use
OnTheFlyORTDiffusionPipeline instead.
InstructPix2Pix uses the SD-1.5 VAE, which is fp16-fragile on some images. The example above exports in fp32 (the model is small, ~1 GB); switch to
torch_dtype=torch.float16for a faster/smaller export once you've confirmed the output quality on your inputs.
export_by_inference=True writes the ONNX graphs to ephemeral RAM (/dev/shm)
and re-exports on every call. To keep them β and reuse them without re-exporting
β save the pipeline to disk and/or push it to the Hugging Face Hub:
# 1. Export once...
pipe = OnTheFlyORTImageEditPipeline.from_pretrained(
"timbrooks/instruct-pix2pix",
provider="CUDAExecutionProvider", torch_dtype=torch.float32,
export_by_inference=True, inference_kwargs=inf_kwargs,
)
# 2. ...then persist locally and/or upload to your Hub repo.
pipe.save_pretrained(
"instruct-pix2pix-onnx", # local copy
push_to_hub=True,
repo_id="your-username/instruct-pix2pix-onnx",
token="hf_β¦", # write-scoped token (or use the cached login)
private=True,
)
save_pretrained writes every submodule's model.onnx (+ external .onnx_data
for graphs over 2 GB), the per-submodule config.json, the scheduler/ /
tokenizer/ / feature_extractor/, the model_index.json, and the
io_binding/ output-shape files β everything needed to reload without tracing.
# From a local directory or a Hub repo β no PyTorch, no re-export.
pipe = OnTheFlyORTImageEditPipeline.from_pretrained(
"your-username/instruct-pix2pix-onnx", # or a local path
export=False,
provider="CUDAExecutionProvider",
)
edited = pipe(**inf_kwargs).images[0]
# Generic text-to-image works the same way:
pipe = OnTheFlyORTDiffusionPipeline.from_pretrained(
"optimum/stable-diffusion-v1-5", # Hub repo with pre-exported ONNX weights
export=False,
)
| Argument | Meaning |
|---|---|
inference_kwargs | Inputs used to trace the model (e.g. a tokenized prompt, or full pipeline kwargs for diffusion). |
export_by_inference=True | Enable the inference-driven export path. |
export=True | Force a fresh ONNX export (transformer models). |
module_fixed_axis_fields | Per-submodule config field names whose values should be treated as fixed (static) tensor dims. |
skip_random_generation | Keep the actual traced tensors as fixed dummy inputs instead of regenerating them. |
n_trials | Number of inference passes for dynamic-axis detection, transformer models only (default 3). |
All live in inference_driven_model_compiler.optimum.onnxruntime and share the
same from_pretrained(...) interface.
Naming convention: every user-facing entry point carries the
OnTheFlyORT prefix β it marks the inference-driven (on-the-fly export) path and
keeps these classes from colliding with stock optimum's ORT* names. Internal
ONNX submodule wrappers (ORTUnet, ORTVaeEncoder, ORTVae, β¦) keep the
bare ORT prefix. The pre-rename names (ORTDiffusionPipeline,
ORTImageEditPipeline, ORTModelForImageTextToText, ORTChatterboxPipeline)
remain importable as deprecated aliases, so existing code keeps working.
| Class | Task |
|---|---|
OnTheFlyORTModelForCausalLM | Text generation (decoder-only, KV cache) |
OnTheFlyORTModelForFeatureExtraction | Embeddings / hidden states |
OnTheFlyORTModelForMaskedLM | Masked language modeling |
OnTheFlyORTModelForSequenceClassification | Sequence classification |
OnTheFlyORTModelForTokenClassification | Token classification / NER |
OnTheFlyORTModelForQuestionAnswering | Extractive QA |
| Class | Purpose |
|---|---|
OnTheFlyORTDiffusionPipeline | Generic base β wraps any diffusers.DiffusionPipeline |
OnTheFlyORTImageEditPipeline | Image-editing models (InstructPix2Pix, img2img, β¦) β encodes an input image via the VAE encoder |
ORTUnet | ORT session wrapper for a UNet2D/3D denoiser |
ORTTransformer | ORT session wrapper for a DiT/transformer denoiser |
ORTTextEncoder | ORT session wrapper for a text encoder |
ORTVaeEncoder | ORT session wrapper for a VAE encoder |
ORTVaeDecoder | ORT session wrapper for a VAE decoder |
ORTVae | Combines ORTVaeEncoder + ORTVaeDecoder behind the standard vae API |
OnTheFlyORTDiffusionPipeline requires no model-specific subclass. When called as the
base class it reads _class_name from the model's model_index.json and creates
an ORT<ClassName> wrapper on the fly via _make_ort_pipeline_class. Every
diffusers pipeline β including ones not yet written β is handled automatically.
Supported text-to-video pipeline names (as of diffusers 0.38):
AnimateDiffPipeline, AnimateDiffSDXLPipeline, CogVideoXPipeline,
HunyuanVideo15Pipeline, HunyuanVideoPipeline, LTXPipeline, LTX2Pipeline,
LattePipeline, MochiPipeline, SanaVideoPipeline, TextToVideoSDPipeline,
WanPipeline, WanAnimatePipeline.
| Model | Type | Task tested |
|---|---|---|
| GPT-2 | decoder-only | text generation (KV cache) |
| Gemma 4 (2B) | decoder-only | text generation (KV cache, fp16) |
| BERT-base | encoder | masked-LM, seq-cls, token-cls, QA, feature extraction |
| Sentence-Transformers / paraphrase-MiniLM-L12-v2 | encoder | feature extraction |
| T5-small | encoder-decoder | feature extraction (encoder) |
| BART-base | encoder-decoder | feature extraction (encoder) |
| ViT-base | vision encoder | feature extraction |
| CLIP-ViT-base | vision encoder | feature extraction |
| Whisper-tiny | audio encoder | feature extraction |
| Model | Pipeline | Submodules exported | Notes |
|---|---|---|---|
| Wan2.1-T2V-1.3B | WanPipeline | text_encoder, transformer, vae_decoder | Verified end-to-end on CUDA; 50-step inference at ~7.4 it/s |
| SDXL-Turbo | StableDiffusionXLPipeline | text_encoder, text_encoder_2, unet, vae_decoder | Text-to-image; verified end-to-end on CUDA (1-step). VAE decoder exported in fp32. |
| InstructPix2Pix | StableDiffusionInstructPix2PixPipeline | text_encoder, unet, vae_encoder, vae_decoder | Image editing via OnTheFlyORTImageEditPipeline; verified end-to-end on CUDA (fp32, 10-step ~9.7 it/s). Exercises the VAE encoder (input image β latents) and the 8-channel UNet. SaveβHubβreload round-trip verified. |
last_hidden_state).model.save_pretrained(...) to persist it.from_pretrained(export_by_inference=True)
β
βΌ
1. Load the PyTorch model (TasksManager)
β
βΌ
2. trace_model_shapes() ββ run N inference passes with varied input shapes
β β’ encoder-only β single forward
β β’ encoder-decoder β encoder submodule only
β β’ decoder-only β prefill + decode (KV cache)
βΌ
3. _compute_dynamic_axes() ββ a dim is "dynamic" iff its size changed across runs
β (dim-0/batch always dynamic; hidden_size, vocab,
βΌ num_heads, head_dim, image H/W β¦ stay static)
4. DummyOnnxConfig ββ a generic OnnxConfig built from the traced shapes + axes
β
βΌ
5. export_models() ββ standard Optimum ONNX export (disable_dynamic_axes_fix=True)
β
βΌ
6. ORTModel._from_pretrained() ββ load the ONNX model into an ORT session
OnTheFlyORTDiffusionPipeline.from_pretrained(export_by_inference=True, inference_kwargs={...})
β
βΌ
1. Load the PyTorch diffusion pipeline (diffusers)
β
βΌ
2. Register forward pre-hooks on each submodule
(text_encoder, transformer/unet, vae.post_quant_conv)
β
βΌ
3. Run ONE full pipeline inference pass with the provided inference_kwargs
β all submodule inputs are captured live as real tensors
β
βΌ
4. For each submodule:
β’ Move captured tensors to CPU
β’ Build DummyOnnxConfig from the observed shapes + dynamic axes
β’ Export to ONNX (constant folding disabled for text encoders to
avoid 89 GB inflation from precomputed attention bias)
β
βΌ
5. VAE decoder special case: export post_quant_conv + decoder as a single
_VaeFullDecodeWrapper (WAN VAE decodes per-frame with caching in PyTorch;
ONNX needs one call over the full latent video)
β
βΌ
6. OnTheFlyORTDiffusionPipeline loaded with each ONNX submodule in an ORT session
Rather than guessing from config fields, the compiler runs the model several
times (default n_trials=3) with randomly varied batch sizes and sequence
lengths (and, for decoder models, a varied decode query length). A
dimension is marked dynamic only if its value actually changes between runs;
everything else β hidden_size, num_attention_heads, head_dim,
vocab_size, image height/width, ViT patch count, etc. β is correctly kept
static.
For each tensor seen across the trial runs:
batch).past_key_values.{i}.key/value, the
past-sequence dimension (axis 2) is dynamic while num_heads (axis 1) and
head_dim (axis 3) stay static.| Standard Optimum export | Inference-driven export |
|---|---|
Needs a hand-written OnnxConfig per architecture | Works with any model that runs a forward pass |
| Dynamic axes declared manually | Dynamic axes inferred from multiple varied runs |
| New architectures require code changes upstream | New architectures work out of the box |
inference_driven_model_compiler/
βββ cli.py # idmc CLI β wraps optimum-cli with extra flags
βββ optimum/
β βββ exporters/onnx/
β β βββ utils.py # trace_model_shapes(), dynamic-axis inference
β β βββ model_configs.py # DummyOnnxConfig β generic shape-driven OnnxConfig
β β βββ input_generators.py # DummyTupleInputGenerator β dtype-aware dummies
β β βββ __init__.py # main_export wrapper (renamed params)
β βββ onnxruntime/
β βββ modeling.py # _OnTheFlyORTMixin + 5 encoder model classes
β βββ modeling_decoder.py # OnTheFlyORTModelForCausalLM
β βββ modeling_diffusion.py # OnTheFlyORTDiffusionPipeline + submodule wrappers
β βββ utils.py # load_shapes_as_torch_size and helpers
βββ on_the_fly_pipeline_tests/ # per-model tests + dynamic-axis + diffusion suites
39 commits
4 commits
Python
99.2%
Export π€ Transformers models to ONNX by observing a real inference pass instead of relying on hand-written, per-architecture ONNX configurations.
Standard Optimum ONNX export requires
a model-specific OnnxConfig that declares every input/output and which tensor
dimensions are dynamic. This project removes that requirement: it runs the model
on your actual inputs, traces the tensor shapes that flow through it, determines
which dimensions are dynamic empirically, and exports the result β all behind
the familiar from_pretrained(...) interface.
It is built entirely on top of an unmodified optimum / optimum-onnx
installation.
pip install torch transformers onnx onnxruntime
pip install "optimum @ git+https://github.com/huggingface/optimum"
# the ONNX exporter/runtime now lives in the separate optimum-onnx package
pip install "optimum-onnx[onnxruntime] @ git+https://github.com/huggingface/optimum-onnx"
git clone https://github.com/naomili0924/inference_driven_model_compiler.git
# Put the *repo directory itself* on PYTHONPATH. This activates the shadow
# `optimum` package, which (a) makes `optimum-cli` use the inference-driven
# exporter and (b) auto-registers the new export flags onto
# `optimum-cli export onnx` (see optimum/commands/register/register_idmc.py).
# Add the parent dir too if you also want `import inference_driven_model_compiler`
# or the `idmc` CLI.
export PYTHONPATH=/path/to/inference_driven_model_compiler:$PYTHONPATH
With this repo off PYTHONPATH, optimum-cli behaves exactly as stock β the
integration is inert unless the shadow optimum is active.
When this repo is on PYTHONPATH (see Installation), the standard
optimum-cli export onnx command gains four extra flags β no separate tool or
launcher needed. They are registered automatically via
optimum/commands/register/register_idmc.py, which optimum-cli auto-discovers:
| Flag | Description |
|---|---|
--export_by_inference | Enable inference-driven export (traces the model instead of using a hand-written OnnxConfig). |
--module_fixed_axis_fields | JSON dict mapping submodule names to config field names whose values should be treated as static tensor dimensions. |
--inference_kwargs | Inputs used to trace the model (overrides the auto-generated dummy inputs). Accepts inline JSON or a file path. |
--fixed_inputs | JSON list of input names whose traced values must be replayed verbatim instead of being randomly regenerated from their shape. Accepts inline JSON or a file path. |
--fixed_inputsβ value-exact inputs: by default the tracer records only the shape of each traced input and regenerates a random tensor of that shape at export/validation time (which also lets it discover dynamic axes). That is fine when only the shape matters, but breaks for inputs whose value drives graph construction β sizes, counts, indices, or anything read via.item()/.tolist()/ a reshape target β or inputs coupled to another (e.g. a vision encoder'simage_grid_thw, whose product must equalpixel_values' patch count). List those names here to replay their real traced values verbatim:--fixed_inputs='["image_grid_thw", "cache_position"]'(For image-text-to-text models the vision encoder's
pixel_values/image_grid_thware pinned automatically; use--fixed_inputsfor any additional value-exact inputs.)
JSON-or-file:
--inference_kwargs,--module_fixed_axis_fieldsand--fixed_inputsaccept either inline JSON or a path to a.jsonfile β handy when the inputs are large (e.g. a multimodalinference_kwargswith a bigpixel_valuesarray). Three accepted forms, resolved in this order:--inference_kwargs=@inputs.json # '@' prefix: read from file (curl-style) --inference_kwargs=inputs.json # bare path to an existing file: read from file --inference_kwargs='{"input_ids": []}' # inline JSON stringThe
@prefix only forces file interpretation; a bare path works too as long as the file exists at parse time.
optimum-cli export onnx \
--model sentence-transformers/paraphrase-MiniLM-L12-v2 \
/dev/shm/paraphrase-MiniLM \
--export_by_inference=true \
--module_fixed_axis_fields='{"transformer": ["hidden_size","intermediate_size","type_vocab_size","vocab_size"]}'
optimum-cli export onnx \
--model Qwen/Qwen3-4B-Thinking-2507 \
/dev/shm/qwen3-4b-thinking-onnx \
--task text-generation-with-past \
--export_by_inference=true \
--dtype fp16 --device cpu
The inference-driven tracer builds its dummy inputs on CPU, so export decoder models with
--device cpu(a--device cudamodel would mismatch the CPU-resident traced inputs).
--module_fixed_axis_fields is optional for decoder models β the dynamic-axis
inference step figures out num_heads, head_dim, etc. automatically.
Tip for large models: if your disk is limited, export to
/dev/shm(a RAM-backed tmpfs typically >= 80 GB on GPU instances) and copy the result elsewhere afterwards.
from transformers import AutoTokenizer
from inference_driven_model_compiler.optimum.onnxruntime import (
OnTheFlyORTModelForFeatureExtraction,
)
ckpt = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(ckpt)
encoded = tokenizer("ONNX Runtime accelerates inference.", return_tensors="pt")
model = OnTheFlyORTModelForFeatureExtraction.from_pretrained(
ckpt,
inference_kwargs=dict(encoded),
export_by_inference=True,
export=True,
module_fixed_axis_fields={"transformer": ["hidden_size", "num_attention_heads"]},
)
out = model(**encoded)
print(out.last_hidden_state.shape) # (1, seq_len, 768)
from transformers import GPT2Tokenizer
from inference_driven_model_compiler.optimum.onnxruntime import OnTheFlyORTModelForCausalLM
ckpt = "gpt2"
tokenizer = GPT2Tokenizer.from_pretrained(ckpt)
encoded = tokenizer("Replace me by any text you'd like.", return_tensors="pt")
model = OnTheFlyORTModelForCausalLM.from_pretrained(
ckpt,
inference_kwargs=dict(encoded),
export_by_inference=True,
export=True,
module_fixed_axis_fields={"transformer": ["n_ctx", "n_embd"]},
)
output_ids = model.generate(**encoded)
print(tokenizer.decode(output_ids[0]))
Pass export_by_inference=True together with inference_kwargs (the same kwargs
you would pass to the pipeline __call__). The pipeline runs once in PyTorch to
capture real tensor shapes for every submodule, then exports each one to ONNX
automatically β no hand-written OnnxConfig required.
import torch
from inference_driven_model_compiler.optimum.onnxruntime import OnTheFlyORTDiffusionPipeline
inf_kwargs = {
"prompt": "A cat walks on the grass, realistic",
"negative_prompt": "low quality, blurred",
"height": 240,
"width": 416,
"num_frames": 21,
"guidance_scale": 5.0,
}
pipe = OnTheFlyORTDiffusionPipeline.from_pretrained(
"Wan-AI/Wan2.1-T2V-1.3B-Diffusers",
provider="CUDAExecutionProvider",
torch_dtype=torch.float16,
export_by_inference=True,
inference_kwargs=inf_kwargs,
module_fixed_axis_fields={
"text_encoder": ["d_model", "vocab_size"],
"transformer": ["in_channels", "text_dim"],
"vae_decoder": ["base_dim", "z_dim"],
},
)
output = pipe(**inf_kwargs).frames[0]
This exports three ONNX files to a temporary directory and immediately loads them
into ORT sessions β all in one from_pretrained call:
| Submodule | ONNX file | Typical size |
|---|---|---|
text_encoder | text_encoder/model.onnx | ~13 GB (fp16) |
transformer | transformer/model.onnx | ~3 GB (fp16) |
vae_decoder | vae_decoder/model.onnx | ~137 MB (fp16) |
The same export_by_inference=True path handles UNet-based text-to-image
pipelines such as SDXL. The compiler traces the two CLIP text encoders, the
UNet (including the pooled added_cond_kwargs micro-conditioning), and the VAE
decoder, then runs the whole pipeline through ONNX Runtime.
import torch
from inference_driven_model_compiler.optimum.onnxruntime import OnTheFlyORTDiffusionPipeline
inf_kwargs = {
"prompt": "A cinematic photo of a red panda astronaut on the moon",
"num_inference_steps": 1, # SDXL-Turbo is a one-step distilled model
"guidance_scale": 0.0, # no classifier-free guidance
}
pipe = OnTheFlyORTDiffusionPipeline.from_pretrained(
"stabilityai/sdxl-turbo",
provider="CUDAExecutionProvider",
torch_dtype=torch.float16,
export_by_inference=True,
inference_kwargs=inf_kwargs,
)
image = pipe(**inf_kwargs).images[0]
image.save("sdxl_turbo_output.png")
This exports four ONNX submodules and immediately loads them into ORT sessions:
| Submodule | ONNX file | Typical size |
|---|---|---|
text_encoder | text_encoder/model.onnx | ~236 MB (fp16) |
text_encoder_2 | text_encoder_2/model.onnx | ~1.3 GB (fp16) |
unet | unet/model.onnx (+ model.onnx_data) | ~4.8 GB (fp16) |
vae_decoder | vae_decoder/model.onnx | ~190 MB (fp32) |
The SDXL VAE is numerically unstable in fp16 (the diffusers pipeline upcasts it to fp32 via
force_upcast), so the VAE decoder is exported in fp32 even when the rest of the pipeline is fp16. The fp16 latents are auto-cast at inference.
Inference-driven diffusion export traces and exports each submodule on the pipeline's device. Use a CUDA provider for large UNet-based models β exporting an fp16 UNet/VAE on CPU is extremely slow.
Image-editing models β e.g. InstructPix2Pix (timbrooks/instruct-pix2pix) β
take an input image plus a text instruction and produce an edited image.
OnTheFlyORTImageEditPipeline handles the two things that make them different from
text-to-image:
vae.encode(image).latent_dist), which text-to-image never touches, andin_channels == 8).Pass an image (and a prompt) in inference_kwargs so the single tracing
pass exercises the VAE encoder and the wide UNet β then every submodule is
exported to ONNX at the traced resolution.
import torch
from PIL import Image
from inference_driven_model_compiler.optimum.onnxruntime import OnTheFlyORTImageEditPipeline
inf_kwargs = {
"prompt": "turn him into a cyborg",
"image": Image.open("input.png").convert("RGB"), # any RGB image
"num_inference_steps": 10,
"image_guidance_scale": 1.5,
"guidance_scale": 7.5,
}
pipe = OnTheFlyORTImageEditPipeline.from_pretrained(
"timbrooks/instruct-pix2pix",
provider="CUDAExecutionProvider",
torch_dtype=torch.float32,
export_by_inference=True,
inference_kwargs=inf_kwargs,
)
edited = pipe(**inf_kwargs).images[0]
edited.save("edited.png")
This exports four ONNX submodules (note the vae_encoder, exported as the
full encode graph β encoder + quant_conv β latent_parameters β so the input
image becomes a proper latent distribution):
| Submodule | ONNX file | Notes |
|---|---|---|
text_encoder | text_encoder/model.onnx | single CLIP encoder (SD-1.5) |
unet | unet/model.onnx (+ model.onnx_data) | 8-channel input (latents β image latents) |
vae_encoder | vae_encoder/model.onnx | encodes the input image β latent_dist |
vae_decoder | vae_decoder/model.onnx | decodes the final latents β image |
The concrete diffusers pipeline is resolved from the checkpoint's _class_name
and mixed into OnTheFlyORTImageEditPipeline on the fly, so other image-conditioned
pipelines (img2img, SDXL InstructPix2Pix, upscaling, β¦) export and run through
the same class without a model-specific subclass. For pure text-to-image, use
OnTheFlyORTDiffusionPipeline instead.
InstructPix2Pix uses the SD-1.5 VAE, which is fp16-fragile on some images. The example above exports in fp32 (the model is small, ~1 GB); switch to
torch_dtype=torch.float16for a faster/smaller export once you've confirmed the output quality on your inputs.
export_by_inference=True writes the ONNX graphs to ephemeral RAM (/dev/shm)
and re-exports on every call. To keep them β and reuse them without re-exporting
β save the pipeline to disk and/or push it to the Hugging Face Hub:
# 1. Export once...
pipe = OnTheFlyORTImageEditPipeline.from_pretrained(
"timbrooks/instruct-pix2pix",
provider="CUDAExecutionProvider", torch_dtype=torch.float32,
export_by_inference=True, inference_kwargs=inf_kwargs,
)
# 2. ...then persist locally and/or upload to your Hub repo.
pipe.save_pretrained(
"instruct-pix2pix-onnx", # local copy
push_to_hub=True,
repo_id="your-username/instruct-pix2pix-onnx",
token="hf_β¦", # write-scoped token (or use the cached login)
private=True,
)
save_pretrained writes every submodule's model.onnx (+ external .onnx_data
for graphs over 2 GB), the per-submodule config.json, the scheduler/ /
tokenizer/ / feature_extractor/, the model_index.json, and the
io_binding/ output-shape files β everything needed to reload without tracing.
# From a local directory or a Hub repo β no PyTorch, no re-export.
pipe = OnTheFlyORTImageEditPipeline.from_pretrained(
"your-username/instruct-pix2pix-onnx", # or a local path
export=False,
provider="CUDAExecutionProvider",
)
edited = pipe(**inf_kwargs).images[0]
# Generic text-to-image works the same way:
pipe = OnTheFlyORTDiffusionPipeline.from_pretrained(
"optimum/stable-diffusion-v1-5", # Hub repo with pre-exported ONNX weights
export=False,
)
| Argument | Meaning |
|---|---|
inference_kwargs | Inputs used to trace the model (e.g. a tokenized prompt, or full pipeline kwargs for diffusion). |
export_by_inference=True | Enable the inference-driven export path. |
export=True | Force a fresh ONNX export (transformer models). |
module_fixed_axis_fields | Per-submodule config field names whose values should be treated as fixed (static) tensor dims. |
skip_random_generation | Keep the actual traced tensors as fixed dummy inputs instead of regenerating them. |
n_trials | Number of inference passes for dynamic-axis detection, transformer models only (default 3). |
All live in inference_driven_model_compiler.optimum.onnxruntime and share the
same from_pretrained(...) interface.
Naming convention: every user-facing entry point carries the
OnTheFlyORT prefix β it marks the inference-driven (on-the-fly export) path and
keeps these classes from colliding with stock optimum's ORT* names. Internal
ONNX submodule wrappers (ORTUnet, ORTVaeEncoder, ORTVae, β¦) keep the
bare ORT prefix. The pre-rename names (ORTDiffusionPipeline,
ORTImageEditPipeline, ORTModelForImageTextToText, ORTChatterboxPipeline)
remain importable as deprecated aliases, so existing code keeps working.
| Class | Task |
|---|---|
OnTheFlyORTModelForCausalLM | Text generation (decoder-only, KV cache) |
OnTheFlyORTModelForFeatureExtraction | Embeddings / hidden states |
OnTheFlyORTModelForMaskedLM | Masked language modeling |
OnTheFlyORTModelForSequenceClassification | Sequence classification |
OnTheFlyORTModelForTokenClassification | Token classification / NER |
OnTheFlyORTModelForQuestionAnswering | Extractive QA |
| Class | Purpose |
|---|---|
OnTheFlyORTDiffusionPipeline | Generic base β wraps any diffusers.DiffusionPipeline |
OnTheFlyORTImageEditPipeline | Image-editing models (InstructPix2Pix, img2img, β¦) β encodes an input image via the VAE encoder |
ORTUnet | ORT session wrapper for a UNet2D/3D denoiser |
ORTTransformer | ORT session wrapper for a DiT/transformer denoiser |
ORTTextEncoder | ORT session wrapper for a text encoder |
ORTVaeEncoder | ORT session wrapper for a VAE encoder |
ORTVaeDecoder | ORT session wrapper for a VAE decoder |
ORTVae | Combines ORTVaeEncoder + ORTVaeDecoder behind the standard vae API |
OnTheFlyORTDiffusionPipeline requires no model-specific subclass. When called as the
base class it reads _class_name from the model's model_index.json and creates
an ORT<ClassName> wrapper on the fly via _make_ort_pipeline_class. Every
diffusers pipeline β including ones not yet written β is handled automatically.
Supported text-to-video pipeline names (as of diffusers 0.38):
AnimateDiffPipeline, AnimateDiffSDXLPipeline, CogVideoXPipeline,
HunyuanVideo15Pipeline, HunyuanVideoPipeline, LTXPipeline, LTX2Pipeline,
LattePipeline, MochiPipeline, SanaVideoPipeline, TextToVideoSDPipeline,
WanPipeline, WanAnimatePipeline.
| Model | Type | Task tested |
|---|---|---|
| GPT-2 | decoder-only | text generation (KV cache) |
| Gemma 4 (2B) | decoder-only | text generation (KV cache, fp16) |
| BERT-base | encoder | masked-LM, seq-cls, token-cls, QA, feature extraction |
| Sentence-Transformers / paraphrase-MiniLM-L12-v2 | encoder | feature extraction |
| T5-small | encoder-decoder | feature extraction (encoder) |
| BART-base | encoder-decoder | feature extraction (encoder) |
| ViT-base | vision encoder | feature extraction |
| CLIP-ViT-base | vision encoder | feature extraction |
| Whisper-tiny | audio encoder | feature extraction |
| Model | Pipeline | Submodules exported | Notes |
|---|---|---|---|
| Wan2.1-T2V-1.3B | WanPipeline | text_encoder, transformer, vae_decoder | Verified end-to-end on CUDA; 50-step inference at ~7.4 it/s |
| SDXL-Turbo | StableDiffusionXLPipeline | text_encoder, text_encoder_2, unet, vae_decoder | Text-to-image; verified end-to-end on CUDA (1-step). VAE decoder exported in fp32. |
| InstructPix2Pix | StableDiffusionInstructPix2PixPipeline | text_encoder, unet, vae_encoder, vae_decoder | Image editing via OnTheFlyORTImageEditPipeline; verified end-to-end on CUDA (fp32, 10-step ~9.7 it/s). Exercises the VAE encoder (input image β latents) and the 8-channel UNet. SaveβHubβreload round-trip verified. |
last_hidden_state).model.save_pretrained(...) to persist it.from_pretrained(export_by_inference=True)
β
βΌ
1. Load the PyTorch model (TasksManager)
β
βΌ
2. trace_model_shapes() ββ run N inference passes with varied input shapes
β β’ encoder-only β single forward
β β’ encoder-decoder β encoder submodule only
β β’ decoder-only β prefill + decode (KV cache)
βΌ
3. _compute_dynamic_axes() ββ a dim is "dynamic" iff its size changed across runs
β (dim-0/batch always dynamic; hidden_size, vocab,
βΌ num_heads, head_dim, image H/W β¦ stay static)
4. DummyOnnxConfig ββ a generic OnnxConfig built from the traced shapes + axes
β
βΌ
5. export_models() ββ standard Optimum ONNX export (disable_dynamic_axes_fix=True)
β
βΌ
6. ORTModel._from_pretrained() ββ load the ONNX model into an ORT session
OnTheFlyORTDiffusionPipeline.from_pretrained(export_by_inference=True, inference_kwargs={...})
β
βΌ
1. Load the PyTorch diffusion pipeline (diffusers)
β
βΌ
2. Register forward pre-hooks on each submodule
(text_encoder, transformer/unet, vae.post_quant_conv)
β
βΌ
3. Run ONE full pipeline inference pass with the provided inference_kwargs
β all submodule inputs are captured live as real tensors
β
βΌ
4. For each submodule:
β’ Move captured tensors to CPU
β’ Build DummyOnnxConfig from the observed shapes + dynamic axes
β’ Export to ONNX (constant folding disabled for text encoders to
avoid 89 GB inflation from precomputed attention bias)
β
βΌ
5. VAE decoder special case: export post_quant_conv + decoder as a single
_VaeFullDecodeWrapper (WAN VAE decodes per-frame with caching in PyTorch;
ONNX needs one call over the full latent video)
β
βΌ
6. OnTheFlyORTDiffusionPipeline loaded with each ONNX submodule in an ORT session
Rather than guessing from config fields, the compiler runs the model several
times (default n_trials=3) with randomly varied batch sizes and sequence
lengths (and, for decoder models, a varied decode query length). A
dimension is marked dynamic only if its value actually changes between runs;
everything else β hidden_size, num_attention_heads, head_dim,
vocab_size, image height/width, ViT patch count, etc. β is correctly kept
static.
For each tensor seen across the trial runs:
batch).past_key_values.{i}.key/value, the
past-sequence dimension (axis 2) is dynamic while num_heads (axis 1) and
head_dim (axis 3) stay static.| Standard Optimum export | Inference-driven export |
|---|---|
Needs a hand-written OnnxConfig per architecture | Works with any model that runs a forward pass |
| Dynamic axes declared manually | Dynamic axes inferred from multiple varied runs |
| New architectures require code changes upstream | New architectures work out of the box |
inference_driven_model_compiler/
βββ cli.py # idmc CLI β wraps optimum-cli with extra flags
βββ optimum/
β βββ exporters/onnx/
β β βββ utils.py # trace_model_shapes(), dynamic-axis inference
β β βββ model_configs.py # DummyOnnxConfig β generic shape-driven OnnxConfig
β β βββ input_generators.py # DummyTupleInputGenerator β dtype-aware dummies
β β βββ __init__.py # main_export wrapper (renamed params)
β βββ onnxruntime/
β βββ modeling.py # _OnTheFlyORTMixin + 5 encoder model classes
β βββ modeling_decoder.py # OnTheFlyORTModelForCausalLM
β βββ modeling_diffusion.py # OnTheFlyORTDiffusionPipeline + submodule wrappers
β βββ utils.py # load_shapes_as_torch_size and helpers
βββ on_the_fly_pipeline_tests/ # per-model tests + dynamic-axis + diffusion suites
39 commits
4 commits
Python
99.2%