Overhaul of splitting, tail strategies, and loop partitioning. #9357
mcourteaux
started this conversation in
Ideas
Replies: 1 comment 1 reply
|
Don't guard_with_if/predicate_loads/predicate_stores also need to take a Partition to control if and how loops are partitioned to remove the guards? |
1 reply
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
API
Tail Strategies
Two fundamental ones:
align_extent(factor)& simple split with split.exact == trueAnd backward compatible ones:
There are now two new combinations possible: ShiftInwards + predicate_stores() or guard_with_if()
For large loop extents, the stores will never be out of bounds, and these will be functionally equivalent to ShiftInwards.
For small loop extents, shifting inward might go before the min, which will now be protected, and therefore possible.
However, for small loop extents, RoundUp + predicate_stores()/guard_with_if() is the same.
So while technically possible, they offer no additional value over the other combinations.
Reordering lowering of Partitioning and Vectorize/Unroll
Simply reordering those causes some failures, and probably a bunch of missed simplifications:
Running the test suite fails these:
So, (crazy?) idea, in Lower:
Partion Policies
Currently, in the main branch.
Loop partitioning is controlled in two ways in the main branch: there are policies attached to For nodes, and there
are likely's injected into index clamping and GuardWithIf.
Currently in main, the likely's are injected in some deterministic way during split() logic.
The user has no control over the injection of likely's in the split().
Having .partition_always() would do nothing if split() did not inject likely.
Proposed set of partition policies
If additionally, we manage to reorder partitioning with unrolling/vectorizing, we can also define these:
Note that
guard_with_if()does not have a Partition policy.It does not make sense to schedule a
guard_with_if()without a split.Nothing would ever go out of bounds without a split.
Partition Policies are tighly bound together with
split()or anything that callssplit()internally.Should we remove
likely_if_innermost()?As split() now fundamentally takes a Partition policy, we can immediately mark the corresponding for loop with that.
However, regarding the
likely()s, there are two possible approaches:likely()orlikely_if_innermost().likely()and have loop partitioning be smart when the policy is set to Auto.Likely's and partition policy are two tools that are meant to control the same thing.
The first approach is to let Partion influence the injection of likely's.
However, that leaves a problem with the boundary conditions:
If boundary conditions did not inject likely's unconditionally, but instead accept a Partition (which is API breaking), then that could controls the injection of likely's, in which case there is no reason to have
parition()as a scheduling directive anymore.However, that would not be able to communicate how we want to handle the edge-case: serial, unroll, or vectorized.
So, instead,
likely()communicates what is the steady state; and the loop partitioning lowering pass should should look at the policy to decide what to do with that info.This is a neat separation of concerns.
However, now we have this awkward
likely_if_innermost(), which is currently used by ShiftInwards and Parition::Auto.I find this to be a hack, and would like to remove
likely_if_innermost()and uselikely()everywhere where we expect steady state.Instead, Loop Partitioning should decide for for-loops marked with Partition::Auto if the
likely()it found to be good reason to partition or not.I think the "smart" rule for Auto could be simple:
Min(Ramp(loop_var))orMax(Ramp(loop_var))and you decide to partition the loop of loop_var (this typically happens in a vectorized x loop)which implicitely then covers the case when you're in an index-clamped y loop:
Ramp(Min(loop_var))orRamp(Max(loop_var))have the Ramp and Min/Max swapped inside-out.This way boundary conditions are also handled.
One pitfall.
A user might do:
As the
.tile()call calls.split()internally, the API should probably NOT overwrite the partition policy which was already set bynever_partition_all()back toAuto(due to Auto being the default argument when omitting it).In that case, whenever a split() gets an
Auto, we should not overwrite the existing policy.Let's assume all loops are initialized with
Partition::Auto.What does this get us?
New scheduling possibility
You can now do:
.align_bounds(x, 6).guard_with_if(x)on output buffers..align_bounds will align both the min and the extent, by increasing the total computed region of the Func.
.guard_with_if(x) will protect against out of bounds writes to the output buffer.
This is now a trivial results from combinatoric possibilities, as opposed to "making a version of align_bounds that predicates".
Note that .vectorize() and .unroll() both take a Partition policy, as they first call split() internally.
This gives us extremely clear scheduling on what happens:
Or, when demosaicing a 6x6 X-trans pattern:
which is equivalent to:
All reactions