[IR Container] Phase 2.5 Copy-Move Semantics#5964
[IR Container] Phase 2.5 Copy-Move Semantics#5964mdavis36 wants to merge 10 commits intomd/phase2-per-fusionfrom
Conversation
|
!test |
|
Review updated until commit d145d9e Description
|
| Relevant files | |||||
|---|---|---|---|---|---|
| Enhancement |
|
PR Reviewer Guide
Here are some key observations to aid the review process:
| 🧪 PR contains tests |
| ⚡ Recommended focus areas for review |
Complex swap logic
|
|
!test |
192fd55 to
35b7405
Compare
33629cb to
8b162d9
Compare
|
!test |
Greptile SummaryThis PR implements shared-container-aware copy, move, and swap semantics for The one remaining concern: the copy assignment operator calls Confidence Score: 3/5
Sequence DiagramsequenceDiagram
participant Caller
participant FusionB as Fusion B (copy ctor)
participant Container as shared IrContainer C
participant IrCloner
participant FusionA as Fusion A (source)
Caller->>FusionB: Fusion B(A) [copy constructor]
FusionB->>Container: share A.ir_container_ (shared_ptr copy)
FusionB->>Container: addFusion(&B)
FusionB->>FusionB: Fusion::copy(&A, &B)
FusionB->>FusionB: clear() → removeStatementsOwnedBy(&B) [no-op]
FusionB->>IrCloner: IrCloner ir_cloner(B)
loop clone vals in insertion order
IrCloner->>Container: clone(val_i) → registerVal(&B) → getValName(vtype)
IrCloner->>IrCloner: setName(src->name()) overrides counter
end
loop wire definitions/uses
IrCloner->>Container: clone(expr_j) → registerExpr(&B) → getExprName()
IrCloner->>FusionB: setDefinition / setUses
end
FusionB->>FusionB: sync val_type_name_map_ = A.val_type_name_map_
FusionB->>FusionB: sync expr_name_counter_ = A.expr_name_counter_
FusionB->>FusionB: remap inputs_, outputs_, io_alias_, axioms_, metadata_
note over Container: C now holds A's vals (keyed &A) AND B's clones (keyed &B)
note over FusionB: B.val_type_name_map_ matches A → new TVs start at max(name)+1
Last reviewed commit: c8ffe1d |
|
!test |
35b7405 to
88b2e60
Compare
8b162d9 to
b8d202d
Compare
88b2e60 to
9737ff1
Compare
Additional Comments (1)
The counter sync at lines 220–221 sets Compare with val counters: the val counter sync is correctly placed after the val-cloning loop so that While this doesn't cause immediate name collisions (the overcounted range is unused by any actual expr), it creates unnecessary name-space gaps and violates the invariant that a cloned Fusion's counter should match the source's counter — which is the premise relied on by |
|
!test |
bc595c5 to
9f944b4
Compare
a9c62ea to
46080be
Compare
Additional Comments (2)
The second loop uses The old // Copy expressions in deterministic order
for (auto expr : from->deterministic_exprs()) {
to->exprs_.insert(ir_cloner.clone(expr));
}With the new code, the destination fusion's The first loop already uses
The move constructor is declared Calling a potentially-throwing function from a The previous discussion noted that // In fusion.h:
Fusion(Fusion&& other); // remove noexcept
Fusion& operator=(Fusion&& other); // remove noexcept
// In fusion.cpp:
Fusion::Fusion(Fusion&& other) : Fusion() { // remove noexcept
FUSER_PERF_SCOPE("Fusion move");
swap(*this, other);
}
Fusion& Fusion::operator=(Fusion&& other) { // remove noexcept
...
}Note: removing |
Additional Comments (3)
The copy assignment operator now produces a destination Fusion that shares Consider the following scenario: Fusion a;
// populate a...
Fusion b;
b = a;
// b now shares a's IrContainer — NOT isolated!
a.clear(); // also removes statements from the container b depends onBefore this PR,
In the same-container path, std::swap(c->per_fusion_vals_[&a], c->per_fusion_vals_[&b]);
std::swap(c->per_fusion_exprs_[&a], c->per_fusion_exprs_[&b]);If Consider using
In if (src_container != dest_container) {
dest_stmt->setName(IrBuilderPasskey(dest_container), src_stmt->name());
}
However, this invariant is fragile: it relies on the fact that A targeted |
|
!test |
Additional Comments (1)
The copy assignment operator is missing the self-assignment check that the move assignment operator already has (
Any external |
|
!test |
Additional Comments (3)
While functionally harmless today, this inconsistency can confuse future callers of auto& a_vals = c->per_fusion_vals_;
auto& a_exprs = c->per_fusion_exprs_;
auto it_av = a_vals.find(&a), it_bv = a_vals.find(&b);
auto it_ae = a_exprs.find(&a), it_be = a_exprs.find(&b);
if (it_av != a_vals.end() || it_bv != a_vals.end())
std::swap(a_vals[&a], a_vals[&b]);
// same for exprs
Similarly, Most importantly, both dead methods still manipulate the container-level name counters ( Consider removing
The sequence: Fusion copy(other); // copy ctor → copy now shares other->ir_container_
clear(); // clears this's statements from its original container
swap(*this, copy); // this ends up sharing other's containerAfter this operation This is correct for the current ownership model, but it means that after |
9f944b4 to
7b3ce1f
Compare
e5256e7 to
058a980
Compare
7b3ce1f to
2e491e9
Compare
Additional Comments (3)
When a Fusion is cleared in a shared container, the destroyed Compare with for (Val* in : e->inputs()) {
in->removeUse(e);
}This is not a risk for today's independent-clone graphs (Phase 2 copy semantics keep IR graphs separate), but the PR description explicitly calls out Phase 3 scalar reuse where Consider at minimum adding a comment warning about this limitation, or adding edge cleanup before the // For each expr owned by fusion, remove it from its inputs' uses_ lists
// to avoid dangling pointers in other Fusions' Vals
if (exprs_it != per_fusion_exprs_.end()) {
for (Expr* e : exprs_it->second) {
for (Val* inp : e->inputs()) {
inp->removeUse(e);
}
}
}
// ... then erase_if loop
Concrete scenario: With
A runtime-checkable condition would be preferable — for example, checking whether the if (num_groups == 1 ||
fusion_->ir_container_ptr()->hasMultipleFusions() ||
isOptionDisabled(DisableOption::ParallelCompile)) {This would restore parallel compilation for non-shared-container fusions immediately, rather than waiting for the TODO to be addressed. |
Additional Comments (2)
While the current internal call sites (
The copy-assignment operator calls The classic copy-and-swap idiom avoids this by not explicitly clearing before the swap: the old state is destroyed by the temporary's destructor after a successful swap. However, with the new shared-container semantics in this PR, directly using a by-value parameter for copy-assignment would create unintended container sharing on the way in. One safe alternative is to check whether the copy completed before clearing: Fusion& Fusion::operator=(const Fusion& other) {
if (this != &other) {
Fusion copy(other); // can throw — *this unchanged
// Only reach swap (which can throw) after copy succeeded.
// If swap throws here, *this is empty; consider moving swap-on-success
// into a noexcept helper or accepting this as a medium-risk invariant.
clear();
swap(*this, copy);
}
return *this;
}At minimum this should be documented as "basic exception safety only" — if |
2e491e9 to
50cb886
Compare
Copy constructor now shares the source's container pointer instead of creating a new one. Fusion::copy clones directly from per-Fusion filtered vals rather than delegating to IrContainer::copy. Swap changed from content-based (IrContainer::swap) to pointer-based with per-Fusion ownership tracking for both same-container and different-container cases.
Move val/expr name counters from IrContainer to Fusion so each Fusion independently tracks name assignment. This fixes CI failures where Fusion::copy left the dest counter at N (number of cloned vals) instead of max(name)+1 when source names were non-sequential, causing newly created TVs to collide with existing names. The fix adds val_type_name_map_ and expr_name_counter_ to Fusion, and updates registerVal/registerExpr to use the Fusion-level counters. Fusion::copy syncs counters from source to dest after cloning. Fusion::swap exchanges counters. Fusion::clear resets them.
d145d9e to
3b9acdb
Compare
Additional Comments (3)
Although the copy constructor and copy assignment operator guard against this scenario,
This works for the current implementation (since
However, a side-effect is that names are unique within a single Fusion but not within the shared IrContainer. If both Fusions independently create new vals post-copy, they each receive the same next name, producing duplicate Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time! |
3b9acdb to
c8ffe1d
Compare
Additional Comments (1)
The standard idiom should be: Fusion& Fusion::operator=(const Fusion& other) {
if (this != &other) {
Fusion copy(other);
swap(*this, copy); // On throw: *this still has original state intact
// copy destructs → calls clear() + removeFusion → cleans up *this's old data
}
return *this;
}Without the explicit |
|
!test |
Summary
Implement shared-container-aware copy, move, and swap operations, plus per-Fusion name counters that ensure cloned Vals get matching names. This PR combines the originally planned Tasks 3 and 4 — per-Fusion name counters were required to fix CI failures from the copy implementation (553 failures from duplicate TV names when name counter synchronization was missing).
Changes
Copy semantics:
shared_ptr, register with container, delegate toFusion::copyFusion::copy: Clear destination, createIrClonertargeting dest, clone source'sdeterministic_valsinto shared container, clone Fusion-level state (inputs, outputs, axioms, metadata)Move semantics:
Swap:
Per-Fusion name counters:
val_type_name_map_andexpr_name_counter_added as Fusion membersgetValName(ValType)andgetExprName()methods on FusionCopy Semantics in Detail
The copy constructor shares the container (increments
shared_ptrrefcount), then clones A's nodes into the same shared storage. Per-Fusion tracking ensures each Fusion's accessors still return only their own nodes.Swap: Three Cases
Why Name Counters Were Merged Into This PR
The initial implementation of
Fusion::copyreplaced the oldIrContainer::copywith directIrCloner-based cloning but dropped name counter synchronization. Without per-Fusion counters, cloned Vals in a shared container received names starting past the source's last name (e.g., T10–T19 instead of T0–T9), breakingalias_memory.cpp(duplicatetv->name()assertions) and cascading into 553 CI failures across codegen, validation, and numerical checks.The fix — per-Fusion name counters as Fusion members — is architecturally cleaner than the originally planned IrContainer-level maps, avoids indirection, and aligns with the per-Fusion state model established in earlier tasks.
Relationship to Phase 2
Copy/move/swap are the operations that make shared containers usable. Without them, the
shared_ptrand tracking infrastructure from PRs 1–2 are inert. This PR enables the core Phase 2 scenario:Phase 2 establishes the copy/move/swap mechanics. Phase 3 simply changes
makeFusionfrom default-ctor +Fusion::copyto copy-ctor (shared container), and the infrastructure from this PR handles everything correctly.Per-Fusion name counters are critical for cross-clone name correspondence required by
GreedyParams::at(tv->name())andnormalization_utils— both of which look up Vals by name as a map key across clone boundaries.CI Risk
Medium. Copy/move/swap are well-defined operations with clear semantics. The 553-failure CI regression from missing name counters was identified and fixed before merge.