Repository navigation
Description
Summary
The backward of torch.segment_reduce / torch._segment_reduce with reduce="max" or reduce="min" on XPU splits the incoming gradient between the elements tied at the segment extremum only when that gradient is positive. For a negative gradient every tied element receives the full gradient, so the segment's input gradients sum to k * grad_output instead of grad_output. XPU amax / amin backward splits evenly for both signs.
The error is silent, sign-dependent and therefore biased (it does not average out over training steps). gradcheck does not see it, because random inputs have no ties, and the existing tests only call .sum().backward(), i.e. use a positive incoming gradient.
Upstream source
- segment_reduce max/min backward does not split negative gradients between tied elements pytorch/pytorch#201633 (open, filed 2026-10-21) reports the same defect for the CPU and CUDA kernels.
- Found by an XPU alignment scan of the window
[2026-10-21T00:00:00Z, 2026-10-22T00:00:00Z)overpytorch/pytorch, frozen default-branch head37f56e48ce431907080585a595c437f9edb23411.
Observed XPU behavior
Validated on XPU with torch 2.16.0.dev20261021+xpu:
case reduce=max grad_out=+1.0 tied=2 grad=[0.5, 0.5, 0.0] grad_sum=+1.000000 amax_amin_grad=[0.5, 0.5, 0.0] amax_amin_sum=+1.000000 matches_reference=True
case reduce=max grad_out=-1.0 tied=2 grad=[-1.0, -1.0, 0.0] grad_sum=-2.000000 amax_amin_grad=[-0.5, -0.5, -0.0] amax_amin_sum=-1.000000 matches_reference=False
case reduce=min grad_out=+1.0 tied=2 grad=[0.5, 0.5, 0.0] grad_sum=+1.000000 amax_amin_grad=[0.5, 0.5, 0.0] amax_amin_sum=+1.000000 matches_reference=True
case reduce=min grad_out=-1.0 tied=2 grad=[-1.0, -1.0, 0.0] grad_sum=-2.000000 amax_amin_grad=[-0.5, -0.5, -0.0] amax_amin_sum=-1.000000 matches_reference=False
Expected for grad_out=-1: [-0.5, -0.5, 0.0], as for amax / amin. The positive-gradient rows already split evenly in the same kernel, so the kernel is inconsistent with itself as well as with amax / amin.
Target path and root cause
segment_reduce_backward_kernel in src/ATen/native/xpu/sycl/SegmentReduceKernels.cpp (line 290). In the MAX/MIN branch (line 338) the first loop selects the tied elements with isnan(values_data[data_index]) || values_data[data_index] == output_data[output_index] and counts them, and the second loop then divides by that count:
// src/ATen/native/xpu/sycl/SegmentReduceKernels.cpp:359
if (grad_input_data[data_index] > 0) {
grad_input_data[data_index] = grad_input_data[data_index] / counter;
}The division loop gates on the sign of the value it just wrote instead of re-using the tie predicate, so only positive gradients are divided. The fix is to select the tied elements in the division loop with the same predicate used in the assignment loop.
This kernel backs both _segment_reduce_lengths_backward_kernel (line 554) and _segment_reduce_offsets_backward_kernel (line 573), so lengths- and offsets-based calls are both affected.
Target-path evidence: aten::_segment_reduce_backward has an XPU kernel registered (dispatch_segment_reduce_backward_xpu True), and both the forward output and the accumulated .grad are xpu tensors. The amax / amin reference was computed from the same XPU inputs on the same device, so the discrepancy is between two XPU paths and is not a host/device comparison artifact.
Reproducer
import torch
for reduce, vals in (("max", [2.0, 2.0, 1.0]), ("min", [1.0, 1.0, 2.0])):
for g in (1.0, -1.0):
data = torch.tensor(vals, device="xpu", requires_grad=True)
out = torch.segment_reduce(data, reduce, lengths=torch.tensor([3], device="xpu"))
out.backward(torch.tensor([g], device="xpu"))
ref = torch.tensor(vals, device="xpu", requires_grad=True)
(ref.amax() if reduce == "max" else ref.amin()).backward(torch.tensor(g, device="xpu"))
print(f"{reduce} grad_out={g:+.0f} segment_reduce: {data.grad.tolist()} "
f"(sum {data.grad.sum().item():+.1f}) "
f"{'amax' if reduce == 'max' else 'amin'}: {ref.grad.tolist()}")Environment
torch 2.16.0.dev20261021+xpu
python 3.13.13
device Intel(R) Arc(TM) Pro B60 Graphics
Ownership
The XPU kernel carries its own copy of the gate. The same grad_input_data[data_index] > 0 gate is present upstream in aten/src/ATen/native/SegmentReduce.cpp:271 and aten/src/ATen/native/cuda/SegmentReduce.cu:222 at the scanned head, so an upstream fix to the CPU and CUDA kernels will not change src/ATen/native/xpu/sycl/SegmentReduceKernels.cpp; the XPU change is independent work in this repository.
Current stage of the related upstream fix: pytorch/pytorch#201633 is open and the reporter states they have a fix and will open a PR; no fix PR is linked to the issue as of this scan, so the upstream change is proposed, not landed. The XPU fix should adopt the same tie predicate so XPU matches the CPU/CUDA convention once that PR lands.
Partially related, not covering this finding: pytorch/pytorch#193476 (open) bounds-checks offsets in the CUDA segment_reduce_backward_kernel to fix an out-of-bounds write; it mentions the tie split only in a review comment and does not change the gate. No open or closed intel/torch-xpu-ops tracker was found for the tie split (searched segment_reduce, segment reduce backward, tied gradient, and PRs touching SegmentReduce*), and upstream test/test_segment_reductions.py carries no XPU skip, xfail or tracker reference at the scanned head.
Regression test
No existing test covers this: the upstream cases use a positive incoming gradient. A fix should add a tied-segment case with a negative grad_output for both max and min, asserting agreement with amax / amin backward on the same inputs.
Activity
laifenxiawucha commentedon Oct 28, 2026
@torchxpubot fix
torchxpubot commentedon Oct 28, 2026
Automated fix session for #5808. Stages are appended below as they complete.
Issue Triage
Automated by issue-triage.
Reproduce
Reproduced against the nightly wheel
torch 2.16.0.dev20261027+xpuon Intel(R) Arc(TM) Pro B60 Graphics (BMG). The reproducer asserts thesegment_reducebackward gradient againstamax/aminbackward on the same XPU inputs, and covers both thelengths=andoffsets=entry points.reduce=max grad_out=-1 lengthssegment_reduce=[-1.0, -1.0, 0.0] (sum -2.0) expected=[-0.5, -0.5, -0.0]reduce=max grad_out=-1 offsetssegment_reduce=[-1.0, -1.0, 0.0] (sum -2.0) expected=[-0.5, -0.5, -0.0]reduce=min grad_out=-1 lengthssegment_reduce=[-1.0, -1.0, 0.0] (sum -2.0) expected=[-0.5, -0.5, -0.0]reduce=min grad_out=-1 offsetssegment_reduce=[-1.0, -1.0, 0.0] (sum -2.0) expected=[-0.5, -0.5, -0.0]The four positive-
grad_outputrows of the same matrix pass, confirming the defect is sign-gated rather than a general tie-splitting failure.Automated by fix-reproduce.
Root Cause
Analyzed at
torch-xpu-ops@73a6552c3, pytorchorigin/main@ec1459e58.segment_reduce_backward_kernelinsrc/ATen/native/xpu/sycl/SegmentReduceKernels.cpp:290handlesMAX/MINin two passes over the segment. The first pass (line 341) selects the elements tied at the segment extremum with the predicatesycl::isnan(values_data[data_index]) || values_data[data_index] == output_data[output_index], writes the incominggrad_data[output_index]to each of them, and counts them intocounter. The second pass (line 356) is supposed to split that gradientcounterways, but it re-selects the elements withif (grad_input_data[data_index] > 0)(line 359) -- the sign of the value it just wrote -- instead of re-using the tie predicate. A negative incoming gradient therefore fails the gate and is never divided, so every one of thektied elements keeps the fullgrad_outputand the segment's input gradients sum tok * grad_output. A positive gradient takes the same path and is divided correctly, which is why the defect is sign-gated and silent.This is not a regression: the gate has been present since the kernel was written, and the two tests that exercise
max/minbackward cannot see it.test/test_segment_reductions.pydrives backward throughactual_result.sum().backward()(line 93), i.e. an all-ones -- positive -- incoming gradient, and itsgradcheckcases (lines 482-483) usesrcvalues that are distinct within each segment, so no tie is ever formed. Both the lengths- and offsets-based entry points route through this one kernel (_segment_reduce_lengths_backward_kernelat line 554,_segment_reduce_offsets_backward_kernelat line 573), and the reproduce stage confirmed all four combinations fail.The same gate exists upstream in
aten/src/ATen/native/SegmentReduce.cpp:271andaten/src/ATen/native/cuda/SegmentReduce.cu:222, so the XPU kernel inherited the defect rather than diverging from a correct reference.pytorch/pytorch#201633reports it for CPU and CUDA and is still open with no fix PR linked, so no upstream change will touch the XPU file -- the fix here is independent work in this repository. The correct behavior is defined byamax/aminbackward, which splits evenly for both signs, and is reached by selecting the tied elements in the division pass with the same predicate the assignment pass uses. No test anywhere covers a tied segment with a negative incoming gradient, so a regression test is part of the fix.Automated by fix-root-cause.
Implement Result
torch-xpu-ops@73a6552c3src/ATen/native/xpu/sycl/SegmentReduceKernels.cpp(+3/-1): the MAX/MIN division pass now selects tied elements with the same predicate the assignment pass uses.test/regressions/test_segment_reduce_backward_ties.py(+71): new XPU regression test.SegmentReduceKernels.cpp:359) rather than on tie membership, so only positive gradients were divided by the tie count. The predicate it now uses is literally the one from the assignment pass atSegmentReduceKernels.cpp:344-346, which is the condition that defines the tie, so both signs are split. The expected result is defined byamax/aminbackward, which the new test uses as its reference on the same XPU inputs. The test is XPU-only because the identical gate is still live upstream ataten/src/ATen/native/SegmentReduce.cpp:271andaten/src/ATen/native/cuda/SegmentReduce.cu:222, so a device-generic version would fail on CPU.Skip-guard review (
allow_skip=false): APPROVE.Unrelated pre-existing issue noticed, not touched:
SegmentReduceKernels.cpp:312-317has theif (idx >= size) { return; }guard duplicated verbatim.Automated by fix-implement.
Verify
Built pytorch
origin/main@ec1459e58from source with this branch asthird_party/torch-xpu-ops,TORCH_XPU_ARCH_LIST=bmg,pvc, on Intel(R) Arc(TM) Pro B60 Graphics (BMG). The baseline and fixed builds differ only by the one hunk inSegmentReduceKernels.cpp.reduce=max grad_out=-1 lengthsreduce=max grad_out=-1 offsetsreduce=min grad_out=-1 lengthsreduce=min grad_out=-1 offsetsBeforewassegment_reduce=[-1.0, -1.0, 0.0] (sum -2.0) expected=[-0.5, -0.5, -0.0]; the issue's own reproducer now returns[-0.5, -0.5, 0.0], matchingamax/aminbackward exactly.The usual caveat that a passing source build only proves "passes with the fix present" was closed out here: reverting only the
SegmentReduceKernels.cpphunk and rebuilding the same tree returned 4 failed / 4 passed (every negative-gradient combination failed, the positive-gradient half unaffected), and restoring the hunk returned all 8 to passing. The fix is what makes the test pass.Regression checks on the fixed build:
test/regressions/test_segment_reduce_backward_ties.pytest/xpu/test_segment_reductions_xpu.pytest/xpu/test_ops_xpu.py -k segment_reducetest/regressions/The single
test/regressions/failure istest_xpu_ops_header, which asserts on installed headers undertorch/include/ATen/ops. An editable source build produces notorch/includeat all (the generated headers stay inbuild/aten/src/ATen/ops/), so it fails for any such build and is unrelated to this change.lintrunner -areported no issues and changed nothing; this repo ships nospinconfig, solintrunneris its canonical linter)Automated by fix-verify.
Summary
Outcome: IMPLEMENTING(fix_verified)
The MAX/MIN branch of the XPU
segment_reducebackward kernel split a tied segment's gradient in two passes, but the second pass picked what to divide by the sign of the value it had just written rather than by tie membership, so a negative gradient was handed in full to each of thektied elements and the segment's gradients summed tok * grad_output. The division pass now selects tied elements with the sameisnan(value) || value == outputpredicate the assignment pass uses. A source build on BMG takes all four reported cases from FAIL to PASS and reproducesamax/aminbackward exactly; reverting only that hunk on the same tree puts the four failures back, so the fix is demonstrably what resolves them, and the existingsegment_reducecoverage (74 + 34 cases) is unchanged. A new XPU-only regression test covers tied segments with a negativegrad_outputacross both thelengths=andoffsets=entry points -- XPU-only becausepytorch/pytorch#201633reports the identical gate in the upstream CPU and CUDA kernels and is still open, so a device-generic version would fail on CPU.agent/fix-issue-580873a6552c3c122614d2651de2a0c6feb8f81ae613src/ATen/native/xpu/sycl/SegmentReduceKernels.cpp,test/regressions/test_segment_reduce_backward_ties.pyOne thing for the reviewer:
SegmentReduceKernels.cpp:312-317carries theif (idx >= size) { return; }guard twice, verbatim. It is pre-existing and untouched here.Generated by fix job.
Review
Copy into a new comment, keep ONE verdict, add a note. Two verdicts in
one comment are dropped as ambiguous.
On a batch run add
item=<pytorch issue>to say which sub-item theverdict is about, in the bold text and in the marker -- the marker
is invisible, so the bold line is all a reader sees. One comment per
sub-item, since a bare verdict on a batch does not say which test it
means and is dropped.
run=<id>only when judging a run other than thenewest, in both places too.
A CI DISABLED sub-item,
item=from the fan-out table:Another sub-item of the same batch, judged on an earlier run:
An
[xpu-alignment]issue that should not have been filed -- one issueper difference, so no
item=:daisyden commentedon Nov 10, 2026
label-issue: #5808
Root cause: in
segment_reduce_backward_kernel, the MAX/MIN tie-split loop divides by the tie count only whengrad_input_data[data_index] > 0(
src/ATen/native/xpu/sycl/SegmentReduceKernels.cpp:359). A negative grad is never split; the fix belongs in torch-xpu-ops.typeBugissue_typeempty; reports an incorrect gradient (wrong result) from the XPU kernel.testtest: utmodulemodule: reductionSegmentReduceKernels.cpp:290.priorityMediumAccuracytrainingduplicateApplied to this issue. Generated by a local
label-issuerun.