[Fix][Relax][Frontend][Torch] Support aten.diagonal from decomposed repeated-subscript einsum - #20237
[Fix][Relax][Frontend][Torch] Support aten.diagonal from decomposed repeated-subscript einsum#20237siyiweigeHEW wants to merge 4 commits into
aten.diagonal from decomposed repeated-subscript einsum#20237Conversation
…ed-subscript einsum
from_exported_program runs run_decompositions() by default, which lowers
torch.einsum with repeated subscripts (diagonal / trace, e.g. "ii->i",
"ii->", "...ii->...i") to aten.diagonal + permute (+ sum). The torch
frontend had no handler for aten.diagonal.default, so every such valid
model failed with `AssertionError: Unsupported function types
['diagonal.default']`. The same root cause blocked torch.diagonal /
torch.trace.
Add BaseFXGraphImporter._diagonal lowering diagonal(x, offset, dim1, dim2)
as permute_dims (move dim1/dim2 to trailing axes) -> two strided_slice
(crop each trailing axis to the diagonal length, offset-adjusted) ->
relax.op.einsum("...zz->...z"). Handles static and dynamic (symbolic)
shapes, positive/negative offsets, and arbitrary dim1/dim2 (incl. negative
indices). Register "diagonal.default" in the exported-program convert_map
and "diagonal" in the from_fx convert_map.
Fixes: apache#20228
Removed unused import statement for torch.
| n = shape.values[dim1] | ||
| m = shape.values[dim2] | ||
| if offset >= 0: | ||
| diag_len = tirx.min(n, m - offset) |
There was a problem hiding this comment.
Please clamp diag_len to zero. An offset outside the axis range is valid and should produce an empty diagonal. For example, a (3, 4) input with offset=5 returns shape (0,) in PyTorch, but this computes diag_len=-1, causing incompatible slice extents; offset=6 even produces the incorrect shape (1,). Please use tirx.max(0, tirx.min(...)) in both branches and add tests for out-of-range positive and negative offsets.
…ge offsets For an out-of-range offset (|offset| >= max(extent1, extent2)), PyTorch's torch.diagonal returns an empty diagonal of shape (0,). The lowering computed diag_len = min(extent1, extent2 - offset), which could go negative: e.g. a (3, 4) input with offset=5 gave diag_len=-1 and incompatible slice extents (the einsum then failed to broadcast extents 2 and 0), and offset=6 even produced an incorrect non-empty shape. Clamp diag_len with tirx.max(0, ...) in both the positive- and negative-offset branches so an out-of-range offset lowers to an empty diagonal, matching PyTorch. Add in-tree regression coverage for out-of-range positive and negative offsets.
|
Thanks for the careful review — you're right, and thanks for the concrete repro. Confirmed: for a Fixed in 1eb5cbb by clamping the diagonal length to zero in both branches: if offset >= 0:
diag_len = tirx.max(0, tirx.min(n, m - offset))
...
else:
diag_len = tirx.max(0, tirx.min(n + offset, m))An out-of-range offset now lowers to an empty diagonal of shape Tests added:
Verification after the fix: baseline reproduces the original assertion on |
Fixes: #20228
Summary
from_exported_programrunsexported_program.run_decompositions()bydefault, and PyTorch's decomposition lowers
torch.einsumwith repeatedsubscripts (diagonal / trace, e.g.
"ii->i","ii->","...ii->...i") toaten.diagonal+permute(+sumfor the trace).aten.diagonal.defaultwas missing from the torch frontend
convert_map, so every such validmodel failed with:
This PR adds an
aten.diagonalconverter and registers it in both theexported-program and
from_fxconvert maps, so repeated-subscript einsum —and the directly-affected ops
torch.diagonal/torch.trace— convert andrun. Verified failing equations from the issue all convert with
max|diff| = 0vs PyTorch.
Root cause
BaseFXGraphImporter._check_unsupported_func_typeasserts when acall_functionnode's target is not inconvert_map. For the einsum familyabove,
run_decompositionsintroducesaten.diagonal.defaultnodes that thetorch frontend had no handler for, so conversion aborts at the assertion. This
is the same root cause for the direct ops
torch.diagonal(lowered todiagonal.defaultas-is) andtorch.trace(lowered todiagonal+clone+sum). Skipping decomposition (run_ep_decomposition=False) keeps the einsumnode intact and works — confirming the defect is the missing
diagonalhandling, not
relax.op.einsumsemantics.Fix
Add
BaseFXGraphImporter._diagonalinbase_fx_graph_translator.py, loweringdiagonal(input, offset=0, dim1=0, dim2=1)as:relax.op.permute_dims— movedim1/dim2to the trailing two axes;relax.op.strided_slice— crop each trailing axis to the diagonallength
min(extent1, extent2 ± offset)(offset-adjusted), so the twotrailing extents are equal;
relax.op.einsum([x], "...zz->...z")— the repeatedzlabel runs overboth trailing axes simultaneously, extracting the diagonal.
The lowering handles static and dynamic (symbolic) shapes, positive/negative
offsets, and arbitrary
dim1/dim2(including negative indices). Register"diagonal.default"inExportedProgramImporter.create_convert_mapand"diagonal"inTorchFXImporter.create_convert_map.Validation
In-tree regression test (added)
test_einsum_repeated_subscriptintests/python/relax/test_frontend_from_exported_program.py:verify_modelagainst the exact lowering IR for"ii->i"on the defaultdecomposition path (this case used to raise the assertion);
verify_model_numericallyfor"ii->"(trace),"...ii->...i"(batcheddiagonal), the attention-style two-operand
"abca,abcb->c", and the directops
torch.diagonal(x, offset, 0, 1)andtorch.trace.Differential test
verify_patch.pyruns on the locked build and simulates the pre-fix behaviorat runtime (popping
diagonal.defaultfrom the generated convert map):equations + 11 direct
torch.diagonal/torch.trace/torch.diag) reproducethe exact
AssertionError: Unsupported function types ['diagonal.default'];1 case (
torch.diagon a 1-D input, which goes throughdiag_embed) isunaffected and stays correct in baseline.
max|diff| = 0."ii->i"and"...ii->...i"with symbolic dims (bothdiagonal dims sharing one
Dim) match PyTorch exactly.batch matmul, ellipsis broadcasting/summation, 3-operand, implicit output) —
15 cases — all still match with
max|diff| = 0.Run:
Files changed
python/tvm/relax/frontend/torch/base_fx_graph_translator.py— add_diagonal(permute_dims + strided_slice crop + einsum...zz->...z).python/tvm/relax/frontend/torch/exported_program_translator.py— register"diagonal.default"in the exported-programconvert_map.python/tvm/relax/frontend/torch/fx_translator.py— register"diagonal"inthe
from_fxconvert_map.tests/python/relax/test_frontend_from_exported_program.py— addtest_einsum_repeated_subscriptregression coverage.