[Fix][Relax][Frontend][PyTorch] Fix x.split(int) with a non-divisible split_size - #20240
Merged
tlopex merged 1 commit intoSep 1, 2026
Merged
Conversation
x.split(int) with a non-divisible split_size
siyiweigeHEW
force-pushed
the
fix/relax-torch-split-int-split-size
branch
2 times, most recently
from
August 30, 2026 19:47
809fca0 to
e907c64
Compare
…t_size torch.split(x, s, dim) splits dim into chunks of size s, with the last chunk smaller when D % s != 0. The frontend `_split` converter (base_fx_graph_translator.py) converted the per-chunk size into a section count n_section = ceil(D / s) and passed it to relax.op.split's integer argument, whose semantics are "split into n_section equal sections" (each of size ceil(D / n_section)). Whenever ceil(D / ceil(D / s)) != s -- e.g. split_size > D/2 with a non-divisible D -- valid PyTorch models silently produced differently-shaped chunks (e.g. x.split(6) on a (10,) tensor yielded (5,), (5,) instead of (6,), (4,)). Convert the int per-chunk size to the cumulative cut positions [s, 2s, ..., (ceil(D/s) - 1)*s] instead, the same list form the list/tuple branch already passes to relax.op.split. Differential testing vs native PyTorch (fixed `_split` injected verbatim): the 10 previously-diverging cases (non-divisible split_size > D/2 across dim=0, dim=1 and negative dims) all now match torch shapes and values; the divisible-size baseline and the list/tuple (split_with_sizes) control group remain unchanged. Added regression test test_split_int_split_size covering the structural IR (R.split(input, indices_or_sections=[6], axis=0) for x.split(6) on a (10,) input) and numerical equivalence for non-divisible sizes/dims. The pre-existing test_split expected IR in tests/python/relax/test_frontend_from_fx.py asserted the old int-section form (indices_or_sections=3 for x.split(1, dim=1)); it is updated to the correct list form ([1, 2]).
siyiweigeHEW
force-pushed
the
fix/relax-torch-split-int-split-size
branch
from
August 30, 2026 21:30
e907c64 to
6590e05
Compare
tlopex
approved these changes
Sep 1, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
[Relax][Frontend][PyTorch] Fix
x.split(int)with a non-divisiblesplit_sizeFixes: #20232
Summary
torch.split(x, s, dim)splitsdiminto chunks of sizes, with thelast chunk smaller when the dimension
Dis not divisible bys. TheRelax PyTorch frontend's
_splitconverter(
base_fx_graph_translator.py) converted the per-chunk size into asection count
n_section = ceil(D / s)and passed it torelax.op.split's integer argument — whose semantics are "split inton_sectionequal sections" (split_len = ceil(D / n_section),src/relax/op/tensor/manipulate.cc). Wheneverceil(D / ceil(D / s)) != s(e.g.split_size > D/2with anon-divisible
D), valid PyTorch models silently produceddifferently-shaped chunks. For example
x.split(6)on a(10,)tensoryielded
(5,), (5,)instead of(6,), (4,).This PR converts the int per-chunk size into the cumulative cut
positions
[s, 2s, ..., (ceil(D/s) - 1) * s]— the sameindicesformthe
list/tuplebranch already passes torelax.op.split— so bothforms produce PyTorch-identical chunk shapes.
Root cause
_splithandles twoatenops:split.Tensor(intsplit_size) andsplit_with_sizes.default(list/tuple). The list branch buildscumulative cut positions and is correct. The int branch instead computed
n_section = ceil(D / split_size)and relied onrelax.op.split'sinteger "equal sections" semantics, which only coincide with PyTorch's
per-chunk-size semantics when
ceil(D / ceil(D / s)) == s(divisiblesizes, or e.g.
D=10, s=3). The bug is a semantic mismatch between"chunks of size
s" (PyTorch) and "ceil(D/s)equal sections"(
relax.op.splitint), not a numerical issue.Fix
python/tvm/relax/frontend/torch/base_fx_graph_translator.py—_split, int branch:The
list/tuplebranch and thesplit_with_sizes.defaultmapping areunchanged.
_splitis shared byfrom_exported_programandfrom_fx(via
BaseFXGraphImporter), so both entry points are covered.Validation
In-tree regression test (added)
test_split_int_split_sizeintests/python/relax/test_frontend_from_exported_program.py:x.split(6, dim=0)on a(10,)input lowers toR.split(input, indices_or_sections=[6], axis=0)with output shapes(6,),(4,)(asserted viaverify_modelstructural equality);(10,) s=6/7/8/9 dim=0,(12,) s=7 dim=0,(12,8) s=5 dim=1,(3,10) s=6 dim=-1— shapes and values all match.Differential test
verify_patch.py(inprove_hum/torch_split/) runs the full suite onthe fixed
_splitinjected verbatim from this branch against thepre-fix
_splitfromorigin/main(tvm-env 0.18, whose_splitis byte-identical to the current frontend):s > D/2acrossdim=0,dim=1, and negative dims) reproduce the bug, e.g.(10,) s=6: torch[(6,), (4,)]vs TVM[(5,), (5,)];baseline (4/4) and the list/tuple control group (3/3) remain
unchanged — no regression.
Known pre-existing limitation (unchanged by this PR, same family as the
ONNX single-output
Splithandling):split_size >= Dor asingle-element
split_with_sizesproduces a single chunk, which olderrelax versions cannot import as a 1-tuple (single-output struct-info
inference). On current main the empty-indices form already lowers to a
proper 1-tuple, and this PR keeps
int split_size >= Dconsistent withthe existing single-element list behavior.
Files changed
python/tvm/relax/frontend/torch/base_fx_graph_translator.py— fixthe int
split_sizebranch of_splitto pass cumulative cutpositions instead of a section count.
tests/python/relax/test_frontend_from_exported_program.py— addtest_split_int_split_sizeregression coverage.tests/python/relax/test_frontend_from_fx.py—test_split'sexpected1asserted the old int-section IR form(
indices_or_sections=3forx.split(1, dim=1)); updated to thecorrect list form (
indices_or_sections=[1, 2]).