From ee96e82a1b50729fea02690cfba07cd5e148a343 Mon Sep 17 00:00:00 2001 From: ydshieh Date: Fri, 21 Aug 2026 18:52:52 +0200 Subject: [PATCH 1/7] [ONNX] Skip affected models on torch >= 2.13 (two dynamo ONNX regressions) torch 2.13.0 introduced two regressions in dynamo ONNX export: - pytorch/pytorch#194381: aten.sub type-promotion failure for scalar - int_tensor - pytorch/pytorch#194382: aten.mul.Scalar missing ONNX decomposition Skip all 35 affected model classes under `EXPORT_SKIPS["onnx"]` when torch >= 2.13, using the existing skip infrastructure. Will be removed once the upstream PyTorch fixes land. Co-Authored-By: Claude Sonnet 4.6 --- tests/exporters/test_export.py | 49 ++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tests/exporters/test_export.py b/tests/exporters/test_export.py index 5d074da906f7..ed5ca9d9299e 100644 --- a/tests/exporters/test_export.py +++ b/tests/exporters/test_export.py @@ -36,6 +36,7 @@ module_dtype, ) from transformers.testing_utils import ( + is_torch_greater_or_equal, require_executorch, require_onnxruntime, require_onnxscript, @@ -306,6 +307,54 @@ }, } +# torch >= 2.13.0 introduced two regressions in dynamo ONNX export that affect models using +# ``scalar - int_tensor`` or ``tensor * float_scalar`` patterns. All affected model classes are +# skipped until the upstream fix lands. +# pytorch/pytorch#194381 — aten.sub type-promotion failure (step 2/3) +# pytorch/pytorch#194382 — aten.mul.Scalar missing ONNX decomposition (step 3/3) +if is_torch_greater_or_equal("2.13"): + _r194381 = "torch >= 2.13 aten.sub type-promotion regression (pytorch/pytorch#194381)" + _r194382 = "torch >= 2.13 aten.mul.Scalar missing ONNX decomposition (pytorch/pytorch#194382)" + EXPORT_SKIPS.setdefault("onnx", {}).update( + { + "BigBirdModel": _r194381, + "BigBirdForPreTraining": _r194381, + "BigBirdForMaskedLM": _r194381, + "BigBirdForCausalLM": _r194381, + "ProphetNetModel": _r194381, + "ProphetNetForConditionalGeneration": _r194381, + "ProphetNetDecoder": _r194381, + "ProphetNetForCausalLM": _r194381, + "ProphetNetEncoder": _r194381, + "BrosModel": _r194382, + "BrosForTokenClassification": _r194382, + "BrosSpadeEEForTokenClassification": _r194382, + "BrosSpadeELForTokenClassification": _r194382, + "DeepseekOcr2Model": _r194382, + "DeepseekOcr2ForConditionalGeneration": _r194382, + "EfficientLoFTRModel": _r194382, + "EfficientLoFTRForKeypointMatching": _r194382, + "GotOcr2Model": _r194382, + "GotOcr2ForConditionalGeneration": _r194382, + "GroundingDinoModel": _r194382, + "GroundingDinoForObjectDetection": _r194382, + "MMGroundingDinoModel": _r194382, + "MMGroundingDinoForObjectDetection": _r194382, + "PPFormulaNetForConditionalGeneration": _r194382, + "SamModel": _r194382, + "SamVisionModel": _r194382, + "SamHQModel": _r194382, + "SamHQVisionModel": _r194382, + "SegGptModel": _r194382, + "SegGptForImageSegmentation": _r194382, + "SLANeXtForTableRecognition": _r194382, + "SplinterModel": _r194382, + "SplinterForQuestionAnswering": _r194382, + "SplinterForPreTraining": _r194382, + "Xcodec2Model": _r194382, + } + ) + # ──────────────────────────── ONNX optimization toggles ──────────────────────────── # Not "skips" — these select whether `onnxscript` optimisation runs for a given model. From 2fd84abb294f98a8277e932e32bc81940388114f Mon Sep 17 00:00:00 2001 From: ydshieh Date: Fri, 21 Aug 2026 18:54:43 +0200 Subject: [PATCH 2/7] Scope ONNX skip to torch == 2.13.x only (auto-runs on 2.14+) Co-Authored-By: Claude Sonnet 4.6 --- tests/exporters/test_export.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/exporters/test_export.py b/tests/exporters/test_export.py index ed5ca9d9299e..112d2d1a4f97 100644 --- a/tests/exporters/test_export.py +++ b/tests/exporters/test_export.py @@ -312,7 +312,7 @@ # skipped until the upstream fix lands. # pytorch/pytorch#194381 — aten.sub type-promotion failure (step 2/3) # pytorch/pytorch#194382 — aten.mul.Scalar missing ONNX decomposition (step 3/3) -if is_torch_greater_or_equal("2.13"): +if is_torch_greater_or_equal("2.13") and not is_torch_greater_or_equal("2.14"): _r194381 = "torch >= 2.13 aten.sub type-promotion regression (pytorch/pytorch#194381)" _r194382 = "torch >= 2.13 aten.mul.Scalar missing ONNX decomposition (pytorch/pytorch#194382)" EXPORT_SKIPS.setdefault("onnx", {}).update( From 1a97988912ff7eb4aaa8844bf8ad32f7c4c7bc19 Mon Sep 17 00:00:00 2001 From: ydshieh Date: Fri, 21 Aug 2026 18:56:02 +0200 Subject: [PATCH 3/7] Add missing BigBird subclasses to torch 2.13 ONNX skip list Co-Authored-By: Claude Sonnet 4.6 --- tests/exporters/test_export.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/exporters/test_export.py b/tests/exporters/test_export.py index 112d2d1a4f97..55e85878d177 100644 --- a/tests/exporters/test_export.py +++ b/tests/exporters/test_export.py @@ -321,6 +321,10 @@ "BigBirdForPreTraining": _r194381, "BigBirdForMaskedLM": _r194381, "BigBirdForCausalLM": _r194381, + "BigBirdForMultipleChoice": _r194381, + "BigBirdForQuestionAnswering": _r194381, + "BigBirdForSequenceClassification": _r194381, + "BigBirdForTokenClassification": _r194381, "ProphetNetModel": _r194381, "ProphetNetForConditionalGeneration": _r194381, "ProphetNetDecoder": _r194381, From 572f5c8f8f0fbf816c3d0403953a18cfd3f0f6b8 Mon Sep 17 00:00:00 2001 From: ydshieh Date: Fri, 21 Aug 2026 19:03:49 +0200 Subject: [PATCH 4/7] [ONNX] Clarify skip guard is torch == 2.13.x only (not >=) Update comment and skip reason strings to say `torch == 2.13` instead of `torch >= 2.13`, making it clear the guard auto-lifts on 2.14+. Co-Authored-By: Claude Sonnet 4.6 --- tests/exporters/test_export.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/exporters/test_export.py b/tests/exporters/test_export.py index 55e85878d177..22406acb1cba 100644 --- a/tests/exporters/test_export.py +++ b/tests/exporters/test_export.py @@ -307,14 +307,14 @@ }, } -# torch >= 2.13.0 introduced two regressions in dynamo ONNX export that affect models using +# torch == 2.13.x introduced two regressions in dynamo ONNX export that affect models using # ``scalar - int_tensor`` or ``tensor * float_scalar`` patterns. All affected model classes are -# skipped until the upstream fix lands. +# skipped for that release only; the guard auto-lifts on torch 2.14+. # pytorch/pytorch#194381 — aten.sub type-promotion failure (step 2/3) # pytorch/pytorch#194382 — aten.mul.Scalar missing ONNX decomposition (step 3/3) if is_torch_greater_or_equal("2.13") and not is_torch_greater_or_equal("2.14"): - _r194381 = "torch >= 2.13 aten.sub type-promotion regression (pytorch/pytorch#194381)" - _r194382 = "torch >= 2.13 aten.mul.Scalar missing ONNX decomposition (pytorch/pytorch#194382)" + _r194381 = "torch == 2.13 aten.sub type-promotion regression (pytorch/pytorch#194381)" + _r194382 = "torch == 2.13 aten.mul.Scalar missing ONNX decomposition (pytorch/pytorch#194382)" EXPORT_SKIPS.setdefault("onnx", {}).update( { "BigBirdModel": _r194381, From 6fa59a8e84e9bf687462309c4f8ec21919e1a775 Mon Sep 17 00:00:00 2001 From: IlyasMoutawwakil Date: Wed, 26 Aug 2026 15:08:35 +0200 Subject: [PATCH 5/7] [ONNX] Fix the torch 2.13 scalar regressions instead of skipping the models MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both regressions are a Python float meeting an integral tensor, whose promotion torch 2.13 mishandles: `1.0 - int_mask` crashes the decomposition pass (pytorch/pytorch#194381) and `int_mask * 2.0` reaches translation with no registered ONNX decomposition (pytorch/pytorch#194382). On the same torch, `1.0 - float_tensor` and `float_tensor * 2.0` export fine — so promoting the tensor operand up front, to the dtype the op already produces, is enough. The op and its overload are left alone, and the inserted cast carries the op's own `meta` because for these elementwise cases it is the same value. Decomposition also emits `mul.Scalar` with a *symbolic* second operand (a division result, not a literal). There is no constant to promote there, so that one is rewritten to `mul.Tensor`, which has the two-operand translation — the same rewrite `_fix_remainder_scalar` makes for the same reason. Reachable because the FX fixes run again right after `run_decompositions`. This drops the 39 skip entries: the 15 affected families export again on 2.13, including the vision models (Sam, SamHQ, GotOcr2, GroundingDino, SegGpt, EfficientLoFTR, DeepseekOcr2). Co-Authored-By: Claude Opus 5 --- src/transformers/exporters/exporter_onnx.py | 81 +++++++++++++++++++++ tests/exporters/test_export.py | 53 -------------- 2 files changed, 81 insertions(+), 53 deletions(-) diff --git a/src/transformers/exporters/exporter_onnx.py b/src/transformers/exporters/exporter_onnx.py index 504ac4085d5a..1ecacc0f7511 100644 --- a/src/transformers/exporters/exporter_onnx.py +++ b/src/transformers/exporters/exporter_onnx.py @@ -727,6 +727,87 @@ def _fix_sort_stable(gm: torch.fx.GraphModule, node: torch.fx.Node) -> bool: return True +# Ops where torch 2.13 mishandles a Python float meeting an integral tensor. `sub`/`rsub` and `mul` are +# what the affected models spell (`1.0 - attention_mask`, `mask * 2.0`); both overloads appear, since +# decomposition rewrites `.Tensor` to `.Scalar` when the operand is a constant. +_INTEGRAL_SCALAR_PROMOTION_OPS = frozenset( + { + torch.ops.aten.rsub.Scalar, + torch.ops.aten.sub.Scalar, + torch.ops.aten.sub.Tensor, + torch.ops.aten.mul.Scalar, + torch.ops.aten.mul.Tensor, + } +) + + +@register_fx_node_fix("onnx") +def _fix_integral_tensor_float_scalar(gm: torch.fx.GraphModule, node: torch.fx.Node) -> bool: + """Promote an integral tensor before it meets a Python float, which torch 2.13 mishandles. + + Two torch 2.13 regressions have the same shape — a float scalar against an *integral* tensor, whose + promotion the export pipeline no longer gets right: + + - `1.0 - int_mask` (`aten.rsub.Scalar`) crashes the decomposition pass (pytorch/pytorch#194381), + - `int_mask * 2.0` (`aten.mul.Tensor`, `aten.mul.Scalar` after decomposition) reaches translation with + no ONNX decomposition registered for it (pytorch/pytorch#194382). + + Both go away once the tensor is already the dtype the op produces: `1.0 - float_tensor` and + `float_tensor * 2.0` export fine on the same torch. So rather than rewriting the op — which would mean + building the constant as a tensor and picking the right overload — cast its tensor operand up front and + leave the op alone. The cast's value is the op's own output for these elementwise cases (same shape, + the promoted dtype), so it carries `node.meta` unchanged. + + Self-limiting: once the operand is floating point the predicate no longer matches, so the walk cannot + revisit it. + """ + if node.target not in _INTEGRAL_SCALAR_PROMOTION_OPS: + return False + if len(node.args) < 2: + return False + tensor_arg, scalar_arg = node.args[0], node.args[1] + # A tensor on the left, a Python float on the right — a `Node` there is already a real tensor operand. + if not isinstance(tensor_arg, torch.fx.Node) or not isinstance(scalar_arg, float): + return False + operand, result = tensor_arg.meta.get("val"), node.meta.get("val") + if operand is None or result is None: + return False + if operand.dtype.is_floating_point or not result.dtype.is_floating_point: + return False + with gm.graph.inserting_before(node): + promoted = gm.graph.call_function( + torch.ops.aten._to_copy.default, args=(tensor_arg,), kwargs={"dtype": result.dtype} + ) + promoted.meta.update(node.meta) + node.replace_input_with(tensor_arg, promoted) + return True + + +@register_fx_node_fix("onnx") +def _fix_mul_scalar_symbolic(gm: torch.fx.GraphModule, node: torch.fx.Node) -> bool: + """Rewrite `mul.Scalar` to `mul.Tensor` when its 'scalar' is a graph node. + + The other half of pytorch/pytorch#194382: torchlib registers no real-valued `aten.mul.Scalar` + translation at all, and decomposition also produces that overload with a *symbolic* second operand — a + division result rather than a literal (`mul.Scalar(x, %truediv_1)`) — which the promotion fix above + cannot address, since there is no Python constant to promote against. `mul.Tensor` has the two-operand + translation, which is the same rewrite `_fix_remainder_scalar` makes for the same reason. + + Reached because the FX fixes run a second time right after `run_decompositions`, where this overload + appears. + """ + if node.target is not torch.ops.aten.mul.Scalar: + return False + if len(node.args) < 2 or not isinstance(node.args[1], torch.fx.Node): + return False + with gm.graph.inserting_before(node): + new = gm.graph.call_function(torch.ops.aten.mul.Tensor, args=node.args) + new.meta.update(node.meta) + node.replace_all_uses_with(new) + gm.graph.erase_node(node) + return True + + @register_fx_node_fix("onnx") def _fix_remainder_scalar(gm: torch.fx.GraphModule, node: torch.fx.Node) -> bool: """Rewrite remainder.Scalar to remainder.Tensor when the 'scalar' arg is actually a tensor. diff --git a/tests/exporters/test_export.py b/tests/exporters/test_export.py index 22406acb1cba..5d074da906f7 100644 --- a/tests/exporters/test_export.py +++ b/tests/exporters/test_export.py @@ -36,7 +36,6 @@ module_dtype, ) from transformers.testing_utils import ( - is_torch_greater_or_equal, require_executorch, require_onnxruntime, require_onnxscript, @@ -307,58 +306,6 @@ }, } -# torch == 2.13.x introduced two regressions in dynamo ONNX export that affect models using -# ``scalar - int_tensor`` or ``tensor * float_scalar`` patterns. All affected model classes are -# skipped for that release only; the guard auto-lifts on torch 2.14+. -# pytorch/pytorch#194381 — aten.sub type-promotion failure (step 2/3) -# pytorch/pytorch#194382 — aten.mul.Scalar missing ONNX decomposition (step 3/3) -if is_torch_greater_or_equal("2.13") and not is_torch_greater_or_equal("2.14"): - _r194381 = "torch == 2.13 aten.sub type-promotion regression (pytorch/pytorch#194381)" - _r194382 = "torch == 2.13 aten.mul.Scalar missing ONNX decomposition (pytorch/pytorch#194382)" - EXPORT_SKIPS.setdefault("onnx", {}).update( - { - "BigBirdModel": _r194381, - "BigBirdForPreTraining": _r194381, - "BigBirdForMaskedLM": _r194381, - "BigBirdForCausalLM": _r194381, - "BigBirdForMultipleChoice": _r194381, - "BigBirdForQuestionAnswering": _r194381, - "BigBirdForSequenceClassification": _r194381, - "BigBirdForTokenClassification": _r194381, - "ProphetNetModel": _r194381, - "ProphetNetForConditionalGeneration": _r194381, - "ProphetNetDecoder": _r194381, - "ProphetNetForCausalLM": _r194381, - "ProphetNetEncoder": _r194381, - "BrosModel": _r194382, - "BrosForTokenClassification": _r194382, - "BrosSpadeEEForTokenClassification": _r194382, - "BrosSpadeELForTokenClassification": _r194382, - "DeepseekOcr2Model": _r194382, - "DeepseekOcr2ForConditionalGeneration": _r194382, - "EfficientLoFTRModel": _r194382, - "EfficientLoFTRForKeypointMatching": _r194382, - "GotOcr2Model": _r194382, - "GotOcr2ForConditionalGeneration": _r194382, - "GroundingDinoModel": _r194382, - "GroundingDinoForObjectDetection": _r194382, - "MMGroundingDinoModel": _r194382, - "MMGroundingDinoForObjectDetection": _r194382, - "PPFormulaNetForConditionalGeneration": _r194382, - "SamModel": _r194382, - "SamVisionModel": _r194382, - "SamHQModel": _r194382, - "SamHQVisionModel": _r194382, - "SegGptModel": _r194382, - "SegGptForImageSegmentation": _r194382, - "SLANeXtForTableRecognition": _r194382, - "SplinterModel": _r194382, - "SplinterForQuestionAnswering": _r194382, - "SplinterForPreTraining": _r194382, - "Xcodec2Model": _r194382, - } - ) - # ──────────────────────────── ONNX optimization toggles ──────────────────────────── # Not "skips" — these select whether `onnxscript` optimisation runs for a given model. From bf700cc41bdc1760a91aa5a2b8a674cbfe863a35 Mon Sep 17 00:00:00 2001 From: IlyasMoutawwakil Date: Wed, 26 Aug 2026 16:00:11 +0200 Subject: [PATCH 6/7] Resolve the promoted-op set on first use, not at import MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `exporter_onnx` is importable without torch — the CI job that imports transformers with PIL only proved this the hard way — and naming `torch.ops.aten.*` overloads in a module-level frozenset broke that with a `NameError` before anything ran. Co-Authored-By: Claude Opus 5 --- src/transformers/exporters/exporter_onnx.py | 33 +++++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/src/transformers/exporters/exporter_onnx.py b/src/transformers/exporters/exporter_onnx.py index 1ecacc0f7511..b0006be3a8bf 100644 --- a/src/transformers/exporters/exporter_onnx.py +++ b/src/transformers/exporters/exporter_onnx.py @@ -727,18 +727,25 @@ def _fix_sort_stable(gm: torch.fx.GraphModule, node: torch.fx.Node) -> bool: return True -# Ops where torch 2.13 mishandles a Python float meeting an integral tensor. `sub`/`rsub` and `mul` are -# what the affected models spell (`1.0 - attention_mask`, `mask * 2.0`); both overloads appear, since -# decomposition rewrites `.Tensor` to `.Scalar` when the operand is a constant. -_INTEGRAL_SCALAR_PROMOTION_OPS = frozenset( - { - torch.ops.aten.rsub.Scalar, - torch.ops.aten.sub.Scalar, - torch.ops.aten.sub.Tensor, - torch.ops.aten.mul.Scalar, - torch.ops.aten.mul.Tensor, - } -) +@functools.cache +def _integral_scalar_promotion_ops() -> frozenset: + """Ops where torch 2.13 mishandles a Python float meeting an integral tensor. + + `sub`/`rsub` and `mul` are what the affected models spell (`1.0 - attention_mask`, `mask * 2.0`); both + overloads appear, since decomposition rewrites `.Tensor` to `.Scalar` when the operand is a constant. + + Resolved on first use rather than at import: this module is importable without torch, and naming an + `OpOverload` at module scope breaks that. + """ + return frozenset( + { + torch.ops.aten.rsub.Scalar, + torch.ops.aten.sub.Scalar, + torch.ops.aten.sub.Tensor, + torch.ops.aten.mul.Scalar, + torch.ops.aten.mul.Tensor, + } + ) @register_fx_node_fix("onnx") @@ -761,7 +768,7 @@ def _fix_integral_tensor_float_scalar(gm: torch.fx.GraphModule, node: torch.fx.N Self-limiting: once the operand is floating point the predicate no longer matches, so the walk cannot revisit it. """ - if node.target not in _INTEGRAL_SCALAR_PROMOTION_OPS: + if node.target not in _integral_scalar_promotion_ops(): return False if len(node.args) < 2: return False From a3e6ec68e7d377ac76deedcf5662bf5b62a004fe Mon Sep 17 00:00:00 2001 From: IlyasMoutawwakil Date: Thu, 27 Aug 2026 13:32:59 +0200 Subject: [PATCH 7/7] Say what the `mul.Scalar` operand is, and check it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review raised two things. The `Node` check assumed a tensor operand: it is never one. Across the affected families all 33 sites are an `operator.truediv` result, i.e. a `SymFloat`, which `mul.Tensor`'s translation does take — so the rewrite stands, but the guard now names the forms that op accepts instead of trusting the node type, and anything else keeps the `mul.Scalar` overload and fails visibly in translation. The fixes are also deliberately not version-gated, which the docstring implied they were. Both rewrites are semantics-preserving on any torch — a cast to the dtype the op already produces, and an overload swap with the same meaning — so gating would only decide which torch exercises the path. Co-Authored-By: Claude Opus 5 --- src/transformers/exporters/exporter_onnx.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/transformers/exporters/exporter_onnx.py b/src/transformers/exporters/exporter_onnx.py index b0006be3a8bf..2da0a286c316 100644 --- a/src/transformers/exporters/exporter_onnx.py +++ b/src/transformers/exporters/exporter_onnx.py @@ -729,7 +729,12 @@ def _fix_sort_stable(gm: torch.fx.GraphModule, node: torch.fx.Node) -> bool: @functools.cache def _integral_scalar_promotion_ops() -> frozenset: - """Ops where torch 2.13 mishandles a Python float meeting an integral tensor. + """Ops where a Python float meeting an integral tensor needs the tensor promoted first. + + Named for torch 2.13, where the mishandling appeared, but applied on every version: both rewrites are + semantics-preserving — a cast to the dtype the op already produces, and an overload swap with the same + meaning — so gating them on a version would add a branch that changes nothing except which torch the + path is exercised on. `sub`/`rsub` and `mul` are what the affected models spell (`1.0 - attention_mask`, `mask * 2.0`); both overloads appear, since decomposition rewrites `.Tensor` to `.Scalar` when the operand is a constant. @@ -800,6 +805,12 @@ def _fix_mul_scalar_symbolic(gm: torch.fx.GraphModule, node: torch.fx.Node) -> b cannot address, since there is no Python constant to promote against. `mul.Tensor` has the two-operand translation, which is the same rewrite `_fix_remainder_scalar` makes for the same reason. + That operand is a `SymFloat`, not a tensor: across the affected families every one of the 33 sites is + an `operator.truediv` result. `mul.Tensor`'s translation takes a symbolic scalar there — it becomes a + graph value like any other — so the rewrite is sound, but the guard below names both accepted forms + rather than trusting that a `Node` implies a tensor. Anything else (a nested list, an unbacked value + with no `val`) is left as `mul.Scalar` to fail visibly in translation instead of silently here. + Reached because the FX fixes run a second time right after `run_decompositions`, where this overload appears. """ @@ -807,6 +818,9 @@ def _fix_mul_scalar_symbolic(gm: torch.fx.GraphModule, node: torch.fx.Node) -> b return False if len(node.args) < 2 or not isinstance(node.args[1], torch.fx.Node): return False + other = node.args[1].meta.get("val") + if not isinstance(other, (torch.Tensor, torch.SymFloat, torch.SymInt, torch.SymBool)): + return False with gm.graph.inserting_before(node): new = gm.graph.call_function(torch.ops.aten.mul.Tensor, args=node.args) new.meta.update(node.meta)