Aligned splits, take 2. - #9409
mcourteaux wants to merge 37 commits into
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #9409 +/- ##
==========================================
+ Coverage 70.02% 70.24% +0.21%
==========================================
Files 261 261
Lines 79761 79874 +113
Branches 19443 19471 +28
==========================================
+ Hits 55855 56105 +250
+ Misses 18004 17957 -47
+ Partials 5902 5812 -90 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
| } else { | ||
| // Legacy: structurally guaranteed to be >= old_min | ||
| guarded = promise_clamped(old_var, old_var, old_max); | ||
| } |
There was a problem hiding this comment.
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);
There was a problem hiding this comment.
Restored the comment. I'll leave the if in place and the cases as separate as I like the style of comments better.
| 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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| rewrite(h_or(broadcast(x, arg_lanes) < ramp(y, z, arg_lanes), 1), | ||
| x < y + max(z * (arg_lanes - 1), 0)) || | ||
| rewrite(h_or(broadcast(x, arg_lanes) < ramp(y, z, arg_lanes), 1), | ||
| rewrite(h_or(broadcast(x, arg_lanes) <= ramp(y, z, arg_lanes), 1), |
There was a problem hiding this comment.
Yes, Claude highlighted this as being a copy-paste error from the same rule above (but with < instead of <=). I guess we (that is Claude and I) ran into this during testing and Claude figured this out. It sure looks like a bug to me.
e7f5f89 to
9cbb7c1
Compare
9cbb7c1 to
49501ce
Compare
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> Co-authored-by: Gemini Pro 3.1 <gemini@aistudio.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
… those blend operations in case of aligned splits. Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Fix old copy-paste bug in simplifier rules. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Rename split_aligned_2d_6x6.cpp to split_aligned_2d_3x3.cpp and shrink the pattern to 3x3, which reproduces the surviving mux with a much smaller amount of IR to read. Also fix the test itself: realize the 3-D output with a 3-D shape, check all three channels, sweep all nine (offset_x, offset_y) alignments, and include c in the reorder so it stays innermost. With c left outermost it was unrolled around the xo/yo nest, triplicating the loop nest and recomputing R/G/B once per channel. The test currently fails at the mux count (27 = 9 tile positions x 3 channels); the runtime results are correct for every alignment.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A loop of eight whose first and last iterations are special and whose interior is periodic with period two. Unrolling the interior by two folds the % away, but only if the unrolled pairs line up with the periodicity, which means the tiles have to start where the interior does. An aligned split says exactly that, and partitioning then peels one iteration at each end rather than two, leaving a steady-state loop of three rather than two. Checks the extent of the remaining loop, that the modulo folded away, and the values. Dropping the alignment from the split fails the extent check, so the test is measuring the thing it claims to.
- <stdio.h> -> <cstdio> (modernize-deprecated-headers) - main() no longer takes unused argc/argv (misc-unused-parameters) - Explicit `protected:` on the visit() overrides in Counter and FindProducer, matching IRVisitor's own visibility instead of the implicit private (misc-override-with-different-visibility) - .extents[0] -> .extents.at(0) (cppcoreguidelines-pro-bounds-avoid-unchecked-container-access) - Nested ternaries replaced with immediately-invoked if/else lambdas (readability-avoid-nested-conditional-operator) - reserve(9) before the two 9-element ways.push_back() loops (performance-inefficient-vector-operation)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Co-Authored-By: Andrew Adams <andrew.b.adams@gmail.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JbXuRsMMLQDkqE3mwtEKMs
This is the align handling from d54fbb3, which was reverted in the working tree while trying a substitution-based rewrite of the split. That rewrite is abandoned; without this, aligned splits do not lower correctly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JbXuRsMMLQDkqE3mwtEKMs
x + x already becomes x*2, but only when the two terms are adjacent. In a nested sum they never meet, so x + y + y stays as written and both the rewrite rules and modulus_remainder lose the fact that it is even in y. Found while investigating why an aligned split's alignment fails to cancel against a matching subtraction at the use site: the sum there is of the form (even + off) + off, whose evenness is exactly what the mux index needs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JbXuRsMMLQDkqE3mwtEKMs
49501ce to
bf66758
Compare
|
Three questions remain @alexreinking @abadams, for the Monday-meeting.
My take: I think
But UPDATE: I did the rename to |
…) to the outside of an index reconstruction of a split. This allows the simplifier to peel the variable into the use-site and let terms cancel in some pipelines. Co-authored-by: Andrew Adams <andrew.b.adams@gmail.com>
Missed in the C++ rename: the pybind11 binding still pointed at the now-removed T::split(...,Expr,Expr,TailStrategy) overload. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Js4TkBazPFvQkr9fnE2aXM
A vectorized loop over a repeating pattern only resolves its phases at compile time if it gets deinterleaved by the pattern's period, so that each resulting slice has a single phase. The stride search stopped at four, which covered a Bayer sensor's period of two but not an X-Trans sensor's six. It can't simply go to eight: deinterleaving a period of eight into eight slices displaces permutes that a target may do in one instruction, and three of simd_op_check_hvx's vdelta checks regress that way. Seven is the largest value that leaves those alone. A period of eight still resolves if the loop is unrolled rather than vectorized. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Js4TkBazPFvQkr9fnE2aXM
Distilled from NeonRAW's BilinearDemosaicAlignedGenerator, which selects an interpolation per Bayer phase with a mux over (x - bayer_offset) % 2 and is scheduled with splits aligned to that same offset so the phase is constant per tile. For every mux to resolve at compile time, the aligned split's alignment has to cancel against the subtraction at the use site (the Simplify_Add rewrite rule) and the vectorized loop has to be deinterleaved by the pattern's period, so each slice has a phase of its own (Deinterleave.cpp). Covers a Bayer sensor's period of two, a three-phase pattern, and an X-Trans sensor's six. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Js4TkBazPFvQkr9fnE2aXM
…t rule The Add simplifier now collects x*c + (z*c + y) into (x+z)*c + y when the two terms share a coefficient, which is sound under wraparound arithmetic even when c is INT_MIN (multiplication distributes over addition mod 2^32 regardless of intermediate overflow). Update the expected output to match; the test's actual purpose, checking that the coefficient itself is never negated, still holds. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Js4TkBazPFvQkr9fnE2aXM
…fferences solve_expression pulls a correlated loop variable leftmost inside a min or max, but PartiallyCancelDifferences had no rule to sink it into the other side, so the cancellation was left to the simplifier that runs afterwards. The simplifier may first collect that term into a product with an unrelated one, after which the loop variable is no longer syntactically available to cancel, and bounds inference then counts its contribution twice. Doing the cancellation here instead makes it independent of what the simplifier chooses to collect. This fixes an over-allocation in nested GuardWithIf / PredicateStores tail strategies, where a compute_root producer was handed roughly twice the memory it needed, even when the extent was an exact multiple of every split factor. Reproduced by correctness_nested_tail_strategies with seed 1789169226. The eight new rules are the min and max forms of two identities, written out for each operand ordering because the matcher does not match commutatively. All eight were proved with apps/simplifier_rule_verifier; they hold under Halide's no-signed-overflow model for Int(32), which is the type the enclosing block is already guarded on, and not under wrapping arithmetic. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Js4TkBazPFvQkr9fnE2aXM
| void set_dim_device_api(const VarOrRVar &var, DeviceAPI device_api); | ||
| void split(const std::string &old, const std::string &outer, const std::string &inner, | ||
| const Expr &factor, bool exact, TailStrategy tail); | ||
| void split(const std::string &old, const std::string &outer, const std::string &inner, |
There was a problem hiding this comment.
The overload that doesn't take an alignment should be removed, or this one should be renamed
| rewrite((x / w) * w + (z + x % w), select(w == 0, 0, x) + z) || | ||
| rewrite(x / 2 + x % 2, (x + 1) / 2) || | ||
|
|
||
| rewrite((0 - (x % 2)) / 2 * 2 + (x % 2), 0 - (x % 2)) || |
There was a problem hiding this comment.
I think this should simplify by a different chain. First:
(0 - x % 2) / 2 is just (0 - x % 2), and then we have the pattern (0 - y) * 2 + y which I think will simplify already.
There was a problem hiding this comment.
Neither of these rules are present in the respective files. Will add those.
| rewrite(x + ((c0 - x) / c1) * c1, c0 - ((c0 - x) % c1), c1 > 0) || | ||
| rewrite(x + ((c0 - x) / c1 + y) * c1, y * c1 - ((c0 - x) % c1) + c0, c1 > 0) || | ||
| rewrite(x + (y + (c0 - x) / c1) * c1, y * c1 - ((c0 - x) % c1) + c0, c1 > 0) || | ||
| rewrite(((0 - x) / c0) + ((x % c0 + c1) / c0), fold(c1 / c0) - (x / c0), c0 > 0 && (c1 + 1) % c0 == 0) || |
There was a problem hiding this comment.
This seems like a hyper-specific rule. What's it for? Is this a pattern produced by the new split logic?
| rewrite(max((x * c0), c1) / c2, max(x * fold(c0 / c2), fold(c1 / c2)), c0 % c2 == 0 && c2 > 0) || | ||
|
|
||
| rewrite((x * c0 + y) / c1, y / c1 + x * fold(c0 / c1), c0 % c1 == 0 && c1 > 0) || | ||
| rewrite((x * c0 - y) / c1, (0 - y) / c1 + x * fold(c0 / c1), c0 % c1 == 0 && c1 > 0) || |
There was a problem hiding this comment.
Does this obey the reduction order? The RHS has more ops than the LHS.
| // simplifier matters: the simplifier may first collect the | ||
| // term into a product with an unrelated one, after which it | ||
| // is no longer syntactically available to cancel. | ||
| rewrite(min(x + y, z) - x, min(y, z - x)) || |
There was a problem hiding this comment.
Shouldn't solve have done this? It removes a repeated instance of the variable, which is solve's job.
|
Many comments on the simplifier rules. Were they all verified? |
|
I'll delete all the simplifier rules, and see what breaks, because I don't know anymore by now. |
Inner loops go from 0 to factor, to help with constant bounds analysis.
2D tiled test: compute_at test overwrites the compute and storage bounds by just passing those in the schedule.
It's now possible to align the first iteration of the inner loop, like so:
Replaces #9371
Breaking changes
Checklist