Centralize Target feature-flag implication logic#9241
Conversation
Feature flags often imply each other: an AVX2 CPU necessarily also
supports AVX, SSE41, F16C, and FMA, and there is no SVE2 device that
isn't at least ARM v8.2-A. Until now the compiler dealt with these
implications in two error-prone ways:
- Inspecting a target by testing several flags at once, so that a
query for "AVX512" had to enumerate every AVX512 variant that
implies it. Miss one and the check is silently wrong.
- Ad-hoc "complete the target" passes at the point of use
(complete_x86_target, complete_arm_target), which hand-sequenced
the implications. The ARM one got the order wrong: SVE/SVE2 set
ARMFp16 only after the step that cascades ARMFp16 down to the v8.x
baseline had already run, so a bare sve2 target ended up without
ARMv8a/8.1a/8.2a and reported the wrong v8 lower bound.
This centralizes the implications into a single topologically-ordered
table on Target, with set_implied_features()/unset_implied_features()/
normalize() (plus with_/without_ copying variants). set walks the table
forwards; unset walks it backwards to recover the minimal flag set. The
table is the one source of truth, so a correctly-ordered single pass
replaces both hand-written completion functions and fixes the SVE bug.
Lowering calls set_implied_features() eagerly at the top, so every pass
and the code generators inspect a fully-completed target and can check a
single flag instead of a set. As a result the emitted Module carries a
normalized Target: complete in code, but unset back to minimal form when
printed (IRPrinter, StmtToHTML) so target strings stay compact.
Also folds the now-redundant multi-flag checks (x86 AVX512/AVX chains,
the tracing loads/stores implication) down to single-flag checks, and
moves the Target self-tests out of libHalide into
test/correctness/target.cpp.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
It had no caller other than the target test. The test now calls set_implied_features() followed by unset_implied_features() directly, which is all normalize() did. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #9241 +/- ##
=======================================
Coverage ? 70.33%
=======================================
Files ? 255
Lines ? 78850
Branches ? 18842
=======================================
Hits ? 55462
Misses ? 17841
Partials ? 5547 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
This is a good change in spirit, but we could go further. The key idea is to lean into the algebra of targets. I think a better design is to keep targets always implied-feature-complete internally (construction and parsing close the feature set) and make For example:
If we do adopt the implied-feature-complete internal representation, we can simplify other code. So I don't think One gotcha is that |
|
The alternative you propose, where non-canonical states aren't even representable, was actually something I proposed at an earlier dev meeting, but it wasn't popular at the time. We can discuss more in person, but I don't think it's a reason to block this PR - this is better than what we have now and it's a step in right direction. |
Oops, I got the issue confused in my notes; I was using t != f.compile_to_module({}, "f", t).target()Meanwhile, this holds: t.with_implied_features() == f.compile_to_module({}, "f", t).target()
I agree.
Do you remember which meeting that was, or what objections were raised? I recall raising the same issue when I implemented
To be clear, I wasn’t blocking the PR; I was giving design feedback. |
|
Opened #9242 to track the |
I asked Claude to clean up something that has been bugging me for a while:
Feature flags often imply each other: an AVX2 CPU necessarily also supports AVX, SSE41, F16C, and FMA, and there is no SVE2 device that isn't at least ARM v8.2-A. Until now the compiler dealt with these implications in two error-prone ways:
Inspecting a target by testing several flags at once, so that a query for "AVX512" had to enumerate every AVX512 variant that implies it. Miss one and the check is silently wrong.
Ad-hoc "complete the target" passes at the point of use (complete_x86_target, complete_arm_target), which hand-sequenced the implications. The ARM one got the order wrong: SVE/SVE2 set ARMFp16 only after the step that cascades ARMFp16 down to the v8.x baseline had already run, so a bare sve2 target ended up without ARMv8a/8.1a/8.2a and reported the wrong v8 lower bound.
This centralizes the implications into a single topologically-ordered table on Target, with set_implied_features()/unset_implied_features() (plus with_/without_ copying variants). set walks the table forwards; unset walks it backwards to recover the minimal flag set. The table is the one source of truth, so a correctly-ordered single pass replaces both hand-written completion functions and fixes the SVE bug.
Lowering calls set_implied_features() eagerly at the top, so every pass and the code generators inspect a fully-completed target and can check a single flag instead of a set. As a result the emitted Module carries a normalized Target: complete in code, but unset back to minimal form when printed (IRPrinter, StmtToHTML) so target strings stay compact.
Also folds the now-redundant multi-flag checks (x86 AVX512/AVX chains, the tracing loads/stores implication) down to single-flag checks, and moves the Target self-tests out of libHalide into
test/correctness/target.cpp.