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 by default; opt-in
21//!     `MEMRA_PP_HOST_BOUNCE=1` uses pinned D2H + H2D instead);
22//!   - the default peer transport grants peer + default-mempool access between EVERY distinct
23//!     pair of devices in use. Host bounce skips those grants and requires sharded weights plus
24//!     stage-local auxiliary buffers so no mapped peer read can bypass the bounced boundary.
25//!
26//! M2 increment 2 (weight sharding): the loader uploads each stage's layer range THROUGH
27//! that stage's engine (`layer_engine`), so weights land on the device that runs them —
28//! the bring-up peer-read placement dies. `output_norm` + lm head load through the LAST
29//! stage's engine; the embed table stays host-side with stage 0. Split-plane/f16 decode
30//! mirrors are built per layer through the owning stage's engine too (the rp4 mirrors ARE
31//! the decode weights on the q8 path — leaving them on dev0 would fake the kill).
32//! Rollback seam: `MEMRA_PP_SHARD=0` = M1 bring-up placement (all weights on primary,
33//! remote stages peer-read).
34//!
35//! M2 increment 3 (deferred readback — the pipelining seed): `PendingLogits` — the eager
36//! decode arm can END a step without the logits D2H (`decode_step_h_ppn_deferred`): the
37//! logits stay device-resident with a completion event; `wait()` drains them through a
38//! DEDICATED readback stream (waits the event, copies, syncs) so tokens t+1.. keep
39//! enqueuing on the stage streams while token t drains. Per-token math is fully
40//! event-ordered (same slots, same ev_tx/ev_rx chain) — scheduling changes, math does
41//! not; the pipelined replay arm of `ppn-gate` proves bit-identity per step.
42//!
43//! Ownership across a boundary (unchanged from M1):
44//!   - hidden state [n_embd] f32 is the ONLY tensor that crosses;
45//!   - KV/linear-attn cache entries are per-layer: stage s exclusively owns cache state
46//!     for its layer range (and, under MEMRA_PP_DEVICES, allocates it on its device);
47//!   - position/rope state is the scalar `cache.pos` snapshot taken once per step; every stage
48//!     uploads its own position buffer on its own stream (no cross-device position pointer);
49//!   - the embed table lives with stage 0, output_norm + lm head with the last stage.
50//!
51//! THE MULTI-STREAM LAW (why this is safe with cudarc event tracking disabled): all
52//! cross-stage bytes flow through the persistent boundary slots, ordered by ev_tx/ev_rx;
53//! per-stage scratch is allocated AND freed on that stage's stream (stream-ordered); the
54//! async mem pool runs with opportunistic reuse OFF + internal dependencies ON
55//! (memra-runtime), so a block freed on stream A and reused on stream B carries a
56//! driver-inserted dependency. Weights are load-time state no stage stream can precede,
57//! and the step's terminal logits readback (sync D2H, or PendingLogits' event-ordered
58//! readback stream) drains the last stage, whose TX-wait chain transitively drains all.
59//!
60//! Scope: plain eager decode only (generic arm N-stage; gemma4 arm 2-stage). NOT wired:
61//! batch/dc/graph/spec loops and the gemma4-E4B eager arm.
62//!
63//! CORRECTION (pp2-hardening 2026-08-06): this header used to add "(`warn_unwired_once`
64//! fires)" to that list, which was wrong. `warn_unwired_once` has exactly two call sites
65//! and BOTH are gemma4-specific (decode.rs, hybrid_forward.rs) — the batch/dc/graph/spec
66//! loops never warned. Worse, the batched loop did not merely run unsplit: it walked the
67//! whole trunk on the primary stream and, under a sharded cross-device placement,
68//! peer-read every remote stage's weights each step — 28x slower at B=1 with all three
69//! `decode-batch-gate` gates PASSING (peer reads are byte-exact, so only perf broke).
70//! `decode_step_batch` now FAILS CLOSED in that regime via `pp_sharded_cross_device()`
71//! (`MEMRA_PP_ALLOW_UNSPLIT_BATCH=1` = measurement override). "Unwired" for dc/graph/spec
72//! still means "runs unsplit, silently" — audit each before trusting it on a pair.
73
74use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
75use std::sync::{Arc, Mutex, OnceLock};
76
77use cudarc::driver::{CudaContext, CudaEvent, CudaSlice, CudaStream};
78
79use crate::Engine;
80
81/// Returns the stage fence iff the ppN door is open: `MEMRA_PP_STAGES=N` (N >= 2) with a
82/// valid cut list. The fence has N+1 entries: `[0, c1, .., cN-1, n_layers]`; stage s runs
83/// layers `[fence[s], fence[s+1])`. Reads the environment on every call (gates toggle the
84/// door in-process); the cost is a few getenv per decode step, eager-loop noise.
85pub fn pp_cuts(n_layers: usize) -> Option<Vec<usize>> {
86    let n_st: usize = match std::env::var("MEMRA_PP_STAGES") {
87        Ok(v) if v.is_empty() || v == "0" || v == "1" => return None,
88        Ok(v) => match v.parse::<usize>() {
89            Ok(n) => n,
90            Err(_) => {
91                warn_bad_once(&format!("MEMRA_PP_STAGES={v} unparseable; door stays OFF"));
92                return None;
93            }
94        },
95        Err(_) => return None,
96    };
97    if n_st < 2 || n_st > n_layers {
98        warn_bad_once(&format!(
99            "MEMRA_PP_STAGES={n_st} outside [2, n_layers={n_layers}]; door stays OFF"
100        ));
101        return None;
102    }
103    let mut fence = Vec::with_capacity(n_st + 1);
104    fence.push(0usize);
105    if let Ok(s) = std::env::var("MEMRA_PP_SPLITS") {
106        let parts: Result<Vec<usize>, _> =
107            s.split(',').map(|p| p.trim().parse::<usize>()).collect();
108        match parts {
109            Ok(cuts) if cuts.len() == n_st - 1 => fence.extend(cuts),
110            _ => {
111                warn_bad_once(&format!(
112                    "MEMRA_PP_SPLITS={s} invalid (want {} comma-separated cuts); door stays OFF",
113                    n_st - 1
114                ));
115                return None;
116            }
117        }
118    } else if let Ok(v) = std::env::var("MEMRA_PP_SPLIT") {
119        // N=2 back-compat spelling. With N>2 a single split is ambiguous — fail the door
120        // loudly rather than guess (a silent even-split would fake a gate config).
121        if n_st != 2 {
122            warn_bad_once(&format!(
123                "MEMRA_PP_SPLIT={v} set with MEMRA_PP_STAGES={n_st}; use MEMRA_PP_SPLITS \
124                 for N>2 — door stays OFF"
125            ));
126            return None;
127        }
128        match v.parse::<usize>() {
129            Ok(c) => fence.push(c),
130            Err(_) => {
131                warn_bad_once(&format!("MEMRA_PP_SPLIT={v} unparseable; door stays OFF"));
132                return None;
133            }
134        }
135    } else {
136        for s in 1..n_st {
137            fence.push(s * n_layers / n_st);
138        }
139    }
140    fence.push(n_layers);
141    for w in fence.windows(2) {
142        if w[0] >= w[1] {
143            warn_bad_once(&format!(
144                "pp stage fence {fence:?} not strictly increasing over [0, {n_layers}]; \
145                 door stays OFF"
146            ));
147            return None;
148        }
149    }
150    Some(fence)
151}
152
153/// N=2 back-compat view of the door (the gemma4 arm and `pp2-gate` are 2-stage): `Some(cut)`
154/// iff the door is open with EXACTLY two stages.
155pub fn pp2_split(n_layers: usize) -> Option<usize> {
156    pp_cuts(n_layers).filter(|f| f.len() == 3).map(|f| f[1])
157}
158
159/// The stage that owns layer `il` under `fence` (see `pp_cuts`).
160pub fn stage_of(fence: &[usize], il: usize) -> usize {
161    debug_assert!(fence.len() >= 2);
162    match fence[1..fence.len() - 1].binary_search(&il) {
163        // fence[1..][k] == il means il is the FIRST layer of stage k+1
164        Ok(k) => k + 1,
165        Err(k) => k,
166    }
167}
168
169/// MEMRA_PP_STREAMS=0: rollback to the increment-1 same-stream seam (boundary = two plain
170/// dtod copies on the ambient compute stream, no per-stage streams/events/devices).
171pub fn pp2_streams_off() -> bool {
172    matches!(std::env::var("MEMRA_PP_STREAMS").as_deref(), Ok("0"))
173}
174
175/// True iff the ppN door would put TWO OR MORE stage streams on ONE device (devices
176/// unset = all stages on the primary; or an explicit placement with a repeated device).
177/// The deferred-readback (pipelined) arm is REFUSED in this regime: the 2026-08-02 x20
178/// soak record — singledev pipelined 13/20 PASS default, 7 failures each diverging at a
179/// different step (timing-race signature); MEMRA_PDL=0 went 20/20 on one soak but a
180/// second same-config soak on the auto-gated build failed 2/20 (n2) and battery-4 failed
181/// n4 — so PDL narrows the window without closing it, and the true root cause (same
182/// Engine kernels concurrent on two streams of one device) is NOT fixed by any flag yet.
183/// Cross-device pipelined (one stage stream per device) is 23/23 clean post-fix. Refuse
184/// loudly rather than return silently-wrong logits. Env-only read (callable pre-runtime).
185pub fn pp_multi_stream_same_device() -> bool {
186    let stages_open = std::env::var("MEMRA_PP_STAGES")
187        .map(|v| v.parse::<usize>().map(|n| n >= 2).unwrap_or(false))
188        .unwrap_or(false);
189    let devices = std::env::var("MEMRA_PP_DEVICES").ok().filter(|v| !v.is_empty());
190    if (!stages_open && devices.is_none()) || pp2_streams_off() {
191        return false;
192    }
193    match devices {
194        None => true, // door open, no placement: every stage stream lands on the primary
195        Some(s) => {
196            let mut v: Vec<&str> = s.split(',').map(|p| p.trim()).collect();
197            let n = v.len();
198            v.sort_unstable();
199            v.dedup();
200            v.len() < n // repeated device = shared-device streams
201        }
202    }
203}
204
205/// True iff the ppN door is open AND the placement spans 2+ DISTINCT devices AND the
206/// per-stage sharded loader is on — i.e. some layers' weights live on a device other than
207/// the primary. Any path that walks the WHOLE trunk on one stream in this regime reads
208/// those weights over PCIe every step. Env-only read (callable pre-runtime).
209///
210/// Measured cost of doing that (pp2-hardening 2026-08-06, 2x RTX PRO 6000, PCIe Gen5 x16
211/// P2P, decode-batch-bench q9, N=5 interleaved, `research/pp2-hardening-20260806`):
212/// **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)**.
213/// The same sweep with `MEMRA_PP_SHARD=0` (weights all home) returns 178.5/491.1/656.6 —
214/// identical to the single-device door-open arm — so the entire cliff is the peer read,
215/// not the door and not the placement plumbing. Exactness is NOT the issue: peer reads
216/// return identical bytes and every `decode-batch-gate` gate PASSED on this config, which
217/// is precisely why it needs a refusal rather than a gate.
218pub fn pp_sharded_cross_device() -> bool {
219    let stages_open = std::env::var("MEMRA_PP_STAGES")
220        .map(|v| v.parse::<usize>().map(|n| n >= 2).unwrap_or(false))
221        .unwrap_or(false);
222    // MEMRA_PP_STREAMS=0 (2026-08-06, pp2-batch): the same-stream rollback seam ALSO turns
223    // the sharded loader off — `layer_engine` returns the primary engine whenever
224    // `pp2_streams_off()`, and `new_cache` skips `Cache::new_ppn` on the same condition. So
225    // in that regime every weight and every cache is home on the primary and an unsplit walk
226    // peer-reads NOTHING. Without this term the guard refused that config too: a spurious
227    // refusal of a placement that is sound and full-speed. Found wiring the batched pp arm.
228    if !stages_open || pp_shard_off() || pp2_streams_off() {
229        return false;
230    }
231    match pp2_devices_env() {
232        None => false, // no placement: every stage is the primary device, nothing remote
233        Some(s) => {
234            let mut v: Vec<&str> = s.split(',').map(|p| p.trim()).collect();
235            v.sort_unstable();
236            v.dedup();
237            v.len() >= 2
238        }
239    }
240}
241
242/// The shared fail-closed guard for EVERY decode path that has no pp stage split.
243/// Returns `Err` iff `pp_sharded_cross_device()` — i.e. the caller would walk the whole
244/// trunk on one stream while some layers' weights live on another device, peer-reading
245/// them every step. `path` names the refusing function so the operator knows which loop
246/// they hit; `alt` names the working alternative for that loop.
247///
248/// One helper rather than four copies because the audit found FOUR paths with the same
249/// hole (`decode_step_batch`, `decode_step_dc`, the graph capture that wraps dc, and
250/// `decode_step_t*` verify), and a per-path copy is how one gets missed on the next
251/// addition. Override: `MEMRA_PP_ALLOW_UNSPLIT_BATCH=1` (one door for all of them —
252/// they are the same measurement question).
253pub fn refuse_unsplit_if_remote(path: &str, alt: &str) -> Result<(), Box<dyn std::error::Error>> {
254    if pp_host_bounce_active() {
255        return Err(format!(
256            "{path}: refused with MEMRA_PP_HOST_BOUNCE=1 on sharded cross-device PP — \
257             this unsplit path peer-reads remote weights, while host bounce covers only \
258             explicit stage-boundary transfers. Use {alt}; the \
259             MEMRA_PP_ALLOW_UNSPLIT_BATCH override is unavailable on a broken-peer host."
260        )
261        .into());
262    }
263    if pp_sharded_cross_device()
264        && std::env::var("MEMRA_PP_ALLOW_UNSPLIT_BATCH").as_deref() != Ok("1")
265    {
266        return Err(format!(
267            "{path}: refused with the ppN door open across 2+ devices — this path has no pp \
268             stage split, so it would walk ALL layers on one stream and peer-read every \
269             remote stage's weights each step (measured 28x slower at B=1, 13.9x at B=8 on \
270             a PRO 6000 pair over PCIe Gen5 x16 P2P; research/pp2-hardening-20260806). \
271             Exactness is unaffected — peer reads return identical bytes and the exactness \
272             gates PASS on this config — which is exactly why it must refuse instead of \
273             being caught by a gate. Fixes, in order: {alt}; or MEMRA_PP_SHARD=0 (all \
274             weights home on the primary — full speed, forfeits the capacity PP-2 exists \
275             for); or close the pp door. MEMRA_PP_ALLOW_UNSPLIT_BATCH=1 overrides for \
276             measurement."
277        )
278        .into());
279    }
280    Ok(())
281}
282
283/// MEMRA_BATCH_PP=0: rollback/A-B seam for the BATCHED stage split (pp2-batch 2026-08-06).
284/// Default ON — with the ppN door open the batched decode step takes its own stage split
285/// (`decode_step_batch_ppn`) exactly as the eager step does. Setting 0 sends the batched
286/// path back through the unsplit body, which under a sharded cross-device placement is
287/// then caught by `refuse_unsplit_if_remote` (the 28x peer-read regime) rather than run
288/// silently. Exists so the bit-identity gate can A/B split vs unsplit IN ONE PROCESS
289/// against the same loaded weights — read per step, never memoized, for that reason.
290pub fn batch_pp_on() -> bool {
291    std::env::var("MEMRA_BATCH_PP").as_deref() != Ok("0")
292}
293
294/// MEMRA_PRIME_PP=0: rollback/A-B seam for the PRIME (chunked prefill) stage split
295/// (lane/pp-leverb 2026-08-08). Default ON — with the ppN door open the chunked prime takes
296/// its own per-stage range walk exactly as the eager/batched/verify steps do. Setting 0 sends
297/// prime back through the unsplit whole-trunk walk. NOTE: unlike batch/dc/graph/spec, prime
298/// keeps NO `refuse_unsplit_if_remote` — its unsplit walk over a sharded placement is the
299/// measured 22% amortized peer-read tax (research/pp-prefill-20260807 anatomy: m=4096
300/// amortizes the weight reads), not the decode 28x cliff, and the unsplit walk IS the
301/// split-vs-unsplit gate's reference arm (`prime-split-gate`), so it must stay callable.
302/// Read per call, never memoized (the gate A/Bs both arms in one process).
303pub fn prime_pp_on() -> bool {
304    std::env::var("MEMRA_PRIME_PP").as_deref() != Ok("0")
305}
306
307/// MEMRA_PRIME_PIPE=0: rollback/A-B seam for the PP-2 PRIME CHUNK PIPELINE
308/// (lane/cx-pipeline-prime 2026-08-08). Default ON when the prime stage split is live;
309/// setting 0 keeps the serial per-chunk stage walk. Read per prime call so the exactness
310/// gate can replay both schedules against one loaded model.
311pub fn prime_pipe_on() -> bool {
312    std::env::var("MEMRA_PRIME_PIPE").as_deref() != Ok("0")
313}
314
315/// SPLIT-LIVENESS COUNTER for the prime stage split: bumped ONCE per prime chunk that
316/// actually executed the per-stage walk. The `prime-split-gate` requires this to ADVANCE
317/// during its split arm — bit-identity of two identical UNSPLIT walks is vacuous, so a gate
318/// that only compared bits would go green while the walker doesn't exist. With the counter,
319/// the gate is RED until the walker lands (the tickinv35 pattern: the gate exists and fails
320/// before the mechanism does). Relaxed ordering: single-threaded host issue, count-only.
321pub static PRIME_SPLIT_CHUNKS: AtomicUsize = AtomicUsize::new(0);
322
323/// Read the split-liveness counter (gate-side).
324pub fn prime_split_chunks() -> usize {
325    PRIME_SPLIT_CHUNKS.load(Ordering::Relaxed)
326}
327
328/// PIPELINE-LIVENESS COUNTER: bumped only when a second PP-2 prime stage enters its layer
329/// walker while the other stage's walker is still active. Step's per-layer router readback
330/// synchronizes the host, so enqueue order alone is not liveness: a single host thread can
331/// call stage 0(N+1) before the stage-1 epilogue and still serialize all trunk computation.
332pub static PRIME_PIPE_OVERLAPS: AtomicUsize = AtomicUsize::new(0);
333
334/// Read the prime-pipeline overlap counter (gate-side).
335pub fn prime_pipe_overlaps() -> usize {
336    PRIME_PIPE_OVERLAPS.load(Ordering::Relaxed)
337}
338
339static PRIME_PIPE_ACTIVE_STAGES: AtomicUsize = AtomicUsize::new(0);
340
341pub(crate) struct PrimePipeStageGuard;
342
343/// Mark one host-driven stage walker active. With PP-2, a transition 1 -> 2 proves the
344/// two device walkers overlap in wall time; exactly one transition is counted per pair.
345pub(crate) fn enter_prime_pipe_stage() -> PrimePipeStageGuard {
346    let active = PRIME_PIPE_ACTIVE_STAGES.fetch_add(1, Ordering::AcqRel);
347    if active > 0 {
348        PRIME_PIPE_OVERLAPS.fetch_add(1, Ordering::Relaxed);
349    }
350    PrimePipeStageGuard
351}
352
353impl Drop for PrimePipeStageGuard {
354    fn drop(&mut self) {
355        let active = PRIME_PIPE_ACTIVE_STAGES.fetch_sub(1, Ordering::AcqRel);
356        debug_assert!(active > 0, "prime pipeline active-stage counter underflow");
357    }
358}
359
360/// Step35 cross-request prime liveness counters (lane/cx-prime-batch, 2026-08-08).
361/// The exactness gate requires BOTH to advance: a successful step35 batch alone is not
362/// sufficient under PP-N if it walked the whole sharded trunk on one stream.
363pub static STEP35_PRIME_BATCHES: AtomicUsize = AtomicUsize::new(0);
364pub static STEP35_PRIME_BATCH_SPLITS: AtomicUsize = AtomicUsize::new(0);
365
366pub fn step35_prime_batches() -> usize {
367    STEP35_PRIME_BATCHES.load(Ordering::Relaxed)
368}
369
370pub fn step35_prime_batch_splits() -> usize {
371    STEP35_PRIME_BATCH_SPLITS.load(Ordering::Relaxed)
372}
373
374/// MEMRA_SPEC_PP=0: rollback/A-B seam for the SPEC VERIFY stage split (pp2-spec 2026-08-06).
375/// Default ON — with the ppN door open the verify forward (`decode_step_t_core_ppn`) takes its
376/// own stage split exactly as the eager and batched steps do. Setting 0 sends verify back through
377/// the unsplit trunk walk, which under a sharded cross-device placement is then caught by
378/// `refuse_unsplit_if_remote` (the 28x peer-read regime) rather than running silently. Exists so
379/// the bit-identity gate can A/B split vs unsplit IN ONE PROCESS against the same loaded weights
380/// — read per verify call, never memoized, for that reason.
381pub fn spec_pp_on() -> bool {
382    std::env::var("MEMRA_SPEC_PP").as_deref() != Ok("0")
383}
384
385/// MEMRA_PP_OVERLAP=1: alternate the double-buffered boundary slots per step (the
386/// pipelining seed). Default OFF — scheduling structure only, never math. Read per step
387/// so gates can A/B in-process.
388pub fn pp2_overlap() -> bool {
389    matches!(std::env::var("MEMRA_PP_OVERLAP").as_deref(), Ok("1"))
390}
391
392/// Broken-peer escape hatch: stage-boundary activations travel through page-locked host
393/// memory instead of `cudaMemcpyPeerAsync`. Default OFF; captured when `PpNRt` is built.
394pub fn pp_host_bounce_on() -> bool {
395    matches!(std::env::var("MEMRA_PP_HOST_BOUNCE").as_deref(), Ok("1"))
396}
397
398/// True when host bounce is the live transport for a sharded cross-device placement.
399/// Callers use this to close paths that still peer-read non-boundary state.
400pub fn pp_host_bounce_active() -> bool {
401    pp_host_bounce_on() && pp_sharded_cross_device()
402}
403
404/// M2 increment 2 rollback seam: MEMRA_PP_SHARD=0 = the M1 bring-up placement (all
405/// weights upload through the primary engine; remote stages peer-read). Default ON —
406/// under MEMRA_PP_DEVICES each stage's layer range uploads through its own engine.
407pub fn pp_shard_off() -> bool {
408    matches!(std::env::var("MEMRA_PP_SHARD").as_deref(), Ok("0"))
409}
410
411/// Raw `MEMRA_PP_DEVICES` (parsed/validated at PpNRt build — a bad string must fail the
412/// decode step loudly, never silently fall back to same-device and fake a gate PASS).
413fn pp2_devices_env() -> Option<String> {
414    std::env::var("MEMRA_PP_DEVICES").ok().filter(|v| !v.is_empty())
415}
416
417static WARNED_BAD: AtomicBool = AtomicBool::new(false);
418fn warn_bad_once(msg: &str) {
419    if !WARNED_BAD.swap(true, Ordering::Relaxed) {
420        eprintln!("[pp] {msg}");
421    }
422}
423
424static WARNED_UNWIRED: AtomicBool = AtomicBool::new(false);
425/// One-time notice when the door is set but the executing path has no pp arm
426/// (M2 wires the generic eager decode at any N and the gemma4 eager arm at N=2).
427pub fn warn_unwired_once(path: &str) {
428    let open = std::env::var("MEMRA_PP_STAGES")
429        .map(|v| !v.is_empty() && v != "0" && v != "1")
430        .unwrap_or(false);
431    if open && !WARNED_UNWIRED.swap(true, Ordering::Relaxed) {
432        eprintln!(
433            "[pp] MEMRA_PP_STAGES set but `{path}` has no pp arm at this N; running unsplit"
434        );
435    }
436}
437
438// ======================================================================================
439//  PpNRt: the M2 transport runtime (per-stage streams, per-boundary events + slots)
440// ======================================================================================
441
442/// One pipeline stage's execution home: device, context, launch stream, and (for a stage
443/// remote to the primary engine's device) a dedicated Engine in that device's primary
444/// context (CUmodules are per-context).
445pub struct StageRt {
446    pub dev: usize,
447    pub ctx: Arc<CudaContext>,
448    pub stream: Arc<CudaStream>,
449    /// `Some` only when `dev` differs from the primary engine's device.
450    engine: Option<Engine>,
451}
452
453/// One boundary slot: a persistent RX-side buffer + its TX/RX completion events.
454/// PERSISTENT because the buffer is written by the TX stage's stream and read by the RX
455/// stage's: a per-step alloc/free would enqueue the free on ONE stream while the other
456/// might still be reading (the cross-stream free hazard) — a never-freed slot cannot race.
457struct BoundarySlot {
458    buf: Mutex<Option<CudaSlice<f32>>>,
459    /// Recorded on the TX stage's stream after the TX copy; RX waits on it. Created in
460    /// the TX stage's context (cuEventRecord requires event ctx == stream ctx).
461    ev_tx: CudaEvent,
462    /// Recorded on the RX stage's stream after the RX copy; the NEXT TX into this slot
463    /// waits on it (write-after-read guard). Created in the RX stage's context. Waiting
464    /// on a never-recorded event is a defined no-op, so step 0 needs no special case.
465    ev_rx: CudaEvent,
466}
467
468/// Boundary b sits between stage b (TX) and stage b+1 (RX). Two slots, alternating per
469/// step under MEMRA_PP_OVERLAP=1 (each boundary counts its own steps — a decode step
470/// crosses every boundary exactly once, so the counters stay in lockstep).
471struct BoundaryRt {
472    slots: [BoundarySlot; 2],
473    step: AtomicUsize,
474    /// true iff stage b and stage b+1 live on different devices (peer transport).
475    cross: bool,
476}
477
478#[derive(Clone, Copy, Debug, PartialEq, Eq)]
479enum BoundaryTransport {
480    Local,
481    Peer,
482    HostBounce,
483}
484
485fn boundary_transport(cross: bool, host_bounce: bool) -> BoundaryTransport {
486    match (cross, host_bounce) {
487        (false, _) => BoundaryTransport::Local,
488        (true, false) => BoundaryTransport::Peer,
489        (true, true) => BoundaryTransport::HostBounce,
490    }
491}
492
493fn host_bounce_capacity(n_embd: usize) -> Result<(usize, usize), String> {
494    if n_embd == 0 {
495        return Err("MEMRA_PP_HOST_BOUNCE needs non-zero model n_embd".into());
496    }
497    let elems = n_embd
498        .checked_mul(crate::cache::PRIME_CHUNK_MAX_TOKENS)
499        .ok_or_else(|| format!("host-bounce element count overflows for n_embd={n_embd}"))?;
500    let bytes = elems
501        .checked_mul(std::mem::size_of::<f32>())
502        .ok_or_else(|| format!("host-bounce byte count overflows for n_embd={n_embd}"))?;
503    Ok((elems, bytes))
504}
505
506/// One bidirectional-DMA staging allocation. `CU_MEMHOSTALLOC_PORTABLE` matters here: the
507/// D2H producer and H2D consumer are in distinct CUDA primary contexts. Cacheable memory is
508/// intentional (rather than cudarc's write-combined pinned slice) because this allocation is
509/// the destination of D2H as well as the source of H2D.
510struct PinnedHostBounce {
511    ptr: *mut f32,
512    len: usize,
513}
514
515unsafe impl Send for PinnedHostBounce {}
516unsafe impl Sync for PinnedHostBounce {}
517
518impl PinnedHostBounce {
519    fn new(len: usize) -> Result<Self, Box<dyn std::error::Error>> {
520        let bytes = len
521            .checked_mul(std::mem::size_of::<f32>())
522            .ok_or("host-bounce pinned allocation size overflow")?;
523        let ptr = unsafe {
524            cudarc::driver::result::malloc_host(
525                bytes,
526                cudarc::driver::sys::CU_MEMHOSTALLOC_PORTABLE,
527            )?
528        } as *mut f32;
529        if ptr.is_null() {
530            return Err("cuMemHostAlloc returned a null host-bounce pointer".into());
531        }
532        Ok(Self { ptr, len })
533    }
534
535    fn prefix(&self, n: usize) -> &[f32] {
536        assert!(n <= self.len, "host-bounce source {n} > capacity {}", self.len);
537        unsafe { std::slice::from_raw_parts(self.ptr, n) }
538    }
539
540    fn prefix_mut(&mut self, n: usize) -> &mut [f32] {
541        assert!(n <= self.len, "host-bounce destination {n} > capacity {}", self.len);
542        unsafe { std::slice::from_raw_parts_mut(self.ptr, n) }
543    }
544}
545
546impl Drop for PinnedHostBounce {
547    fn drop(&mut self) {
548        let _ = unsafe { cudarc::driver::result::free_host(self.ptr.cast()) };
549    }
550}
551
552struct HostBounceRt {
553    n_embd: usize,
554    capacity: usize,
555    slots: Vec<Option<[Mutex<PinnedHostBounce>; 2]>>,
556}
557
558impl HostBounceRt {
559    fn new(n_embd: usize, boundaries: &[BoundaryRt]) -> Result<Self, Box<dyn std::error::Error>> {
560        let (capacity, _) = host_bounce_capacity(n_embd)?;
561        let mut slots = Vec::with_capacity(boundaries.len());
562        for boundary in boundaries {
563            slots.push(if boundary.cross {
564                Some([
565                    Mutex::new(PinnedHostBounce::new(capacity)?),
566                    Mutex::new(PinnedHostBounce::new(capacity)?),
567                ])
568            } else {
569                None
570            });
571        }
572        Ok(Self { n_embd, capacity, slots })
573    }
574
575    fn slot(
576        &self,
577        boundary: usize,
578        slot: usize,
579    ) -> Result<&Mutex<PinnedHostBounce>, Box<dyn std::error::Error>> {
580        self.slots
581            .get(boundary)
582            .and_then(Option::as_ref)
583            .and_then(|slots| slots.get(slot))
584            .ok_or_else(|| format!("host-bounce slot {boundary}:{slot} is not initialized").into())
585    }
586}
587
588pub struct PpNRt {
589    stages: Vec<StageRt>,
590    boundaries: Vec<BoundaryRt>,
591    /// true iff ANY boundary crosses devices.
592    cross_any: bool,
593    /// Captured once at runtime construction; default false preserves the peer transport.
594    host_bounce: bool,
595    /// Lazily allocated after the authoritative model width is known at cache creation.
596    bounce: OnceLock<Result<HostBounceRt, String>>,
597    /// Dedicated readback stream in the LAST stage's context (deferred logits D2H —
598    /// waiting there instead of on the compute stream keeps later tokens enqueuable).
599    readback: Arc<CudaStream>,
600}
601
602/// M1 name kept alive for external callers (`pp-transport-smoke`, receipts, docs).
603pub type Pp2Rt = PpNRt;
604
605static RTN: OnceLock<Result<PpNRt, String>> = OnceLock::new();
606
607impl PpNRt {
608    /// The process-wide transport runtime, built on first use against the primary engine.
609    /// The stage count + device map freeze at first build (one config per process — gates
610    /// run one placement per invocation). Build errors are sticky and loud.
611    pub fn get(e: &Engine) -> Result<&'static PpNRt, Box<dyn std::error::Error>> {
612        RTN.get_or_init(|| Self::build(e).map_err(|err| err.to_string()))
613            .as_ref()
614            .map_err(|s| -> Box<dyn std::error::Error> { s.clone().into() })
615    }
616
617    fn build(e: &Engine) -> Result<PpNRt, Box<dyn std::error::Error>> {
618        let primary_dev = e.ctx().ordinal();
619        // Stage count: MEMRA_PP_DEVICES length wins when set (it IS the placement);
620        // else MEMRA_PP_STAGES; else 2 (the M1 default — pp-transport-smoke runs doorless).
621        let devices: Vec<usize> = match pp2_devices_env() {
622            Some(s) => {
623                let parts: Result<Vec<usize>, _> =
624                    s.split(',').map(|p| p.trim().parse::<usize>()).collect();
625                match parts {
626                    Ok(v) if v.len() >= 2 => v,
627                    _ => {
628                        return Err(format!(
629                            "MEMRA_PP_DEVICES={s} unparseable (want <d0>,..,<dN-1> e.g. 0,1,2,3)"
630                        )
631                        .into())
632                    }
633                }
634            }
635            None => {
636                let n_st = std::env::var("MEMRA_PP_STAGES")
637                    .ok()
638                    .and_then(|v| v.parse::<usize>().ok())
639                    .filter(|&n| n >= 2)
640                    .unwrap_or(2);
641                vec![primary_dev; n_st]
642            }
643        };
644        if let Ok(v) = std::env::var("MEMRA_PP_STAGES") {
645            if let Ok(n) = v.parse::<usize>() {
646                if n >= 2 && n != devices.len() {
647                    return Err(format!(
648                        "MEMRA_PP_DEVICES lists {} devices but MEMRA_PP_STAGES={n} — \
649                         refusing an ambiguous placement",
650                        devices.len()
651                    )
652                    .into());
653                }
654            }
655        }
656        let n_st = devices.len();
657        let cross_any = devices.iter().any(|&d| d != devices[0]);
658        let host_bounce = pp_host_bounce_on();
659        if host_bounce && cross_any {
660            if pp_shard_off() {
661                return Err(
662                    "MEMRA_PP_HOST_BOUNCE=1 refuses MEMRA_PP_SHARD=0: the boundary can bounce, \
663                     but remote stages would still peer-read primary-device weights"
664                        .into(),
665                );
666            }
667            if devices.last().copied() != Some(primary_dev) {
668                return Err(format!(
669                    "MEMRA_PP_HOST_BOUNCE=1 requires the primary engine on the last/head stage \
670                     (primary dev{primary_dev}, placement {devices:?}); otherwise returned \
671                     logits/hidden state remain peer reads"
672                )
673                .into());
674            }
675        }
676
677        // Validate every placement ordinal in both transports. The peer transport additionally
678        // requires peer access both ways; host bounce deliberately skips that capability gate.
679        let mut used: Vec<usize> = devices.clone();
680        used.push(primary_dev);
681        used.sort_unstable();
682        used.dedup();
683        if used.len() > 1 {
684            let n = cudarc::driver::result::device::get_count()? as usize;
685            for &d in &used {
686                if d >= n {
687                    return Err(format!(
688                        "MEMRA_PP_DEVICES={devices:?} but only {n} CUDA device(s) present"
689                    )
690                    .into());
691                }
692            }
693            if !host_bounce {
694                for &a in &used {
695                    for &b in &used {
696                        if a == b {
697                            continue;
698                        }
699                        let da = cudarc::driver::result::device::get(a as i32)?;
700                        let db = cudarc::driver::result::device::get(b as i32)?;
701                        let mut can: i32 = 0;
702                        unsafe {
703                            cudarc::driver::sys::cuDeviceCanAccessPeer(&mut can, da, db).result()?;
704                        }
705                        if can == 0 {
706                            return Err(format!(
707                                "device {a} cannot peer-access device {b} \
708                                 (cuDeviceCanAccessPeer=0); ppN cross-device needs P2P — \
709                                 refusing a silently-staged path"
710                            )
711                            .into());
712                        }
713                    }
714                }
715            }
716        }
717
718        // PER-STAGE ENGINE ISOLATION (2026-08-02 singledev pipelined find): Engine owns
719        // lazily-grown SHARED scratch pools (fa_part_pool, fa_vf16_scratch, argmax
720        // partials, ...) that are stable-pointer by design — safe on one stream, a data
721        // race the moment two stage streams run concurrently through the SAME Engine
722        // (deferred readback, >=2 tokens in flight: token t+1's stage-0 fa memsets the
723        // partials while token t's stage-s fa still reads them — the nondeterministic
724        // all-logits divergence; cross-device arms were immune because remote stages
725        // already got their own Engine). Every stage s>0 gets its OWN Engine even on the
726        // primary device: same CUcontext (primary retain), so the per-context CUmodule
727        // cache makes it cheap; scratch pools are per-Engine, so stages never share.
728        // Stage 0 keeps the primary engine (single-threaded host issue: the only
729        // concurrent user of `e` during a pp walk is stage 0 itself).
730        let mk_stage = |dev: usize, s: usize| -> Result<StageRt, Box<dyn std::error::Error>> {
731            if dev == primary_dev && s == 0 {
732                let ctx = e.ctx().clone();
733                let stream = ctx.new_stream()?;
734                Ok(StageRt { dev, ctx, stream, engine: None })
735            } else {
736                let eng = Engine::new(dev)?;
737                let ctx = eng.ctx().clone();
738                let stream = ctx.new_stream()?;
739                Ok(StageRt { dev, ctx, stream, engine: Some(eng) })
740            }
741        };
742        let mut stages = Vec::with_capacity(n_st);
743        for (s, &d) in devices.iter().enumerate() {
744            stages.push(mk_stage(d, s)?);
745        }
746
747        if used.len() > 1 {
748            if !host_bounce {
749            // A context per distinct device (first stage that lives there; the primary's
750            // context for the primary device).
751            let ctx_of = |d: usize| -> &Arc<CudaContext> {
752                if d == primary_dev {
753                    e.ctx()
754                } else {
755                    &stages.iter().find(|s| s.dev == d).unwrap().ctx
756                }
757            };
758            // Enable peer access BOTH ways for every distinct pair (idempotent;
759            // ALREADY_ENABLED is success).
760            for &a in &used {
761                for &b in &used {
762                    if a == b {
763                        continue;
764                    }
765                    ctx_of(a).bind_to_thread()?;
766                    let rc = unsafe {
767                        cudarc::driver::sys::cuCtxEnablePeerAccess(ctx_of(b).cu_ctx(), 0)
768                    };
769                    use cudarc::driver::sys::cudaError_enum as E;
770                    if rc != E::CUDA_SUCCESS && rc != E::CUDA_ERROR_PEER_ACCESS_ALREADY_ENABLED {
771                        return Err(format!(
772                            "cuCtxEnablePeerAccess(dev{a} -> dev{b}) failed: {rc:?}"
773                        )
774                        .into());
775                    }
776                }
777            }
778            // MEM-POOL access grant (8x box 2026-08-02, M1 cross-device fix #2):
779            // cuCtxEnablePeerAccess does NOT map STREAM-ORDERED POOL allocations, and every
780            // engine buffer/weight goes through the device default pool (cuMemAllocAsync via
781            // cudarc; memra-runtime configures that pool). A stage kernel dereferencing
782            // another device's weights — or a boundary peer TX writing the RX slot — needs
783            // cuMemPoolSetAccess on the OWNING device's default pool for the ACCESSING
784            // device; without it the first remote dereference is CUDA_ERROR_ILLEGAL_ADDRESS
785            // (reported at the next API call in the poisoned context). Grant all pairs.
786            for &owner in &used {
787                for &accessor in &used {
788                    if owner == accessor {
789                        continue;
790                    }
791                    let dev = cudarc::driver::result::device::get(owner as i32)?;
792                    let mut pool: cudarc::driver::sys::CUmemoryPool = std::ptr::null_mut();
793                    unsafe {
794                        cudarc::driver::sys::cuDeviceGetDefaultMemPool(&mut pool, dev).result()?;
795                    }
796                    let desc = cudarc::driver::sys::CUmemAccessDesc {
797                        location: cudarc::driver::sys::CUmemLocation {
798                            type_: cudarc::driver::sys::CUmemLocationType::CU_MEM_LOCATION_TYPE_DEVICE,
799                            id: accessor as i32,
800                        },
801                        flags: cudarc::driver::sys::CUmemAccess_flags::CU_MEM_ACCESS_FLAGS_PROT_READWRITE,
802                    };
803                    let rc = unsafe { cudarc::driver::sys::cuMemPoolSetAccess(pool, &desc, 1) };
804                    if rc != cudarc::driver::sys::cudaError_enum::CUDA_SUCCESS {
805                        return Err(format!(
806                            "cuMemPoolSetAccess(dev{owner} pool -> dev{accessor}) failed: {rc:?}"
807                        )
808                        .into());
809                    }
810                }
811            }
812            // MEM-POOL access grant (8x box 2026-08-02, cross-device fix #2):
813            // cuCtxEnablePeerAccess does NOT map STREAM-ORDERED POOL allocations, and every
814            // engine buffer/weight goes through the device default pool (cuMemAllocAsync via
815            // cudarc; memra-runtime configures that pool). A stage-1 kernel dereferencing
816            // dev0 weights — or the stage-0 peer TX writing dev1's RX slot — needs
817            // cuMemPoolSetAccess on the OWNING device's default pool for the ACCESSING
818            // device; without it the first remote dereference is CUDA_ERROR_ILLEGAL_ADDRESS
819            // (reported at the next API call in the poisoned context). Grant both ways.
820            for (owner, accessor) in [(stages[0].dev, stages[1].dev), (stages[1].dev, stages[0].dev)] {
821                let dev = cudarc::driver::result::device::get(owner as i32)?;
822                let mut pool: cudarc::driver::sys::CUmemoryPool = std::ptr::null_mut();
823                unsafe {
824                    cudarc::driver::sys::cuDeviceGetDefaultMemPool(&mut pool, dev).result()?;
825                }
826                let desc = cudarc::driver::sys::CUmemAccessDesc {
827                    location: cudarc::driver::sys::CUmemLocation {
828                        type_: cudarc::driver::sys::CUmemLocationType::CU_MEM_LOCATION_TYPE_DEVICE,
829                        id: accessor as i32,
830                    },
831                    flags: cudarc::driver::sys::CUmemAccess_flags::CU_MEM_ACCESS_FLAGS_PROT_READWRITE,
832                };
833                let rc = unsafe { cudarc::driver::sys::cuMemPoolSetAccess(pool, &desc, 1) };
834                if rc != cudarc::driver::sys::cudaError_enum::CUDA_SUCCESS {
835                    return Err(format!(
836                        "cuMemPoolSetAccess(dev{owner} pool -> dev{accessor}) failed: {rc:?}"
837                    )
838                    .into());
839                }
840            }
841            // restore the primary context for the caller's subsequent work
842            e.ctx().bind_to_thread()?;
843            eprintln!(
844                "[pp] cross-device transport: {} (cudaMemcpyPeerAsync per cross boundary; \
845                 peer + default-pool access granted all pairs over {used:?}; weight home: {})",
846                devices
847                    .iter()
848                    .enumerate()
849                    .map(|(s, d)| format!("stage{s}=dev{d}"))
850                    .collect::<Vec<_>>()
851                    .join(" "),
852                if pp_shard_off() {
853                    format!("dev{primary_dev} (MEMRA_PP_SHARD=0 bring-up placement)")
854                } else {
855                    "per-stage (sharded loader)".to_string()
856                }
857            );
858            } else {
859                e.ctx().bind_to_thread()?;
860                eprintln!(
861                    "[pp] cross-device transport: {} (HOST-STAGED pinned D2H -> H2D per cross \
862                     boundary; MEMRA_PP_HOST_BOUNCE=1; peer access and peer-pool grants \
863                     bypassed; weight home: per-stage (sharded loader))",
864                    devices
865                        .iter()
866                        .enumerate()
867                        .map(|(s, d)| format!("stage{s}=dev{d}"))
868                        .collect::<Vec<_>>()
869                        .join(" "),
870                );
871            }
872        }
873
874        let mk_slot = |tx: &StageRt, rx: &StageRt| -> Result<BoundarySlot, Box<dyn std::error::Error>> {
875            Ok(BoundarySlot {
876                buf: Mutex::new(None),
877                ev_tx: tx.ctx.new_event(None)?,
878                ev_rx: rx.ctx.new_event(None)?,
879            })
880        };
881        let mut boundaries = Vec::with_capacity(n_st - 1);
882        for b in 0..n_st - 1 {
883            let (tx, rx) = (&stages[b], &stages[b + 1]);
884            boundaries.push(BoundaryRt {
885                slots: [mk_slot(tx, rx)?, mk_slot(tx, rx)?],
886                step: AtomicUsize::new(0),
887                cross: tx.dev != rx.dev,
888            });
889        }
890        let readback = stages[n_st - 1].ctx.new_stream()?;
891        Ok(PpNRt {
892            stages,
893            boundaries,
894            cross_any,
895            host_bounce,
896            bounce: OnceLock::new(),
897            readback,
898        })
899    }
900
901    pub fn n_stages(&self) -> usize {
902        self.stages.len()
903    }
904
905    /// True iff any boundary crosses devices.
906    pub fn cross_device(&self) -> bool {
907        self.cross_any
908    }
909
910    /// Allocate both pinned slots for every cross-device boundary exactly once. `new_cache`
911    /// calls this with the GGUF model geometry before the first forward; the fixed 4096-token
912    /// prime cap makes the largest payload `4096 * n_embd * sizeof(f32)` without hard-coding a
913    /// model width here.
914    pub fn init_host_bounce(
915        &self,
916        e: &Engine,
917        n_embd: usize,
918    ) -> Result<(), Box<dyn std::error::Error>> {
919        if !self.host_bounce || !self.cross_any {
920            return Ok(());
921        }
922        e.ctx().bind_to_thread()?;
923        let result = self.bounce.get_or_init(|| {
924            HostBounceRt::new(n_embd, &self.boundaries)
925                .map(|rt| {
926                    let bytes = rt.capacity * std::mem::size_of::<f32>();
927                    eprintln!(
928                        "[pp] host-bounce staging ready: n_embd={n_embd} max_tokens={} \
929                         slot_bytes={bytes} slots_per_cross_boundary=2",
930                        crate::cache::PRIME_CHUNK_MAX_TOKENS,
931                    );
932                    rt
933                })
934                .map_err(|err| err.to_string())
935        });
936        let bounce = result
937            .as_ref()
938            .map_err(|err| -> Box<dyn std::error::Error> { err.clone().into() })?;
939        if bounce.n_embd != n_embd {
940            return Err(format!(
941                "host-bounce runtime initialized for n_embd={} but model requests n_embd={n_embd}; \
942                 one PP runtime supports one model geometry per process",
943                bounce.n_embd,
944            )
945            .into());
946        }
947        Ok(())
948    }
949
950    fn bounce_rt(&self) -> Result<&HostBounceRt, Box<dyn std::error::Error>> {
951        self.bounce
952            .get()
953            .ok_or_else(|| -> Box<dyn std::error::Error> {
954                "MEMRA_PP_HOST_BOUNCE=1 staging was not initialized from model geometry".into()
955            })?
956            .as_ref()
957            .map_err(|err| -> Box<dyn std::error::Error> { err.clone().into() })
958    }
959
960    /// The engine a stage's subgraph must run through: the primary engine when the stage
961    /// lives on the primary device, else the stage's own (remote-context) engine.
962    pub fn engine<'a>(&'a self, s: usize, primary: &'a Engine) -> &'a Engine {
963        self.stages[s].engine.as_ref().unwrap_or(primary)
964    }
965
966    /// Bind this OS thread to stage `s`'s CUDA context before issuing work there.
967    pub fn bind_stage(&self, s: usize) -> Result<(), Box<dyn std::error::Error>> {
968        self.stages[s].ctx.bind_to_thread()?;
969        Ok(())
970    }
971
972    /// Enter stage `s`: until the guard drops, every engine op on this thread launches on
973    /// the stage's stream (memra_runtime ambient-stream override).
974    pub fn enter(&self, s: usize) -> memra_runtime::StreamOverride {
975        memra_runtime::push_stream_override(self.stages[s].stream.clone())
976    }
977
978    /// Allocate/grow BOTH slots for a boundary before pipelined issue starts. `tx()` can
979    /// grow a slot lazily, but first-use ordering requires synchronizing the RX stream
980    /// after that allocation. If slot 1 first grows after stage 1 of chunk N has already
981    /// been queued, that sync drains chunk N and erases the only overlap in a two-chunk
982    /// prime. Prewarming both slots pays the same one-time sync before either stage starts.
983    pub fn prepare_overlap_slots(&self, b: usize, n: usize)
984                                 -> Result<(), Box<dyn std::error::Error>> {
985        let bd = &self.boundaries[b];
986        let s_rx = &self.stages[b + 1].stream;
987        let mut grew = false;
988        for sl in &bd.slots {
989            let mut guard = sl.buf.lock().unwrap();
990            if guard.as_ref().map(|bf| bf.len() < n).unwrap_or(true) {
991                *guard = Some(s_rx.alloc_zeros::<f32>(n)?);
992                grew = true;
993            }
994        }
995        if grew {
996            s_rx.synchronize()?;
997        }
998        Ok(())
999    }
1000
1001    /// Boundary TX at boundary `b` (call within the stage-`b` scope; `x` = the
1002    /// materialized [n] residual): wait for the slot's previous RX (write-after-read
1003    /// guard), copy `x` into the slot's persistent buffer via the boundary's transport on
1004    /// stage-b's stream (the owning-stream/publication law), record ev_tx. Returns the
1005    /// slot index for the paired rx().
1006    ///
1007    /// `n` is the PAYLOAD ELEMENT COUNT, not a fixed model constant: the eager arm passes
1008    /// `n_embd` (one row), the batched arm passes `b_n * n_embd` (B stacked rows, the
1009    /// [B, n_embd] boundary). The slot buffer is GROW-ONLY and the transport moves exactly
1010    /// the first `n` elements — batched serving changes B every tick (chunk fill), and a
1011    /// realloc-on-every-size-change would host-sync the RX stream per width change (see the
1012    /// SLOT FIRST-USE ORDERING note below for why each allocation needs that sync). Growing
1013    /// to the high-water mark makes the syncs O(distinct widths) instead of O(width changes).
1014    pub fn tx(&self, b: usize, x: &CudaSlice<f32>, n: usize)
1015              -> Result<usize, Box<dyn std::error::Error>> {
1016        assert_eq!(x.len(), n, "pp tx: residual length mismatch");
1017        let bd = &self.boundaries[b];
1018        let slot_idx = if pp2_overlap() {
1019            bd.step.fetch_add(1, Ordering::Relaxed) % 2
1020        } else {
1021            0
1022        };
1023        self.tx_slot(b, x, n, slot_idx)
1024    }
1025
1026    /// Pipelined boundary TX: always alternate the shared double-buffer slots, independent
1027    /// of the decode-side `MEMRA_PP_OVERLAP` experiment flag. The boundary-local atomic
1028    /// keeps concurrent callers on one slot sequence rather than each restarting at A.
1029    pub fn tx_pipelined(&self, b: usize, x: &CudaSlice<f32>, n: usize)
1030                        -> Result<usize, Box<dyn std::error::Error>> {
1031        assert_eq!(x.len(), n, "pp tx: residual length mismatch");
1032        let slot_idx = self.boundaries[b].step.fetch_add(1, Ordering::Relaxed) % 2;
1033        self.tx_slot(b, x, n, slot_idx)
1034    }
1035
1036    fn tx_slot(&self, b: usize, x: &CudaSlice<f32>, n: usize, slot_idx: usize)
1037               -> Result<usize, Box<dyn std::error::Error>> {
1038        debug_assert!(slot_idx < 2);
1039        let bd = &self.boundaries[b];
1040        let sl = &bd.slots[slot_idx];
1041        let s_tx = &self.stages[b].stream;
1042        s_tx.wait(&sl.ev_rx)?;
1043        let mut guard = sl.buf.lock().unwrap();
1044        if guard.as_ref().map(|bf| bf.len() < n).unwrap_or(true) {
1045            // allocated on the RX stage's stream: the buffer lives on the RX device.
1046            let s_rx = &self.stages[b + 1].stream;
1047            *guard = Some(s_rx.alloc_zeros::<f32>(n)?);
1048            // SLOT FIRST-USE ORDERING (2026-08-02 pipelined-gate find): the lazy alloc's
1049            // pool-alloc + memset enqueue on the RX stream; the TX copy below issues on
1050            // the TX stream, and on a slot's FIRST use ev_rx has never been recorded —
1051            // nothing orders them. With >=2 tokens in flight the RX stream is still busy
1052            // with the previous token, the memset lands AFTER the TX copy, and the
1053            // boundary residual is zeroed (window=1 passed, window>=2 failed at the
1054            // slot-1 first-use step; -overlap arms passed because the synchronous serial
1055            // arm pre-warmed both slots). Host-sync the RX stream once per slot
1056            // allocation — at most 2*(N-1) one-time syncs per process, all during prime.
1057            s_rx.synchronize()?;
1058        }
1059        let buf = guard.as_mut().unwrap();
1060        match boundary_transport(bd.cross, self.host_bounce) {
1061            BoundaryTransport::Local => s_tx.memcpy_dtod(x, buf)?,
1062            BoundaryTransport::HostBounce => {
1063                let bounce = self.bounce_rt()?;
1064                if n > bounce.capacity {
1065                    return Err(format!(
1066                        "pp host-bounce payload {n} exceeds geometry-sized capacity {} \
1067                         (n_embd={}, max prime tokens={})",
1068                        bounce.capacity,
1069                        bounce.n_embd,
1070                        crate::cache::PRIME_CHUNK_MAX_TOKENS,
1071                    )
1072                    .into());
1073                }
1074                let mut host = bounce.slot(b, slot_idx)?.lock().unwrap();
1075                // D2H is issued on the producing stage's stream. ev_tx below publishes the
1076                // completed host bytes to the receiving stream; the exact prefix avoids moving
1077                // a full 64 MiB slot for a one-row decode, and no peer pointer is formed here.
1078                s_tx.memcpy_dtoh(x, host.prefix_mut(n))?;
1079            }
1080            BoundaryTransport::Peer => {
1081                // cudaMemcpyPeerAsync (M0: 2.8x NCCL at PP activation sizes), issued on the
1082                // publishing TX stream with explicit src/dst contexts.
1083                use cudarc::driver::{DevicePtr, DevicePtrMut};
1084                let (sp, _g0) = x.device_ptr(s_tx);
1085                let (dp, _g1) = buf.device_ptr_mut(s_tx);
1086                self.stages[b].ctx.bind_to_thread()?;
1087                unsafe {
1088                    cudarc::driver::result::memcpy_peer_async(
1089                        self.stages[b + 1].ctx.cu_ctx(),
1090                        dp,
1091                        self.stages[b].ctx.cu_ctx(),
1092                        sp,
1093                        n * std::mem::size_of::<f32>(),
1094                        s_tx.cu_stream(),
1095                    )?;
1096                }
1097            }
1098        }
1099        sl.ev_tx.record(s_tx)?;
1100        Ok(slot_idx)
1101    }
1102
1103    /// Boundary RX at boundary `b` (call within the stage-`b+1` scope): wait on the slot's
1104    /// ev_tx, copy the boundary buffer into a fresh working buffer (dtod on the RX stream —
1105    /// local on the RX device in both transports), record ev_rx. The returned buffer is
1106    /// RX-stage-owned: allocated, consumed, and eventually freed on that stage's stream.
1107    pub fn rx(&self, b: usize, slot_idx: usize, n: usize)
1108              -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
1109        let sl = &self.boundaries[b].slots[slot_idx];
1110        let s_rx = &self.stages[b + 1].stream;
1111        s_rx.wait(&sl.ev_tx)?;
1112        let mut guard = sl.buf.lock().unwrap();
1113        let buf = guard.as_mut().expect("pp rx before tx");
1114        assert!(buf.len() >= n, "pp rx: slot holds {} < requested {n}", buf.len());
1115        if boundary_transport(self.boundaries[b].cross, self.host_bounce)
1116            == BoundaryTransport::HostBounce
1117        {
1118            let bounce = self.bounce_rt()?;
1119            let host = bounce.slot(b, slot_idx)?.lock().unwrap();
1120            let mut dst = buf.slice_mut(0..n);
1121            // The destination stream already waits ev_tx, so this H2D cannot observe the
1122            // staging slot before the source stream's D2H completes.
1123            s_rx.memcpy_htod(host.prefix(n), &mut dst)?;
1124        }
1125        // uninit working buffer (fully overwritten by the copy), allocated explicitly on
1126        // the stage stream so rx() is correct even outside an enter() scope.
1127        let mut work = unsafe { s_rx.alloc::<f32>(n)? };
1128        // Slice the slot to the payload: the buffer is grow-only (see tx), so at a narrower
1129        // width it is LONGER than `work` and cudarc's memcpy_dtod (dst.len() >= src.len())
1130        // would assert. The paired tx wrote exactly these first n elements.
1131        s_rx.memcpy_dtod(&buf.slice(0..n), &mut work)?;
1132        sl.ev_rx.record(s_rx)?;
1133        Ok(work)
1134    }
1135
1136    /// PUBLISH a DEVICE-RESIDENT result off the last stage to the caller's stream
1137    /// (lane/pp2-spec 2026-08-06).
1138    ///
1139    /// Every ppN body before this one returned HOST values — `decode_step_h_ppn` and
1140    /// `decode_step_batch_ppn` both `dtoh` inside the last-stage scope, and a dtoh on the
1141    /// producing stream is self-ordering. The verify trunk is the FIRST ppN body whose
1142    /// contract is device-resident output (`decode_step_t_h_emb_dev` exists precisely so the
1143    /// accept walk argmaxes on-device instead of moving T x n_vocab f32 per round), and
1144    /// device slices carry no stream affinity: the caller resumes on the PRIMARY stream and
1145    /// dereferences buffers whose producing kernels are still queued on the last stage's
1146    /// stream. Nothing orders them.
1147    ///
1148    /// Why this only ever failed on ONE device: with stages on separate devices the caller's
1149    /// first touch is a cross-device copy that the driver orders against the source context,
1150    /// and the readback path syncs. Two streams on the SAME device genuinely overlap, so the
1151    /// primary stream reads a buffer whose matmul has not run — nondeterministic garbage
1152    /// (measured: NaN, 3155.677, and 2.87e-5 where the reference had -2.0048926), and it
1153    /// poisons the NEXT arm in the same process because the corrupted KV persists. This is
1154    /// the same class as the SLOT FIRST-USE ORDERING find above, one level up: there the
1155    /// unordered pair was alloc-memset vs TX copy, here it is stage-N compute vs the
1156    /// caller's consumer.
1157    ///
1158    /// Fix = the boundary law applied to the exit: record an event on the producing stage
1159    /// stream, make the caller's stream wait on it. Event-wait, not a device sync, so the
1160    /// stage streams keep running for the deferred-readback arm. Call INSIDE the last-stage
1161    /// scope, after the last enqueue, with the caller's (pre-`enter`) stream.
1162    pub fn publish_to(&self, s: usize, dst: &Arc<CudaStream>)
1163                      -> Result<(), Box<dyn std::error::Error>> {
1164        let st = &self.stages[s];
1165        // Same stream (STREAMS=0 rollback, or a caller already on the stage stream): the
1166        // stream orders itself; recording+waiting would be a no-op with a stray event.
1167        if Arc::ptr_eq(&st.stream, dst) {
1168            return Ok(());
1169        }
1170        let ev = st.ctx.new_event(None)?;
1171        ev.record(&st.stream)?;
1172        dst.wait(&ev)?;
1173        Ok(())
1174    }
1175
1176    /// REVERSE PUBLICATION (#87 root cause, lane/pp2spec-crash 2026-08-07): order every
1177    /// STAGE stream behind the CALLER's stream — the mirror of `publish_to`.
1178    ///
1179    /// `publish_to` orders caller READS behind stage COMPUTE. Nothing ordered the other
1180    /// direction: buffers ALLOCATED on a stage stream (the verify's returned logits/hidden,
1181    /// the VerifyCkpt stashes) are CONSUMED by kernels the caller enqueues on the PRIMARY
1182    /// stream, and when they drop, cudarc enqueues `free_async` on the ALLOCATING (stage)
1183    /// stream. With event tracking elided (the decode-path default) the drop carries no
1184    /// read-guard, so the pool can hand the block to the NEXT stage-stream allocation and
1185    /// its writes overwrite memory the queued primary-stream consumer has not read yet.
1186    /// Measured (research/pp2spec-crash-20260807): the spec round-seed read 13/4096 NaN =
1187    /// the uninitialized-bits signature (P(NaN|random u32) ~ 1/256), clean by host re-read
1188    /// time — a read-before-write race, fatal via the argmax-sentinel -> embed_gather MMU
1189    /// fault, and gated on c>=2 because a backed-up primary stream widens the window.
1190    ///
1191    /// Fix law: before a ppN body enqueues NEW stage-stream work (allocations that may
1192    /// reuse freed blocks), every stage stream waits the caller's stream at its current
1193    /// point. All primary consumers of the previous round's stage-allocated buffers are
1194    /// enqueued by then (single host thread), so reuse-writes land strictly after them.
1195    /// Call at ppN-body ENTRY with the pre-`enter` caller stream. Door-shut configs never
1196    /// build a PpNRt, so single-card behavior is untouched.
1197    pub fn fence_stages_behind(&self, src: &Arc<CudaStream>)
1198                               -> Result<(), Box<dyn std::error::Error>> {
1199        let ev = src.context().new_event(None)?;
1200        ev.record(src)?;
1201        for st in &self.stages {
1202            if Arc::ptr_eq(&st.stream, src) {
1203                continue;
1204            }
1205            st.stream.wait(&ev)?;
1206        }
1207        Ok(())
1208    }
1209
1210    /// Deferred readback: record a fresh completion event on the LAST stage's stream
1211    /// (call after the step's logits matmul has been enqueued there).
1212    pub fn record_done(&self) -> Result<CudaEvent, Box<dyn std::error::Error>> {
1213        let last = &self.stages[self.stages.len() - 1];
1214        let ev = last.ctx.new_event(None)?;
1215        ev.record(&last.stream)?;
1216        Ok(ev)
1217    }
1218
1219    /// The dedicated readback stream (last stage's context).
1220    pub fn readback_stream(&self) -> &Arc<CudaStream> {
1221        &self.readback
1222    }
1223}
1224
1225/// M2 increment 3: a step's logits, still device-resident on the LAST stage. `wait()`
1226/// orders the readback stream behind the step's completion event, copies, and syncs —
1227/// tokens enqueued after this step keep running on the stage streams while the caller
1228/// drains token t. Dropping without waiting is safe (buffers free stream-ordered).
1229pub struct PendingLogits {
1230    logits: CudaSlice<f32>,
1231    ev: CudaEvent,
1232    rb: Arc<CudaStream>,
1233}
1234
1235impl PendingLogits {
1236    pub fn new(logits: CudaSlice<f32>, ev: CudaEvent, rb: Arc<CudaStream>) -> Self {
1237        PendingLogits { logits, ev, rb }
1238    }
1239
1240    /// Blocks until this step's logits are computed, returns them host-side. Only this
1241    /// step's work is waited on (event-ordered) — NOT later tokens already enqueued on
1242    /// the stage streams.
1243    pub fn wait(self) -> Result<Vec<f32>, Box<dyn std::error::Error>> {
1244        self.rb.wait(&self.ev)?;
1245        let host = self.rb.clone_dtoh(&self.logits)?;
1246        self.rb.synchronize()?;
1247        // logits drop AFTER the sync: the D2H has fully completed, so the stream-ordered
1248        // free on the compute stream cannot race the copy.
1249        Ok(host)
1250    }
1251}
1252
1253/// Stage-owned cache allocation door: when the ppN door is open AND `MEMRA_PP_DEVICES`
1254/// is set (placement plumbing), each layer's cache is allocated by its OWNING stage's
1255/// engine — on one device this is byte-for-byte today's allocation (gated); cross-device
1256/// it puts each stage's KV on that stage's HBM. Door shut or devices unset: plain
1257/// `Cache::new` (zero behavior change). Trailing MTP/NextN layers (beyond the trunk)
1258/// map to the LAST stage.
1259pub fn new_cache(e: &Engine, cfg: &memra_gguf::config::ModelConfig, max_ctx: usize)
1260                 -> Result<crate::cache::Cache, Box<dyn std::error::Error>> {
1261    let n_trunk = (cfg.n_layer - cfg.nextn_predict_layers) as usize;
1262    if let Some(fence) = pp_cuts(n_trunk) {
1263        if pp2_devices_env().is_some() && !pp2_streams_off() {
1264            let rt = PpNRt::get(e)?;
1265            rt.init_host_bounce(e, cfg.n_embd as usize)?;
1266            let n_st = fence.len() - 1;
1267            assert_eq!(
1268                rt.n_stages(), n_st,
1269                "PpNRt stage count {} != fence stages {n_st}", rt.n_stages()
1270            );
1271            // #87 REVERSE PUBLICATION at ADMISSION (lane/pp2spec-crash): this is the one
1272            // stage-stream allocation site OUTSIDE the ppN step bodies — a NEW session's
1273            // KV alloc_zeros enqueue on the STAGE streams, and their pool blocks can be
1274            // reuse of buffers freed from ANOTHER session's in-flight verify whose
1275            // primary-stream reads are still queued (the c=2 residual: exactly one trap
1276            // per admission collision, round 0, after the step-body fences landed).
1277            // Order the stage streams behind the caller before the memsets can clobber.
1278            // Anatomy: `PpNRt::fence_stages_behind`.
1279            rt.fence_stages_behind(&e.stream())?;
1280            let devs: Vec<&dyn memra_kv::KvDev> =
1281                (0..n_st).map(|s| rt.engine(s, e) as &dyn memra_kv::KvDev).collect();
1282            let cache = crate::cache::Cache::new_ppn(&devs, &fence, cfg, max_ctx)?;
1283            sync_stages_after_load(e, n_trunk)?;
1284            return Ok(cache);
1285        }
1286        if !pp2_streams_off() {
1287            // CACHE BIRTH BARRIER (2026-08-02 pipelined-arm residual race): with the door
1288            // open but no device placement, Cache::new's alloc_zeros memsets enqueue on
1289            // the PRIMARY worker stream while the first KV appends / recurrent-state
1290            // reads run on the per-stage streams — no event orders them, and under
1291            // deferred readback the stage streams are hot immediately (a memset tail
1292            // can zero an already-appended KV row; intermittent, ~1-in-3 gate FAIL).
1293            // One context-sync per cache creation kills the class.
1294            let cache = crate::cache::Cache::new(e, cfg, max_ctx)?;
1295            sync_stages_after_load(e, n_trunk)?;
1296            return Ok(cache);
1297        }
1298    }
1299    crate::cache::Cache::new(e, cfg, max_ctx)
1300}
1301
1302/// M2 increment 2 LOAD BARRIER: weight uploads and decode-mirror builds enqueue on the
1303/// loading engines' WORKER streams; the first consumer launches on a DIFFERENT stream
1304/// with no load->decode event — the door-off reference walk on the primary worker
1305/// stream (sharded load: remote builds still in flight), or a fresh per-stage stream.
1306/// The 2026-08-02 gate finds (n2-dev01 step-0 168k-logit graze; split5 ref=0.0 head —
1307/// a half-built rp4 mirror — poisoning step-0 KV and every later step): one
1308/// context-wide synchronize per stage at load end kills the class. No-op when the door
1309/// is shut at load (single-stream load+decode is ordered by the stream itself).
1310pub fn sync_stages_after_load(e: &Engine, n_trunk: usize)
1311                              -> Result<(), Box<dyn std::error::Error>> {
1312    if pp2_streams_off() || pp_cuts(n_trunk).is_none() {
1313        return Ok(());
1314    }
1315    let rt = PpNRt::get(e)?;
1316    for s in 0..rt.n_stages() {
1317        rt.stages[s].ctx.bind_to_thread()?;
1318        unsafe {
1319            cudarc::driver::sys::cuCtxSynchronize().result()?;
1320        }
1321    }
1322    e.ctx().bind_to_thread()?;
1323    unsafe {
1324        cudarc::driver::sys::cuCtxSynchronize().result()?;
1325    }
1326    Ok(())
1327}
1328
1329/// M2 increment 2 (weight sharding): the engine that should UPLOAD layer `il`'s weights
1330/// (and build its decode mirrors) — the owning stage's engine when the door is open with
1331/// device placement and sharding not rolled back; else the primary. `il >= n_trunk`
1332/// (MTP/NextN blocks) maps to the last stage. The head (output_norm + lm head) belongs
1333/// to the last trunk layer's stage — call with `il = n_trunk - 1`.
1334pub fn layer_engine<'a>(e: &'a Engine, n_trunk: usize, il: usize)
1335                        -> Result<&'a Engine, Box<dyn std::error::Error>> {
1336    if pp_shard_off() || pp2_devices_env().is_none() || pp2_streams_off() {
1337        return Ok(e);
1338    }
1339    let Some(fence) = pp_cuts(n_trunk) else { return Ok(e) };
1340    let rt = PpNRt::get(e)?;
1341    let s = stage_of(&fence, il.min(n_trunk - 1));
1342    Ok(rt.engine(s, e))
1343}
1344
1345/// Restore a cache checkpoint through each layer's owning engine.
1346///
1347/// `source = None` is an in-place rewind: the target already owns the append-only KV bytes and
1348/// only its lengths plus recurrent state move back to the snapshot. `Some(source)` restores into
1349/// a freshly allocated larger cache: checkpoint-valid KV rows are copied from the parked cache,
1350/// while recurrent state always comes from the checkpoint's owned device copies.
1351///
1352/// This cannot use `Cache::rollback(e, ...)` under cross-device PP: a single primary engine is
1353/// not the owner of every stage's cache buffers. The rare rewind/grow boundary synchronizes open
1354/// PP contexts before publishing the restored cache to the next request.
1355pub fn restore_cache_checkpoint(
1356    e: &Engine,
1357    cfg: &memra_gguf::config::ModelConfig,
1358    source: Option<&crate::cache::Cache>,
1359    target: &mut crate::cache::Cache,
1360    snap: &crate::cache::CacheSnapshot,
1361) -> Result<(), Box<dyn std::error::Error>> {
1362    let n = target.kv.len();
1363    if target.recur.len() != n
1364        || snap.kv_len.len() != n
1365        || snap.conv.len() != n
1366        || snap.ssm.len() != n
1367        || source.is_some_and(|s| s.kv.len() != n || s.recur.len() != n)
1368    {
1369        return Err("checkpoint cache layer-count mismatch".into());
1370    }
1371    if snap.pos > target.max_ctx {
1372        return Err(format!(
1373            "checkpoint pos {} exceeds target capacity {}",
1374            snap.pos, target.max_ctx,
1375        )
1376        .into());
1377    }
1378
1379    let n_trunk = (cfg.n_layer - cfg.nextn_predict_layers) as usize;
1380    for il in 0..n {
1381        let owner = layer_engine(e, n_trunk, il)?;
1382        let src_kv = source.map(|s| &s.kv[il]);
1383        match (src_kv, target.kv[il].as_mut(), snap.kv_len[il]) {
1384            (Some(Some(src)), Some(dst), Some(len)) => {
1385                if len > src.len || len > target.max_ctx {
1386                    return Err(format!(
1387                        "checkpoint layer {il} len {len} exceeds source {} or target {}",
1388                        src.len, target.max_ctx,
1389                    )
1390                    .into());
1391                }
1392                if src.kv_dim_k != dst.kv_dim_k
1393                    || src.kv_dim_v != dst.kv_dim_v
1394                    || src.k_tok_bytes != dst.k_tok_bytes
1395                    || src.v_tok_bytes != dst.v_tok_bytes
1396                {
1397                    return Err(format!("checkpoint KV layout mismatch at layer {il}").into());
1398                }
1399                let kb = len * src.k_tok_bytes;
1400                let vb = len * src.v_tok_bytes;
1401                if kb > 0 {
1402                    owner.copy_u8_into(&mut dst.k, 0, &src.k, kb)?;
1403                }
1404                if vb > 0 {
1405                    owner.copy_u8_into(&mut dst.v, 0, &src.v, vb)?;
1406                }
1407                dst.len = len;
1408                owner.set_i32_one(&mut dst.len_d, len as i32)?;
1409            }
1410            (None, Some(dst), Some(len)) => {
1411                if len > dst.len || len > target.max_ctx {
1412                    return Err(format!(
1413                        "checkpoint layer {il} len {len} exceeds live {} or target {}",
1414                        dst.len, target.max_ctx,
1415                    )
1416                    .into());
1417                }
1418                dst.len = len;
1419                owner.set_i32_one(&mut dst.len_d, len as i32)?;
1420            }
1421            (Some(None), None, None) | (None, None, None) => {}
1422            _ => return Err(format!("checkpoint KV kind mismatch at layer {il}").into()),
1423        }
1424
1425        match (
1426            target.recur[il].as_mut(),
1427            &snap.conv[il],
1428            &snap.ssm[il],
1429        ) {
1430            (Some(dst), Some(conv), Some(ssm)) => {
1431                if conv.len() != dst.conv_state.len() || ssm.len() != dst.ssm_state.len() {
1432                    return Err(
1433                        format!("checkpoint recurrent layout mismatch at layer {il}").into(),
1434                    );
1435                }
1436                owner.copy_into(&mut dst.conv_state, 0, conv, conv.len())?;
1437                owner.copy_into(&mut dst.ssm_state, 0, ssm, ssm.len())?;
1438            }
1439            (None, None, None) => {}
1440            _ => {
1441                return Err(
1442                    format!("checkpoint recurrent kind mismatch at layer {il}").into(),
1443                );
1444            }
1445        }
1446    }
1447    target.pos = snap.pos;
1448
1449    // Open PP uses per-stage streams/contexts; publish every restored plane before the caller
1450    // starts the next prime. Door-shut single-stream restores remain naturally ordered.
1451    sync_stages_after_load(e, n_trunk)?;
1452    if source.is_some() {
1453        // A grown cache replaces and drops the source immediately after this returns. Bound the
1454        // D2D copies first so an async-pool free cannot recycle a source plane prematurely.
1455        e.stream().synchronize()?;
1456    }
1457    Ok(())
1458}
1459
1460#[cfg(test)]
1461mod host_bounce_tests {
1462    use super::{boundary_transport, host_bounce_capacity, BoundaryTransport};
1463
1464    #[test]
1465    fn transport_selection_keeps_peer_default_and_bounces_only_cross_device() {
1466        assert_eq!(boundary_transport(false, false), BoundaryTransport::Local);
1467        assert_eq!(boundary_transport(false, true), BoundaryTransport::Local);
1468        assert_eq!(boundary_transport(true, false), BoundaryTransport::Peer);
1469        assert_eq!(
1470            boundary_transport(true, true),
1471            BoundaryTransport::HostBounce
1472        );
1473    }
1474
1475    #[test]
1476    fn step37_geometry_sizes_each_slot_from_the_prime_cap() {
1477        let (elems, bytes) = host_bounce_capacity(4096).expect("valid Step-3.7 geometry");
1478        assert_eq!(elems, 4096 * crate::cache::PRIME_CHUNK_MAX_TOKENS);
1479        assert_eq!(bytes, 64 * 1024 * 1024);
1480    }
1481
1482    #[test]
1483    fn host_bounce_capacity_rejects_invalid_or_overflowing_geometry() {
1484        assert!(host_bounce_capacity(0).is_err());
1485        assert!(host_bounce_capacity(usize::MAX).is_err());
1486    }
1487}