Speed up bounds inference for funcs with many pure update dims - #9442
Speed up bounds inference for funcs with many pure update dims#9442abadams wants to merge 2 commits into
Conversation
For dimensions that are pure in every update stage of a Func, there is a single required region shared by all stages rather than one per stage. Previously populate_scope still created a distinct per-stage bound variable (f.sK.x.min/max) for these dimensions, so when a many-staged Func was a consumer, merge_boxes had to combine one structurally distinct term per stage, building large min/max expressions and calling simplify on each. define_bounds then discarded those pure-dimension results and aliased them to the last stage anyway. Instead, key the required region of an always-pure dimension off the last stage's bound variables directly in populate_scope. The per-stage boxes are then structurally identical in that dimension and collapse on merge instead of growing one term per stage. On the bgu app (whose solve is expressed as a Func with 100 update stages, pure in x/y/z), this drops computation bounds inference from ~381ms to ~23ms and total lowering from ~578ms to ~210ms, with no change in generated code. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Keying the required region of an always-pure dimension off the last stage's bound variables in populate_scope is wrong: those variables are only narrowed to the current iteration for the stage that owns the loop nest we are in. A producer computed inside stage k's nest therefore saw the last stage's unnarrowed bound for such a dimension. gpu_mixed_dimensionality hits this directly. It tiles the pure definition of out over x, y and z but its update over x and y only, so out.s1 has no z loop to narrow out.s1.z.min/max. Inside out.s0's nest the region required of h in z widened from the current 4-tile to the full extent, and cascaded through h, g and f: the kernel ended up with 64x64x4 thread extents and a 2MB per-thread local depot, and the test no longer finished. Do the collapse in define_bounds instead, which knows what loop level it is at. Each entry of a stage's bounds now carries the region both in terms of the consumer stage's own bound variables and with the always-pure dimensions phrased via the last stage's. A consumer stage that owns or is fused with the current loop nest uses the former; a consumer produced further in has every stage's bounds defined here, so the two alias each other and the latter is equivalent and smaller. The last stage's bound variables are built once per Func and shared by all of its stages, so merge_boxes' same_as fast path fires. On bgu, computation bounds inference goes from ~746ms to ~25ms. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Gn5nH7e378aGqdHmo7Mdm
|
This PR makes I bisected it to a single line, and I think the rest of the PR is fine. Details below, plus a fix that keeps the speedup. The assumption that breaks
That observation is true of the bounds required of a Func at root, but not at inner loop levels, and that's the gap. In The old code respected this. It aliased The new Why this test in particular
out.gpu_tile(x, y, z, xi, yi, zi, 4, 4, 4); // stage 0: 3D tiled
out.update().gpu_tile(x, y, xi, yi, 4, 4); // stage 1: 2D only -- no z loop at all
h.compute_at(out, x).gpu_threads(x, y); // h lives inside stage 0's nestInside Same pipeline, only that line differing,
2097152 B is two 1 MiB buffers = 2 × 64³ × 4 B — Suggested fixThe collapse is worth having — it just has to happen somewhere that knows the loop level. So Each entry of a stage's struct RequiredRegion {
Box per_stage; // consumer stage's own bound vars -- correct at any loop level
Box canonical; // always-pure dims phrased via the last stage's vars
};and if (owns_loop_nest) { // == producing_stage_index, or fused with it
merge_boxes(b, i.second.per_stage);
} else if (inner_productions.count(func_name)) {
merge_boxes(b, i.second.canonical.empty() ? i.second.per_stage : i.second.canonical);
}The safety argument for the canonical branch: One implementation note: the last stage's bound variable Numbers
Verification:
No new regression test seemed warranted, since 🤖 Generated with Claude Code |
|
@abadams @alexreinking Please review. This is a fully automated fix by Claude for the CI hang. This was Opus 5 on medium effort. I'm still not familiar enough with BoundsInference to be able to review this meaningfully. All CI should go green after this though, but perhaps there is a different more elegant fix you can come up with. |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #9442 +/- ##
==========================================
+ Coverage 69.95% 70.01% +0.06%
==========================================
Files 261 261
Lines 79596 79663 +67
Branches 19400 19421 +21
==========================================
+ Hits 55678 55776 +98
+ Misses 18004 17997 -7
+ Partials 5914 5890 -24 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
That's quite a complicated additional commit. I suspect there's a simpler approach. Removing Alex's approval so I don't absent-mindedly merge this. |
Problem
Computation bounds inference dominates lowering time for the
bguapp. Its solve is expressed as aFuncwith ~100 update stages (unrolled Cholesky), but almost every dimension (x,y,z) is pure in all stages — only the two matrix-index dimensions vary per update. Onbgu, computation bounds inference alone took ~381ms out of ~578ms total lowering.Cause
Stage::populate_scopecreates a distinct per-stage bound variable (f.sK.x.min/.max) for every dimension, including dimensions that are pure in every stage. When a many-staged Func is a consumer,merge_boxesthen has to combine one structurally distinct term per stage for those pure dimensions, building large nestedmin/maxexpressions and callingsimplifyon each — O(stages) work per pure dimension, per consumed producer.define_boundsalready contained the observation that a dimension pure in every update has a single bound shared by all stages (it aliases non-final stages to the last stage), but it did so only after the expensive merge had already built and simplified the per-stage expressions, then threw that work away.Fix
Key the required region of an always-pure dimension off the last stage's bound variables directly in
populate_scope. The per-stage boxes are then structurally identical in that dimension, so they collapse on merge instead of growing one term per stage.always_pure_dimsis computed once per Func (shared across its stages) rather than recomputed insidedefine_bounds.Impact
On
bgu(measured withHL_TIME_LOWERING_PASSES=1):Generated code is unchanged.
Testing
make test_correctnesspasses except for the pre-existingsimd_op_check_sve2failure (an unrelated LLVM 23 backend crash on this tree). No new regression test is included since this is a compile-time optimization with no change to generated code; the existing suite covers correctness of multi-stage / pure-dimension bounds inference.🤖 Generated with Claude Code