Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
fe05aaa
PoC: Aligned split directive.
mcourteaux Aug 17, 2026
9ebb032
Restore likely_if_innermost for ShiftInwards.
mcourteaux Aug 20, 2026
1315f7a
Add Python binding
mcourteaux Aug 20, 2026
97dfaee
Add serialization.
mcourteaux Aug 20, 2026
5afb9fc
Fix the incorrectly assumed fast-path for this aligned splits.
mcourteaux Aug 20, 2026
0ab58c4
Add three rfactor + aligned split tests.
mcourteaux Aug 20, 2026
f201909
Add test for nested aligned splits.
mcourteaux Aug 20, 2026
bacdbc8
Test varying tail strategies for nested aligned splits.
mcourteaux Aug 20, 2026
1a5f500
Documentation for the aligned split.
mcourteaux Aug 20, 2026
31bd47a
Fix ShiftInwardsAndBlend, RoundUpAndBlend. Claude rederived the masks…
mcourteaux Aug 20, 2026
884b58c
Add simplifier rules for broadcast() <= ramp() && ramp() <= broadcast().
mcourteaux Aug 19, 2026
978ab08
Test simple aligned split in an RVar.
mcourteaux Aug 20, 2026
a7405cd
Reduce aligned-split compute_at mux test to 3x3 and fix its checks
mcourteaux Aug 24, 2026
546075e
Rewrite aligned split inner loops to range from 0 to factor.
mcourteaux Aug 29, 2026
6f12d14
Override the compute and storage bounds of the test to relieve the si…
mcourteaux Aug 29, 2026
483927f
Add a test for an aligned split feeding loop partitioning
mcourteaux Aug 25, 2026
63fbab3
Explain the schedule of aligned_split_2d.
mcourteaux Aug 30, 2026
603e14e
Document a scheduling order dependence.
mcourteaux Aug 30, 2026
c8e7c74
Tutorial 25 for aligned split.
mcourteaux Aug 30, 2026
1bed655
Guard lesson_25's expected-CompileError catch with HALIDE_WITH_EXCEPT…
mcourteaux Aug 31, 2026
d0495db
Fix clang-tidy findings in lesson_25_aligned_split
mcourteaux Aug 31, 2026
88a4398
Feedback from Andrew.
mcourteaux Sep 1, 2026
f1871f4
Simplify the bounds calculation of a split.
mcourteaux Sep 1, 2026
c0a1a9a
Peeling variants of the ramp <= broadcast style rules.
mcourteaux Sep 1, 2026
7435de5
Remove tutorial.
mcourteaux Sep 2, 2026
f13a94d
WIP checkpoint before substitution-based apply_split rewrite
mcourteaux Sep 3, 2026
0bd306a
Fix simplifier rule I f*kd up earlier.
mcourteaux Sep 3, 2026
4e4be66
Restore the aligned split logic in apply_split
mcourteaux Sep 3, 2026
49501ce
Collect a repeated term across a nested sum in the Add simplifier
mcourteaux Sep 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions python_bindings/halide/src/halide_/PyScheduleMethods.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ HALIDE_NEVER_INLINE void add_schedule_methods(PythonClass &class_instance) {

.def("split", (T & (T::*)(const VarOrRVar &, const VarOrRVar &, const VarOrRVar &, const Expr &, TailStrategy)) & T::split,
py::arg("old"), py::arg("outer"), py::arg("inner"), py::arg("factor"), py::arg("tail") = TailStrategy::Auto)
.def("split", (T & (T::*)(const VarOrRVar &, const VarOrRVar &, const VarOrRVar &, const Expr &, const Expr &, TailStrategy)) & T::split,
py::arg("old"), py::arg("outer"), py::arg("inner"), py::arg("factor"), py::arg("align"), py::arg("tail") = TailStrategy::Auto)

.def("fuse", &T::fuse,
py::arg("inner"), py::arg("outer"), py::arg("fused"))
Expand Down
171 changes: 144 additions & 27 deletions src/ApplySplit.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,17 @@ vector<ApplySplitResult> apply_split(const Split &split, const string &prefix,
Expr old_max = Variable::make(Int(32), prefix + split.old_var + ".loop_max");
Expr old_min = Variable::make(Int(32), prefix + split.old_var + ".loop_min");
Expr old_extent = (old_max - old_min) + 1;
Expr outer_min = Variable::make(Int(32), prefix + split.outer + ".loop_min");

dim_extent_alignment[split.inner] = split.factor;

Expr base = outer * split.factor + old_min;
Expr base;
if (split.align.defined()) {
base = outer * split.factor;
} else {
base = outer * split.factor + old_min;
}

string base_name = prefix + split.inner + ".base";
Expr base_var = Variable::make(Int(32), base_name);
string old_var_name = prefix + split.old_var;
Expand All @@ -38,8 +45,17 @@ vector<ApplySplitResult> apply_split(const Split &split, const string &prefix,
internal_assert(tail != TailStrategy::Auto)
<< "An explicit tail strategy should exist at this point\n";

// When align is defined, tiles are anchored to align instead of to
// old_min, so knowing that the factor divides the extent is not
// enough to prove no boundary guard is needed: we additionally need
// the tiling anchored at align to line up with the tiling anchored
// at old_min, i.e. old_min and align must be congruent mod factor.
bool alignment_matches_old_min = !split.align.defined() ||
is_const_zero(simplify((old_min - split.align) % split.factor));

if ((iter != dim_extent_alignment.end()) &&
is_const_zero(simplify(iter->second % split.factor))) {
is_const_zero(simplify(iter->second % split.factor)) &&
alignment_matches_old_min) {
// We have proved that the split factor divides the
// old extent. No need to adjust the base or add an if
// statement.
Expand All @@ -62,10 +78,19 @@ vector<ApplySplitResult> apply_split(const Split &split, const string &prefix,
// condition. We'll directly tell it that the loop
// variable is bounded above by the original loop max by
// replacing the variable with a promise-clamped version
// of it. We don't also use the original loop min because
// it needlessly complicates the expressions and doesn't
// actually communicate anything new.
Expr guarded = promise_clamped(old_var, old_var, old_max);
// of it.
Expr guarded;
if (split.align.defined()) {
// Because the un-rebased base block can start before old_min,
// we must clamp both the minimum and maximum boundaries.
guarded = promise_clamped(old_var, old_min, old_max);
} else {
// We don't also use the original loop min because
// it needlessly complicates the expressions and doesn't
// actually communicate anything new.
guarded = promise_clamped(old_var, old_var, old_max);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original comment explaining why promise_clamped is necessary seems to have been removed. Also, imo would be simpler as:

Expr guarded = promise_clamped(old_var, split.align.defined() ? old_min : old_var, old_max);

@mcourteaux mcourteaux Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Restored the comment. I'll leave the if in place and the cases as separate as I like the style of comments better.


string guarded_var_name = prefix + split.old_var + ".guarded";
Expr guarded_var = Variable::make(Int(32), guarded_var_name);

Expand All @@ -76,8 +101,6 @@ vector<ApplySplitResult> apply_split(const Split &split, const string &prefix,
predicate_type = ApplySplitResult::Predicate;
break;
case TailStrategy::Predicate:
// This is identical to GuardWithIf, but maybe it makes
// sense to keep it anyways?
substitution_type = ApplySplitResult::Substitution;
predicate_type = ApplySplitResult::Predicate;
break;
Expand All @@ -97,36 +120,123 @@ vector<ApplySplitResult> apply_split(const Split &split, const string &prefix,
// for the guarded version.
result.emplace_back(prefix + split.old_var, guarded_var, substitution_type);
result.emplace_back(guarded_var_name, guarded, ApplySplitResult::LetStmt);
result.emplace_back(likely(old_var <= old_max), predicate_type);

Expr guard_cond = likely(old_var <= old_max);
if (split.align.defined()) {
guard_cond = likely(old_var >= old_min && old_var <= old_max);
}
result.emplace_back(guard_cond, predicate_type);

} else if (tail == TailStrategy::ShiftInwards) {
// Adjust the base downwards to not compute off the
// end of the realization.

// We'll only mark the base as likely (triggering a loop
// partition) if we're at or inside the innermost
// non-trivial loop.
base = likely_if_innermost(base);
base = Min::make(base, old_max + (1 - split.factor));
if (split.align.defined()) {
base = Max::make(base, old_min - split.align);
base = Min::make(base, old_max + (1 - split.factor) - split.align);
} else {
base = Min::make(base, old_max + (1 - split.factor));
}
} else if (tail == TailStrategy::ShiftInwardsAndBlend) {
// Unclamped base, saved before the Min/Max below adjust it. Used
// to figure out how much (if at all) the boundary tile got
// shifted, so we know which elements of it are redundant with a
// neighboring tile and must be masked out rather than
// recomputed (to avoid double-counting in a reduction).
Expr old_base = base;
base = likely(base);
base = Min::make(base, old_max + (1 - split.factor));
// Make a mask which will be a loop invariant if inner gets
// vectorized, and apply it if we're in the tail.
Expr unwanted_elems = (-old_extent) % split.factor;
Expr mask = inner >= unwanted_elems;
mask = select(base == old_base, likely(const_true()), mask);
Expr mask;
if (split.align.defined()) {
// Because base is anchored to align instead of old_min, the

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we have a nested tail strategy tail that tries lots of things in combination. It would be good to add aligned splits to it to get more coverage of this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have two tests added: split_aligned_nested and rfactor_split_aligned_nested which do this. I'm a bit hesitant to conflate the existing nested_tail_strategies with another axis of tests.

// boundary tile can now be shifted at either end (whereas
// without align only the max end is reachable, since base
// is structurally >= old_min already). Elements shifted in
// from the low end overlap the tile above (mask out the
// last shift_low of them); elements shifted in from the
// high end overlap the tile below (mask out the first
// shift_high of them).
Expr low_bound = old_min - split.align;
Expr high_bound = old_max + (1 - split.factor) - split.align;
Expr shift_low = low_bound - old_base;
Expr shift_high = old_base - high_bound;
base = Max::make(base, low_bound);
base = Min::make(base, high_bound);
Expr mask_low = inner < split.factor - shift_low;
Expr mask_high = inner >= shift_high;
mask = select(old_base < low_bound, mask_low,
select(old_base > high_bound, mask_high, likely(const_true())));
} else {
// Without align, base is structurally >= old_min (outer
// starts at 0), so only the max end can ever be shifted.
base = Min::make(base, old_max + (1 - split.factor));
Expr unwanted_elems = (-old_extent) % split.factor;
mask = inner >= unwanted_elems;
mask = select(base == old_base, likely(const_true()), mask);
}
result.emplace_back(mask, ApplySplitResult::BlendProvides);
} else if (tail == TailStrategy::RoundUpAndBlend) {
Expr unwanted_elems = (-old_extent) % split.factor;
Expr mask = inner < split.factor - unwanted_elems;
mask = select(outer < outer_max, likely(const_true()), mask);
Expr mask;
if (split.align.defined()) {
// Unlike ShiftInwardsAndBlend, the max end is intentionally
// left unclamped here (RoundUp relies on padding, not on
// shifting, to handle overrun at the max end) -- but the min
// end still needs clamping: align can make the min-end tile
// start before old_min, and unlike ShiftInwards/blend at the
// max end, there's no padding below old_min to absorb an
// underrun into, so it has to be prevented outright.
//
// The mask below compares old_base (the unclamped base)
// against low_bound/high_bound directly, rather than
// comparing outer against outer_min/outer_max: the latter
// needs loop partitioning to split the loop into three
// pieces (prologue/steady-state/epilogue) to stay correct,
// and partition_loops doesn't reliably do that here when
// both boundaries are data-dependent, silently dropping the
// last tile. Comparing old_base against the bounds directly
// is correct regardless of how (or whether) the loop gets
// partitioned, matching the approach already proven correct
// above for ShiftInwardsAndBlend.
Expr old_base = base;
Expr low_bound = old_min - split.align;
Expr high_bound = old_max + (1 - split.factor) - split.align;
Expr shift_low = low_bound - old_base;
Expr shift_high = old_base - high_bound;
base = Max::make(likely(base), low_bound);
// The min end is clamped (shifted forward), so its overlap
// is with the tile *above* -- same geometry as
// ShiftInwardsAndBlend, mask out the trailing shift_low
// elements. The max end is left unclamped, so shift_high
// counts a genuine overrun past old_max with no
// neighboring tile to defer to -- mask out the trailing
// shift_high elements too (the opposite convention from
// ShiftInwardsAndBlend's clamped max end, which instead
// masks out the *leading* elements of a shifted-back tile).
Expr mask_low = inner < split.factor - shift_low;
Expr mask_high = inner < split.factor - shift_high;
mask = select(old_base < low_bound, mask_low,
select(old_base > high_bound, mask_high, likely(const_true())));
} else {
Expr unwanted_elems = (-old_extent) % split.factor;
Expr fresh_high = inner < split.factor - unwanted_elems;
mask = select(outer < outer_max, likely(const_true()), fresh_high);
}
result.emplace_back(mask, ApplySplitResult::BlendProvides);
} else {
internal_assert(tail == TailStrategy::RoundUp);
}

// Add align back in last, after all tail-strategy clamping/masking is
// done in terms of the unaligned base: this keeps align as a bare
// top-level addend in the final expressions (so e.g. it can still
// cancel algebraically against a matching subtraction elsewhere)
// rather than being smeared into a Max/Min-clamped expression, while
// letting the inner loop variable itself range over the simple,
// often-constant [0, factor) instead of [align, align + factor).
if (split.align.defined()) {
base = base + split.align;
}

// Define the original variable as the base value computed above plus the inner loop variable.
result.emplace_back(old_var_name, base_var + inner, ApplySplitResult::LetStmt);
result.emplace_back(base_name, base, ApplySplitResult::LetStmt);
Expand Down Expand Up @@ -173,12 +283,19 @@ vector<std::pair<string, Expr>> compute_loop_bounds_after_split(const Split &spl
Expr old_var_min = Variable::make(Int(32), prefix + split.old_var + ".loop_min");
switch (split.split_type) {
case Split::SplitVar: {
Expr inner_extent = split.factor;
Expr outer_extent = (old_var_max - old_var_min + split.factor) / split.factor;
let_stmts.emplace_back(prefix + split.inner + ".loop_min", 0);
let_stmts.emplace_back(prefix + split.inner + ".loop_max", inner_extent - 1);
let_stmts.emplace_back(prefix + split.outer + ".loop_min", 0);
let_stmts.emplace_back(prefix + split.outer + ".loop_max", outer_extent - 1);
let_stmts.emplace_back(prefix + split.inner + ".loop_max", split.factor - 1);
if (split.align.defined()) {
Expr align = split.align;
Expr outer_min = (old_var_min - align) / split.factor;
Expr outer_max = (old_var_max - align) / split.factor;
let_stmts.emplace_back(prefix + split.outer + ".loop_min", outer_min);
let_stmts.emplace_back(prefix + split.outer + ".loop_max", outer_max);
} else {
Expr outer_max = (old_var_max - old_var_min) / split.factor;
let_stmts.emplace_back(prefix + split.outer + ".loop_min", 0);
let_stmts.emplace_back(prefix + split.outer + ".loop_max", outer_max);
}
} break;
case Split::FuseVars: {
// Define bounds on the fused var using the bounds on the inner and outer
Expand Down
2 changes: 2 additions & 0 deletions src/Deserialization.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1152,6 +1152,7 @@ Split Deserializer::deserialize_split(const Serialize::Split *split) {
const auto exact = split->exact();
const auto tail = deserialize_tail_strategy(split->tail());
const auto split_type = deserialize_split_type(split->split_type());
const auto align = deserialize_expr(split->align_type(), split->align());
auto hl_split = Split();
hl_split.old_var = old_var;
hl_split.outer = outer;
Expand All @@ -1160,6 +1161,7 @@ Split Deserializer::deserialize_split(const Serialize::Split *split) {
hl_split.exact = exact;
hl_split.tail = tail;
hl_split.split_type = split_type;
hl_split.align = align;
return hl_split;
}

Expand Down
38 changes: 31 additions & 7 deletions src/Func.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1103,9 +1103,9 @@ Func Stage::rfactor(const vector<pair<RVar, Var>> &preserved) {
return intm;
}

void Stage::split(const string &old, const string &outer, const string &inner, const Expr &factor_arg, bool exact, TailStrategy tail) {
void Stage::split(const string &old, const string &outer, const string &inner, const Expr &factor_arg, const Expr &align_arg, bool exact, TailStrategy tail) {
debug(4) << "In schedule for " << name() << ", split " << old << " into "
<< outer << " and " << inner << " with factor of " << factor_arg << "\n";
<< outer << " and " << inner << " with factor of " << factor_arg << " and align " << align_arg << "\n";

user_assert(factor_arg.defined())
<< "In schedule for " << name() << ", split factor for splitting "
Expand All @@ -1115,6 +1115,14 @@ void Stage::split(const string &old, const string &outer, const string &inner, c
<< old << " has type " << factor_arg.type()
<< ", which is not representable as int32.\n";
Expr factor = cast<int32_t>(factor_arg);
Expr align;
if (align_arg.defined()) {
user_assert(Int(32).can_represent(align_arg.type()))
<< "In schedule for " << name() << ", split align for splitting "
<< old << " has type " << align_arg.type()
<< ", which is not representable as int32.\n";
align = cast<int32_t>(align_arg);
}

vector<Dim> &dims = definition.schedule().dims();

Expand Down Expand Up @@ -1318,11 +1326,15 @@ void Stage::split(const string &old, const string &outer, const string &inner, c
}

// Add the split to the splits list
Split split = {old_name, outer_name, inner_name, factor, exact, tail, Split::SplitVar};
Split split = {old_name, outer_name, inner_name, factor, align, exact, tail, Split::SplitVar};
definition.schedule().splits().push_back(split);
}

Stage &Stage::split(const VarOrRVar &old, const VarOrRVar &outer, const VarOrRVar &inner, const Expr &factor, TailStrategy tail) {
void Stage::split(const std::string &old, const std::string &outer, const std::string &inner, const Expr &factor, bool exact, TailStrategy tail) {
split(old, outer, inner, factor, Expr(), exact, tail);
}

Stage &Stage::split(const VarOrRVar &old, const VarOrRVar &outer, const VarOrRVar &inner, const Expr &factor, const Expr &align, TailStrategy tail) {
definition.schedule().touched() = true;
if (old.is_rvar) {
user_assert(outer.is_rvar) << "Can't split RVar " << old.name() << " into Var " << outer.name() << "\n";
Expand All @@ -1331,7 +1343,13 @@ Stage &Stage::split(const VarOrRVar &old, const VarOrRVar &outer, const VarOrRVa
user_assert(!outer.is_rvar) << "Can't split Var " << old.name() << " into RVar " << outer.name() << "\n";
user_assert(!inner.is_rvar) << "Can't split Var " << old.name() << " into RVar " << inner.name() << "\n";
}
split(old.name(), outer.name(), inner.name(), factor, old.is_rvar, tail);
split(old.name(), outer.name(), inner.name(), factor, align, old.is_rvar, tail);
return *this;
}

Stage &Stage::split(const VarOrRVar &old, const VarOrRVar &outer, const VarOrRVar &inner, const Expr &factor, TailStrategy tail) {
definition.schedule().touched() = true;
split(old.name(), outer.name(), inner.name(), factor, Expr(), old.is_rvar, tail);
return *this;
}

Expand Down Expand Up @@ -1413,7 +1431,7 @@ Stage &Stage::fuse(const VarOrRVar &inner, const VarOrRVar &outer, const VarOrRV
set_dim_type(fused, dims[inner_pos].for_type);

// Add the fuse to the splits list
Split split = {fused_name, outer_name, inner_name, Expr(), true, TailStrategy::RoundUp, Split::FuseVars};
Split split = {fused_name, outer_name, inner_name, Expr(), Expr(), true, TailStrategy::RoundUp, Split::FuseVars};
definition.schedule().splits().push_back(split);
return *this;
}
Expand Down Expand Up @@ -1664,7 +1682,7 @@ Stage &Stage::rename(const VarOrRVar &old_var, const VarOrRVar &new_var) {
}

if (!found) {
Split split = {old_name, new_name, "", 1, old_var.is_rvar, TailStrategy::RoundUp, Split::RenameVar};
Split split = {old_name, new_name, "", 1, Expr(), old_var.is_rvar, TailStrategy::RoundUp, Split::RenameVar};
definition.schedule().splits().push_back(split);
}

Expand Down Expand Up @@ -2545,6 +2563,12 @@ Func &Func::split(const VarOrRVar &old, const VarOrRVar &outer, const VarOrRVar
return *this;
}

Func &Func::split(const VarOrRVar &old, const VarOrRVar &outer, const VarOrRVar &inner, const Expr &factor, const Expr &align, TailStrategy tail) {
invalidate_cache();
Stage(func, func.definition(), 0).split(old, outer, inner, factor, align, tail);
return *this;
}

Func &Func::fuse(const VarOrRVar &inner, const VarOrRVar &outer, const VarOrRVar &fused) {
invalidate_cache();
Stage(func, func.definition(), 0).fuse(inner, outer, fused);
Expand Down
Loading
Loading