Skip to content

More profiler counters and warnings for recomputation (or lack thereof) - #9368

Open
abadams wants to merge 4 commits into
mainfrom
abadams/profiler_recompute_locality
Open

More profiler counters and warnings for recomputation (or lack thereof)#9368
abadams wants to merge 4 commits into
mainfrom
abadams/profiler_recompute_locality

Conversation

@abadams

@abadams abadams commented Aug 19, 2026

Copy link
Copy Markdown
Member

This PR does two things. It adds new counters so that the recompute warning can be more granular. It also adds a warning if you're pointlessly compute_ating something too far out, and computing it further in is both legal and also wouldn't incur recompute. E.g. why compute a pointwise op at scanlines? This one is interesting because it requires another counterfactual in bounds inference - what would the bounds required have been if I had computed this at a different loop nest? The legality check exploits the existing enumeration of legal compute_at sites.

These are duals. Recompute warnings tell you if you've computing something too far in, and the inwards warnings tell you if you've computed it too far out. The inwards warnings only check the loop one further in, so it provides a gradient, not the optimum compute_at location. If you're computing something way too far out, it'll just keep telling you to move it one more in until you reach the minimum. Some examples of it in action, courtesy of claude:

Could compute further inwards

A pointwise Func computed (and allocated) a whole row at a time, when it
could be computed one loop level further in — at out's x loop — for
free, avoiding the allocation.

Func p("could_move_inward"), out("out");
p(x, y)   = /* expensive elementwise body */;
out(x, y) = /* expensive elementwise body */(p(x, y));   // pointwise consumer

out.compute_root().split(y, yo, yi, 8).parallel(yo).vectorize(x, 8);
p.compute_at(out, yi).vectorize(x, 8);   // per row; could move inward to out.x
  name                   │ time     percent │ ... │ heap │ ... │recompute│notes│
  └could_move_inward     │   5.37ms (22.1%) │ ... │ 2048 │ ... │    1.00 │2,3  │

 Performance warnings:
  2) could_move_inward could be computed further inside the loop nest of its consumers without
     incurring significant redundant recompute.

Redundant realizations — store_at/compute_at too far in

Producer with a 5-tap vertical stencil, computed in 2-row strips with no
store_at
, so the halo is re-realized for every strip.

Func p("redundant_realization"), out("out");
p(x, y)   = /* expensive elementwise body */;
out(x, y) = p(x, y - 2) + p(x, y - 1) + p(x, y) + p(x, y + 1) + p(x, y + 2);

out.compute_root()
   .split(y, yo, yi, 2).split(yo, yb, yo, 16)   // 2-row strips, coarse parallel bands
   .parallel(yb).vectorize(x, 8);
p.compute_at(out, yo).vectorize(x, 8);           // no store_at
  name                   │ time     percent │ ... │recompute│notes│
  └redundant_realization │  27.40ms (60.5%) │ ... │    2.99 │2,3  │

 Performance warnings:
  2) redundant_realization redundantly recomputes each value 2.994152 times on average. The region
     realized across all store_at sites is 2.994152x the root footprint; consider a store_at/compute_at
     location further outwards in the parent's loop nest.

b. Sliding-window failure — production exceeds realization

store_at outer / compute_at inner, but the consumer indexes the
producer at a runtime stride, so the monotonicity check fails and
sliding can't peel off the leading edge; each iteration re-produces the
full footprint.

Param<int> stride;   // set to 1 at runtime
Func g("sliding_window_failed"), f("out");
g(x, y) = /* expensive elementwise body */;
f(x, y) = g(stride * x, y) + g(stride * x + 3, y);

f.compute_root().split(x, xo, xi, 8).parallel(y);
g.store_at(f, y).compute_at(f, xi);
  name                   │ time     percent │ ... │recompute│notes│
  └sliding_window_failed │  95.58ms (91.7%) │ ... │    3.99 │2,3  │

 Performance warnings:
  2) sliding_window_failed redundantly recomputes each value 3.994149 times on average. The points
     required at the compute_at site are 3.994149x those at the store_at site; sliding window
     optimization may have failed.

c. Tail / split over-compute — computed exceeds production

Producer computed per 4-wide production tile, but its own split uses
RoundUp to a factor of 16, so it writes 4x the points required.

Func p("tail_overcompute"), out("out");
p(x, y)   = /* expensive elementwise body */;
out(x, y) = p(x, y);

out.compute_root().split(x, xo, xi, 4).parallel(y);
p.compute_at(out, xo)                                  // 4-wide production tile...
 .split(x, pxo, pxi, 16, TailStrategy::RoundUp);       // ...rounded up to 16
  name                   │ time     percent │ ... │recompute│notes│
  └tail_overcompute      │  42.00ms (95.1%) │ ... │    4.00 │2,3  │

 Performance warnings:
  2) tail_overcompute redundantly recomputes each value 4.000000 times on average. The points actually
     computed are 4.000000x those required at the compute_at site; the schedule may be using
     excessively large split factors or a wasteful tail strategy.

abadams and others added 3 commits August 19, 2026 11:29
Restores the per-Func counters that describe where recompute happens and
whether a Func could be scheduled more tightly:

  - realizations, productions
  - points_required_at_realization, points_required_at_production
  - points_required_inwards, productions_if_inwards

These are emitted via three markers (declare_box_required_at_realization /
_at_production / _inwards) that ScheduleFunctions places at the realize,
produce, and one-level-further-in loop sites, consumed in Profiling.cpp,
stored on halide_profiler_func_stats, and dumped to JSON.

They drive the could_compute_further_inside warning and let high_recompute
attribute the recompute to its dominant cause (root->realization,
realization->production sliding-window failure, or production->computed
split factors).

The inwards marker asks bounds inference to compute a Func's box as if it
were produced one loop level further in. That needs a localized box at a
scope where the Func isn't actually produced, so BoundsInference's
bounds_needed rule no longer cancels a Func whose bounds are wanted because
it's inwards-marked (in_pipeline is now overridden by inner_productions).

validate_schedule computes the one-level-inwards LoopLevel and locks it so
the injector can inspect it; unset levels stay unlocked and are skipped.

Adds check_tiled_stencil_modest_recompute, check_sliding_window_counters,
check_sliding_window_failure_counters, and check_inwards_counter to the
profiler_instances test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The recompute factorizes into three independent stages whose product is
the total: root->realization (redundant realizations),
realization->production (sliding-window failure), and
production->computed (tail strategy / split factors). The message used to
force-pick one and always append a realization-vs-production sentence,
which printed a meaningless "1.00x ... split factors" clause whenever a
single stage dominated. Now each stage is named independently when it
contributes more than ~10%, so the advice points only at causes that are
actually present. Denominators are guarded so a missing stage reads as 1x.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@codecov

codecov Bot commented Aug 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.37931% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 70.11%. Comparing base (bb5426d) to head (867fdf6).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
src/BoundsInference.cpp 77.77% 0 Missing and 2 partials ⚠️
src/ScheduleFunctions.cpp 93.10% 0 Missing and 2 partials ⚠️
src/Profiling.cpp 94.11% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #9368      +/-   ##
==========================================
+ Coverage   70.08%   70.11%   +0.03%     
==========================================
  Files         259      259              
  Lines       79158    79207      +49     
  Branches    19293    19312      +19     
==========================================
+ Hits        55477    55537      +60     
+ Misses      17886    17869      -17     
- Partials     5795     5801       +6     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@alexreinking
alexreinking self-requested a review August 20, 2026 19:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants