Fuse matching product scf.if regions - #484
Conversation
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b4213c67d7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
raghav198
left a comment
There was a problem hiding this comment.
The constrain use is remapped to the branch-local compute value instead of hoisting the read.
I may be misunderstanding, but I think this is not safe when the struct.readm is a constrain op:
%0 = felt.add %a, %b {source = "compute"}
struct.writem %self[@foo] = %0 {source = "compute"}
%foo = struct.readm %self[@foo] {source = "constrain"}
constrain.eq %foo, %c {source = "constrain"}
emits a constraint guaranteeing that the @foo signal equals the result of %c, whereas in the remapped
%0 = felt.add %a, %b {source = "compute"}
struct.writem %self[@foo] = %0 {source = "compute"}
constrain.eq %0, %c {source = "constrain"}
the emitted constraint doesn't refer to a struct signal at all.
|
Thanks for catching this, Raghav. You're right. Remapping the struct.readm result to the branch local compute value can remove the member signal from the emitted constraint. I'll make this fusion case conservative for now and add a regression test so we keep the constraint tied to the member read. |
There was a problem hiding this comment.
Pull request overview
This PR extends the -llzk-fuse-product-loops pass to also fuse matching compute/constrain scf.if regions in struct product programs (Fixes #300), and adds regression tests covering fusable and non-fusable scf.if patterns.
Changes:
- Add
scf.iffusion logic that clones compute + constrain branches into a single fusedscf.ifwhen conditions and structural constraints match. - Refactor pass entry/recursion to fuse both
scf.ifandscf.forcontrol flow via a sharedfuseMatchingRegionControlFlowroutine. - Add lit/FileCheck tests for
scf.iffusion and “do not fuse” guardrails; add an unreleased changelog entry.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| lib/Transforms/LLZKFuseProductLoopsPass.cpp | Implements scf.if fusion and refactors recursion over region control flow. |
| test/Transforms/FuseProductLoops/fuse_if_no_member_read.llzk | Positive test ensuring scf.if fusion occurs in a safe case. |
| test/Transforms/FuseProductLoops/fuse_loop_then_if.llzk | Ensures loop fusion still works and enables nested scf.if fusion after loop fusion. |
| test/Transforms/FuseProductLoops/no_fuse_if_call_after_write.llzk | Negative test: blocks fusion when a call occurs after a mapped write. |
| test/Transforms/FuseProductLoops/no_fuse_if_condition_mismatch.llzk | Negative test: blocks fusion when conditions differ. |
| test/Transforms/FuseProductLoops/no_fuse_if_internal_read.llzk | Negative test: blocks fusion when constrain side reads members inside the scf.if. |
| test/Transforms/FuseProductLoops/no_fuse_if_read_after_write.llzk | Negative test: blocks fusion when a member read appears between the ifs. |
| changelogs/unreleased/fuse-product-if-regions.yaml | Changelog entry for the new scf.if fusion capability. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
@codex review |
|
Codex Review: Didn't find any major issues. Can't wait for the next one! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
|
I seem to have found some loose ends, will update in a bit. |
|
Pushed a conservative safety guard for moved constrain branches. |
|
@codex review |
|
Codex Review: Didn't find any major issues. Hooray! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
| return effectOp.hasEffect<MemoryEffects::Write>() || | ||
| effectOp.hasEffect<MemoryEffects::Allocate>() || effectOp.hasEffect<MemoryEffects::Free>(); |
There was a problem hiding this comment.
I believe you can either do:
effectOp.hasEffect<MemoryEffects::Write, MemoryEffects::Allocate, MemoeryEffects::Free>(); or at least hasEffect<MemoryEffects::Write, MemoryEffects::Allocate, MemoeryEffects::Free>(effectOp);
| static std::optional<llvm::StringRef> getProductSource(Operation *op) { | ||
| if (StringAttr source = op->getAttrOfType<StringAttr>(PRODUCT_SOURCE)) { | ||
| return source.getValue(); | ||
| } | ||
| return std::nullopt; | ||
| } | ||
|
|
||
| static inline bool hasProductSource(Operation *op, llvm::StringRef source) { | ||
| std::optional<llvm::StringRef> productSource = getProductSource(op); | ||
| return productSource && *productSource == source; | ||
| } |
There was a problem hiding this comment.
I think we should move these functions to a shared helper file so they can be reused, e.g. by llzk-compute-constrain-to-product-pass
| effectOp.hasEffect<MemoryEffects::Allocate>() || effectOp.hasEffect<MemoryEffects::Free>(); | ||
| } | ||
|
|
||
| static bool hasUnsafeMovedConstrainOp(scf::IfOp constrainIf) { |
There was a problem hiding this comment.
Add documentation to functions, especially here, since it's not clear what you're trying to do.
| if (isa<MemberReadOp, MemberWriteOp, llzk::global::GlobalWriteOp, llzk::array::WriteArrayOp, | ||
| llzk::array::InsertArrayOp, llzk::pod::WritePodOp, llzk::ram::StoreOp>(op)) { | ||
| return WalkResult::interrupt(); | ||
| } | ||
|
|
||
| if (op->hasTrait<llzk::function::WitnessGen>() || hasWriteLikeMemoryEffect(op)) { | ||
| return WalkResult::interrupt(); | ||
| } | ||
|
|
||
| if (isa<CallOpInterface>(op)) { | ||
| return WalkResult::interrupt(); | ||
| } |
There was a problem hiding this comment.
This could all be one if statement.
| ) { | ||
| // `scf.if` fusion moves the constrain branch before intervening member writes of compute-if | ||
| // results. The moved branch must not read members, write storage, call functions, or have | ||
| // write-like effects. |
There was a problem hiding this comment.
Currently this loop only checks member reads/writes, but ignores RAM/globals
| // CHECK: struct.writem %{{[0-9a-zA-Z_\.]+}}[@foo] = %[[RESULT]] : <@A>, !felt.type | ||
| // CHECK: %[[FOO:[0-9a-zA-Z_\.]+]] = struct.readm %{{[0-9a-zA-Z_\.]+}}[@foo] : <@A>, !felt.type | ||
| // CHECK: scf.if %[[B]] { | ||
| // CHECK: constrain.eq %[[FOO]] |
There was a problem hiding this comment.
Lit test output should show the whole module, like all other lit tests in this repo.
53ba923 to
6719d38
Compare
Summary
Partially addresses #300.
Adds conservative
scf.iffusion to-llzk-fuse-product-loopsfor matching compute/constrain product regions.The fusion handles narrow cases where matching compute and constrain
scf.ifregions have the same SSA condition and can be fused without changing member read/write ordering. It rejects constrain-side member reads, storage writes, calls, witness generation, and write-like memory effects, so member reads remain tied to their original storage semantics until a more precise remapping analysis is added.Product control-flow fusion remains scoped to explicitly marked product-source control ops.
Testing
git diff --checktest/Transforms/FuseProductLoops/for:scf.ifcases, including direct compute-result uses and multi-result remappingscf.if/scf.forproduct-source inference no-fuse cases