Skip to main content

fuel_graph/
run.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Run extraction + the transient `lower_run` view + the fewness gate.
3//!
4//! Phase A PR-A2 of the "plan IS the graph" rebuild.
5//!
6//! A **run** is the maximal straight-line op-sequence between two
7//! *decision points* — the future dispatch / CUDA-graph-capture unit
8//! (see [`../docs/architecture/06-runtime.md`](../../docs/architecture/06-runtime.md)
9//! §"Dispatch: runs, not single ops"). This module *derives* runs from
10//! the graph structure; it does **not** execute them (Phase C) and does
11//! **not** add an optimizer (Phase A3+). A run is a single-device
12//! contiguous chain with one `entry` and one `exit`; its members are
13//! [`NodeId`]s in topological order.
14//!
15//! Run boundaries (a new run starts at a node when *any* hold):
16//! - **(a) graph entry** — a node with no predecessor inside the
17//!   reachable set (a source / root input of the walk).
18//! - **(b) reconverge** — a node named as some [`Op::Branch`]'s
19//!   `reconverge_at` (the post-merge region is a fresh run).
20//! - **(c) arm entry** — a node whose sole predecessor is a Branch's
21//!   *diverge* point (the first node of each candidate route).
22//! - **(d) residency seam** — the node's [`Graph::target_backend`]
23//!   differs from its sole predecessor's, **or** the node is an
24//!   [`Op::Copy`] / [`Op::Move`] device-transfer.
25//! - **(e) fan-in** — a node with more than one predecessor inside the
26//!   reachable set.
27//!
28//! Consequently a run never spans a `Branch` boundary or a device
29//! change. A graph with **zero** `Op::Branch` nodes and a single
30//! residency extracts to exactly **one** run covering all reachable
31//! nodes — exactly today's single-route graph.
32
33use crate::{Graph, NodeId, Op, topo_order_multi};
34use fuel_ir::probe::BackendId;
35use std::collections::{HashMap, HashSet};
36
37/// A resolved route through the multi-path graph: for each `Op::Branch`
38/// decision point (keyed by the Branch node's [`NodeId`]) the **arm
39/// index** the runtime route picker (Picker 2) chose. Arm `i` is the
40/// branch's `inputs[i]` (arm-0 = `inputs[0]` = the cost-vector winner).
41///
42/// A branch **absent** from the map defaults to **arm 0** — so the empty
43/// route is exactly the arm-0-everywhere lowering ([`lower_runs_arm0`]),
44/// which keeps the no-pressure / no-telemetry behavior identical to
45/// Phase B. The picker in `fuel-dispatch` produces one of these by
46/// walking the branches in topological order and consulting a runtime
47/// selector over each branch's arms; `fuel-graph` only *consumes* it to
48/// lower the chosen route (it has no selector / telemetry knowledge).
49pub type PickedRoute = HashMap<NodeId, usize>;
50
51/// A maximal straight-line, single-device op-sequence between two
52/// decision points — the dispatch / command-buffer-capture unit.
53///
54/// `members` are in topological order; `entry == members[0]` and
55/// `exit == *members.last()`. `device` is the resolved
56/// [`BackendId`] shared by every member (via
57/// [`Graph::target_backend`]); `None` means "inherit the executor
58/// default" (no member carries an explicit backend) — a run is
59/// single-device, so every member agrees on this value by construction
60/// (a residency change is itself a run boundary).
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct Run {
63    /// First node of the run (a decision-point boundary).
64    pub entry: NodeId,
65    /// Last node of the run (the node the next decision point reads,
66    /// or a root output).
67    pub exit: NodeId,
68    /// Every node of the run, in topological order, `entry` first.
69    pub members: Vec<NodeId>,
70    /// The single resolved backend shared by all members, or `None`
71    /// when no member carries an explicit `target_backend` (executor
72    /// default).
73    pub device: Option<BackendId>,
74}
75
76/// Extract the runs of the graph reachable from `root`, in
77/// topological order of their entries.
78///
79/// Walks the single existing [`topo_order_multi`] order once and cuts
80/// a new run at every boundary (see the module docs). Each emitted run
81/// is single-entry / single-exit and single-device.
82pub fn extract_runs(graph: &Graph, root: NodeId) -> Vec<Run> {
83    extract_runs_multi(graph, &[root])
84}
85
86/// Multi-root variant of [`extract_runs`] — the reachable set is the
87/// union over `roots` (mirrors [`topo_order_multi`]) **plus the arms of
88/// every reachable [`Op::Branch`]**. A Branch's non-arm-0 arms are read
89/// only by the Branch node itself (arm 0 is the runnability fallback the
90/// `reconverge_at` node reads directly, per PR-A1), so seeding the walk
91/// from the Branch nodes pulls every candidate route into the reachable
92/// set.
93pub fn extract_runs_multi(graph: &Graph, roots: &[NodeId]) -> Vec<Run> {
94    let eff_roots = effective_roots(graph, roots);
95    let order = topo_order_multi(graph, &eff_roots);
96    let reachable: HashSet<NodeId> = order.iter().copied().collect();
97
98    // Nodes named as some Branch's `reconverge_at` (the post-merge region
99    // opens a fresh run).
100    let reconverge_points: HashSet<NodeId> = order
101        .iter()
102        .filter_map(|&id| match graph.node(id).op {
103            Op::Branch { reconverge_at } => Some(reconverge_at),
104            _ => None,
105        })
106        .collect();
107
108    // Arm-entry nodes: the first node of each candidate route, departing
109    // from the shared diverge point. A run never spans into an arm.
110    let arm_entries = compute_arm_entries(graph, &order, &reachable);
111
112    // Predecessors inside the reachable set, per node (for fan-in + the
113    // residency-seam sole-predecessor test).
114    let mut pred_inside: HashMap<NodeId, Vec<NodeId>> = HashMap::new();
115    for &id in &order {
116        let preds: Vec<NodeId> = graph
117            .node(id)
118            .inputs
119            .iter()
120            .copied()
121            .filter(|p| reachable.contains(p))
122            .collect();
123        pred_inside.insert(id, preds);
124    }
125
126    let mut runs: Vec<Run> = Vec::new();
127    let mut current: Vec<NodeId> = Vec::new();
128    let mut current_device: Option<BackendId> = None;
129
130    for &id in &order {
131        // The `Op::Branch` (phi/merge) node is structural bookkeeping —
132        // the decision point *between* runs, not executable work. It is
133        // never a run member; once a route is picked it is invisible to
134        // the hot path. Skipping it also means a consumer that reads a
135        // Branch (rather than the `reconverge_at` node) finds its
136        // predecessor absent from the open run and so opens a fresh run.
137        if matches!(graph.node(id).op, Op::Branch { .. }) {
138            continue;
139        }
140        let preds = &pred_inside[&id];
141        let device = graph.target_backend(id);
142
143        let starts_new = {
144            // (a) graph entry — no predecessor in the reachable set.
145            let is_entry = preds.is_empty();
146            // (b) reconverge point.
147            let is_reconverge = reconverge_points.contains(&id);
148            // (c) arm entry.
149            let is_arm = arm_entries.contains(&id);
150            // (e) fan-in — more than one reachable predecessor.
151            let is_fan_in = preds.len() > 1;
152            // (d) residency seam: a device-transfer op, or a backend that
153            // differs from the sole predecessor's backend.
154            let is_transfer = matches!(graph.node(id).op, Op::Copy { .. } | Op::Move { .. });
155            let is_seam = preds.len() == 1 && graph.target_backend(preds[0]) != device;
156            // Contiguity: a run is a straight-line chain, so a node only
157            // *extends* the current run when its sole predecessor IS the
158            // current run's last member. Otherwise it begins a new run
159            // even if no other rule fired (guards against topo interleave
160            // and the Branch node itself, whose inputs are the arm exits).
161            let breaks_chain = match current.last() {
162                None => true,
163                Some(&last) => !(preds.len() == 1 && preds[0] == last),
164            };
165            // A node whose backend differs from the open run's device
166            // cannot extend it (single-device runs).
167            let device_mismatch = !current.is_empty() && device != current_device;
168            is_entry
169                || is_reconverge
170                || is_arm
171                || is_fan_in
172                || is_transfer
173                || is_seam
174                || breaks_chain
175                || device_mismatch
176        };
177
178        if starts_new && !current.is_empty() {
179            runs.push(finish_run(&current, current_device));
180            current = Vec::new();
181        }
182        if current.is_empty() {
183            current_device = device;
184        }
185        current.push(id);
186    }
187    if !current.is_empty() {
188        runs.push(finish_run(&current, current_device));
189    }
190    runs
191}
192
193/// The effective root set for a run/density walk: the caller's `roots`
194/// plus every [`Op::Branch`] node *participating in* the reachable
195/// computation. A finalized `Op::Branch` is typically **orphaned** —
196/// nothing downstream reads it (the `reconverge_at` node reads arm 0
197/// directly, per PR-A1's runnability invariant), so it would never be
198/// found by a plain forward walk. We therefore scan the whole arena and
199/// seed any Branch whose `reconverge_at` (or any arm exit) is already
200/// reachable; seeding it pulls its candidate arms into the walk. Run to
201/// a fixpoint so a Branch reached only through another Branch's arm is
202/// also covered.
203///
204/// Public so the optimizer's residency pass (`fuel-dispatch`) can stitch
205/// inbound cross-device copies for EVERY surviving arm — a plain
206/// `topo_order_multi(roots)` walk misses orphaned Branch nodes (and thus
207/// their non-arm-0 arms), which would leave a re-pickable arm's device-
208/// inputs un-bridged (Step E Phase C, PR C-0).
209pub fn effective_roots(graph: &Graph, roots: &[NodeId]) -> Vec<NodeId> {
210    let mut seeds: Vec<NodeId> = roots.to_vec();
211    let mut seen: HashSet<NodeId> = seeds.iter().copied().collect();
212    loop {
213        let reachable: HashSet<NodeId> = topo_order_multi(graph, &seeds).into_iter().collect();
214        let mut added = false;
215        for idx in 0..graph.len() {
216            let id = NodeId(idx);
217            if seen.contains(&id) {
218                continue;
219            }
220            let Op::Branch { reconverge_at } = graph.node(id).op else {
221                continue;
222            };
223            // The branch participates if its merge target or any arm exit
224            // is already part of the reachable computation.
225            let participates = reachable.contains(&reconverge_at)
226                || graph.node(id).inputs.iter().any(|a| reachable.contains(a));
227            if participates {
228                seen.insert(id);
229                seeds.push(id);
230                added = true;
231            }
232        }
233        if !added {
234            break;
235        }
236    }
237    seeds
238}
239
240/// The reachable [`Op::Branch`] decision points over `roots`, in
241/// **topological order** of their `reconverge_at` merge points — the
242/// order the runtime route picker (Picker 2) resolves them in, so that
243/// coupled upstream decisions are committed before a downstream branch
244/// is reached (06-runtime §"Resolution order matters when decisions are
245/// coupled").
246///
247/// A finalized `Op::Branch` is typically orphaned (the `reconverge_at`
248/// node reads arm 0 directly, per PR-A1), so a plain forward walk would
249/// miss it; we therefore order by each branch's `reconverge_at` position
250/// in the [`effective_roots`] walk — the merge point is downstream of the
251/// arms, so its topo position is a faithful "where this decision sits"
252/// key. Branches whose merge point is unreachable are dropped.
253pub fn branches_in_topo_order(graph: &Graph, roots: &[NodeId]) -> Vec<NodeId> {
254    let eff_roots = effective_roots(graph, roots);
255    let order = topo_order_multi(graph, &eff_roots);
256    let position: HashMap<NodeId, usize> =
257        order.iter().enumerate().map(|(i, &id)| (id, i)).collect();
258
259    let mut branches: Vec<(usize, NodeId)> = Vec::new();
260    for idx in 0..graph.len() {
261        let id = NodeId(idx);
262        let Op::Branch { reconverge_at } = graph.node(id).op else {
263            continue;
264        };
265        // Only branches whose merge point participates in this realize.
266        if let Some(&pos) = position.get(&reconverge_at) {
267            branches.push((pos, id));
268        }
269    }
270    branches.sort_by_key(|&(pos, _)| pos);
271    branches.into_iter().map(|(_, id)| id).collect()
272}
273
274fn finish_run(members: &[NodeId], device: Option<BackendId>) -> Run {
275    Run {
276        entry: members[0],
277        exit: *members.last().expect("a run has at least one member"),
278        members: members.to_vec(),
279        device,
280    }
281}
282
283/// For every [`Op::Branch`] reachable in `order`, find the arm-entry
284/// nodes: the first node on each candidate route after the shared
285/// diverge point. These are run boundaries (rule (c)).
286///
287/// The op carries `reconverge_at` but not the diverge, so we recover the
288/// branch's shared prefix structurally: intersect the backward cones of
289/// all arm exits. Every node in that intersection is shared (the diverge
290/// point and its ancestors); the per-arm interior is everything else in
291/// the cone. An interior node is an *arm entry* when one of its
292/// predecessors lies in the shared prefix — i.e. it is the point where
293/// the route departs from the diverge region.
294fn compute_arm_entries(
295    graph: &Graph,
296    order: &[NodeId],
297    reachable: &HashSet<NodeId>,
298) -> HashSet<NodeId> {
299    let mut arm_entries: HashSet<NodeId> = HashSet::new();
300    for &id in order {
301        let Op::Branch { .. } = graph.node(id).op else {
302            continue;
303        };
304        let arm_exits: Vec<NodeId> = graph.node(id).inputs.clone();
305        if arm_exits.len() < 2 {
306            continue;
307        }
308        // Backward cone of each arm exit, bounded to the reachable set.
309        let cones: Vec<HashSet<NodeId>> = arm_exits
310            .iter()
311            .map(|&e| backward_cone(graph, e, reachable))
312            .collect();
313        // The diverge point is shared by every arm — it lies in the
314        // intersection of all cones. The arm interiors are the
315        // per-cone nodes NOT in that shared prefix. The arm entry is the
316        // interior node whose sole predecessor lies in the shared prefix.
317        let mut shared: HashSet<NodeId> = cones[0].clone();
318        for c in &cones[1..] {
319            shared = shared.intersection(c).copied().collect();
320        }
321        for cone in &cones {
322            for &n in cone {
323                if shared.contains(&n) {
324                    continue;
325                }
326                // Arm-interior node. It is an arm *entry* if any of its
327                // predecessors is in the shared prefix (it departs from
328                // the diverge region).
329                let departs = graph.node(n).inputs.iter().any(|p| shared.contains(p));
330                if departs {
331                    arm_entries.insert(n);
332                }
333            }
334        }
335    }
336    arm_entries
337}
338
339/// Backward-reachable cone of `from` (the node and all transitive
340/// inputs), bounded to `reachable`.
341fn backward_cone(graph: &Graph, from: NodeId, reachable: &HashSet<NodeId>) -> HashSet<NodeId> {
342    let mut seen: HashSet<NodeId> = HashSet::new();
343    let mut stack = vec![from];
344    while let Some(n) = stack.pop() {
345        if !reachable.contains(&n) || !seen.insert(n) {
346            continue;
347        }
348        for &inp in &graph.node(n).inputs {
349            if !seen.contains(&inp) {
350                stack.push(inp);
351            }
352        }
353    }
354    seen
355}
356
357/// The transient executable view of a single run: its ordered member
358/// [`NodeId`]s, ready to be recorded into a command buffer (Phase C).
359///
360/// A **borrow / transient** view — it is NOT stored on the graph. This
361/// is the shape Phase C records; PR-A2 only derives it.
362pub fn lower_run(run: &Run) -> &[NodeId] {
363    &run.members
364}
365
366/// The **arm-0 single-route lowering** (Phase A PR-A4, architect-
367/// approved temporary fix). The flat executable dispatch order that
368/// **follows arm 0 through every [`Op::Branch`]** — the pre-run, arm 0's
369/// run, then the post-run — and **skips every non-arm-0 arm's run**.
370///
371/// Why this is needed: [`extract_runs_multi`] partitions *every* arm
372/// into its own run (so the future runtime picker can choose among
373/// them). A naive concatenation of all runs would execute every arm.
374/// Until the Phase-C route picker lands, realize must default to arm 0
375/// — the route a finalized-but-unpicked graph runs on (the A1 arm-0
376/// runnability invariant). This walk drops the runs that lie entirely
377/// inside a non-arm-0 arm and keeps the rest (pre / arm-0 / post), so a
378/// branched graph realizes to the same result as its arm-0 route.
379///
380/// A graph with **zero `Op::Branch` nodes** has no arm to skip, so this
381/// is byte-identical to concatenating [`lower_run`] over
382/// [`extract_runs_multi`] — today's single-route order.
383pub fn lower_runs_arm0(graph: &Graph, roots: &[NodeId]) -> Vec<NodeId> {
384    // Arm-0 everywhere is the empty-route special case of the general
385    // route-aware lowering (an unmentioned branch defaults to arm 0).
386    lower_picked_route(graph, roots, &PickedRoute::new())
387}
388
389/// **Route-aware lowering** (Phase C PR-C1) — the generalization of
390/// [`lower_runs_arm0`]. The flat executable dispatch order that follows
391/// the **chosen arm at each [`Op::Branch`]** (per `picked`, defaulting to
392/// arm 0 for any branch the picker did not resolve) and **skips every
393/// non-chosen arm's run**.
394///
395/// This is exactly [`lower_runs_arm0`] generalized from "arm 0 always"
396/// to "the picked arm per branch": it reuses the same run partition
397/// ([`extract_runs_multi`]) and the same single-contiguous-region
398/// property of a run (extract never spans a branch boundary, so a run is
399/// either wholly inside a non-chosen arm or wholly outside). The skip set
400/// is `non_chosen_arm_nodes` — `non_arm0_arm_nodes` generalized to "any
401/// arm but the chosen one."
402///
403/// Value-preserving contract: every arm is a valid kernel for the same
404/// op (cast-to-uniform at `reconverge_at` per PR-A1), so following any
405/// arm yields the same result within tolerance. With `picked` empty
406/// (no-pressure / no-telemetry route) this is byte-identical to
407/// [`lower_runs_arm0`], and a graph with **zero `Op::Branch` nodes** has
408/// no arm to skip, so it equals concatenating [`lower_run`] over
409/// [`extract_runs_multi`] — today's single-route order.
410pub fn lower_picked_route(graph: &Graph, roots: &[NodeId], picked: &PickedRoute) -> Vec<NodeId> {
411    let runs = extract_runs_multi(graph, roots);
412    let skip = non_chosen_arm_nodes(graph, roots, picked);
413    // The KEPT runs (chosen arms + pre/post/independent regions), in topo
414    // order — the runs the un-reordered lowering would concatenate.
415    let kept: Vec<Run> = runs
416        .into_iter()
417        // A run is a single contiguous arm/region (extract never spans a
418        // branch boundary), so it is either wholly inside a non-chosen
419        // arm or wholly outside. Keep iff its entry is not a skipped node.
420        .filter(|run| !skip.contains(&run.entry))
421        .collect();
422    // PR-C3: device-alternating reorder of the kept runs (auto-overlap).
423    // Single-device ⇒ identity ⇒ byte-identical to the prior concatenation.
424    // Multi-device ⇒ a valid topological reordering that interleaves
425    // devices so independent device sub-DAGs dispatch adjacently (the A4b
426    // overlap topology, for an arbitrary graph). The reorder is a hint: the
427    // emitted NodeIds are identical, only their order across independent
428    // runs changes.
429    let perm = device_alternating_order(graph, &kept);
430    let mut order = Vec::new();
431    for &ri in &perm {
432        order.extend_from_slice(lower_run(&kept[ri]));
433    }
434    order
435}
436
437/// **Device-overlap topological reorder** of a run list (Step E Phase C,
438/// PR C3 — the *auto-overlap* pass). Returns a permutation of
439/// `0..runs.len()` (indices into `runs`) that is a **valid topological
440/// reordering** — every run still appears after all the runs it depends
441/// on — chosen so independent cross-device sub-DAGs DISPATCH so they
442/// overlap: the longest compute chunk of each device is enqueued/recorded
443/// BEFORE the host-blocking cross-device drain that feeds the join.
444///
445/// # Why (the A4b caveat this removes)
446///
447/// [`extract_runs_multi`] emits runs in [`topo_order_multi`]'s fixed-DFS
448/// order. For two INDEPENDENT reconverging sub-DAGs — one on CUDA, one on
449/// Vulkan — that DFS emits one sub-DAG *fully* (including the cross-device
450/// `Op::Copy` that feeds the join) before the other is even recorded. The
451/// executor then blocks the host on that copy's producer (a D2H wait)
452/// while the OTHER device sits idle — the two devices serialize. A4b built
453/// the cross-device concurrency MECHANISM (eager Vulkan submit + in-flight
454/// CUDA handles) but it only overlaps when the auto-submitting device's
455/// chunk (CUDA, A3 stream-ordered) is enqueued — and thus running — BEFORE
456/// the host blocks on the deferred device's (Vulkan) drain. The A4b overlap
457/// benchmark gets that order by HAND (a specific reconverge input order +
458/// CPU-primary pinning so the DFS happens to pop the heavy CUDA chunk
459/// first). This pass produces that order for an ARBITRARY graph.
460///
461/// # The heuristic — longest-downstream-compute (critical-path) list scheduling
462///
463/// A Kahn pump that, among the runs whose producers are all emitted, picks
464/// the run with the **largest compute chunk reachable downstream**
465/// (including itself) — a classic critical-path / HLFET list-scheduling
466/// priority. Concretely, each run is weighted by `max over its downstream
467/// cone of compute_size(run)`, where a compute run's size is its node count
468/// and a cross-device **transfer** (`Op::Copy`/`Op::Move`) has size 0. The
469/// effect:
470///
471/// - The path to the **heaviest compute chunk** (e.g. the long CUDA chain)
472///   is emitted FIRST — so on the auto-submitting device it is enqueued and
473///   running earliest, maximizing the window the other device's work
474///   overlaps. (Empirically: enqueuing the heavy CUDA chunk first is what
475///   makes the eager-submit overlap; the inverse order serializes.)
476/// - The next-heaviest device chunk is emitted next (its device's chunk is
477///   recorded while the first device runs).
478/// - A cross-device **drain** (the D2H `Op::Copy` feeding the join) has
479///   near-zero downstream compute weight, so it sorts AFTER both device
480///   chunks — exactly the A4b overlap topology (record/enqueue both chunks,
481///   THEN the drain whose fence-wait the other device's in-flight work
482///   hides). Among equal-weight ready runs, ties break toward a device
483///   DIFFERENT from the last emitted (interleave), then the smallest
484///   original index (determinism).
485///
486/// # The invariant — a HINT, never a correctness lever
487///
488/// Reordering INDEPENDENT runs changes only *when* work dispatches, never
489/// the result: the emitted order is always a valid topological order (a
490/// run after its producers), every op is bit-deterministic, and every
491/// cross-device wait/copy keys on a node's *inputs*, not on dispatch
492/// position — so no reorder can drop a wait or change a value. The output
493/// is byte-identical regardless of the reorder (proved by the
494/// reorder-invariance tests).
495///
496/// # The multi-device gate (single-device = identity)
497///
498/// If every run shares a single resolved device (≤ 1 distinct
499/// [`Run::device`] across the list), there is nothing to overlap, so this
500/// returns the identity permutation `0..runs.len()` and the lowering is
501/// **byte-identical to today's** topo-DFS order. The pass only diverges
502/// from the input order when the run list genuinely spans two or more
503/// devices.
504///
505/// # Determinism
506///
507/// Deterministic for a given run list: the priority key is
508/// `(−downstream_weight, not_different_device, original_index)`, fully
509/// ordered, so the reorder is reproducible and a degenerate input (one
510/// device) reproduces the input order exactly.
511///
512/// `runs` must be the partition [`extract_runs_multi`] produced for the
513/// same `graph` (the node→run mapping is rebuilt from `runs`), and the
514/// dependency edges are derived from each member's graph inputs.
515pub fn device_alternating_order(graph: &Graph, runs: &[Run]) -> Vec<usize> {
516    let n = runs.len();
517    if n <= 1 {
518        return (0..n).collect();
519    }
520
521    // The multi-device gate: count distinct resolved devices. `None`
522    // (executor-default, no explicit backend) is its own bucket — a list
523    // that is all-`None` or all-one-backend has nothing to interleave.
524    let mut distinct: Vec<Option<BackendId>> = Vec::new();
525    for r in runs {
526        if !distinct.contains(&r.device) {
527            distinct.push(r.device);
528        }
529    }
530    if distinct.len() <= 1 {
531        // Single-device (or single executor-default) — identity. This is
532        // the byte-identical-to-today path (the headline single-device
533        // regression gate).
534        return (0..n).collect();
535    }
536
537    // node -> the index of the run that contains it (every reachable node
538    // is in exactly one run; the Branch node is not a member of any run,
539    // so a cross-run input that is a Branch node simply finds no producer
540    // run, which is correct — arms feed the reconverge, not the Branch).
541    let mut run_of: HashMap<NodeId, usize> = HashMap::new();
542    for (ri, r) in runs.iter().enumerate() {
543        for &m in &r.members {
544            run_of.insert(m, ri);
545        }
546    }
547
548    // Run-level dependency edges. Run `r` depends on run `s` (s != r) when
549    // some member of `r` reads a node that is a member of `s` (a cross-run
550    // input). `preds[r]` = the set of producer runs; `succs[s]` = the runs
551    // that read `s`; `indeg[r]` = count of not-yet-emitted producers.
552    let mut preds: Vec<HashSet<usize>> = vec![HashSet::new(); n];
553    let mut succs: Vec<Vec<usize>> = vec![Vec::new(); n];
554    for (ri, r) in runs.iter().enumerate() {
555        for &m in &r.members {
556            for &inp in &graph.node(m).inputs {
557                if let Some(&pi) = run_of.get(&inp)
558                    && pi != ri
559                    && preds[ri].insert(pi)
560                {
561                    succs[pi].push(ri);
562                }
563            }
564        }
565    }
566    let mut indeg: Vec<usize> = preds.iter().map(|p| p.len()).collect();
567
568    // Per-run COMPUTE size: node count for a real compute chunk, 0 for a
569    // pure cross-device TRANSFER (`Op::Copy`/`Op::Move` — always its own
570    // single-member run, the residency-seam boundary). A transfer is the
571    // run that FORCES a cross-device wait at dispatch (the executor drains
572    // the source / eager-submits + fences the in-flight batch when it hits a
573    // cross-device copy); it does no compute, so it weighs 0 and a drain is
574    // never the heaviest-downstream pick — it sorts after the compute chunks.
575    let compute_size = |ri: usize| -> u64 {
576        if matches!(
577            graph.node(runs[ri].entry).op,
578            Op::Copy { .. } | Op::Move { .. }
579        ) {
580            0
581        } else {
582            runs[ri].members.len() as u64
583        }
584    };
585
586    // `down_weight[r]` = the LARGEST compute chunk reachable from `r`
587    // downstream, including `r` itself — the critical-path priority. A
588    // memoized DFS over `succs` (the run-DAG is acyclic, so the recursion
589    // terminates; memoization makes it linear). Computed with an explicit
590    // stack (never-panic on deep graphs — no recursion).
591    let mut down_weight: Vec<u64> = vec![u64::MAX; n]; // MAX = not yet computed
592    for start in 0..n {
593        if down_weight[start] != u64::MAX {
594            continue;
595        }
596        // Post-order: push (node, children_done?).
597        let mut stack: Vec<(usize, bool)> = vec![(start, false)];
598        while let Some((r, done)) = stack.pop() {
599            if done {
600                let mut w = compute_size(r);
601                for &s in &succs[r] {
602                    w = w.max(down_weight[s]);
603                }
604                down_weight[r] = w;
605                continue;
606            }
607            if down_weight[r] != u64::MAX {
608                continue;
609            }
610            // Tentatively mark in-progress so a (DAG-impossible) revisit
611            // doesn't loop; finalized on the `done` pass.
612            stack.push((r, true));
613            for &s in &succs[r] {
614                if down_weight[s] == u64::MAX {
615                    stack.push((s, false));
616                }
617            }
618        }
619    }
620
621    // Kahn pump with critical-path priority. `ready` is every run whose
622    // producers are all emitted. Among the ready runs we pick the one whose
623    // priority key is smallest:
624    //
625    //   ( -down_weight ,  not_different_device ,  original_index )
626    //
627    // i.e. (1) the LARGEST downstream compute chunk first — emit the path to
628    // the heaviest device chunk earliest so the auto-submitting device
629    // (CUDA) is enqueued + running before the deferred device's (Vulkan)
630    // host-blocking drain, which is what the eager-submit mechanism turns
631    // into overlap (and a drain weighs 0, so it sorts after both chunks);
632    // (2) among equal-weight ready runs, a device DIFFERENT from the last
633    // emitted (interleave); (3) the SMALLEST original index (determinism +
634    // topo stability — the degenerate single-device case already returned
635    // identity above). `ready` is a plain Vec scanned each step; run counts
636    // are tiny (a run is a whole straight-line chunk; the fewness gate keeps
637    // decision points sparse), so the O(ready) scan is cheap. This is a
638    // HINT: every emitted order is a valid topological order regardless of
639    // the priority (Kahn guarantees producers-first), so the result is
640    // byte-identical — only the dispatch timing changes.
641    let mut emitted: Vec<bool> = vec![false; n];
642    let mut ready: Vec<usize> = (0..n).filter(|&i| indeg[i] == 0).collect();
643    let mut order: Vec<usize> = Vec::with_capacity(n);
644    let mut last_device: Option<Option<BackendId>> = None;
645
646    while !ready.is_empty() {
647        // Priority key (smaller wins): heaviest downstream compute first,
648        // then a different device than last (interleave), then smallest idx.
649        let key_of = |ri: usize| -> (std::cmp::Reverse<u64>, bool, usize) {
650            let same_device = last_device.is_some_and(|ld| runs[ri].device == ld);
651            (std::cmp::Reverse(down_weight[ri]), same_device, ri)
652        };
653        let mut best_pos = 0usize;
654        let mut best_key = key_of(ready[0]);
655        for (pos, &ri) in ready.iter().enumerate().skip(1) {
656            let key = key_of(ri);
657            if key < best_key {
658                best_key = key;
659                best_pos = pos;
660            }
661        }
662        let ri = ready.swap_remove(best_pos);
663        emitted[ri] = true;
664        order.push(ri);
665        last_device = Some(runs[ri].device);
666
667        for &si in &succs[ri] {
668            if emitted[si] {
669                continue;
670            }
671            indeg[si] -= 1;
672            if indeg[si] == 0 {
673                ready.push(si);
674            }
675        }
676    }
677
678    // A cyclic run graph is impossible (the node graph is a DAG and runs
679    // partition it), so the pump emits every run. Defend never-panic: if
680    // somehow a run was not emitted (a structural surprise), fall back to
681    // the input order for the remainder so the lowering stays total rather
682    // than dropping work.
683    if order.len() != n {
684        #[expect(
685            clippy::needless_range_loop,
686            reason = "the bound `n` is the node count checked above (order.len() != n), not `emitted.len()`; enumerate could change the iteration count -- a wrong-number bug"
687        )]
688        for i in 0..n {
689            if !emitted[i] {
690                order.push(i);
691            }
692        }
693    }
694    order
695}
696
697/// **Streaming route-aware lowering** (Step E Phase C, PR C1) — the
698/// incremental form of [`lower_picked_route`]. Instead of taking a
699/// fully-resolved [`PickedRoute`] up front, it walks the run partition in
700/// topological order and **resolves each branch lazily, the first time the
701/// walk reaches one of its arm-entry runs**, by calling `resolve(branch)`.
702/// It emits only the chosen arm's runs (skipping the non-chosen arms),
703/// exactly like [`lower_picked_route`] — but the arm decision happens *at
704/// the frontier*, not before the walk.
705///
706/// This is the substrate that lets the runtime picker re-pick a branch's
707/// arm **by the live device load at the moment the frontier reaches it**
708/// (C2): the compiler thread drives this walk while the executor drains
709/// the previous runs, so `resolve` sees the load current to that decision
710/// point. `resolve` returns the chosen arm index for a branch (a stale /
711/// out-of-range index is clamped to arm 0 by the same skip logic, so it
712/// never panics — never-panic on a production path).
713///
714/// **Byte-identity with the one-shot lowering (the C1 gate).** Because
715/// `resolve` is the same per-branch decision the eager picker makes (the
716/// production VRAM-pressure chain reads only free-memory state, not walk
717/// progress), the route this walk accumulates equals
718/// `fuel_graph::PickedRoute` the eager `pick_route` produces, and the
719/// emitted order therefore equals `lower_picked_route(graph, roots,
720/// pick_route(..))` on every input. C1 changes *when* a branch resolves,
721/// never *which* arm — and a branchless graph never enters this walk at
722/// all (the caller's branchless fast path returns before streaming).
723/// `resolve` is invoked **at most once per branch** (memoized in the
724/// accumulated route), so a coupled downstream branch that reads upstream
725/// picks still resolves upstream-first (arm-entries of an upstream branch
726/// precede a downstream branch's in run-topo order).
727pub fn lower_picked_route_streaming<F>(
728    graph: &Graph,
729    roots: &[NodeId],
730    mut resolve: F,
731) -> Vec<NodeId>
732where
733    F: FnMut(NodeId) -> usize,
734{
735    let mut order = Vec::new();
736    walk_picked_route_streaming(graph, roots, &mut resolve, |members| {
737        order.extend_from_slice(members);
738    });
739    order
740}
741
742/// The callback core of the streaming lowering — the form the compiler
743/// thread drives so resolution and emission genuinely interleave (the
744/// thread compiles + dispatches each chosen run as the walk reaches it,
745/// rather than first materializing a flat order). `resolve(branch)` yields
746/// the chosen arm index at the frontier; `emit(&[NodeId])` receives each
747/// kept run's members in topological order. [`lower_picked_route_streaming`]
748/// is the `Vec`-collecting wrapper over this (used by the byte-identity
749/// test); the executor's compiler thread passes an `emit` that
750/// `compile_one`'s + sends each node.
751///
752/// Walks the run partition once, in topological order of run entries
753/// (identical to [`extract_runs_multi`]); at the first run that opens an
754/// arm of a not-yet-resolved branch it calls `resolve`, folds that
755/// branch's non-chosen arms into the skip set, then emits the run iff its
756/// entry is not skipped. See [`lower_picked_route_streaming`] for the
757/// byte-identity argument.
758pub fn walk_picked_route_streaming<R, E>(
759    graph: &Graph,
760    roots: &[NodeId],
761    mut resolve: R,
762    mut emit: E,
763) where
764    R: FnMut(NodeId) -> usize,
765    E: FnMut(&[NodeId]),
766{
767    let runs = extract_runs_multi(graph, roots);
768
769    // Per-branch lowering metadata, precomputed once from the reachable
770    // structure (cones + the diverge/shared prefix + each arm's entry
771    // node). The *decision* — which arm — is still deferred to `resolve`
772    // at the frontier; only the structure is precomputed.
773    let branch_meta = branch_arm_meta(graph, roots);
774
775    // arm-entry node -> the branches it opens an arm of. A node can be the
776    // arm-entry of more than one branch only in degenerate shared-prefix
777    // overlaps; resolving every such branch on first encounter is correct
778    // (each resolves independently) and keeps the walk single-pass.
779    let mut entry_to_branches: HashMap<NodeId, Vec<NodeId>> = HashMap::new();
780    for meta in &branch_meta {
781        for &(entry, _arm) in &meta.arm_entries {
782            entry_to_branches
783                .entry(entry)
784                .or_default()
785                .push(meta.branch);
786        }
787    }
788    let meta_of: HashMap<NodeId, &BranchArmMeta> =
789        branch_meta.iter().map(|m| (m.branch, m)).collect();
790
791    let mut skip: HashSet<NodeId> = HashSet::new();
792    let mut resolved: HashSet<NodeId> = HashSet::new();
793
794    // PR-C3 composition: the device-alternating reorder (auto-overlap) runs
795    // over the kept runs, but it must NOT reorder across an unresolved
796    // branch decision (a downstream run's keep/skip depends on the arm
797    // picked upstream, and the picker reads live load AT the frontier, so
798    // moving a run past an unresolved branch would change what is even
799    // resolvable). So we reorder within **branch-free segments**: buffer
800    // kept runs; when the frontier reaches a run that resolves a NEW branch,
801    // FLUSH the buffered segment (device-alternation-reordered) BEFORE
802    // resolving, then start a fresh segment. The flush is the only place a
803    // reorder happens, and it never crosses a `resolve` call — so arm
804    // resolution stays strictly upstream-first and the reorder is confined
805    // to runs already committed to their devices. Branchless graphs (the
806    // auto-overlap benchmark + every existing multi-device live suite) have
807    // exactly one segment = the whole run list, so this is identical to the
808    // one-shot `lower_picked_route` reorder.
809    let mut segment: Vec<&Run> = Vec::new();
810    let flush = |segment: &mut Vec<&Run>, emit: &mut E| {
811        if segment.is_empty() {
812            return;
813        }
814        // `device_alternating_order` wants `&[Run]`; collect the segment's
815        // runs (cheap — members are NodeId Vecs, but a segment is small).
816        let owned: Vec<Run> = segment.iter().map(|r| (*r).clone()).collect();
817        let perm = device_alternating_order(graph, &owned);
818        for &ri in &perm {
819            emit(lower_run(&owned[ri]));
820        }
821        segment.clear();
822    };
823
824    for run in &runs {
825        // Frontier reached this run's entry. If it opens an arm of one or
826        // more not-yet-resolved branches, the route past this point is about
827        // to be decided — flush the branch-free segment accumulated so far
828        // (reordered) BEFORE resolving, so no reorder crosses the decision.
829        if let Some(branches) = entry_to_branches.get(&run.entry) {
830            let opens_new = branches.iter().any(|b| !resolved.contains(b));
831            if opens_new {
832                flush(&mut segment, &mut emit);
833            }
834            for &branch in branches {
835                if !resolved.insert(branch) {
836                    continue;
837                }
838                let meta = meta_of[&branch];
839                // Resolve NOW (reading live load via `resolve` once C2 lands)
840                // and fold this branch's non-chosen arms into the skip set —
841                // before we decide whether to keep this run.
842                let chosen = resolve(branch);
843                add_non_chosen_skip(meta, chosen, &mut skip);
844            }
845        }
846        // A run is a single contiguous arm/region (extract never spans a
847        // branch boundary), so it is either wholly inside a non-chosen arm
848        // or wholly outside. Skip iff its entry is a skipped node; otherwise
849        // buffer it into the current branch-free segment (emitted at the
850        // next flush, device-alternation-reordered).
851        if skip.contains(&run.entry) {
852            continue;
853        }
854        segment.push(run);
855    }
856    // Flush the final branch-free segment (the post-region + any tail).
857    flush(&mut segment, &mut emit);
858}
859
860/// Per-branch structural metadata for the streaming lowering: the branch
861/// node, its arms' `(arm-entry node, arm index)` pairs, the per-arm
862/// backward cones, and the shared diverge prefix. The arm *decision* is
863/// not here — only the structure the skip-set computation needs once an
864/// arm is chosen.
865struct BranchArmMeta {
866    branch: NodeId,
867    /// `(arm-entry node, arm index)` for each arm — the run entry that
868    /// opens that arm. Used to detect, while walking runs, which branch a
869    /// run opens an arm of.
870    arm_entries: Vec<(NodeId, usize)>,
871    /// Backward cone of each arm exit (`inputs[i]`), bounded to reachable.
872    cones: Vec<HashSet<NodeId>>,
873    /// Shared diverge prefix = intersection of every arm's cone.
874    shared: HashSet<NodeId>,
875}
876
877/// Precompute [`BranchArmMeta`] for every reachable ≥2-arm [`Op::Branch`].
878/// Mirrors `non_chosen_arm_nodes`'s cone/shared-prefix derivation and
879/// `compute_arm_entries`'s arm-entry recovery (the op carries only
880/// `reconverge_at`, so the diverge prefix is recovered as the cone
881/// intersection), so the streaming skip set is byte-identical to the
882/// one-shot `non_chosen_arm_nodes` for the same picks.
883fn branch_arm_meta(graph: &Graph, roots: &[NodeId]) -> Vec<BranchArmMeta> {
884    let eff_roots = effective_roots(graph, roots);
885    let order = topo_order_multi(graph, &eff_roots);
886    let reachable: HashSet<NodeId> = order.iter().copied().collect();
887
888    let mut metas = Vec::new();
889    for &id in &order {
890        let Op::Branch { .. } = graph.node(id).op else {
891            continue;
892        };
893        let arm_exits = graph.node(id).inputs.clone();
894        if arm_exits.len() < 2 {
895            continue;
896        }
897        let cones: Vec<HashSet<NodeId>> = arm_exits
898            .iter()
899            .map(|&e| backward_cone(graph, e, &reachable))
900            .collect();
901        let mut shared: HashSet<NodeId> = cones[0].clone();
902        for c in &cones[1..] {
903            shared = shared.intersection(c).copied().collect();
904        }
905        // Each arm's entry: the interior node (cone minus shared) whose
906        // sole/any predecessor lies in the shared prefix — where the arm
907        // departs from the diverge region (mirrors `compute_arm_entries`).
908        let mut arm_entries: Vec<(NodeId, usize)> = Vec::new();
909        for (arm_idx, cone) in cones.iter().enumerate() {
910            for &n in cone {
911                if shared.contains(&n) {
912                    continue;
913                }
914                let departs = graph.node(n).inputs.iter().any(|p| shared.contains(p));
915                if departs {
916                    arm_entries.push((n, arm_idx));
917                }
918            }
919        }
920        metas.push(BranchArmMeta {
921            branch: id,
922            arm_entries,
923            cones,
924            shared,
925        });
926    }
927    metas
928}
929
930/// Fold one branch's non-chosen arms into `skip`, given the chosen arm
931/// index. Identical to the per-branch body of [`non_chosen_arm_nodes`]: a
932/// node is skipped when it lies in a non-chosen arm's cone but is neither
933/// shared nor part of the chosen arm's cone. A stale / out-of-range
934/// `chosen` clamps to arm 0 (never panic).
935fn add_non_chosen_skip(meta: &BranchArmMeta, chosen: usize, skip: &mut HashSet<NodeId>) {
936    let chosen = if chosen < meta.cones.len() { chosen } else { 0 };
937    let chosen_cone = &meta.cones[chosen];
938    for (i, cone) in meta.cones.iter().enumerate() {
939        if i == chosen {
940            continue;
941        }
942        for &n in cone {
943            if !meta.shared.contains(&n) && !chosen_cone.contains(&n) {
944                skip.insert(n);
945            }
946        }
947    }
948}
949
950/// The set of nodes that belong to a **non-chosen arm** of some reachable
951/// [`Op::Branch`] — the nodes the route-aware lowering skips. The arm-0
952/// generalization: when `picked` is empty (or a branch is absent from
953/// it), the chosen arm defaults to arm 0, so this reduces to the former
954/// `non_arm0_arm_nodes`.
955///
956/// For each reachable branch, the chosen arm is `inputs[chosen]`
957/// (`chosen = picked[branch]`, default 0) and the non-chosen arms are
958/// every other input. The shared prefix (the diverge region) is the
959/// intersection of every arm exit's backward cone (PR-A2's
960/// `compute_arm_entries` recovers the diverge the same way — the op
961/// carries `reconverge_at`, not the diverge). A node is skipped when it
962/// lies in a non-chosen arm's cone but is neither shared nor part of the
963/// chosen arm's cone — i.e. it is interior to a route the picker did not
964/// take. Arms are internally disjoint by the PR-A1 build-time
965/// validation, so these sets don't overlap the chosen arm.
966fn non_chosen_arm_nodes(graph: &Graph, roots: &[NodeId], picked: &PickedRoute) -> HashSet<NodeId> {
967    let eff_roots = effective_roots(graph, roots);
968    let order = topo_order_multi(graph, &eff_roots);
969    let reachable: HashSet<NodeId> = order.iter().copied().collect();
970
971    let mut skip: HashSet<NodeId> = HashSet::new();
972    for &id in &order {
973        let Op::Branch { .. } = graph.node(id).op else {
974            continue;
975        };
976        let arm_exits = &graph.node(id).inputs;
977        if arm_exits.len() < 2 {
978            continue;
979        }
980        // The chosen arm index — default arm 0 (the winner) for any
981        // branch the picker did not resolve. Clamp to a valid arm so a
982        // stale/out-of-range pick degrades to arm 0 rather than panicking
983        // (never panic on a production path).
984        let chosen = picked
985            .get(&id)
986            .copied()
987            .filter(|&c| c < arm_exits.len())
988            .unwrap_or(0);
989
990        let cones: Vec<HashSet<NodeId>> = arm_exits
991            .iter()
992            .map(|&e| backward_cone(graph, e, &reachable))
993            .collect();
994        // Shared prefix = intersection of every arm's cone (the diverge
995        // region the arms depart from).
996        let mut shared: HashSet<NodeId> = cones[0].clone();
997        for c in &cones[1..] {
998            shared = shared.intersection(c).copied().collect();
999        }
1000        // Skip every non-chosen arm's interior: its cone minus the shared
1001        // prefix minus the chosen arm's cone.
1002        let chosen_cone = &cones[chosen];
1003        for (i, cone) in cones.iter().enumerate() {
1004            if i == chosen {
1005                continue;
1006            }
1007            for &n in cone {
1008                if !shared.contains(&n) && !chosen_cone.contains(&n) {
1009                    skip.insert(n);
1010                }
1011            }
1012        }
1013    }
1014    skip
1015}
1016
1017/// Branch density: the share of reachable nodes that are decision
1018/// points ([`Op::Branch`] nodes), over `root`.
1019///
1020/// The **fewness gate** — deliberate-fork branching must stay sparse
1021/// (the granularity crux locked before Phase B). A real decode graph
1022/// branches only at "layer" boundaries and sits far below the threshold;
1023/// a synthetic per-op-branch graph blows past it.
1024pub fn branch_density(graph: &Graph, root: NodeId) -> f32 {
1025    branch_density_multi(graph, &[root])
1026}
1027
1028/// Multi-root variant of [`branch_density`].
1029pub fn branch_density_multi(graph: &Graph, roots: &[NodeId]) -> f32 {
1030    let eff_roots = effective_roots(graph, roots);
1031    let order = topo_order_multi(graph, &eff_roots);
1032    if order.is_empty() {
1033        return 0.0;
1034    }
1035    let branches = order
1036        .iter()
1037        .filter(|&&id| matches!(graph.node(id).op, Op::Branch { .. }))
1038        .count();
1039    branches as f32 / order.len() as f32
1040}
1041
1042/// The fewness-gate threshold: branch points must be at most ~5% of
1043/// reachable nodes. Deliberate-fork branching stays this sparse;
1044/// per-op branching does not.
1045pub const FEWNESS_THRESHOLD: f32 = 0.05;
1046
1047/// Whether the graph passes the fewness gate over `root` (branch
1048/// density strictly below [`FEWNESS_THRESHOLD`]).
1049pub fn passes_fewness_gate(graph: &Graph, root: NodeId) -> bool {
1050    branch_density(graph, root) < FEWNESS_THRESHOLD
1051}
1052
1053#[cfg(test)]
1054mod tests {
1055    use super::*;
1056    use crate::Node;
1057    use fuel_ir::{DType, Shape};
1058
1059    fn f32_node(g: &mut Graph, op: Op, inputs: Vec<NodeId>) -> NodeId {
1060        g.push(Node {
1061            op,
1062            inputs,
1063            shape: Shape::from_dims(&[2]),
1064            dtype: DType::F32,
1065        })
1066    }
1067
1068    /// Hand-build the same 2-arm diamond the branch-builder tests use,
1069    /// then finalize it into a real `Op::Branch`. Returns the graph and
1070    /// `(diverge, arm0, arm1, reconverge, branch)`.
1071    ///
1072    /// Topology: `diverge -> {arm0, arm1}`; `reconverge` reads arm0 (the
1073    /// runnability invariant); a `Branch` node merges {arm0, arm1} with
1074    /// `reconverge_at = reconverge`. `post` reads `reconverge` so the
1075    /// post-merge region is non-empty.
1076    fn diamond_with_branch() -> (Graph, NodeId, NodeId, NodeId, NodeId, NodeId, NodeId) {
1077        let mut g = Graph::new();
1078        let pre = f32_node(&mut g, Op::Const, vec![]);
1079        let diverge = f32_node(&mut g, Op::Relu, vec![pre]);
1080        let arm0 = f32_node(&mut g, Op::Silu, vec![diverge]);
1081        let arm1 = f32_node(&mut g, Op::GeluTanh, vec![diverge]);
1082        // reconverge reads arm0 (arm-0 runnability).
1083        let reconverge = f32_node(&mut g, Op::Relu, vec![arm0]);
1084        let mut b = g.open_branch(diverge);
1085        b.add_arm(arm0);
1086        b.add_arm(arm1);
1087        let branch = b
1088            .finalize_branches(&mut g, reconverge)
1089            .expect("well-formed 2-arm branch finalizes")
1090            .expect("2 arms survive");
1091        // A post node that reads the merge, so there is a real post-run.
1092        let post = f32_node(&mut g, Op::Tanh, vec![reconverge]);
1093        (g, diverge, arm0, arm1, reconverge, branch, post)
1094    }
1095
1096    /// (a) A straight-line graph with no branches extracts to exactly
1097    /// ONE run covering all reachable nodes, single-entry / single-exit.
1098    #[test]
1099    fn straight_line_extracts_to_one_run() {
1100        let mut g = Graph::new();
1101        let a = f32_node(&mut g, Op::Const, vec![]);
1102        let b = f32_node(&mut g, Op::Relu, vec![a]);
1103        let c = f32_node(&mut g, Op::Silu, vec![b]);
1104        let d = f32_node(&mut g, Op::Tanh, vec![c]);
1105
1106        let runs = extract_runs(&g, d);
1107        assert_eq!(runs.len(), 1, "a straight-line graph is exactly one run");
1108        let run = &runs[0];
1109        assert_eq!(run.entry, a, "the run enters at the source");
1110        assert_eq!(run.exit, d, "the run exits at the root");
1111        assert_eq!(
1112            run.members,
1113            vec![a, b, c, d],
1114            "the single run covers every reachable node in topo order",
1115        );
1116        // single-device: no explicit backend anywhere -> None.
1117        assert_eq!(run.device, None);
1118        // lower_run reproduces the ordered members.
1119        assert_eq!(lower_run(run), &[a, b, c, d]);
1120    }
1121
1122    /// (b) A finalized 2-arm branch extracts to exactly four runs —
1123    /// {pre-run, arm0-run, arm1-run, post-run} — each single-entry /
1124    /// single-exit.
1125    #[test]
1126    fn two_arm_branch_extracts_to_four_runs() {
1127        let (g, diverge, arm0, arm1, reconverge, _branch, post) = diamond_with_branch();
1128
1129        let runs = extract_runs(&g, post);
1130
1131        // Each run is single-entry/single-exit by construction.
1132        for r in &runs {
1133            assert_eq!(r.entry, r.members[0]);
1134            assert_eq!(r.exit, *r.members.last().unwrap());
1135            assert!(!r.members.is_empty());
1136        }
1137
1138        // Locate the run that contains each landmark node.
1139        let run_of = |node: NodeId| -> usize {
1140            runs.iter()
1141                .position(|r| r.members.contains(&node))
1142                .unwrap_or_else(|| panic!("no run contains Node#{}", node.0))
1143        };
1144        let pre_run = run_of(diverge);
1145        let arm0_run = run_of(arm0);
1146        let arm1_run = run_of(arm1);
1147        let post_run = run_of(reconverge);
1148
1149        // Four distinct runs: pre, arm0, arm1, post.
1150        let distinct: HashSet<usize> = [pre_run, arm0_run, arm1_run, post_run]
1151            .into_iter()
1152            .collect();
1153        assert_eq!(
1154            distinct.len(),
1155            4,
1156            "a 2-arm branch yields exactly {{pre, arm0, arm1, post}} = 4 runs; got runs={runs:?}",
1157        );
1158        assert_eq!(runs.len(), 4, "no run beyond the four; got {runs:?}");
1159
1160        // arm0 and arm1 are in *different* runs (a run never spans a
1161        // branch boundary).
1162        assert_ne!(arm0_run, arm1_run, "the two arms must be separate runs");
1163        // diverge is upstream of both arms and in neither arm's run.
1164        assert_ne!(pre_run, arm0_run);
1165        assert_ne!(pre_run, arm1_run);
1166        // the post region (reconverge + Branch + post) is its own run.
1167        assert!(runs[post_run].members.contains(&post));
1168    }
1169
1170    /// PR-A4 arm-0 single-route lowering: [`lower_runs_arm0`] follows
1171    /// arm 0 through the branch (pre, arm0, post) and SKIPS arm 1's run.
1172    /// arm 1's interior node never appears; arm 0's does. On a branchless
1173    /// graph it is identical to concatenating [`lower_run`] over the runs.
1174    #[test]
1175    fn lower_runs_arm0_follows_arm0_and_skips_other_arms() {
1176        let (g, diverge, arm0, arm1, reconverge, _branch, post) = diamond_with_branch();
1177
1178        let order = lower_runs_arm0(&g, &[post]);
1179
1180        // arm 1's interior is skipped; arm 0 + pre + post execute.
1181        assert!(
1182            !order.contains(&arm1),
1183            "arm-0 lowering skips arm 1's run; order={order:?} arm1={arm1:?}",
1184        );
1185        assert!(order.contains(&diverge), "the pre-run (diverge) executes");
1186        assert!(order.contains(&arm0), "arm 0 executes");
1187        assert!(order.contains(&reconverge), "the reconverge executes");
1188        assert!(order.contains(&post), "the post-run executes");
1189        // The phi/merge Branch node is structural — never an executable
1190        // member.
1191        for &id in &order {
1192            assert!(
1193                !matches!(g.node(id).op, Op::Branch { .. }),
1194                "the Branch node is never an executable member",
1195            );
1196        }
1197
1198        // Branchless: identical to concatenating lower_run over runs.
1199        let mut g2 = Graph::new();
1200        let a = f32_node(&mut g2, Op::Const, vec![]);
1201        let b = f32_node(&mut g2, Op::Relu, vec![a]);
1202        let c = f32_node(&mut g2, Op::Silu, vec![b]);
1203        let d = f32_node(&mut g2, Op::Tanh, vec![c]);
1204        let flat: Vec<NodeId> = extract_runs(&g2, d)
1205            .iter()
1206            .flat_map(|r| lower_run(r).to_vec())
1207            .collect();
1208        assert_eq!(
1209            lower_runs_arm0(&g2, &[d]),
1210            flat,
1211            "on a branchless graph the arm-0 lowering equals the flat concatenation",
1212        );
1213    }
1214
1215    /// PR-C1 (e) route-aware lowering: [`lower_picked_route`] follows the
1216    /// **chosen** arm and skips the non-chosen arms' runs, and equals
1217    /// [`lower_runs_arm0`] when every pick is arm 0 (the empty route).
1218    ///
1219    /// On the 2-arm diamond:
1220    /// - empty route (== arm 0 everywhere): arm 0 in, arm 1 out, and
1221    ///   byte-identical to `lower_runs_arm0`.
1222    /// - pick arm 1 for the branch: arm 1 in, arm 0's interior out — the
1223    ///   mirror image, proving the lowering follows the picked arm rather
1224    ///   than hard-coding arm 0.
1225    #[test]
1226    fn lower_picked_route_follows_chosen_arm_and_skips_others() {
1227        let (g, _diverge, arm0, arm1, _reconverge, branch, post) = diamond_with_branch();
1228
1229        // (1) Empty route == arm-0 everywhere == lower_runs_arm0.
1230        let empty = PickedRoute::new();
1231        let route_order = lower_picked_route(&g, &[post], &empty);
1232        let arm0_order = lower_runs_arm0(&g, &[post]);
1233        assert_eq!(
1234            route_order, arm0_order,
1235            "the empty route lowers byte-identically to lower_runs_arm0",
1236        );
1237        assert!(route_order.contains(&arm0), "empty route follows arm 0");
1238        assert!(
1239            !route_order.contains(&arm1),
1240            "empty route skips arm 1's run; order={route_order:?}",
1241        );
1242
1243        // (2) Pick arm 1 for the branch — now arm 1 is followed, arm 0's
1244        // interior is skipped.
1245        let mut picked = PickedRoute::new();
1246        picked.insert(branch, 1);
1247        let order = lower_picked_route(&g, &[post], &picked);
1248        assert!(
1249            order.contains(&arm1),
1250            "picking arm 1 follows arm 1's run; order={order:?}",
1251        );
1252        assert!(
1253            !order.contains(&arm0),
1254            "picking arm 1 skips arm 0's interior run; order={order:?}",
1255        );
1256        // The Branch node is never an executable member regardless of pick.
1257        for &id in &order {
1258            assert!(
1259                !matches!(g.node(id).op, Op::Branch { .. }),
1260                "the Branch node is never an executable member",
1261            );
1262        }
1263
1264        // (3) A branch absent from the route still defaults to arm 0.
1265        let absent = PickedRoute::new();
1266        let absent_order = lower_picked_route(&g, &[post], &absent);
1267        assert_eq!(
1268            absent_order, arm0_order,
1269            "a branch the picker did not resolve defaults to arm 0",
1270        );
1271
1272        // (4) An out-of-range pick degrades to arm 0 (never panic).
1273        let mut bad = PickedRoute::new();
1274        bad.insert(branch, 99);
1275        let bad_order = lower_picked_route(&g, &[post], &bad);
1276        assert_eq!(
1277            bad_order, arm0_order,
1278            "an out-of-range arm index clamps to arm 0 rather than panicking",
1279        );
1280    }
1281
1282    /// **Optimize-time variant bake (collapse) — the graph-side primitive.**
1283    /// `Graph::collapse_variant_branch(branch, winner)` rewires the decision
1284    /// point so the chosen arm is the realized route WITHOUT any runtime pick
1285    /// (kernel-variant choice baked at optimize time):
1286    ///
1287    /// - collapse to arm 1 ⇒ `reconverge` reads arm 1, the branch is a single
1288    ///   inert arm `[arm1]`, and `lower_runs_arm0` (the empty-route lowering)
1289    ///   now follows arm 1 and drops arm 0's interior — the mirror image of the
1290    ///   default, achieved structurally rather than via a `PickedRoute`;
1291    /// - collapse to arm 0 ⇒ the merge is unchanged, the branch reduces to
1292    ///   `[arm0]`, and the lowering is byte-identical to today (arm 1 pruned).
1293    #[test]
1294    fn collapse_variant_branch_bakes_the_winner_into_the_route() {
1295        // --- (a) collapse to the variant arm (arm 1) ---
1296        let (mut g, _diverge, arm0, arm1, reconverge, branch, post) = diamond_with_branch();
1297        // Before: default lowering realizes arm 0 (the finding — a same-device
1298        // variant arm is never selected).
1299        let before = lower_runs_arm0(&g, &[post]);
1300        assert!(
1301            before.contains(&arm0) && !before.contains(&arm1),
1302            "before the bake the default route is arm 0"
1303        );
1304
1305        g.collapse_variant_branch(branch, 1)
1306            .expect("collapse to arm 1");
1307
1308        // The merge now reads the winner (arm 1); the branch is a single arm.
1309        assert!(
1310            g.node(reconverge).inputs.contains(&arm1),
1311            "reconverge rewired to read the baked winner (arm 1)"
1312        );
1313        assert!(
1314            !g.node(reconverge).inputs.contains(&arm0),
1315            "reconverge no longer reads the pruned arm 0"
1316        );
1317        assert_eq!(
1318            g.node(branch).inputs,
1319            vec![arm1],
1320            "the branch collapsed to the single winning arm"
1321        );
1322
1323        // The empty-route lowering now follows the baked winner (arm 1) and
1324        // drops arm 0's interior — with NO PickedRoute.
1325        let after = lower_runs_arm0(&g, &[post]);
1326        assert!(
1327            after.contains(&arm1),
1328            "after the bake the default route follows the winner (arm 1); order={after:?}"
1329        );
1330        assert!(
1331            !after.contains(&arm0),
1332            "after the bake arm 0's interior is pruned; order={after:?}"
1333        );
1334
1335        // --- (b) collapse to arm 0 is byte-identical to today (arm 1 pruned) ---
1336        let (mut g2, _d2, a0_2, a1_2, recon2, branch2, post2) = diamond_with_branch();
1337        let recon2_inputs_before = g2.node(recon2).inputs.clone();
1338        g2.collapse_variant_branch(branch2, 0)
1339            .expect("collapse to arm 0");
1340        assert_eq!(
1341            g2.node(recon2).inputs,
1342            recon2_inputs_before,
1343            "arm-0 collapse leaves the merge unchanged"
1344        );
1345        assert_eq!(
1346            g2.node(branch2).inputs,
1347            vec![a0_2],
1348            "arm-0 collapse reduces the branch to [arm0] (variant arm pruned)"
1349        );
1350        let order2 = lower_runs_arm0(&g2, &[post2]);
1351        assert!(
1352            order2.contains(&a0_2) && !order2.contains(&a1_2),
1353            "arm-0 collapse realizes arm 0 (byte-identical to today)"
1354        );
1355
1356        // --- (c) never-panic guards: bad node / out-of-range winner / non-branch ---
1357        let (mut g3, _d3, _a0_3, _a1_3, _r3, branch3, post3) = diamond_with_branch();
1358        assert!(
1359            g3.collapse_variant_branch(NodeId(9999), 0).is_err(),
1360            "out-of-bounds branch node ⇒ Err, not panic"
1361        );
1362        assert!(
1363            g3.collapse_variant_branch(branch3, 7).is_err(),
1364            "out-of-range winner_arm ⇒ Err, not panic"
1365        );
1366        assert!(
1367            g3.collapse_variant_branch(post3, 0).is_err(),
1368            "non-Op::Branch node ⇒ Err, not panic"
1369        );
1370    }
1371
1372    /// Build two coupled diamonds back-to-back (diamond-2 diverges off
1373    /// diamond-1's reconverge) so the streaming-walk equality test exercises
1374    /// upstream-first lazy resolution over more than one branch. Returns the
1375    /// graph + `(branch1, branch2, post, arm1_of_b1, arm1_of_b2)`.
1376    fn two_coupled_diamonds() -> (Graph, NodeId, NodeId, NodeId, NodeId, NodeId) {
1377        let mut g = Graph::new();
1378        let pre = f32_node(&mut g, Op::Const, vec![]);
1379        // diamond 1
1380        let div1 = f32_node(&mut g, Op::Relu, vec![pre]);
1381        let a0_1 = f32_node(&mut g, Op::Silu, vec![div1]);
1382        let a1_1 = f32_node(&mut g, Op::GeluTanh, vec![div1]);
1383        let recon1 = f32_node(&mut g, Op::Relu, vec![a0_1]);
1384        let mut b1 = g.open_branch(div1);
1385        b1.add_arm(a0_1);
1386        b1.add_arm(a1_1);
1387        let branch1 = b1
1388            .finalize_branches(&mut g, recon1)
1389            .expect("branch1 valid")
1390            .expect("2 arms");
1391        // diamond 2 (downstream of diamond 1's merge)
1392        let div2 = f32_node(&mut g, Op::Tanh, vec![recon1]);
1393        let a0_2 = f32_node(&mut g, Op::Silu, vec![div2]);
1394        let a1_2 = f32_node(&mut g, Op::GeluTanh, vec![div2]);
1395        let recon2 = f32_node(&mut g, Op::Relu, vec![a0_2]);
1396        let mut b2 = g.open_branch(div2);
1397        b2.add_arm(a0_2);
1398        b2.add_arm(a1_2);
1399        let branch2 = b2
1400            .finalize_branches(&mut g, recon2)
1401            .expect("branch2 valid")
1402            .expect("2 arms");
1403        let post = f32_node(&mut g, Op::Tanh, vec![recon2]);
1404        (g, branch1, branch2, post, a1_1, a1_2)
1405    }
1406
1407    /// **C1 byte-identity gate (fuel-graph layer).** The streaming lowering
1408    /// — which resolves each branch lazily, at the run-walk frontier, via a
1409    /// closure — emits the SAME `NodeId` order as the one-shot
1410    /// [`lower_picked_route`] over the route the same closure would produce,
1411    /// for every combination of arm picks across one and two branches. This
1412    /// is the structural proof that C1 changes *when* a branch resolves, not
1413    /// *which* arm it picks (or the order it emits).
1414    #[test]
1415    fn streaming_lowering_equals_one_shot_on_every_pick() {
1416        // --- single diamond: all four pick combinations ---
1417        let (g, _diverge, _arm0, _arm1, _reconverge, branch, post) = diamond_with_branch();
1418        for chosen in [0usize, 1, 99] {
1419            // One-shot: build the route then lower.
1420            let mut route = PickedRoute::new();
1421            if chosen != 0 {
1422                route.insert(branch, chosen);
1423            }
1424            let one_shot = lower_picked_route(&g, &[post], &route);
1425            // Streaming: resolve the branch lazily at its arm-entry.
1426            let streamed = lower_picked_route_streaming(&g, &[post], |b| {
1427                assert_eq!(b, branch, "only the diamond's branch is resolved");
1428                chosen
1429            });
1430            assert_eq!(
1431                streamed, one_shot,
1432                "streamed order must equal one-shot for branch pick {chosen}",
1433            );
1434        }
1435
1436        // --- two coupled diamonds: every (arm1, arm2) combination ---
1437        let (g2, branch1, branch2, post2, _a1_1, _a1_2) = two_coupled_diamonds();
1438        for c1 in [0usize, 1] {
1439            for c2 in [0usize, 1] {
1440                let mut route = PickedRoute::new();
1441                if c1 != 0 {
1442                    route.insert(branch1, c1);
1443                }
1444                if c2 != 0 {
1445                    route.insert(branch2, c2);
1446                }
1447                let one_shot = lower_picked_route(&g2, &[post2], &route);
1448
1449                // The streaming closure resolves each branch at most once
1450                // and upstream-first (branch1 before branch2). Assert that
1451                // discipline AND the resulting byte-identity.
1452                let mut seen: Vec<NodeId> = Vec::new();
1453                let streamed = lower_picked_route_streaming(&g2, &[post2], |b| {
1454                    assert!(
1455                        !seen.contains(&b),
1456                        "each branch is resolved at most once; b={b:?} seen={seen:?}",
1457                    );
1458                    seen.push(b);
1459                    if b == branch1 {
1460                        c1
1461                    } else if b == branch2 {
1462                        c2
1463                    } else {
1464                        panic!("unexpected branch {b:?}")
1465                    }
1466                });
1467                assert_eq!(
1468                    seen,
1469                    vec![branch1, branch2],
1470                    "coupled branches resolve upstream-first at the frontier",
1471                );
1472                assert_eq!(
1473                    streamed, one_shot,
1474                    "streamed == one-shot for picks ({c1},{c2})",
1475                );
1476            }
1477        }
1478    }
1479
1480    /// (c) A `target_backend` change mid-chain (set via the existing
1481    /// accessor) starts a new run — a residency seam.
1482    #[test]
1483    fn residency_seam_starts_new_run() {
1484        let mut g = Graph::new();
1485        let a = f32_node(&mut g, Op::Const, vec![]);
1486        let b = f32_node(&mut g, Op::Relu, vec![a]);
1487        let c = f32_node(&mut g, Op::Silu, vec![b]);
1488        let d = f32_node(&mut g, Op::Tanh, vec![c]);
1489        // a,b live on CPU; c,d live on CUDA -> a seam between b and c.
1490        g.set_target_backend(a, BackendId::Cpu);
1491        g.set_target_backend(b, BackendId::Cpu);
1492        g.set_target_backend(c, BackendId::Cuda);
1493        g.set_target_backend(d, BackendId::Cuda);
1494
1495        let runs = extract_runs(&g, d);
1496        assert_eq!(
1497            runs.len(),
1498            2,
1499            "a residency change mid-chain cuts the chain into two runs; got {runs:?}",
1500        );
1501        assert_eq!(runs[0].members, vec![a, b]);
1502        assert_eq!(runs[0].device, Some(BackendId::Cpu));
1503        assert_eq!(runs[1].members, vec![c, d]);
1504        assert_eq!(runs[1].device, Some(BackendId::Cuda));
1505    }
1506
1507    /// (d) A multi-predecessor fan-in starts a fresh run: the joining
1508    /// node is the entry of a new run, distinct from either feeder run.
1509    #[test]
1510    fn fan_in_starts_new_run() {
1511        let mut g = Graph::new();
1512        // Two independent feeders that join at `sum`.
1513        let a = f32_node(&mut g, Op::Const, vec![]);
1514        let a1 = f32_node(&mut g, Op::Relu, vec![a]);
1515        let b = f32_node(&mut g, Op::Const, vec![]);
1516        let b1 = f32_node(&mut g, Op::Silu, vec![b]);
1517        let sum = f32_node(&mut g, Op::Add, vec![a1, b1]);
1518
1519        let runs = extract_runs(&g, sum);
1520        // The fan-in node `sum` must be the entry of its own run.
1521        let sum_run = runs
1522            .iter()
1523            .find(|r| r.members.contains(&sum))
1524            .expect("sum is in some run");
1525        assert_eq!(
1526            sum_run.entry, sum,
1527            "a fan-in node starts a fresh run (it is that run's entry)",
1528        );
1529        // `sum` shares a run with neither feeder chain.
1530        assert!(
1531            !sum_run.members.contains(&a1) && !sum_run.members.contains(&b1),
1532            "the fan-in run does not absorb either feeder; got {sum_run:?}",
1533        );
1534    }
1535
1536    /// (e) Fewness gate — PASS side: a graph that branches only at a
1537    /// "layer" boundary sits below the ~5% threshold.
1538    #[test]
1539    fn fewness_gate_passes_on_sparse_branching() {
1540        // A long straight-line "model" with a single deliberate branch.
1541        let mut g = Graph::new();
1542        let mut prev = f32_node(&mut g, Op::Const, vec![]);
1543        // ~40 ops of straight-line body.
1544        let mut body: Vec<NodeId> = vec![prev];
1545        for _ in 0..40 {
1546            prev = f32_node(&mut g, Op::Relu, vec![prev]);
1547            body.push(prev);
1548        }
1549        // One deliberate fork off `prev` (the layer boundary).
1550        let diverge = prev;
1551        let arm0 = f32_node(&mut g, Op::Silu, vec![diverge]);
1552        let arm1 = f32_node(&mut g, Op::GeluTanh, vec![diverge]);
1553        let reconverge = f32_node(&mut g, Op::Relu, vec![arm0]);
1554        let mut b = g.open_branch(diverge);
1555        b.add_arm(arm0);
1556        b.add_arm(arm1);
1557        b.finalize_branches(&mut g, reconverge)
1558            .expect("valid branch")
1559            .expect("2 arms survive");
1560        let root = f32_node(&mut g, Op::Tanh, vec![reconverge]);
1561
1562        let density = branch_density(&g, root);
1563        assert!(
1564            density < FEWNESS_THRESHOLD,
1565            "sparse layer-boundary branching must pass the fewness gate; density={density}",
1566        );
1567        assert!(passes_fewness_gate(&g, root));
1568    }
1569
1570    /// (e) Fewness gate — FAIL side: a synthetic graph that forks at
1571    /// (nearly) every op blows past the ~5% threshold.
1572    #[test]
1573    fn fewness_gate_fails_on_per_op_branching() {
1574        // Build a chain of N diamonds back-to-back, so a large fraction
1575        // of nodes are Op::Branch decision points.
1576        let mut g = Graph::new();
1577        let mut prev = f32_node(&mut g, Op::Const, vec![]);
1578        for _ in 0..12 {
1579            let diverge = prev;
1580            let arm0 = f32_node(&mut g, Op::Silu, vec![diverge]);
1581            let arm1 = f32_node(&mut g, Op::GeluTanh, vec![diverge]);
1582            let reconverge = f32_node(&mut g, Op::Relu, vec![arm0]);
1583            let mut b = g.open_branch(diverge);
1584            b.add_arm(arm0);
1585            b.add_arm(arm1);
1586            b.finalize_branches(&mut g, reconverge)
1587                .expect("valid branch")
1588                .expect("2 arms survive");
1589            prev = reconverge;
1590        }
1591        let root = prev;
1592
1593        let density = branch_density(&g, root);
1594        assert!(
1595            density >= FEWNESS_THRESHOLD,
1596            "per-op branching must FAIL the fewness gate; density={density}",
1597        );
1598        assert!(!passes_fewness_gate(&g, root));
1599    }
1600
1601    // ===== PR-C3: device-alternating reorder (auto-overlap) =====
1602
1603    /// Build the "arbitrary graph" the auto-overlap benchmark needs WITHOUT
1604    /// the hand-constructed crutch: two INDEPENDENT same-length device
1605    /// chains (one CUDA, one Vulkan) over distinct consts, reconverging at a
1606    /// final CUDA `add`. The reconverge inputs are `[cuda_root, vulkan_root]`
1607    /// — the "wrong" order that the un-reordered topo DFS pops CUDA-last, so
1608    /// it would emit the Vulkan chain + its cross-device copy FIRST (host
1609    /// blocks on Vulkan before CUDA is enqueued → serialized). One explicit
1610    /// `Op::Copy{target:Cuda}` bridges the Vulkan root to the CUDA
1611    /// reconverge (a run boundary). Returns `(graph, root, cuda_chain_nodes,
1612    /// vk_chain_nodes, copy_node)`.
1613    fn two_device_reconverge(
1614        cuda_len: usize,
1615        vk_len: usize,
1616    ) -> (Graph, NodeId, Vec<NodeId>, Vec<NodeId>, NodeId) {
1617        let mut g = Graph::new();
1618        // Distinct consts, one per device (CPU-resident sources H2D'd to
1619        // each device — modeled here as the chain heads carrying the device
1620        // stamp; the const itself is unplaced/default).
1621        let cc = f32_node(&mut g, Op::Const, vec![]);
1622        let vc = f32_node(&mut g, Op::Const, vec![]);
1623
1624        // CUDA chain.
1625        let mut cuda_nodes = Vec::new();
1626        let mut cprev = cc;
1627        for _ in 0..cuda_len {
1628            let n = f32_node(&mut g, Op::Relu, vec![cprev]);
1629            g.set_target_backend(n, BackendId::Cuda);
1630            cuda_nodes.push(n);
1631            cprev = n;
1632        }
1633        // Vulkan chain.
1634        let mut vk_nodes = Vec::new();
1635        let mut vprev = vc;
1636        for _ in 0..vk_len {
1637            let n = f32_node(&mut g, Op::Silu, vec![vprev]);
1638            g.set_target_backend(n, BackendId::Vulkan);
1639            vk_nodes.push(n);
1640            vprev = n;
1641        }
1642        // Cross-device copy: Vulkan root -> CUDA (a residency seam = its own
1643        // run). Reconverge on CUDA reads [cuda_root, copy].
1644        let copy = g.push(Node {
1645            op: Op::Copy {
1646                target: fuel_ir::DeviceLocation::Cuda { gpu_id: 0 },
1647            },
1648            inputs: vec![vprev],
1649            shape: Shape::from_dims(&[2]),
1650            dtype: DType::F32,
1651        });
1652        g.set_target_backend(copy, BackendId::Cuda);
1653        // Reconverge inputs in the "wrong" (cuda-first) order, so the DFS
1654        // pops the LAST input (the Vulkan-fed copy) first.
1655        let out = g.push(Node {
1656            op: Op::Add,
1657            inputs: vec![cprev, copy],
1658            shape: Shape::from_dims(&[2]),
1659            dtype: DType::F32,
1660        });
1661        g.set_target_backend(out, BackendId::Cuda);
1662        (g, out, cuda_nodes, vk_nodes, copy)
1663    }
1664
1665    /// Assert `order` is a valid topological reordering of `runs`: every run
1666    /// appears after all the runs it depends on (a member reads a member of
1667    /// the producer run).
1668    fn assert_valid_topo_run_order(graph: &Graph, runs: &[Run], order: &[usize]) {
1669        let mut run_of: HashMap<NodeId, usize> = HashMap::new();
1670        for (ri, r) in runs.iter().enumerate() {
1671            for &m in &r.members {
1672                run_of.insert(m, ri);
1673            }
1674        }
1675        let mut emitted: HashSet<usize> = HashSet::new();
1676        for &ri in order {
1677            for &m in &runs[ri].members {
1678                for &inp in &graph.node(m).inputs {
1679                    if let Some(&pi) = run_of.get(&inp)
1680                        && pi != ri
1681                    {
1682                        assert!(
1683                            emitted.contains(&pi),
1684                            "run {ri} emitted before its producer run {pi}",
1685                        );
1686                    }
1687                }
1688            }
1689            emitted.insert(ri);
1690        }
1691        assert_eq!(order.len(), runs.len(), "every run emitted exactly once");
1692        let distinct: HashSet<usize> = order.iter().copied().collect();
1693        assert_eq!(distinct.len(), runs.len(), "the order is a permutation");
1694    }
1695
1696    /// C3 core: a two-device run list is reordered into a valid topological
1697    /// order that INTERLEAVES the devices — the independent CUDA and Vulkan
1698    /// producer chunks are both emitted BEFORE the cross-device copy that
1699    /// feeds the reconverge (the A4b overlap topology), regardless of the
1700    /// reconverge's input order.
1701    #[test]
1702    fn device_alternating_reorder_interleaves_two_devices() {
1703        let (g, out, cuda_nodes, vk_nodes, copy) = two_device_reconverge(4, 4);
1704        let runs = extract_runs(&g, out);
1705        let perm = device_alternating_order(&g, &runs);
1706
1707        // (1) A valid topological reordering.
1708        assert_valid_topo_run_order(&g, &runs, &perm);
1709
1710        // Map each landmark node to its position in the FLATTENED reordered
1711        // order, so we can assert the overlap-relevant ordering.
1712        let flat: Vec<NodeId> = perm
1713            .iter()
1714            .flat_map(|&ri| runs[ri].members.clone())
1715            .collect();
1716        let pos = |n: NodeId| flat.iter().position(|&x| x == n).expect("node present");
1717
1718        let cuda_root = *cuda_nodes.last().unwrap();
1719        let vk_root = *vk_nodes.last().unwrap();
1720
1721        // (2) BOTH device producer chunks are emitted before the
1722        //     cross-device copy (the host-blocking D2H). This is the
1723        //     property the un-reordered cuda-last DFS violates and that the
1724        //     A4b eager-submit mechanism turns into overlap.
1725        assert!(
1726            pos(cuda_root) < pos(copy),
1727            "the CUDA chunk must be enqueued before the cross-device copy; \
1728             flat={flat:?}",
1729        );
1730        assert!(
1731            pos(vk_root) < pos(copy),
1732            "the Vulkan chunk must be recorded before the cross-device copy; \
1733             flat={flat:?}",
1734        );
1735
1736        // (3) The devices INTERLEAVE rather than one draining fully first:
1737        //     the run-device sequence has at least one CUDA<->Vulkan
1738        //     transition before the copy. (With one chunk per device this is
1739        //     just "both chunks precede the copy", already asserted; assert
1740        //     the device sequence explicitly too.)
1741        let dev_seq: Vec<Option<BackendId>> = perm.iter().map(|&ri| runs[ri].device).collect();
1742        let switches = dev_seq.windows(2).filter(|w| w[0] != w[1]).count();
1743        assert!(
1744            switches >= 1,
1745            "a two-device run list must contain at least one device switch; \
1746             dev_seq={dev_seq:?}",
1747        );
1748    }
1749
1750    /// C3 gate: a SINGLE-device run list is the IDENTITY permutation — the
1751    /// reorder never diverges from the input order, so the lowering is
1752    /// byte-identical to today's (the primary single-device regression
1753    /// gate).
1754    #[test]
1755    fn device_alternating_reorder_single_device_is_identity() {
1756        // All-CUDA chain (one distinct device) → identity.
1757        let mut g = Graph::new();
1758        let mut prev = f32_node(&mut g, Op::Const, vec![]);
1759        for _ in 0..6 {
1760            prev = f32_node(&mut g, Op::Relu, vec![prev]);
1761            g.set_target_backend(prev, BackendId::Cuda);
1762        }
1763        let runs = extract_runs(&g, prev);
1764        let perm = device_alternating_order(&g, &runs);
1765        assert_eq!(
1766            perm,
1767            (0..runs.len()).collect::<Vec<_>>(),
1768            "a single-device run list reorders to the identity permutation",
1769        );
1770
1771        // A graph with NO explicit backend anywhere (all device == None) is
1772        // also a single bucket → identity (the branchless single-route CPU
1773        // graph — must stay byte-identical).
1774        let mut g2 = Graph::new();
1775        let a = f32_node(&mut g2, Op::Const, vec![]);
1776        let b = f32_node(&mut g2, Op::Relu, vec![a]);
1777        let c = f32_node(&mut g2, Op::Silu, vec![b]);
1778        let runs2 = extract_runs(&g2, c);
1779        assert_eq!(
1780            device_alternating_order(&g2, &runs2),
1781            (0..runs2.len()).collect::<Vec<_>>(),
1782            "an all-default (device=None) run list is identity",
1783        );
1784    }
1785
1786    /// C3 correctness: the reorder NEVER violates a data dependency, even
1787    /// when alternation "wants" to move a run earlier. A linear cross-device
1788    /// chain CPU→CUDA→Vulkan→CUDA (each a residency seam, so 4 runs, each
1789    /// depending strictly on the prior) must come out in dependency order —
1790    /// the device preference can't reorder a strict chain.
1791    #[test]
1792    fn device_alternating_reorder_respects_dependencies() {
1793        let mut g = Graph::new();
1794        let a = f32_node(&mut g, Op::Const, vec![]);
1795        g.set_target_backend(a, BackendId::Cpu);
1796        let b = f32_node(&mut g, Op::Relu, vec![a]);
1797        g.set_target_backend(b, BackendId::Cuda);
1798        let c = f32_node(&mut g, Op::Silu, vec![b]);
1799        g.set_target_backend(c, BackendId::Vulkan);
1800        let d = f32_node(&mut g, Op::Tanh, vec![c]);
1801        g.set_target_backend(d, BackendId::Cuda);
1802
1803        let runs = extract_runs(&g, d);
1804        let perm = device_alternating_order(&g, &runs);
1805        assert_valid_topo_run_order(&g, &runs, &perm);
1806        // A strict chain has exactly one valid topological order; the
1807        // alternation cannot change it.
1808        let flat: Vec<NodeId> = perm
1809            .iter()
1810            .flat_map(|&ri| runs[ri].members.clone())
1811            .collect();
1812        assert_eq!(
1813            flat,
1814            vec![a, b, c, d],
1815            "a strict cross-device chain keeps dependency order despite \
1816             device alternation",
1817        );
1818    }
1819
1820    /// C3 invariance (fuel-graph layer): the device-alternating lowering of
1821    /// a multi-device graph emits the SAME multiset of NodeIds as the
1822    /// un-reordered concatenation, in a VALID topological order — only the
1823    /// order across independent runs changes. (The byte-identical-OUTPUT
1824    /// proof is the live reorder-invariance test in `fuel-dispatch`; this is
1825    /// the structural guard that the reorder is a permutation, not a
1826    /// rewrite.)
1827    #[test]
1828    fn lower_picked_route_multidevice_is_a_valid_reordering() {
1829        let (g, out, _cuda, _vk, _copy) = two_device_reconverge(4, 4);
1830        let runs = extract_runs(&g, out);
1831
1832        // Un-reordered reference: concatenate runs in topo order.
1833        let reference: Vec<NodeId> = runs.iter().flat_map(|r| lower_run(r).to_vec()).collect();
1834        // The production lowering (now reorders multi-device).
1835        let lowered = lower_picked_route(&g, &[out], &PickedRoute::new());
1836
1837        // Same multiset of nodes (a permutation, no add/drop).
1838        let mut a = reference.clone();
1839        let mut b = lowered.clone();
1840        a.sort();
1841        b.sort();
1842        assert_eq!(a, b, "the reorder is a permutation of the same node set");
1843
1844        // The lowered order is a valid topological order of the graph: every
1845        // node appears after all of its (reachable, lowered) inputs.
1846        let lowered_pos: HashMap<NodeId, usize> =
1847            lowered.iter().enumerate().map(|(i, &n)| (n, i)).collect();
1848        for (i, &n) in lowered.iter().enumerate() {
1849            for &inp in &g.node(n).inputs {
1850                if let Some(&j) = lowered_pos.get(&inp) {
1851                    assert!(j < i, "input {inp:?} of {n:?} must precede it");
1852                }
1853            }
1854        }
1855
1856        // And it genuinely diverged from the reference (the reorder fired) —
1857        // the cuda-last DFS reference puts the Vulkan-fed copy's chain before
1858        // the CUDA chain; the reorder interleaves them.
1859        assert_ne!(
1860            lowered, reference,
1861            "on this two-device graph the reorder must change the order \
1862             (else auto-overlap did nothing)",
1863        );
1864    }
1865
1866    /// C3 + C1 composition: the STREAMING lowering of a multi-device graph
1867    /// equals the one-shot `lower_picked_route` (both now reorder), and the
1868    /// streaming reorder is confined to branch-free segments. On a
1869    /// branchless multi-device graph there is exactly one segment, so the
1870    /// streamed order equals the one-shot reordered order byte-for-byte.
1871    #[test]
1872    fn streaming_multidevice_equals_one_shot_reordered() {
1873        let (g, out, _cuda, _vk, _copy) = two_device_reconverge(4, 4);
1874        let one_shot = lower_picked_route(&g, &[out], &PickedRoute::new());
1875        let streamed = lower_picked_route_streaming(&g, &[out], |_b| 0);
1876        assert_eq!(
1877            streamed, one_shot,
1878            "streamed multi-device order equals the one-shot reordered order",
1879        );
1880    }
1881
1882    /// C3 critical-path property (the live-overlap regression guard): when
1883    /// the two device chunks differ in size, the reorder emits the HEAVIER
1884    /// chunk's whole path FIRST — so the auto-submitting device (the one with
1885    /// the long chain) is enqueued and running before the lighter device's
1886    /// host-blocking drain. This mirrors the live benchmark (CUDA-768 vs
1887    /// Vulkan-120): the proven-overlap order is CUDA-chunk-first; the inverse
1888    /// (Vulkan-first) serialized at 0.0 efficiency. This test locks the
1889    /// heavy-first ordering that makes the arbitrary-graph benchmark
1890    /// auto-overlap.
1891    #[test]
1892    fn device_alternating_reorder_emits_heavier_chunk_first() {
1893        // CUDA chain HEAVY (8), Vulkan chain LIGHT (2). Reconverge inputs are
1894        // cuda-first (the "wrong" DFS order). The reorder must still emit the
1895        // heavy CUDA chunk's path before the light Vulkan chunk.
1896        let (g, out, cuda_nodes, vk_nodes, copy) = two_device_reconverge(8, 2);
1897        let runs = extract_runs(&g, out);
1898        let perm = device_alternating_order(&g, &runs);
1899        assert_valid_topo_run_order(&g, &runs, &perm);
1900
1901        let flat: Vec<NodeId> = perm
1902            .iter()
1903            .flat_map(|&ri| runs[ri].members.clone())
1904            .collect();
1905        let pos = |n: NodeId| flat.iter().position(|&x| x == n).expect("present");
1906        let cuda_root = *cuda_nodes.last().unwrap();
1907        let vk_root = *vk_nodes.last().unwrap();
1908
1909        // The HEAVY (CUDA) chunk's root is emitted BEFORE the LIGHT (Vulkan)
1910        // chunk's root — the heavy device is enqueued first.
1911        assert!(
1912            pos(cuda_root) < pos(vk_root),
1913            "the heavier CUDA chunk must be emitted before the lighter Vulkan \
1914             chunk (the auto-submit device runs first); flat={flat:?}",
1915        );
1916        // And BOTH chunks precede the host-blocking cross-device copy.
1917        assert!(pos(cuda_root) < pos(copy), "heavy chunk before the drain");
1918        assert!(pos(vk_root) < pos(copy), "light chunk before the drain");
1919
1920        // Symmetric check: make VULKAN the heavy chunk; now Vulkan's path
1921        // must come first. (Proves it's size-driven, not a CUDA hard-code.)
1922        let (g2, out2, cuda2, vk2, copy2) = two_device_reconverge(2, 8);
1923        let runs2 = extract_runs(&g2, out2);
1924        let perm2 = device_alternating_order(&g2, &runs2);
1925        assert_valid_topo_run_order(&g2, &runs2, &perm2);
1926        let flat2: Vec<NodeId> = perm2
1927            .iter()
1928            .flat_map(|&ri| runs2[ri].members.clone())
1929            .collect();
1930        let pos2 = |n: NodeId| flat2.iter().position(|&x| x == n).expect("present");
1931        assert!(
1932            pos2(*vk2.last().unwrap()) < pos2(*cuda2.last().unwrap()),
1933            "when Vulkan is the heavier chunk it is emitted first; flat2={flat2:?}",
1934        );
1935        assert!(pos2(*vk2.last().unwrap()) < pos2(copy2));
1936        assert!(pos2(*cuda2.last().unwrap()) < pos2(copy2));
1937    }
1938}