docs/source/en/exporters_extend.md
torch.export traces the model into a graph, later stages transform
that graph, and a final stage lowers or emits it for the target runtime. Most models pass through
untouched. When there's a PyTorch pattern the backend can't handle, the exporter applies a
small workaround at that stage rather than editing the model.
Add a workaround by writing one function and registering it with a decorator. Each workaround belongs at the lowest stage that can express it cleanly.
A workaround is either a patch or a fix. The two differ in whether they can be reverted.
| Patch | Fix | |
|---|---|---|
| What it does | Swaps out an attribute (a torch op, an ExecuTorch internal, or a model method) for the duration of the export | Rewrites the traced graph or program |
| Reverted | Yes, the original is restored afterward | No, it repairs the artifact before the next stage runs |
| Register with | @register_patch(backend, *paths) | @register_fx_node_fix(backend) or @register_fx_program_fix(backend) |
Both live in a registry in exporters/utils.py, and the exporter installs everything registered for its backend at the right stage.
Reach for a patch when the issue is a single backend's lowering bug: a missing ONNX translation, an ORT validation quirk, or an FX decomposition that emits a dead op. The workaround stays in the exporter, and the modeling code stays clean.
When the pattern blocks export across multiple backends, such as data-dependent loops, stateful
caches outside Cache, or hand-written split-loop attention, fix the model instead. Fixing it
once in the model helps every exporter.
Suppose a model method does something torch.export can't trace. NLLB-MoE's
NllbMoeTop2Router._cast_classifier casts the classifier weights to another dtype,
which isn't traceable. Replace it with a no-op for the duration of the export.
Write a factory that takes the original method and returns its replacement, then register the factory against the method's dotted path:
from transformers.exporters.utils import register_patch
@register_patch("dynamo", "transformers.models.nllb_moe.modeling_nllb_moe.NllbMoeTop2Router._cast_classifier")
def _patch_classifier_cast(_original):
# Replace the untraceable dtype cast with a no-op during export.
return lambda self, *args, **kwargs: None
The exporter swaps the method in before tracing and restores it afterward, so the patch only affects export. A few variations:
@register_patch("dynamo", path_a, path_b).torch op by pointing the path at it, for example @register_patch("onnx", "torch.where").
The factory receives the real op as its argument, so the replacement can call through to it.Each exporter's source labels its stages as # ── Stage N: … ── comment blocks, so the file and
this reference line up. Look there for the exact ops and classes each stage handles.
The base exporter runs one patch stage and four helpers, in order, inside DynamoExporter.export
(see exporter_dynamo.py).
model.forward a flat argument signature so torch.export
doesn't bundle inputs into one **kwargs tuple. This is internal and not an extension point.@register_patch("dynamo", ...).Cache and ModelOutput so torch.export can flatten and
rebuild it (usually happens automatically). Add a branch to _flatten_to_context / _unflatten_from_context
for a type the attribute walk can't reach.Dim.AUTO to every tensor and cache leaf when dynamic=True. Override
with DynamoConfig.dynamic_shapes.forward that torch.export leaves
as fake tensors. Extend by adding the attribute name to _STATEFUL_CACHE_ATTRS.OnnxExporter adds five stages around torch.onnx.export (see
exporter_onnx.py).
Grep the file for the full list of patches:
grep -nE "^def (_patch_|_fix_|_aten_)" src/transformers/exporters/exporter_onnx.py
torch ops the ONNX exporter can't translate as-is. Extend with
@register_patch("onnx", ...).run_decompositions so newly introduced shape-guard
nodes get repaired before lowering. Uses the same @register_patch("onnx", ...) registry.@register_fx_node_fix("onnx").aten.index_put or aten._grouped_mm). Add an _aten_* function to
_ONNX_TRANSLATION_TABLE.TopK(sorted=True)). Add a _fix_ir_* function to _IR_FIXES.ExecutorchExporter adds five stages around to_edge_transform_and_lower and to_executorch,
starting with backend preparation (see
exporter_executorch.py).
prepare_for_xnnpack, prepare_for_cuda). Add a backend by registering prepare_for_<name> in
_BACKEND_PREPARE.torch ops the ExecuTorch backends can't accept, such as split_copy,
chunk, and topk(k>dim). Extend with @register_patch("executorch", ...).@register_patch("executorch", ...) registry.@register_fx_program_fix("executorch").executorch_prim.* or
rewriting pow as a mul chain. Extend with @register_fx_node_fix("executorch").A few model classes hit confirmed bugs in the onnxscript graph optimizer (constant folding crashing
on SplitToSequence, FPN initializers being dropped). ONNX_DISABLE_OPTIMIZE
disables onnxscript optimization for those models. Each entry records the
upstream issue next to the model name. The list is expected to shrink as upstream bugs land, so a
new entry must reference a specific upstream bug rather than disable optimization arbitrarily.
EXPORT_SKIPS, opts a handful of model classes out of the export sweep entirely when the model is fundamentally non-exportable as-is (data-dependent control flow that can't be vectorized, or modules treated as forward arguments). Each entry carries a reason naming the model-side change needed. This list is also expected to shrink, not grow.