[BugFix] Support keep-dim destinations in fragment reduce - #2815
[BugFix] Support keep-dim destinations in fragment reduce#2815li-ruinan wants to merge 3 commits into
Conversation
|
👋 Hi! Thank you for contributing to the TileLang project. Please remember to run We appreciate you taking this step! Our team will review your contribution, and we look forward to your awesome work! 🚀 |
📝 WalkthroughWalkthroughChangesKeep-dimension reduction support
Estimated code review effort: 3 (Moderate) | ~20 minutes Suggested reviewers: Sequence Diagram(s)sequenceDiagram
participant ReduceOpNode
participant ComputeReducerLayout
participant ReduceLowerer
participant KeepdimTest
ReduceOpNode->>ComputeReducerLayout: derive layout with keepdim
ComputeReducerLayout-->>ReduceOpNode: return rank-preserving reducer layout
ReduceLowerer->>ComputeReducerLayout: construct lowering layout with keepdim
ReduceLowerer->>ReduceLowerer: validate destination rank and extent 1
ReduceLowerer->>KeepdimTest: execute reduction kernel
KeepdimTest-->>ReduceLowerer: compare shape and values with keepdim reference
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/op/reduce.cc`:
- Around line 218-224: Require the destination extent at the reduced axis to be
1 before enabling keep-dim in the reducer-layout selection: update the predicate
using dst->shape[this->dim] in src/op/reduce.cc lines 218-224, and mirror it
with op.dst->shape[op.dim] in src/backend/common/op/reduce.h lines 802-808.
Ensure same-rank destinations with a non-unit reduced axis are rejected clearly
rather than treated as keep-dim.
In `@testing/python/language/test_tilelang_language_reduce.py`:
- Around line 659-700: Extend REDUCE_KEEPDIM_CASES and test_reduce_keepdim to
include non-clearing min cases. In the accumulator initialization, use positive
infinity for min and negative infinity for max; update the non-sum reduction
dispatch so op == "min" invokes T.reduce_min rather than T.reduce_max, while
preserving sum and clearing behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 990138ef-c02d-445b-b3fa-c710983af178
📒 Files selected for processing (3)
src/backend/common/op/reduce.hsrc/op/reduce.cctesting/python/language/test_tilelang_language_reduce.py
[BugFix] Support keep-dim destinations in fragment reduce
Summary
Fixes #2530.
T.reduce_sum/T.reduce_max/ etc. crash at compile time when thedestination buffer keeps the reduced axis with extent 1 (e.g. reducing
(64, 64)alongdim=1into a(64, 1)fragment):This shape is part of the documented frontend contract —
tilelang/language/reduce_op.py:50-56explicitly accepts both
[X, Y]and[X, 1, Y]output shapes and buildsexpected_shapesfor both — so this is a backend gap, not user misuse. The fixteaches both the layout-inference and lowering stages to produce a reducer
layout whose rank matches the keep-dim destination.
Root Cause
ComputeReducerLayoutunconditionally drops the reduced axis when buildingthe reducer fragment, so it always returns a layout of rank
n-1for a rank-nsource. Two places consume it, and both assume the destination is rank
n-1.Stage 1 — layout inference (
src/op/reduce.cc)ReduceOpNode::InferLayoutbinds that rank-n-1layout straight to thedestination buffer. When
dsthas no layout yet it takes an earlyreturn,which skips the rank check further down in the same function — the one place that
would have caught the mismatch. A rank-1 layout gets bound to a rank-2 buffer,
and nothing complains until a much later
Forwardcall trips the invariant guardat
src/layout/layout.cc:929:ICHECK_EQ(vars.size(), InputDim())That is where the opaque
(2 vs. 1)surfaces. The reported location is thedetection point, not the introduction point.
Stage 2 — lowering (
src/backend/common/op/reduce.h)Fixing stage 1 alone only relocates the crash. The lowering path derives its own
red_layoutfrom the same helper, then feeds one shareddst_varsarray totwo different layouts:
dst_varshas exactly one length, sored_layoutanddst_layoutmust agree onrank or one of the two calls fails the same assertion. On top of that,
ICHECK_EQ(src_dim, dst_dim + 1)rejects keep-dim outright (2 vs. 3), andsrc_vars.insert(begin() + op.dim, reduce_iv)would pushsrc_varsto rankn+1becausedst_varsalready carries the dummy axis.Note that
LowerLocal(src/backend/common/op/reduce.h:531) already handleskeep-dim correctly for the
localscope — it readsop.dst->shape.size()andbranches on
dst_dim == src_dim. Only thefragmentpath was missing it, whichis why the bug is scope-specific.
Changes
ComputeReducerLayout— newbool keepdim = falseparameter (both copies:src/op/reduce.cc:134andsrc/backend/common/op/reduce.h:63, which arebyte-for-byte duplicates apart from
staticvsinline)reducer_shape:Set(dim, 1)instead oferase(dim), keeping the axis atextent 1 so
InputDim()matches the destination rank.fwd:InputPlaceholders(n)+Set(dim, FloorMod(rep, ...))instead ofInputPlaceholders(n-1)+insert. Non-keep-dim deliberately offsetsplaceholder numbering from position; keep-dim needs them aligned one-to-one,
and the extent-1 dummy coordinate contributes nothing to the thread map, so
overwriting it is correct.
reducer_rep_extent,CondenseReplicateVar,BindThreadRange) is untouched: keep-dim and non-keep-dim describe the samephysical data with the same thread mapping, differing only in how many logical
coordinates they accept.
Stage 1 (
src/op/reduce.cc:235)keepdimfromdst->shape.size() == src_layout->InputDim()andis_one(dst->shape[this->dim]). The destination buffer is the only carrierof this intent(keepdim) —
ReduceOpNodehas no keepdim field, and the functionnever read
dst->shapebefore.src_layout->InputDim() > 1: a rank-1 source already degenerates to[1]via thereducer_shape.empty()fallback and would otherwise bemisclassified.
ICHECK(is_one(dst->shape[this->dim]))for same-rank destinations(
src/op/reduce.cc:226), before thekeepdimdecision. A destination of thesame rank as the source is only meaningful as a keep-dim reduction, so a
non-unit reduced axis is a defect in the extent, and this reports it as one.
Without it such a buffer would take
keepdim == false, build a rank-n-1layout, and fail the rank check below with "reducer layout rank does not match
the destination buffer rank" — a message that does not describe the actual
problem.
tilelang/language/reduce_op.py:51-57already rejects this shape atthe frontend (
expected shapes are [64] or [64, 1]), and every_REDUCE_OP_KEYconstruction site sits behind that check, so the new ICHECK isdefensive: it keeps the C++ layer self-consistent without assuming that
frontend guard survives future refactoring.
ICHECK_EQ(reducer_layout->InputDim(), dst->shape.size())on the freebinding path (
src/op/reduce.cc:242), before the early return, so any futurerank divergence fails here with a readable message instead of sliding into
layout.cc. The pre-existing rank check at:250only covers thealready-bound path.
Stage 2 (
src/backend/common/op/reduce.h:807)keepdimcriterion — including theis_one(op.dst->shape[op.dim])extentcheck — so both stages cannot disagree. Stage 1 runs first and already
diagnoses a same-rank destination with a non-unit reduced axis, so lowering
never sees one.
ICHECK_EQ(src_dim, dst_dim)plusICHECK(is_one(dst_layout->InputShape()[op.dim])), replacing theunconditional
ICHECK_EQ(src_dim, dst_dim + 1).src_vars:Set(op.dim, reduce_iv)instead ofinsertunder keep-dim.src_varsstarts as a copy ofdst_vars, which already holds the extent-1dummy at
op.dim; replacing that slot keepssrc_varsat source rank, whileinserting would overshoot to
n+1. This mirrors whatLowerLocalalready does.Deliberately unchanged
src/layout/layout.cc:929— a correct invariant guard. Relaxing it would turn acompile error into silent numerical corruption.
tilelang/language/reduce_op.py— the frontend contract is right; the backendjust had not honoured it.
LowerLocal— already correct, and used as the reference for the fragment path.Tested
repro.pyfrom the issue: both the keep-dim(64, 1)case and thenon-keep-dim
(64,)control compile and match the torch reference.dim=0,reduce_max/reduce_minkeep-dim, 3D(32, 16, 64)reducing the middle axis into(32, 1, 64), plus non-keep-dim controls for each.test_reduce_keepdimintesting/python/language/test_tilelang_language_reduce.py— 8 cases coveringsum/max/min,
dim=0anddim=1, 2D and 3D, and bothclear=Trueandclear=False. The accumulate (clear=False) path is the one that exercisesred_indicesagainstclear_bufferdirectly, so it is covered for sum, max andmin. Each case asserts the output shape as well as the values, so a silent
rank regression fails too.
(64, 64)source reduced alongdim=1into a(64, 64)destination raisesValueError: Invalid reduce output shape ... expected shapes are [64] or [64, 1]before reaching the backend.
test_tilelang_language_reduce.pyre-run withTILELANG_DISABLE_CACHE=1afteradding the extent check: 0 failed.
testing/python/layout—test_tilelang_language_reduce.py,test_tilelang_language_warp_reduce.py,test_tilelang_language_reduce_maxmin_nan.py,test_math_bitwise_reduce.py,layout/: 0 failed.testing/python/issue: 0 failed.src/cuda/op/reduce.ccandsrc/rocm/op/reduce.cc.Follow-up (not in this PR)
ComputeReducerLayoutand its helperInputPlaceholdersexist as identicalcopies in
src/op/reduce.ccandsrc/backend/common/op/reduce.h, in differentnamespaces (
tvm::tlvstvm::tl::backend::reduce). Merging them would needsrc/op/reduce.ccto depend onsrc/backend/, or a new shared header — alayering change too broad for a bugfix. Both copies now carry comments pointing
at each other; a dedicated refactor PR could unify them if necessary.
Summary
keepdim=Trueresults.C++ style / lint notes
docs/developer_guide/cpp_style.mdand the warning-only “C++ API Style Audit” CI step for any advisory findings.