spv/lift: canonicalize loop shortcut before phi materialization#30
spv/lift: canonicalize loop shortcut before phi materialization#30niklasha wants to merge 7 commits intoRust-GPU:mainfrom
Conversation
Add a regression test with a fixed SPIR-V fixture that exercises a loop CFG containing a pass-through body-select arm feeding a body merge and conditional continue. The test lowers, structurizes, links, and lifts the module, then inspects lifted blocks and fails if the shortcut shape remains, because that shape can violate intended edge-local value flow through the loop backedge/exit region.
Extend the regression to check two independent shortcut-shape fixtures (base + nested-loop variant) and report all offending fixtures in one failure. Also add a non-loop SPIR-V fixture sanity check so the matcher is exercised on unrelated control flow.
Expand the loop-continue shortcut regression to a wider fixture set: pre-test while (const/len), nested pre-test while (const/len), and two control variants (post-test loop and no-loop). The reproducer assertion now reports all offending fixtures, while control fixtures are asserted non-matching to guard detector specificity.
Add a guarded CFG canonicalization pass in FuncLifting that rewrites a strict loop shortcut shape before dead-block pruning and OpPhi case collection. Matched shape: loop header branches to a body select with one empty pass-through arm into body merge, body merge branches to loop_continue, and loop_continue conditionally branches to loop header or loop merge on the same boolean condition (or its negation). Rewrite: retarget pass-through arm directly to loop merge, drop the body selection merge marker, and canonicalize loop_continue to an unconditional backedge while preserving only header-target phi payloads. Then recompute predecessor/use counts before block retention. This preserves CFG/phi invariants through explicit guards on terminator kinds, merge topology, predecessor counts, and payload arity, while eliminating a fragile structured-loop encoding that can mis-handle edge-carried values.
It would help a lot to have a GLSL/WGSL example, I believe you mean that: while(f()) {
g();
}ends up encoded like this in SPIR-T (for reasons that go back to RVSDG): do {
bool cond = f();
if(cond) {
g();
}
} while(cond);And if you were to use SPIRV-Cross or Naga to get GLSL/WGSL after SPIR-V -> SPIR-T -> SPIR-V, you'd see the latter Whether or not we improve this aspect, could you first open an issue about it? Any amount of cleverness comes at a cost, and SPIR-T may be suboptimal in a few places, to maximize the likelihood of correctness (as we don't have the tools for formally verified translation etc.). |
75a375b to
1f62277
Compare
|
Thanks, this is a helpful framing. Yes, your understanding is exactly the family I was targeting, with one extra detail: the problematic case is when a value carried around the loop gets a merge input from the pass-through arm that is A clear source-level shape (GLSL/WGSL) is: #version 450
layout(local_size_x = 1) in;
layout(set = 0, binding = 0, std430) buffer OutBuf { uint out_data[]; } out_buf;
void main() {
uint i = 0u;
while (i < 1u) {
if (i < 1u) {
i = i + 1u;
}
}
out_buf.out_data[0] = i; // expected: 1
}@group(0) @binding(0)
var<storage, read_write> out_data: array<u32>;
@compute @workgroup_size(1)
fn main() {
var i: u32 = 0u;
while (i < 1u) {
if (i < 1u) {
i = i + 1u;
}
}
out_data[0] = i; // expected: 1u
}The problematic lowered form (in pseudo-GLSL) is: uint i = 0u;
do {
uint next; // corresponds to OpUndef on one incoming path
if (i < 1u) {
next = i + 1u;
}
i = next; // value merge can carry undef from pass-through arm
} while (i < 1u);In the concrete failing fixture, the key pattern is: So the issue is not just Also per your suggestion: I tested I can open a dedicated issue with:
|
While playing around with rust-gpu I stepped upon a miscompilation, which I debuggged to be a problem in spirt where loops could clobber values in non-deterministic ways. After quite some bughunt, spirt's "lift" logic seems to be at error. The included commit-set adds regression tests matching the problem I faced, and a patch for spirt, that fixes them (and my original miscompilation problem).
Disclosure: This change was developed with LLM assistance (gpt-5.3-codex); I reviewed, tested, and take responsibility for the final patch.
spv/lift: canonicalize loop shortcut to preserve loop edge value mapping
Summary
This MR fixes a
spv::liftloop-shape issue where a specific structured loop encoding can preserve a fragile shortcut CFG pattern through lifting.The fix adds a guarded canonicalization in
FuncLifting::from_func_decl(after linear branch fusion, before dead-block pruning and phi case collection) and adds fixture-backed structural + semantic regression tests.Problem
In a narrow loop pattern:
SelectBranch(BoolCond)loop_continueloop_continueconditionally branches to{loop_header, loop_merge}on the same condition (or negation)this preserved shape can mis-handle edge-carried values in later phi materialization-sensitive paths.
Changes
1)
spv::liftcanonicalizationFile:
src/spv/lift.rsAdded helper methods in
FuncLifting:recompute_use_countsis_passthrough_branch_tois_const_opcodecontinue_cond_relationrewrite_continue_as_unconditional_backedgecanonicalize_loop_continue_shortcutsCanonicalization behavior (only when strict guards match):
loop_mergeSelectionmerge marker for that caseloop_continueinto unconditional backedge to loop header2) Regression coverage
Files:
tests/regression_loop_continue_shortcut.rstests/data/loop-continue-shortcut*.spvbintests/data/basic.frag.glsl.dbg.spvbinTest strategy:
OpUndefdetector on the shortcut pathOpUndefthrough shortcut phisNamed tests:
no_loop_continue_shortcut_shape_after_liftno_loop_carried_undef_from_shortcut_after_liftdetector_does_not_trigger_on_non_loop_fixtureFixture sets include:
const,len)const,len)post-testloop, no-loop, basic fragment)Safety/Correctness guards
The canonicalization is conservative and only fires when all of the following hold:
Loop+ bodySelection)No rewrite occurs when checks fail.
Validation
Executed locally:
cargo test --test regression_loop_continue_shortcutcargo test -qcargo test --release --all-targetscargo build -q --examplesAll pass.
Scope
Semantics-affecting code changes are isolated to:
src/spv/lift.rsTest-only additions are isolated to:
tests/regression_loop_continue_shortcut.rstests/data/*.spvbinfixture filesTracking