Skip to main content

memra_engine/
pp.rs

1//! M2 pipeline-parallel N-stage runtime (generalizes the M1 2-stage seam).
2//!
3//! Door: `MEMRA_PP_STAGES=N` (default OFF — unset/0/1 = no behavior change anywhere).
4//! Stage map: N stages over the trunk layers with N-1 cuts. `MEMRA_PP_SPLITS=c1,..,cN-1`
5//! sets the cuts explicitly (strictly increasing, in (0, n_layers)); `MEMRA_PP_SPLIT=<i>`
6//! is the N=2 back-compat spelling; default = even split (cut s = s*n_layers/N).
7//! Placement: `MEMRA_PP_DEVICES=d0,..,dN-1` maps stage s to device ds (default: all on
8//! the primary engine's device).
9//!
10//! M1 history (increments 1-2, merged + hardened on the 8x box 2026-08-02): seam + gate
11//! single-device; then real transport — per-stage streams/events, device placement,
12//! peer-copy boundary (M0: cudaMemcpyPeerAsync beats NCCL 2.8x at PP activation sizes),
13//! per-context PDL module caches, default-mempool peer grants. All five r3 gates PASS
14//! bit-identical (receipts ~/receipts/m1-pp2/ on darklanes-bench).
15//!
16//! M2 increment 1 (this file): N-STAGE GENERALIZATION — `Pp2Rt` becomes `PpNRt`:
17//!   - `stages`: Vec of per-stage execution homes (device, context, stream, remote Engine);
18//!   - `boundaries`: N-1 boundary runtimes, each with TWO persistent double-buffered slots
19//!     (ev_tx/ev_rx per slot) and its own overlap step counter; transport is selected PER
20//!     BOUNDARY (dtod same-device / cudaMemcpyPeerAsync cross-device);
21//!   - peer + default-mempool access is granted between EVERY distinct pair of devices in
22//!     use (stage devices + the primary): stage kernels may dereference the primary's
23//!     weights (bring-up placement) and stage-0's pos_d, and each boundary peer-copies.
24//!
25//! M2 increment 2 (weight sharding): the loader uploads each stage's layer range THROUGH
26//! that stage's engine (`layer_engine`), so weights land on the device that runs them —
27//! the bring-up peer-read placement dies. `output_norm` + lm head load through the LAST
28//! stage's engine; the embed table stays host-side with stage 0. Split-plane/f16 decode
29//! mirrors are built per layer through the owning stage's engine too (the rp4 mirrors ARE
30//! the decode weights on the q8 path — leaving them on dev0 would fake the kill).
31//! Rollback seam: `MEMRA_PP_SHARD=0` = M1 bring-up placement (all weights on primary,
32//! remote stages peer-read).
33//!
34//! M2 increment 3 (deferred readback — the pipelining seed): `PendingLogits` — the eager
35//! decode arm can END a step without the logits D2H (`decode_step_h_ppn_deferred`): the
36//! logits stay device-resident with a completion event; `wait()` drains them through a
37//! DEDICATED readback stream (waits the event, copies, syncs) so tokens t+1.. keep
38//! enqueuing on the stage streams while token t drains. Per-token math is fully
39//! event-ordered (same slots, same ev_tx/ev_rx chain) — scheduling changes, math does
40//! not; the pipelined replay arm of `ppn-gate` proves bit-identity per step.
41//!
42//! Ownership across a boundary (unchanged from M1):
43//!   - hidden state [n_embd] f32 is the ONLY tensor that crosses;
44//!   - KV/linear-attn cache entries are per-layer: stage s exclusively owns cache state
45//!     for its layer range (and, under MEMRA_PP_DEVICES, allocates it on its device);
46//!   - position/rope state is the scalar `cache.pos` snapshot taken once per step, uploaded
47//!     on stage-0's stream BEFORE the first TX event — every later stage's wait chain
48//!     transitively orders it (stage s waits boundary s-1's ev_tx, which was recorded after
49//!     stage s-1's work, which waited boundary s-2's ev_tx, ... back to stage 0);
50//!   - the embed table lives with stage 0, output_norm + lm head with the last stage.
51//!
52//! THE MULTI-STREAM LAW (why this is safe with cudarc event tracking disabled): all
53//! cross-stage bytes flow through the persistent boundary slots, ordered by ev_tx/ev_rx;
54//! per-stage scratch is allocated AND freed on that stage's stream (stream-ordered); the
55//! async mem pool runs with opportunistic reuse OFF + internal dependencies ON
56//! (memra-runtime), so a block freed on stream A and reused on stream B carries a
57//! driver-inserted dependency. Weights are load-time state no stage stream can precede,
58//! and the step's terminal logits readback (sync D2H, or PendingLogits' event-ordered
59//! readback stream) drains the last stage, whose TX-wait chain transitively drains all.
60//!
61//! Scope: plain eager decode only (generic arm N-stage; gemma4 arm 2-stage). NOT wired:
62//! batch/dc/graph/spec loops and the gemma4-E4B eager arm.
63//!
64//! CORRECTION (pp2-hardening 2026-08-06): this header used to add "(`warn_unwired_once`
65//! fires)" to that list, which was wrong. `warn_unwired_once` has exactly two call sites
66//! and BOTH are gemma4-specific (decode.rs, hybrid_forward.rs) — the batch/dc/graph/spec
67//! loops never warned. Worse, the batched loop did not merely run unsplit: it walked the
68//! whole trunk on the primary stream and, under a sharded cross-device placement,
69//! peer-read every remote stage's weights each step — 28x slower at B=1 with all three
70//! `decode-batch-gate` gates PASSING (peer reads are byte-exact, so only perf broke).
71//! `decode_step_batch` now FAILS CLOSED in that regime via `pp_sharded_cross_device()`
72//! (`MEMRA_PP_ALLOW_UNSPLIT_BATCH=1` = measurement override). "Unwired" for dc/graph/spec
73//! still means "runs unsplit, silently" — audit each before trusting it on a pair.
74
75use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
76use std::sync::{Arc, Mutex, OnceLock};
77
78use cudarc::driver::{CudaContext, CudaEvent, CudaSlice, CudaStream};
79
80use crate::Engine;
81
82/// Returns the stage fence iff the ppN door is open: `MEMRA_PP_STAGES=N` (N >= 2) with a
83/// valid cut list. The fence has N+1 entries: `[0, c1, .., cN-1, n_layers]`; stage s runs
84/// layers `[fence[s], fence[s+1])`. Reads the environment on every call (gates toggle the
85/// door in-process); the cost is a few getenv per decode step, eager-loop noise.
86pub fn pp_cuts(n_layers: usize) -> Option<Vec<usize>> {
87    let n_st: usize = match std::env::var("MEMRA_PP_STAGES") {
88        Ok(v) if v.is_empty() || v == "0" || v == "1" => return None,
89        Ok(v) => match v.parse::<usize>() {
90            Ok(n) => n,
91            Err(_) => {
92                warn_bad_once(&format!("MEMRA_PP_STAGES={v} unparseable; door stays OFF"));
93                return None;
94            }
95        },
96        Err(_) => return None,
97    };
98    if n_st < 2 || n_st > n_layers {
99        warn_bad_once(&format!(
100            "MEMRA_PP_STAGES={n_st} outside [2, n_layers={n_layers}]; door stays OFF"
101        ));
102        return None;
103    }
104    let mut fence = Vec::with_capacity(n_st + 1);
105    fence.push(0usize);
106    if let Ok(s) = std::env::var("MEMRA_PP_SPLITS") {
107        let parts: Result<Vec<usize>, _> =
108            s.split(',').map(|p| p.trim().parse::<usize>()).collect();
109        match parts {
110            Ok(cuts) if cuts.len() == n_st - 1 => fence.extend(cuts),
111            _ => {
112                warn_bad_once(&format!(
113                    "MEMRA_PP_SPLITS={s} invalid (want {} comma-separated cuts); door stays OFF",
114                    n_st - 1
115                ));
116                return None;
117            }
118        }
119    } else if let Ok(v) = std::env::var("MEMRA_PP_SPLIT") {
120        // N=2 back-compat spelling. With N>2 a single split is ambiguous — fail the door
121        // loudly rather than guess (a silent even-split would fake a gate config).
122        if n_st != 2 {
123            warn_bad_once(&format!(
124                "MEMRA_PP_SPLIT={v} set with MEMRA_PP_STAGES={n_st}; use MEMRA_PP_SPLITS \
125                 for N>2 — door stays OFF"
126            ));
127            return None;
128        }
129        match v.parse::<usize>() {
130            Ok(c) => fence.push(c),
131            Err(_) => {
132                warn_bad_once(&format!("MEMRA_PP_SPLIT={v} unparseable; door stays OFF"));
133                return None;
134            }
135        }
136    } else {
137        for s in 1..n_st {
138            fence.push(s * n_layers / n_st);
139        }
140    }
141    fence.push(n_layers);
142    for w in fence.windows(2) {
143        if w[0] >= w[1] {
144            warn_bad_once(&format!(
145                "pp stage fence {fence:?} not strictly increasing over [0, {n_layers}]; \
146                 door stays OFF"
147            ));
148            return None;
149        }
150    }
151    Some(fence)
152}
153
154/// N=2 back-compat view of the door (the gemma4 arm and `pp2-gate` are 2-stage): `Some(cut)`
155/// iff the door is open with EXACTLY two stages.
156pub fn pp2_split(n_layers: usize) -> Option<usize> {
157    pp_cuts(n_layers).filter(|f| f.len() == 3).map(|f| f[1])
158}
159
160/// The stage that owns layer `il` under `fence` (see `pp_cuts`).
161pub fn stage_of(fence: &[usize], il: usize) -> usize {
162    debug_assert!(fence.len() >= 2);
163    match fence[1..fence.len() - 1].binary_search(&il) {
164        // fence[1..][k] == il means il is the FIRST layer of stage k+1
165        Ok(k) => k + 1,
166        Err(k) => k,
167    }
168}
169
170/// MEMRA_PP_STREAMS=0: rollback to the increment-1 same-stream seam (boundary = two plain
171/// dtod copies on the ambient compute stream, no per-stage streams/events/devices).
172pub fn pp2_streams_off() -> bool {
173    matches!(std::env::var("MEMRA_PP_STREAMS").as_deref(), Ok("0"))
174}
175
176/// True iff the ppN door would put TWO OR MORE stage streams on ONE device (devices
177/// unset = all stages on the primary; or an explicit placement with a repeated device).
178/// The deferred-readback (pipelined) arm is REFUSED in this regime: the 2026-08-02 x20
179/// soak record — singledev pipelined 13/20 PASS default, 7 failures each diverging at a
180/// different step (timing-race signature); MEMRA_PDL=0 went 20/20 on one soak but a
181/// second same-config soak on the auto-gated build failed 2/20 (n2) and battery-4 failed
182/// n4 — so PDL narrows the window without closing it, and the true root cause (same
183/// Engine kernels concurrent on two streams of one device) is NOT fixed by any flag yet.
184/// Cross-device pipelined (one stage stream per device) is 23/23 clean post-fix. Refuse
185/// loudly rather than return silently-wrong logits. Env-only read (callable pre-runtime).
186pub fn pp_multi_stream_same_device() -> bool {
187    let stages_open = std::env::var("MEMRA_PP_STAGES")
188        .map(|v| v.parse::<usize>().map(|n| n >= 2).unwrap_or(false))
189        .unwrap_or(false);
190    let devices = std::env::var("MEMRA_PP_DEVICES").ok().filter(|v| !v.is_empty());
191    if (!stages_open && devices.is_none()) || pp2_streams_off() {
192        return false;
193    }
194    match devices {
195        None => true, // door open, no placement: every stage stream lands on the primary
196        Some(s) => {
197            let mut v: Vec<&str> = s.split(',').map(|p| p.trim()).collect();
198            let n = v.len();
199            v.sort_unstable();
200            v.dedup();
201            v.len() < n // repeated device = shared-device streams
202        }
203    }
204}
205
206/// True iff the ppN door is open AND the placement spans 2+ DISTINCT devices AND the
207/// per-stage sharded loader is on — i.e. some layers' weights live on a device other than
208/// the primary. Any path that walks the WHOLE trunk on one stream in this regime reads
209/// those weights over PCIe every step. Env-only read (callable pre-runtime).
210///
211/// Measured cost of doing that (pp2-hardening 2026-08-06, 2x RTX PRO 6000, PCIe Gen5 x16
212/// P2P, decode-batch-bench q9, N=5 interleaved, `research/pp2-hardening-20260806`):
213/// **B=1 7.4 vs 208.9 tok/s (28x), B=4 29.8 vs 491.3 (16.5x), B=8 47.4 vs 657.0 (13.9x)**.
214/// The same sweep with `MEMRA_PP_SHARD=0` (weights all home) returns 178.5/491.1/656.6 —
215/// identical to the single-device door-open arm — so the entire cliff is the peer read,
216/// not the door and not the placement plumbing. Exactness is NOT the issue: peer reads
217/// return identical bytes and every `decode-batch-gate` gate PASSED on this config, which
218/// is precisely why it needs a refusal rather than a gate.
219pub fn pp_sharded_cross_device() -> bool {
220    let stages_open = std::env::var("MEMRA_PP_STAGES")
221        .map(|v| v.parse::<usize>().map(|n| n >= 2).unwrap_or(false))
222        .unwrap_or(false);
223    // MEMRA_PP_STREAMS=0 (2026-08-06, pp2-batch): the same-stream rollback seam ALSO turns
224    // the sharded loader off — `layer_engine` returns the primary engine whenever
225    // `pp2_streams_off()`, and `new_cache` skips `Cache::new_ppn` on the same condition. So
226    // in that regime every weight and every cache is home on the primary and an unsplit walk
227    // peer-reads NOTHING. Without this term the guard refused that config too: a spurious
228    // refusal of a placement that is sound and full-speed. Found wiring the batched pp arm.
229    if !stages_open || pp_shard_off() || pp2_streams_off() {
230        return false;
231    }
232    match pp2_devices_env() {
233        None => false, // no placement: every stage is the primary device, nothing remote
234        Some(s) => {
235            let mut v: Vec<&str> = s.split(',').map(|p| p.trim()).collect();
236            v.sort_unstable();
237            v.dedup();
238            v.len() >= 2
239        }
240    }
241}
242
243/// The shared fail-closed guard for EVERY decode path that has no pp stage split.
244/// Returns `Err` iff `pp_sharded_cross_device()` — i.e. the caller would walk the whole
245/// trunk on one stream while some layers' weights live on another device, peer-reading
246/// them every step. `path` names the refusing function so the operator knows which loop
247/// they hit; `alt` names the working alternative for that loop.
248///
249/// One helper rather than four copies because the audit found FOUR paths with the same
250/// hole (`decode_step_batch`, `decode_step_dc`, the graph capture that wraps dc, and
251/// `decode_step_t*` verify), and a per-path copy is how one gets missed on the next
252/// addition. Override: `MEMRA_PP_ALLOW_UNSPLIT_BATCH=1` (one door for all of them —
253/// they are the same measurement question).
254pub fn refuse_unsplit_if_remote(path: &str, alt: &str) -> Result<(), Box<dyn std::error::Error>> {
255    if pp_sharded_cross_device()
256        && std::env::var("MEMRA_PP_ALLOW_UNSPLIT_BATCH").as_deref() != Ok("1")
257    {
258        return Err(format!(
259            "{path}: refused with the ppN door open across 2+ devices — this path has no pp \
260             stage split, so it would walk ALL layers on one stream and peer-read every \
261             remote stage's weights each step (measured 28x slower at B=1, 13.9x at B=8 on \
262             a PRO 6000 pair over PCIe Gen5 x16 P2P; research/pp2-hardening-20260806). \
263             Exactness is unaffected — peer reads return identical bytes and the exactness \
264             gates PASS on this config — which is exactly why it must refuse instead of \
265             being caught by a gate. Fixes, in order: {alt}; or MEMRA_PP_SHARD=0 (all \
266             weights home on the primary — full speed, forfeits the capacity PP-2 exists \
267             for); or close the pp door. MEMRA_PP_ALLOW_UNSPLIT_BATCH=1 overrides for \
268             measurement."
269        )
270        .into());
271    }
272    Ok(())
273}
274
275/// MEMRA_BATCH_PP=0: rollback/A-B seam for the BATCHED stage split (pp2-batch 2026-08-06).
276/// Default ON — with the ppN door open the batched decode step takes its own stage split
277/// (`decode_step_batch_ppn`) exactly as the eager step does. Setting 0 sends the batched
278/// path back through the unsplit body, which under a sharded cross-device placement is
279/// then caught by `refuse_unsplit_if_remote` (the 28x peer-read regime) rather than run
280/// silently. Exists so the bit-identity gate can A/B split vs unsplit IN ONE PROCESS
281/// against the same loaded weights — read per step, never memoized, for that reason.
282pub fn batch_pp_on() -> bool {
283    std::env::var("MEMRA_BATCH_PP").as_deref() != Ok("0")
284}
285
286/// MEMRA_PRIME_PP=0: rollback/A-B seam for the PRIME (chunked prefill) stage split
287/// (lane/pp-leverb 2026-08-08). Default ON — with the ppN door open the chunked prime takes
288/// its own per-stage range walk exactly as the eager/batched/verify steps do. Setting 0 sends
289/// prime back through the unsplit whole-trunk walk. NOTE: unlike batch/dc/graph/spec, prime
290/// keeps NO `refuse_unsplit_if_remote` — its unsplit walk over a sharded placement is the
291/// measured 22% amortized peer-read tax (research/pp-prefill-20260807 anatomy: m=4096
292/// amortizes the weight reads), not the decode 28x cliff, and the unsplit walk IS the
293/// split-vs-unsplit gate's reference arm (`prime-split-gate`), so it must stay callable.
294/// Read per call, never memoized (the gate A/Bs both arms in one process).
295pub fn prime_pp_on() -> bool {
296    std::env::var("MEMRA_PRIME_PP").as_deref() != Ok("0")
297}
298
299/// SPLIT-LIVENESS COUNTER for the prime stage split: bumped ONCE per prime chunk that
300/// actually executed the per-stage walk. The `prime-split-gate` requires this to ADVANCE
301/// during its split arm — bit-identity of two identical UNSPLIT walks is vacuous, so a gate
302/// that only compared bits would go green while the walker doesn't exist. With the counter,
303/// the gate is RED until the walker lands (the tickinv35 pattern: the gate exists and fails
304/// before the mechanism does). Relaxed ordering: single-threaded host issue, count-only.
305pub static PRIME_SPLIT_CHUNKS: AtomicUsize = AtomicUsize::new(0);
306
307/// Read the split-liveness counter (gate-side).
308pub fn prime_split_chunks() -> usize {
309    PRIME_SPLIT_CHUNKS.load(Ordering::Relaxed)
310}
311
312/// MEMRA_SPEC_PP=0: rollback/A-B seam for the SPEC VERIFY stage split (pp2-spec 2026-08-06).
313/// Default ON — with the ppN door open the verify forward (`decode_step_t_core_ppn`) takes its
314/// own stage split exactly as the eager and batched steps do. Setting 0 sends verify back through
315/// the unsplit trunk walk, which under a sharded cross-device placement is then caught by
316/// `refuse_unsplit_if_remote` (the 28x peer-read regime) rather than running silently. Exists so
317/// the bit-identity gate can A/B split vs unsplit IN ONE PROCESS against the same loaded weights
318/// — read per verify call, never memoized, for that reason.
319pub fn spec_pp_on() -> bool {
320    std::env::var("MEMRA_SPEC_PP").as_deref() != Ok("0")
321}
322
323/// MEMRA_PP_OVERLAP=1: alternate the double-buffered boundary slots per step (the
324/// pipelining seed). Default OFF — scheduling structure only, never math. Read per step
325/// so gates can A/B in-process.
326pub fn pp2_overlap() -> bool {
327    matches!(std::env::var("MEMRA_PP_OVERLAP").as_deref(), Ok("1"))
328}
329
330/// M2 increment 2 rollback seam: MEMRA_PP_SHARD=0 = the M1 bring-up placement (all
331/// weights upload through the primary engine; remote stages peer-read). Default ON —
332/// under MEMRA_PP_DEVICES each stage's layer range uploads through its own engine.
333pub fn pp_shard_off() -> bool {
334    matches!(std::env::var("MEMRA_PP_SHARD").as_deref(), Ok("0"))
335}
336
337/// Raw `MEMRA_PP_DEVICES` (parsed/validated at PpNRt build — a bad string must fail the
338/// decode step loudly, never silently fall back to same-device and fake a gate PASS).
339fn pp2_devices_env() -> Option<String> {
340    std::env::var("MEMRA_PP_DEVICES").ok().filter(|v| !v.is_empty())
341}
342
343static WARNED_BAD: AtomicBool = AtomicBool::new(false);
344fn warn_bad_once(msg: &str) {
345    if !WARNED_BAD.swap(true, Ordering::Relaxed) {
346        eprintln!("[pp] {msg}");
347    }
348}
349
350static WARNED_UNWIRED: AtomicBool = AtomicBool::new(false);
351/// One-time notice when the door is set but the executing path has no pp arm
352/// (M2 wires the generic eager decode at any N and the gemma4 eager arm at N=2).
353pub fn warn_unwired_once(path: &str) {
354    let open = std::env::var("MEMRA_PP_STAGES")
355        .map(|v| !v.is_empty() && v != "0" && v != "1")
356        .unwrap_or(false);
357    if open && !WARNED_UNWIRED.swap(true, Ordering::Relaxed) {
358        eprintln!(
359            "[pp] MEMRA_PP_STAGES set but `{path}` has no pp arm at this N; running unsplit"
360        );
361    }
362}
363
364// ======================================================================================
365//  PpNRt: the M2 transport runtime (per-stage streams, per-boundary events + slots)
366// ======================================================================================
367
368/// One pipeline stage's execution home: device, context, launch stream, and (for a stage
369/// remote to the primary engine's device) a dedicated Engine in that device's primary
370/// context (CUmodules are per-context).
371pub struct StageRt {
372    pub dev: usize,
373    pub ctx: Arc<CudaContext>,
374    pub stream: Arc<CudaStream>,
375    /// `Some` only when `dev` differs from the primary engine's device.
376    engine: Option<Engine>,
377}
378
379/// One boundary slot: a persistent RX-side buffer + its TX/RX completion events.
380/// PERSISTENT because the buffer is written by the TX stage's stream and read by the RX
381/// stage's: a per-step alloc/free would enqueue the free on ONE stream while the other
382/// might still be reading (the cross-stream free hazard) — a never-freed slot cannot race.
383struct BoundarySlot {
384    buf: Mutex<Option<CudaSlice<f32>>>,
385    /// Recorded on the TX stage's stream after the TX copy; RX waits on it. Created in
386    /// the TX stage's context (cuEventRecord requires event ctx == stream ctx).
387    ev_tx: CudaEvent,
388    /// Recorded on the RX stage's stream after the RX copy; the NEXT TX into this slot
389    /// waits on it (write-after-read guard). Created in the RX stage's context. Waiting
390    /// on a never-recorded event is a defined no-op, so step 0 needs no special case.
391    ev_rx: CudaEvent,
392}
393
394/// Boundary b sits between stage b (TX) and stage b+1 (RX). Two slots, alternating per
395/// step under MEMRA_PP_OVERLAP=1 (each boundary counts its own steps — a decode step
396/// crosses every boundary exactly once, so the counters stay in lockstep).
397struct BoundaryRt {
398    slots: [BoundarySlot; 2],
399    step: AtomicUsize,
400    /// true iff stage b and stage b+1 live on different devices (peer transport).
401    cross: bool,
402}
403
404pub struct PpNRt {
405    stages: Vec<StageRt>,
406    boundaries: Vec<BoundaryRt>,
407    /// true iff ANY boundary crosses devices.
408    cross_any: bool,
409    /// Dedicated readback stream in the LAST stage's context (deferred logits D2H —
410    /// waiting there instead of on the compute stream keeps later tokens enqueuable).
411    readback: Arc<CudaStream>,
412}
413
414/// M1 name kept alive for external callers (`pp-transport-smoke`, receipts, docs).
415pub type Pp2Rt = PpNRt;
416
417static RTN: OnceLock<Result<PpNRt, String>> = OnceLock::new();
418
419impl PpNRt {
420    /// The process-wide transport runtime, built on first use against the primary engine.
421    /// The stage count + device map freeze at first build (one config per process — gates
422    /// run one placement per invocation). Build errors are sticky and loud.
423    pub fn get(e: &Engine) -> Result<&'static PpNRt, Box<dyn std::error::Error>> {
424        RTN.get_or_init(|| Self::build(e).map_err(|err| err.to_string()))
425            .as_ref()
426            .map_err(|s| -> Box<dyn std::error::Error> { s.clone().into() })
427    }
428
429    fn build(e: &Engine) -> Result<PpNRt, Box<dyn std::error::Error>> {
430        let primary_dev = e.ctx().ordinal();
431        // Stage count: MEMRA_PP_DEVICES length wins when set (it IS the placement);
432        // else MEMRA_PP_STAGES; else 2 (the M1 default — pp-transport-smoke runs doorless).
433        let devices: Vec<usize> = match pp2_devices_env() {
434            Some(s) => {
435                let parts: Result<Vec<usize>, _> =
436                    s.split(',').map(|p| p.trim().parse::<usize>()).collect();
437                match parts {
438                    Ok(v) if v.len() >= 2 => v,
439                    _ => {
440                        return Err(format!(
441                            "MEMRA_PP_DEVICES={s} unparseable (want <d0>,..,<dN-1> e.g. 0,1,2,3)"
442                        )
443                        .into())
444                    }
445                }
446            }
447            None => {
448                let n_st = std::env::var("MEMRA_PP_STAGES")
449                    .ok()
450                    .and_then(|v| v.parse::<usize>().ok())
451                    .filter(|&n| n >= 2)
452                    .unwrap_or(2);
453                vec![primary_dev; n_st]
454            }
455        };
456        if let Ok(v) = std::env::var("MEMRA_PP_STAGES") {
457            if let Ok(n) = v.parse::<usize>() {
458                if n >= 2 && n != devices.len() {
459                    return Err(format!(
460                        "MEMRA_PP_DEVICES lists {} devices but MEMRA_PP_STAGES={n} — \
461                         refusing an ambiguous placement",
462                        devices.len()
463                    )
464                    .into());
465                }
466            }
467        }
468        let n_st = devices.len();
469        let cross_any = devices.iter().any(|&d| d != devices[0]);
470
471        // Every distinct device pair in use must peer-access BOTH ways: boundaries copy
472        // between consecutive stages, stage kernels may dereference primary-device weights
473        // (bring-up placement / MEMRA_PP_SHARD=0) and stage-0's pos_d upload.
474        let mut used: Vec<usize> = devices.clone();
475        used.push(primary_dev);
476        used.sort_unstable();
477        used.dedup();
478        if used.len() > 1 {
479            let n = cudarc::driver::result::device::get_count()? as usize;
480            for &d in &used {
481                if d >= n {
482                    return Err(format!(
483                        "MEMRA_PP_DEVICES={devices:?} but only {n} CUDA device(s) present"
484                    )
485                    .into());
486                }
487            }
488            for &a in &used {
489                for &b in &used {
490                    if a == b {
491                        continue;
492                    }
493                    let da = cudarc::driver::result::device::get(a as i32)?;
494                    let db = cudarc::driver::result::device::get(b as i32)?;
495                    let mut can: i32 = 0;
496                    unsafe {
497                        cudarc::driver::sys::cuDeviceCanAccessPeer(&mut can, da, db).result()?;
498                    }
499                    if can == 0 {
500                        return Err(format!(
501                            "device {a} cannot peer-access device {b} (cuDeviceCanAccessPeer=0); \
502                             ppN cross-device needs P2P — refusing a silently-staged path"
503                        )
504                        .into());
505                    }
506                }
507            }
508        }
509
510        // PER-STAGE ENGINE ISOLATION (2026-08-02 singledev pipelined find): Engine owns
511        // lazily-grown SHARED scratch pools (fa_part_pool, fa_vf16_scratch, argmax
512        // partials, ...) that are stable-pointer by design — safe on one stream, a data
513        // race the moment two stage streams run concurrently through the SAME Engine
514        // (deferred readback, >=2 tokens in flight: token t+1's stage-0 fa memsets the
515        // partials while token t's stage-s fa still reads them — the nondeterministic
516        // all-logits divergence; cross-device arms were immune because remote stages
517        // already got their own Engine). Every stage s>0 gets its OWN Engine even on the
518        // primary device: same CUcontext (primary retain), so the per-context CUmodule
519        // cache makes it cheap; scratch pools are per-Engine, so stages never share.
520        // Stage 0 keeps the primary engine (single-threaded host issue: the only
521        // concurrent user of `e` during a pp walk is stage 0 itself).
522        let mk_stage = |dev: usize, s: usize| -> Result<StageRt, Box<dyn std::error::Error>> {
523            if dev == primary_dev && s == 0 {
524                let ctx = e.ctx().clone();
525                let stream = ctx.new_stream()?;
526                Ok(StageRt { dev, ctx, stream, engine: None })
527            } else {
528                let eng = Engine::new(dev)?;
529                let ctx = eng.ctx().clone();
530                let stream = ctx.new_stream()?;
531                Ok(StageRt { dev, ctx, stream, engine: Some(eng) })
532            }
533        };
534        let mut stages = Vec::with_capacity(n_st);
535        for (s, &d) in devices.iter().enumerate() {
536            stages.push(mk_stage(d, s)?);
537        }
538
539        if used.len() > 1 {
540            // A context per distinct device (first stage that lives there; the primary's
541            // context for the primary device).
542            let ctx_of = |d: usize| -> &Arc<CudaContext> {
543                if d == primary_dev {
544                    e.ctx()
545                } else {
546                    &stages.iter().find(|s| s.dev == d).unwrap().ctx
547                }
548            };
549            // Enable peer access BOTH ways for every distinct pair (idempotent;
550            // ALREADY_ENABLED is success).
551            for &a in &used {
552                for &b in &used {
553                    if a == b {
554                        continue;
555                    }
556                    ctx_of(a).bind_to_thread()?;
557                    let rc = unsafe {
558                        cudarc::driver::sys::cuCtxEnablePeerAccess(ctx_of(b).cu_ctx(), 0)
559                    };
560                    use cudarc::driver::sys::cudaError_enum as E;
561                    if rc != E::CUDA_SUCCESS && rc != E::CUDA_ERROR_PEER_ACCESS_ALREADY_ENABLED {
562                        return Err(format!(
563                            "cuCtxEnablePeerAccess(dev{a} -> dev{b}) failed: {rc:?}"
564                        )
565                        .into());
566                    }
567                }
568            }
569            // MEM-POOL access grant (8x box 2026-08-02, M1 cross-device fix #2):
570            // cuCtxEnablePeerAccess does NOT map STREAM-ORDERED POOL allocations, and every
571            // engine buffer/weight goes through the device default pool (cuMemAllocAsync via
572            // cudarc; memra-runtime configures that pool). A stage kernel dereferencing
573            // another device's weights — or a boundary peer TX writing the RX slot — needs
574            // cuMemPoolSetAccess on the OWNING device's default pool for the ACCESSING
575            // device; without it the first remote dereference is CUDA_ERROR_ILLEGAL_ADDRESS
576            // (reported at the next API call in the poisoned context). Grant all pairs.
577            for &owner in &used {
578                for &accessor in &used {
579                    if owner == accessor {
580                        continue;
581                    }
582                    let dev = cudarc::driver::result::device::get(owner as i32)?;
583                    let mut pool: cudarc::driver::sys::CUmemoryPool = std::ptr::null_mut();
584                    unsafe {
585                        cudarc::driver::sys::cuDeviceGetDefaultMemPool(&mut pool, dev).result()?;
586                    }
587                    let desc = cudarc::driver::sys::CUmemAccessDesc {
588                        location: cudarc::driver::sys::CUmemLocation {
589                            type_: cudarc::driver::sys::CUmemLocationType::CU_MEM_LOCATION_TYPE_DEVICE,
590                            id: accessor as i32,
591                        },
592                        flags: cudarc::driver::sys::CUmemAccess_flags::CU_MEM_ACCESS_FLAGS_PROT_READWRITE,
593                    };
594                    let rc = unsafe { cudarc::driver::sys::cuMemPoolSetAccess(pool, &desc, 1) };
595                    if rc != cudarc::driver::sys::cudaError_enum::CUDA_SUCCESS {
596                        return Err(format!(
597                            "cuMemPoolSetAccess(dev{owner} pool -> dev{accessor}) failed: {rc:?}"
598                        )
599                        .into());
600                    }
601                }
602            }
603            // MEM-POOL access grant (8x box 2026-08-02, cross-device fix #2):
604            // cuCtxEnablePeerAccess does NOT map STREAM-ORDERED POOL allocations, and every
605            // engine buffer/weight goes through the device default pool (cuMemAllocAsync via
606            // cudarc; memra-runtime configures that pool). A stage-1 kernel dereferencing
607            // dev0 weights — or the stage-0 peer TX writing dev1's RX slot — needs
608            // cuMemPoolSetAccess on the OWNING device's default pool for the ACCESSING
609            // device; without it the first remote dereference is CUDA_ERROR_ILLEGAL_ADDRESS
610            // (reported at the next API call in the poisoned context). Grant both ways.
611            for (owner, accessor) in [(stages[0].dev, stages[1].dev), (stages[1].dev, stages[0].dev)] {
612                let dev = cudarc::driver::result::device::get(owner as i32)?;
613                let mut pool: cudarc::driver::sys::CUmemoryPool = std::ptr::null_mut();
614                unsafe {
615                    cudarc::driver::sys::cuDeviceGetDefaultMemPool(&mut pool, dev).result()?;
616                }
617                let desc = cudarc::driver::sys::CUmemAccessDesc {
618                    location: cudarc::driver::sys::CUmemLocation {
619                        type_: cudarc::driver::sys::CUmemLocationType::CU_MEM_LOCATION_TYPE_DEVICE,
620                        id: accessor as i32,
621                    },
622                    flags: cudarc::driver::sys::CUmemAccess_flags::CU_MEM_ACCESS_FLAGS_PROT_READWRITE,
623                };
624                let rc = unsafe { cudarc::driver::sys::cuMemPoolSetAccess(pool, &desc, 1) };
625                if rc != cudarc::driver::sys::cudaError_enum::CUDA_SUCCESS {
626                    return Err(format!(
627                        "cuMemPoolSetAccess(dev{owner} pool -> dev{accessor}) failed: {rc:?}"
628                    )
629                    .into());
630                }
631            }
632            // restore the primary context for the caller's subsequent work
633            e.ctx().bind_to_thread()?;
634            eprintln!(
635                "[pp] cross-device transport: {} (cudaMemcpyPeerAsync per cross boundary; \
636                 peer + default-pool access granted all pairs over {used:?}; weight home: {})",
637                devices
638                    .iter()
639                    .enumerate()
640                    .map(|(s, d)| format!("stage{s}=dev{d}"))
641                    .collect::<Vec<_>>()
642                    .join(" "),
643                if pp_shard_off() {
644                    format!("dev{primary_dev} (MEMRA_PP_SHARD=0 bring-up placement)")
645                } else {
646                    "per-stage (sharded loader)".to_string()
647                }
648            );
649        }
650
651        let mk_slot = |tx: &StageRt, rx: &StageRt| -> Result<BoundarySlot, Box<dyn std::error::Error>> {
652            Ok(BoundarySlot {
653                buf: Mutex::new(None),
654                ev_tx: tx.ctx.new_event(None)?,
655                ev_rx: rx.ctx.new_event(None)?,
656            })
657        };
658        let mut boundaries = Vec::with_capacity(n_st - 1);
659        for b in 0..n_st - 1 {
660            let (tx, rx) = (&stages[b], &stages[b + 1]);
661            boundaries.push(BoundaryRt {
662                slots: [mk_slot(tx, rx)?, mk_slot(tx, rx)?],
663                step: AtomicUsize::new(0),
664                cross: tx.dev != rx.dev,
665            });
666        }
667        let readback = stages[n_st - 1].ctx.new_stream()?;
668        Ok(PpNRt { stages, boundaries, cross_any, readback })
669    }
670
671    pub fn n_stages(&self) -> usize {
672        self.stages.len()
673    }
674
675    /// True iff any boundary crosses devices (transport = cudaMemcpyPeerAsync there).
676    pub fn cross_device(&self) -> bool {
677        self.cross_any
678    }
679
680    /// The engine a stage's subgraph must run through: the primary engine when the stage
681    /// lives on the primary device, else the stage's own (remote-context) engine.
682    pub fn engine<'a>(&'a self, s: usize, primary: &'a Engine) -> &'a Engine {
683        self.stages[s].engine.as_ref().unwrap_or(primary)
684    }
685
686    /// Enter stage `s`: until the guard drops, every engine op on this thread launches on
687    /// the stage's stream (memra_runtime ambient-stream override).
688    pub fn enter(&self, s: usize) -> memra_runtime::StreamOverride {
689        memra_runtime::push_stream_override(self.stages[s].stream.clone())
690    }
691
692    /// Boundary TX at boundary `b` (call within the stage-`b` scope; `x` = the
693    /// materialized [n] residual): wait for the slot's previous RX (write-after-read
694    /// guard), copy `x` into the slot's persistent buffer via the boundary's transport on
695    /// stage-b's stream (the owning-stream/publication law), record ev_tx. Returns the
696    /// slot index for the paired rx().
697    ///
698    /// `n` is the PAYLOAD ELEMENT COUNT, not a fixed model constant: the eager arm passes
699    /// `n_embd` (one row), the batched arm passes `b_n * n_embd` (B stacked rows, the
700    /// [B, n_embd] boundary). The slot buffer is GROW-ONLY and the transport moves exactly
701    /// the first `n` elements — batched serving changes B every tick (chunk fill), and a
702    /// realloc-on-every-size-change would host-sync the RX stream per width change (see the
703    /// SLOT FIRST-USE ORDERING note below for why each allocation needs that sync). Growing
704    /// to the high-water mark makes the syncs O(distinct widths) instead of O(width changes).
705    pub fn tx(&self, b: usize, x: &CudaSlice<f32>, n: usize)
706              -> Result<usize, Box<dyn std::error::Error>> {
707        assert_eq!(x.len(), n, "pp tx: residual length mismatch");
708        let bd = &self.boundaries[b];
709        let slot_idx = if pp2_overlap() {
710            bd.step.fetch_add(1, Ordering::Relaxed) % 2
711        } else {
712            0
713        };
714        let sl = &bd.slots[slot_idx];
715        let s_tx = &self.stages[b].stream;
716        s_tx.wait(&sl.ev_rx)?;
717        let mut guard = sl.buf.lock().unwrap();
718        if guard.as_ref().map(|bf| bf.len() < n).unwrap_or(true) {
719            // allocated on the RX stage's stream: the buffer lives on the RX device.
720            let s_rx = &self.stages[b + 1].stream;
721            *guard = Some(s_rx.alloc_zeros::<f32>(n)?);
722            // SLOT FIRST-USE ORDERING (2026-08-02 pipelined-gate find): the lazy alloc's
723            // pool-alloc + memset enqueue on the RX stream; the TX copy below issues on
724            // the TX stream, and on a slot's FIRST use ev_rx has never been recorded —
725            // nothing orders them. With >=2 tokens in flight the RX stream is still busy
726            // with the previous token, the memset lands AFTER the TX copy, and the
727            // boundary residual is zeroed (window=1 passed, window>=2 failed at the
728            // slot-1 first-use step; -overlap arms passed because the synchronous serial
729            // arm pre-warmed both slots). Host-sync the RX stream once per slot
730            // allocation — at most 2*(N-1) one-time syncs per process, all during prime.
731            s_rx.synchronize()?;
732        }
733        let buf = guard.as_mut().unwrap();
734        if !bd.cross {
735            s_tx.memcpy_dtod(x, buf)?;
736        } else {
737            // cudaMemcpyPeerAsync (M0: 2.8x NCCL at PP activation sizes), issued on the
738            // publishing TX stream with explicit src/dst contexts.
739            use cudarc::driver::{DevicePtr, DevicePtrMut};
740            let (sp, _g0) = x.device_ptr(s_tx);
741            let (dp, _g1) = buf.device_ptr_mut(s_tx);
742            self.stages[b].ctx.bind_to_thread()?;
743            unsafe {
744                cudarc::driver::result::memcpy_peer_async(
745                    self.stages[b + 1].ctx.cu_ctx(),
746                    dp,
747                    self.stages[b].ctx.cu_ctx(),
748                    sp,
749                    n * std::mem::size_of::<f32>(),
750                    s_tx.cu_stream(),
751                )?;
752            }
753        }
754        sl.ev_tx.record(s_tx)?;
755        Ok(slot_idx)
756    }
757
758    /// Boundary RX at boundary `b` (call within the stage-`b+1` scope): wait on the slot's
759    /// ev_tx, copy the boundary buffer into a fresh working buffer (dtod on the RX stream —
760    /// local on the RX device in both transports), record ev_rx. The returned buffer is
761    /// RX-stage-owned: allocated, consumed, and eventually freed on that stage's stream.
762    pub fn rx(&self, b: usize, slot_idx: usize, n: usize)
763              -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
764        let sl = &self.boundaries[b].slots[slot_idx];
765        let s_rx = &self.stages[b + 1].stream;
766        s_rx.wait(&sl.ev_tx)?;
767        let guard = sl.buf.lock().unwrap();
768        let buf = guard.as_ref().expect("pp rx before tx");
769        assert!(buf.len() >= n, "pp rx: slot holds {} < requested {n}", buf.len());
770        // uninit working buffer (fully overwritten by the copy), allocated explicitly on
771        // the stage stream so rx() is correct even outside an enter() scope.
772        let mut work = unsafe { s_rx.alloc::<f32>(n)? };
773        // Slice the slot to the payload: the buffer is grow-only (see tx), so at a narrower
774        // width it is LONGER than `work` and cudarc's memcpy_dtod (dst.len() >= src.len())
775        // would assert. The paired tx wrote exactly these first n elements.
776        s_rx.memcpy_dtod(&buf.slice(0..n), &mut work)?;
777        sl.ev_rx.record(s_rx)?;
778        Ok(work)
779    }
780
781    /// PUBLISH a DEVICE-RESIDENT result off the last stage to the caller's stream
782    /// (lane/pp2-spec 2026-08-06).
783    ///
784    /// Every ppN body before this one returned HOST values — `decode_step_h_ppn` and
785    /// `decode_step_batch_ppn` both `dtoh` inside the last-stage scope, and a dtoh on the
786    /// producing stream is self-ordering. The verify trunk is the FIRST ppN body whose
787    /// contract is device-resident output (`decode_step_t_h_emb_dev` exists precisely so the
788    /// accept walk argmaxes on-device instead of moving T x n_vocab f32 per round), and
789    /// device slices carry no stream affinity: the caller resumes on the PRIMARY stream and
790    /// dereferences buffers whose producing kernels are still queued on the last stage's
791    /// stream. Nothing orders them.
792    ///
793    /// Why this only ever failed on ONE device: with stages on separate devices the caller's
794    /// first touch is a cross-device copy that the driver orders against the source context,
795    /// and the readback path syncs. Two streams on the SAME device genuinely overlap, so the
796    /// primary stream reads a buffer whose matmul has not run — nondeterministic garbage
797    /// (measured: NaN, 3155.677, and 2.87e-5 where the reference had -2.0048926), and it
798    /// poisons the NEXT arm in the same process because the corrupted KV persists. This is
799    /// the same class as the SLOT FIRST-USE ORDERING find above, one level up: there the
800    /// unordered pair was alloc-memset vs TX copy, here it is stage-N compute vs the
801    /// caller's consumer.
802    ///
803    /// Fix = the boundary law applied to the exit: record an event on the producing stage
804    /// stream, make the caller's stream wait on it. Event-wait, not a device sync, so the
805    /// stage streams keep running for the deferred-readback arm. Call INSIDE the last-stage
806    /// scope, after the last enqueue, with the caller's (pre-`enter`) stream.
807    pub fn publish_to(&self, s: usize, dst: &Arc<CudaStream>)
808                      -> Result<(), Box<dyn std::error::Error>> {
809        let st = &self.stages[s];
810        // Same stream (STREAMS=0 rollback, or a caller already on the stage stream): the
811        // stream orders itself; recording+waiting would be a no-op with a stray event.
812        if Arc::ptr_eq(&st.stream, dst) {
813            return Ok(());
814        }
815        let ev = st.ctx.new_event(None)?;
816        ev.record(&st.stream)?;
817        dst.wait(&ev)?;
818        Ok(())
819    }
820
821    /// REVERSE PUBLICATION (#87 root cause, lane/pp2spec-crash 2026-08-07): order every
822    /// STAGE stream behind the CALLER's stream — the mirror of `publish_to`.
823    ///
824    /// `publish_to` orders caller READS behind stage COMPUTE. Nothing ordered the other
825    /// direction: buffers ALLOCATED on a stage stream (the verify's returned logits/hidden,
826    /// the VerifyCkpt stashes) are CONSUMED by kernels the caller enqueues on the PRIMARY
827    /// stream, and when they drop, cudarc enqueues `free_async` on the ALLOCATING (stage)
828    /// stream. With event tracking elided (the decode-path default) the drop carries no
829    /// read-guard, so the pool can hand the block to the NEXT stage-stream allocation and
830    /// its writes overwrite memory the queued primary-stream consumer has not read yet.
831    /// Measured (research/pp2spec-crash-20260807): the spec round-seed read 13/4096 NaN =
832    /// the uninitialized-bits signature (P(NaN|random u32) ~ 1/256), clean by host re-read
833    /// time — a read-before-write race, fatal via the argmax-sentinel -> embed_gather MMU
834    /// fault, and gated on c>=2 because a backed-up primary stream widens the window.
835    ///
836    /// Fix law: before a ppN body enqueues NEW stage-stream work (allocations that may
837    /// reuse freed blocks), every stage stream waits the caller's stream at its current
838    /// point. All primary consumers of the previous round's stage-allocated buffers are
839    /// enqueued by then (single host thread), so reuse-writes land strictly after them.
840    /// Call at ppN-body ENTRY with the pre-`enter` caller stream. Door-shut configs never
841    /// build a PpNRt, so single-card behavior is untouched.
842    pub fn fence_stages_behind(&self, src: &Arc<CudaStream>)
843                               -> Result<(), Box<dyn std::error::Error>> {
844        let ev = src.context().new_event(None)?;
845        ev.record(src)?;
846        for st in &self.stages {
847            if Arc::ptr_eq(&st.stream, src) {
848                continue;
849            }
850            st.stream.wait(&ev)?;
851        }
852        Ok(())
853    }
854
855    /// Deferred readback: record a fresh completion event on the LAST stage's stream
856    /// (call after the step's logits matmul has been enqueued there).
857    pub fn record_done(&self) -> Result<CudaEvent, Box<dyn std::error::Error>> {
858        let last = &self.stages[self.stages.len() - 1];
859        let ev = last.ctx.new_event(None)?;
860        ev.record(&last.stream)?;
861        Ok(ev)
862    }
863
864    /// The dedicated readback stream (last stage's context).
865    pub fn readback_stream(&self) -> &Arc<CudaStream> {
866        &self.readback
867    }
868}
869
870/// M2 increment 3: a step's logits, still device-resident on the LAST stage. `wait()`
871/// orders the readback stream behind the step's completion event, copies, and syncs —
872/// tokens enqueued after this step keep running on the stage streams while the caller
873/// drains token t. Dropping without waiting is safe (buffers free stream-ordered).
874pub struct PendingLogits {
875    logits: CudaSlice<f32>,
876    ev: CudaEvent,
877    rb: Arc<CudaStream>,
878}
879
880impl PendingLogits {
881    pub fn new(logits: CudaSlice<f32>, ev: CudaEvent, rb: Arc<CudaStream>) -> Self {
882        PendingLogits { logits, ev, rb }
883    }
884
885    /// Blocks until this step's logits are computed, returns them host-side. Only this
886    /// step's work is waited on (event-ordered) — NOT later tokens already enqueued on
887    /// the stage streams.
888    pub fn wait(self) -> Result<Vec<f32>, Box<dyn std::error::Error>> {
889        self.rb.wait(&self.ev)?;
890        let host = self.rb.clone_dtoh(&self.logits)?;
891        self.rb.synchronize()?;
892        // logits drop AFTER the sync: the D2H has fully completed, so the stream-ordered
893        // free on the compute stream cannot race the copy.
894        Ok(host)
895    }
896}
897
898/// Stage-owned cache allocation door: when the ppN door is open AND `MEMRA_PP_DEVICES`
899/// is set (placement plumbing), each layer's cache is allocated by its OWNING stage's
900/// engine — on one device this is byte-for-byte today's allocation (gated); cross-device
901/// it puts each stage's KV on that stage's HBM. Door shut or devices unset: plain
902/// `Cache::new` (zero behavior change). Trailing MTP/NextN layers (beyond the trunk)
903/// map to the LAST stage.
904pub fn new_cache(e: &Engine, cfg: &memra_gguf::config::ModelConfig, max_ctx: usize)
905                 -> Result<crate::cache::Cache, Box<dyn std::error::Error>> {
906    let n_trunk = (cfg.n_layer - cfg.nextn_predict_layers) as usize;
907    if let Some(fence) = pp_cuts(n_trunk) {
908        if pp2_devices_env().is_some() && !pp2_streams_off() {
909            let rt = PpNRt::get(e)?;
910            let n_st = fence.len() - 1;
911            assert_eq!(
912                rt.n_stages(), n_st,
913                "PpNRt stage count {} != fence stages {n_st}", rt.n_stages()
914            );
915            // #87 REVERSE PUBLICATION at ADMISSION (lane/pp2spec-crash): this is the one
916            // stage-stream allocation site OUTSIDE the ppN step bodies — a NEW session's
917            // KV alloc_zeros enqueue on the STAGE streams, and their pool blocks can be
918            // reuse of buffers freed from ANOTHER session's in-flight verify whose
919            // primary-stream reads are still queued (the c=2 residual: exactly one trap
920            // per admission collision, round 0, after the step-body fences landed).
921            // Order the stage streams behind the caller before the memsets can clobber.
922            // Anatomy: `PpNRt::fence_stages_behind`.
923            rt.fence_stages_behind(&e.stream())?;
924            let devs: Vec<&dyn memra_kv::KvDev> =
925                (0..n_st).map(|s| rt.engine(s, e) as &dyn memra_kv::KvDev).collect();
926            let cache = crate::cache::Cache::new_ppn(&devs, &fence, cfg, max_ctx)?;
927            sync_stages_after_load(e, n_trunk)?;
928            return Ok(cache);
929        }
930        if !pp2_streams_off() {
931            // CACHE BIRTH BARRIER (2026-08-02 pipelined-arm residual race): with the door
932            // open but no device placement, Cache::new's alloc_zeros memsets enqueue on
933            // the PRIMARY worker stream while the first KV appends / recurrent-state
934            // reads run on the per-stage streams — no event orders them, and under
935            // deferred readback the stage streams are hot immediately (a memset tail
936            // can zero an already-appended KV row; intermittent, ~1-in-3 gate FAIL).
937            // One context-sync per cache creation kills the class.
938            let cache = crate::cache::Cache::new(e, cfg, max_ctx)?;
939            sync_stages_after_load(e, n_trunk)?;
940            return Ok(cache);
941        }
942    }
943    crate::cache::Cache::new(e, cfg, max_ctx)
944}
945
946/// M2 increment 2 LOAD BARRIER: weight uploads and decode-mirror builds enqueue on the
947/// loading engines' WORKER streams; the first consumer launches on a DIFFERENT stream
948/// with no load->decode event — the door-off reference walk on the primary worker
949/// stream (sharded load: remote builds still in flight), or a fresh per-stage stream.
950/// The 2026-08-02 gate finds (n2-dev01 step-0 168k-logit graze; split5 ref=0.0 head —
951/// a half-built rp4 mirror — poisoning step-0 KV and every later step): one
952/// context-wide synchronize per stage at load end kills the class. No-op when the door
953/// is shut at load (single-stream load+decode is ordered by the stream itself).
954pub fn sync_stages_after_load(e: &Engine, n_trunk: usize)
955                              -> Result<(), Box<dyn std::error::Error>> {
956    if pp2_streams_off() || pp_cuts(n_trunk).is_none() {
957        return Ok(());
958    }
959    let rt = PpNRt::get(e)?;
960    for s in 0..rt.n_stages() {
961        rt.stages[s].ctx.bind_to_thread()?;
962        unsafe {
963            cudarc::driver::sys::cuCtxSynchronize().result()?;
964        }
965    }
966    e.ctx().bind_to_thread()?;
967    unsafe {
968        cudarc::driver::sys::cuCtxSynchronize().result()?;
969    }
970    Ok(())
971}
972
973/// M2 increment 2 (weight sharding): the engine that should UPLOAD layer `il`'s weights
974/// (and build its decode mirrors) — the owning stage's engine when the door is open with
975/// device placement and sharding not rolled back; else the primary. `il >= n_trunk`
976/// (MTP/NextN blocks) maps to the last stage. The head (output_norm + lm head) belongs
977/// to the last trunk layer's stage — call with `il = n_trunk - 1`.
978pub fn layer_engine<'a>(e: &'a Engine, n_trunk: usize, il: usize)
979                        -> Result<&'a Engine, Box<dyn std::error::Error>> {
980    if pp_shard_off() || pp2_devices_env().is_none() || pp2_streams_off() {
981        return Ok(e);
982    }
983    let Some(fence) = pp_cuts(n_trunk) else { return Ok(e) };
984    let rt = PpNRt::get(e)?;
985    let s = stage_of(&fence, il.min(n_trunk - 1));
986    Ok(rt.engine(s, e))
987}