memra_engine/tp_transport.rs
1//! TP-N TRANSPORT — `MEMRA_TP_TRANSPORT` (born lane/glm5-tp-transport 2026-09-01 as
2//! `MEMRA_GLM5_TP_TRANSPORT`, rank-widened by lane/glm5-composition 2026-09-01, generalized
3//! lane/glm5-extract2; the family alias stays honored).
4//!
5//! ENGINE-GENERIC BY CONSTRUCTION, and this is a claim about content rather than a hope: not
6//! one function below knows a layer type, a mixer, an expert or a model family. The whole
7//! module is rank indices, publication events, and named point-to-point hop shapes over dense
8//! blocks. The glm5 TP-N shard walk is today's ONLY CONSUMER (it owns its rank geometry, its
9//! owner-first law and its shard maps); an hy3/step TP walk will arm the same transport once
10//! it exists, and `pp.rs`'s `BoundarySlot` is where the shape came from.
11//!
12//! WHAT THIS IS. The data-movement layer under a TP execution program (`MEMRA_GLM5_TP` is the
13//! first). The TP
14//! seam's arithmetic is column-parallel-over-gather: every cross-rank hop is PURE MOVEMENT
15//! (the one arithmetic site is the MoE slot-ordered combine, and it stays on root). So the
16//! transport is swappable WITHOUT changing a single bit, and this module is the seam where
17//! the swap happens — one named function per hop shape, two arms behind each:
18//!
19//! * `host-canonical` (v1, the DEFAULT): every hop is `dtoh` -> host buffer -> `htod`.
20//! `Engine::dtoh` ends in `stream().synchronize()`, so each leg is a full stream drain
21//! plus a PCIe host round trip. This is what every banked glm5 TP number was measured
22//! on (`research/glm53-flash-bringup-20260827/tp2-battery-20260831/RESULTS.md` cell 2:
23//! "v1 transport = host-canonical (every boot announces `transport=host-canonical` on
24//! all four seams)").
25//! * `peer-pull`: the hop is a DEVICE copy issued on the CONSUMER's stream, reading the
26//! producer's buffer, ordered by a publication event. Zero host legs, zero stream
27//! drains. PULL and not push by design (see the transport laws below).
28//!
29//! RANK WIDENING (lane/glm5-composition). The 2026-09-01 original of this module hardcoded
30//! two ranks (`Rank::Root`/`Rank::Peer`). Ranks are now plain indices (`0` = root, the
31//! owner-first law unchanged) and every hop shape takes per-rank parts; at two ranks each
32//! arm reproduces the v1 walk HOP FOR HOP (same primitive count, same order, same census),
33//! which is what keeps the banked TP-2 numbers comparable across this widening. The
34//! publication link carries one pub event and one release event PER RANK — `cuEventRecord`
35//! overwrites and `cuStreamWaitEvent` captures state at call time, so the record-then-wait
36//! pairs a single host thread issues in program order stay correct at any rank count.
37//!
38//! WHY PULL. On the RTX PRO 6000 PCIe fabric a peer transport's direction is not symmetric
39//! and the consumer-issued shape wins twice:
40//!
41//! 1. `research/pro6000-multicard-research-20260901/RESEARCH.md` §2.2b measures
42//! "approximately 52 GB/s for peer reads and 2.6 GB/s for peer writes, so all three
43//! communication phases use pull transfers" (b12x#139, 16x PRO 6000) — 110 us push vs
44//! 26 us pull for the same collective. That asymmetry is scoped to SM-ISSUED
45//! (kernel load/store) traffic; a copy-engine `cuMemcpyDtoDAsync` measures ~54-56 GB/s
46//! in BOTH directions (§2.2, §3.2 `read_ce`/`write_ce`). We use the copy engine, so the
47//! 20x is NOT our headline reason — it is the reason a future fused collective must be
48//! pull-shaped, and the reason we never build the push twin.
49//! 2. The ORDERING reason, which IS our headline reason and is specific to our tax. When
50//! the consumer issues the copy, the consuming kernel on that same stream is ordered
51//! after it for free. A push (producer-issued, the `pp.rs` `BoundaryTransport::Peer`
52//! shape) needs a second event + wait to publish into the consumer's stream. Our
53//! measured tax is a ROUND-TRIP and LAUNCH count tax, not a byte tax
54//! (RESEARCH.md §2.6, §6.3c; three upstream maintainers agree, §1.5g), so removing
55//! launches per hop is the lever, and pull removes one ordering primitive per hop.
56//!
57//! NO ATOMICS, ANYWHERE. Every SM120 PCIe pair reports `NativeAtomicSupported=0`
58//! (RESEARCH.md §2.1): "CAS on peer memory silently loses barrier tokens under PCIe load"
59//! and it fails LOAD-DEPENDENTLY, so microbenchmarks pass. This transport uses only
60//! copy-engine copies and CUDA events — no peer flag polling, no compare-and-swap, no
61//! payload-value sentinels (RESEARCH.md §6.13: a payload-sentinel prototype caused a
62//! launch-day production stall). If a fused device-resident collective is ever built here,
63//! §6.13's protocol is pre-specified and its bug list is pre-registered.
64//!
65//! WHAT THIS IS NOT. This is not a collective. There is no reduce anywhere in the glm5 TP
66//! program (that is what makes decode BYTE identity the gate bar instead of a tolerance
67//! band), so there is no all-reduce to fuse, no ring, no barrier. Everything here is a
68//! point-to-point copy of a dense block plus a byte-preserving strided placement.
69//!
70//! FAIL-CLOSED. Arming `peer-pull` runs a byte-integrity pull ladder through the EXACT
71//! primitive the walk uses, in EVERY ordered rank pair, before any layer is sharded; a
72//! single differing byte refuses the load by name. That is a TRANSFER-tier check by
73//! RESEARCH.md §2.4's ladder and it is honest about it: it proves our copy path moves the
74//! right bytes, and it does NOT prove a kernel peer-dereference works (only a
75//! `simpleP2P`-class KERNEL peer read does — banked separately as the lane's
76//! `peer-read-probe.cu`, run by `HEALTH.sh` at every box window).
77//!
78//! Engagement markers: the TAG IS THE CALLER'S (the phase-1 `spec_phase` pattern) — glm5
79//! passes `"glm5-tp-transport"`, so every banked receipt line from lane/glm5-tp-transport and
80//! lane/glm5-composition keeps its exact bytes while a second family gets its own marker.
81//! Counters below are the per-token instrument and are the TRANSPORT's, not the family's.
82
83// lane/clippy-zero-restore-20260901: this transport's exact code shape is receipt-bound
84// (lane/glm5-composition keeps its exact bytes, header above); index loops stay as gated.
85#![allow(clippy::needless_range_loop)]
86
87use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
88
89use cudarc::driver::{CudaEvent, CudaSlice};
90
91use crate::Engine;
92
93/// Rank index. `0` is root (the model's own engine — the owner-first rank law); `1..ranks`
94/// are the peer engines in `MEMRA_GLM5_TP` device order.
95pub const ROOT: usize = 0;
96
97// ------------------------------------------------------------------------------------------
98// Flag
99// ------------------------------------------------------------------------------------------
100
101/// Which transport the armed `MEMRA_GLM5_TP` seam moves its bytes with.
102#[derive(Clone, Copy, PartialEq, Eq, Debug)]
103pub enum TpTransport {
104 /// v1: `dtoh` -> host -> `htod` per hop. Every banked glm5 TP number.
105 HostCanonical,
106 /// Consumer-issued device peer copy per hop, ordered by a publication event.
107 PeerPull,
108}
109
110impl TpTransport {
111 /// The receipt spelling that goes in every announce line and every gate log.
112 pub fn name(self) -> &'static str {
113 match self {
114 Self::HostCanonical => "host-canonical",
115 Self::PeerPull => "peer-pull",
116 }
117 }
118}
119
120/// The general fleet flag.
121pub const TRANSPORT_ENV: &str = "MEMRA_TP_TRANSPORT";
122/// The family alias lane/glm5-tp-transport shipped. Still honored — its gate arms, its box
123/// scripts and every banked transport receipt set it. Never silently dead.
124pub const TRANSPORT_ENV_GLM5: &str = "MEMRA_GLM5_TP_TRANSPORT";
125
126/// Parse a transport value. Pure so the law is unit-testable without touching the process
127/// environment. `flag` is the name the operator actually set, so a refusal names it.
128///
129/// DEFAULT IS `host-canonical` (OFF), by decision, not by accident: on the day this flag
130/// landed the peer-pull arm had zero receipts on real peer hardware (the rig is a single
131/// card — `LAW:rig-exactness-only` — so its gate arms run the code path over N contexts on
132/// ONE device and can only prove bit-preservation, never fabric engagement). Unmeasured
133/// behaviour does not default ON. The default flips in the same commit as the box window's
134/// interleaved re-price receipt, and the FLAGS.md row carries both arms plus the rollback
135/// seam (`=0`).
136pub fn parse_transport(flag: &str, value: Option<&str>) -> Result<TpTransport, String> {
137 match value {
138 None | Some("") | Some("0") | Some("host-canonical") => Ok(TpTransport::HostCanonical),
139 Some("1") | Some("peer-pull") => Ok(TpTransport::PeerPull),
140 Some(other) => Err(format!(
141 "{flag}={other:?} is not a known transport \
142 (host-canonical | 0 = the v1 host-staged arm, the default; peer-pull | 1 = the \
143 consumer-issued device peer copy arm)"
144 )),
145 }
146}
147
148/// Resolve the general name and the family alias to ONE armed `(name, value)`, then parse.
149///
150/// This is a VALUED flag resolved ONCE at arm time, not a per-call boolean door, so a
151/// disagreeing pair REFUSES the load naming both names — the `ep_map::resolve_ep_map_env`
152/// stance. (Falling closed, which `lib.rs::alias_door_from` does for per-call doors, is only
153/// correct where an abort would kill live sessions; arming happens before any session exists,
154/// and a load that silently picked a transport the operator did not choose is worse than a
155/// refused load.)
156pub fn resolve_transport(
157 general: Option<&str>,
158 alias: Option<&str>,
159) -> Result<(TpTransport, &'static str), String> {
160 let (armed, value) = match (general, alias) {
161 (Some(g), Some(a)) if g != a => {
162 return Err(format!(
163 "{TRANSPORT_ENV}={g:?} and {TRANSPORT_ENV_GLM5}={a:?} disagree — the alias and \
164 the general flag name ONE transport (unset one); refused rather than silently \
165 picking a precedence winner"
166 ));
167 }
168 (Some(g), _) => (TRANSPORT_ENV, Some(g)),
169 (None, Some(a)) => (TRANSPORT_ENV_GLM5, Some(a)),
170 (None, None) => (TRANSPORT_ENV, None),
171 };
172 Ok((parse_transport(armed, value)?, armed))
173}
174
175/// Every transport name currently SET, for ROLLBACK ADVICE only — never for policy.
176///
177/// Why advice needs this and the armed name is not enough: a disagreeing pair is already
178/// refused at resolve time, so by the time anything can fail we are in one of two states —
179/// exactly one name set, or BOTH set to the same value. In the second state the armed name is
180/// the general one, so telling the operator to zero only that leaves the alias still asking
181/// for `peer-pull`, and THAT disagreement refuses the load. Advice that creates a second
182/// failure is worse than no advice.
183pub fn set_transport_names() -> Vec<&'static str> {
184 [TRANSPORT_ENV, TRANSPORT_ENV_GLM5]
185 .into_iter()
186 .filter(|k| std::env::var_os(k).is_some_and(|v| !v.is_empty()))
187 .collect()
188}
189
190/// Render [`set_transport_names`] as `NAME=0 [NAME=0]` — what to actually type.
191fn rollback_advice() -> String {
192 let names = set_transport_names();
193 if names.is_empty() {
194 return format!("{TRANSPORT_ENV}=0");
195 }
196 names
197 .iter()
198 .map(|n| format!("{n}=0"))
199 .collect::<Vec<_>>()
200 .join(" ")
201}
202
203/// Live read of [`resolve_transport`]. Returns the transport and the ARMED FLAG NAME, so
204/// downstream refusals and peer-access grants cite the flag the operator typed.
205pub fn transport_env() -> Result<(TpTransport, &'static str), String> {
206 resolve_transport(
207 std::env::var(TRANSPORT_ENV).ok().as_deref(),
208 std::env::var(TRANSPORT_ENV_GLM5).ok().as_deref(),
209 )
210}
211
212// ------------------------------------------------------------------------------------------
213// Instrument — the per-token movement census (lane stage 1b)
214// ------------------------------------------------------------------------------------------
215//
216// RESEARCH.md's shortlist item 2 asks for "our per-token collective COUNT and total BYTES on
217// the TP arm" and warns that bytes alone cannot explain the tax. These counters answer both
218// halves from the live walk instead of from arithmetic, in BOTH arms, so an A/B reads the
219// movement change directly rather than inferring it from tok/s.
220
221/// Host legs: one per `dtoh` or `htod` a TP hop performed. Pinned 0 on the peer-pull arm.
222pub static TP_HOST_LEGS: AtomicU64 = AtomicU64::new(0);
223
224/// Host legs that ended in a full stream drain (`Engine::dtoh` synchronizes). This is the
225/// count the 13-18 ms/token v1 join tax reconstructs from. Pinned 0 on the peer-pull arm.
226pub static TP_HOST_SYNCS: AtomicU64 = AtomicU64::new(0);
227
228/// Consumer-issued cross-rank device copies. Pinned 0 on the host-canonical arm.
229pub static TP_PEER_PULLS: AtomicU64 = AtomicU64::new(0);
230
231/// Publication events recorded/awaited to order a cross-rank copy. Pinned 0 on the
232/// host-canonical arm (whose ordering is the host sync itself).
233pub static TP_PUB_EVENTS: AtomicU64 = AtomicU64::new(0);
234
235/// Same-rank device copies a hop issued (own-part placements and dense block moves). Present
236/// in both arms; the host-canonical arm reaches its own part through the host instead.
237pub static TP_LOCAL_COPIES: AtomicU64 = AtomicU64::new(0);
238
239/// Total bytes a TP hop moved across a rank boundary, both arms, counted once per crossing
240/// (a host-canonical hop crosses PCIe twice and is charged twice — that IS its cost).
241pub static TP_XFER_BYTES: AtomicU64 = AtomicU64::new(0);
242
243macro_rules! snapshot_fns {
244 ($($name:ident => $counter:ident),* $(,)?) => {
245 $(
246 /// Snapshot for a gate's before/after delta.
247 pub fn $name() -> u64 { $counter.load(Ordering::Relaxed) }
248 )*
249 };
250}
251
252snapshot_fns! {
253 tp_host_legs => TP_HOST_LEGS,
254 tp_host_syncs => TP_HOST_SYNCS,
255 tp_peer_pulls => TP_PEER_PULLS,
256 tp_pub_events => TP_PUB_EVENTS,
257 tp_local_copies => TP_LOCAL_COPIES,
258 tp_xfer_bytes => TP_XFER_BYTES,
259}
260
261/// One line carrying every movement counter, for a gate log or a box-window receipt.
262pub fn transport_census_line(tag: &str, transport: TpTransport) -> String {
263 format!(
264 "[{tag}] census transport={} host_legs={} host_syncs={} peer_pulls={} \
265 pub_events={} local_copies={} xfer_bytes={}",
266 transport.name(),
267 tp_host_legs(),
268 tp_host_syncs(),
269 tp_peer_pulls(),
270 tp_pub_events(),
271 tp_local_copies(),
272 tp_xfer_bytes(),
273 )
274}
275
276fn charge_host_leg(bytes: usize, sync: bool) {
277 TP_HOST_LEGS.fetch_add(1, Ordering::Relaxed);
278 if sync {
279 TP_HOST_SYNCS.fetch_add(1, Ordering::Relaxed);
280 }
281 TP_XFER_BYTES.fetch_add(bytes as u64, Ordering::Relaxed);
282}
283
284fn charge_peer_pull(bytes: usize) {
285 TP_PEER_PULLS.fetch_add(1, Ordering::Relaxed);
286 TP_XFER_BYTES.fetch_add(bytes as u64, Ordering::Relaxed);
287}
288
289fn charge_local_copy() {
290 TP_LOCAL_COPIES.fetch_add(1, Ordering::Relaxed);
291}
292
293// ------------------------------------------------------------------------------------------
294// The publication link
295// ------------------------------------------------------------------------------------------
296
297/// The peer-pull ordering primitives, built once per TP runtime and REUSED per hop.
298///
299/// One publication event and one release event PER RANK, not per hop or per pair:
300/// `cuEventRecord` overwrites, and `cuStreamWaitEvent` captures the event's state at call
301/// time, so a single host thread issuing record-then-wait in program order gets correct
302/// ordering with `2 * ranks` events total. Creating an event per hop would put a driver
303/// allocation on the per-token path — the exact class of cost this lane exists to remove.
304///
305/// The RELEASE (write-after-read) half is NOT optional. Building this transport with only
306/// the publication half is exactly the defect the rig gate caught on its FIRST run: arm X1
307/// (peer-pull, sequential EP walk) failed DECODE SELF-CONSISTENCY at step 17 — two
308/// repetitions of the same greedy walk diverged — because every per-slot expert row is a
309/// fresh stream-ordered allocation on the peer whose async free was enqueued on the PEER
310/// stream while the ROOT stream's pull was still reading it. The step seam banks the same
311/// hazard as a keepalive comment (`tp.rs:7831-7834`). And note the more valuable half of
312/// that gate run: arm X2 (peer-pull composed with the EP dispatch diet) PASSED EVERY BAR on
313/// the same broken build — a VACUOUS GREEN; the undieted arm is the one that can see it,
314/// which is why both arms are gated and why the sequential walk is not retired.
315pub struct PeerPullLink {
316 /// `pub_ev[r]` is recorded on rank r's stream; awaited by a consumer before it reads
317 /// rank r's memory.
318 pub_ev: Vec<CudaEvent>,
319 /// `rel_ev[r]` is recorded on rank r's stream AFTER it has finished READING another
320 /// rank's buffer; the PRODUCING rank waits on it before proceeding, which is what stops
321 /// its stream-ordered allocator from recycling the buffer the reader's copy engine is
322 /// still reading.
323 rel_ev: Vec<CudaEvent>,
324}
325
326impl PeerPullLink {
327 pub fn new(engines: &[&Engine]) -> Result<Self, Box<dyn std::error::Error>> {
328 let mut pub_ev = Vec::with_capacity(engines.len());
329 let mut rel_ev = Vec::with_capacity(engines.len());
330 for e in engines {
331 pub_ev.push(e.ctx().new_event(None)?);
332 rel_ev.push(e.ctx().new_event(None)?);
333 }
334 Ok(Self { pub_ev, rel_ev })
335 }
336}
337
338/// Everything a hop needs: the rank engines (index = rank, `[0]` = root), the transport arm,
339/// and (on the peer-pull arm) the publication link. Built by the caller per hop from the TP
340/// runtime, so this module never has to own the runtime type and the borrow shapes at the
341/// call sites stay exactly as they are today.
342pub struct Hop<'a> {
343 pub engines: Vec<&'a Engine>,
344 pub transport: TpTransport,
345 pub link: Option<&'a PeerPullLink>,
346}
347
348impl<'a> Hop<'a> {
349 pub fn ranks(&self) -> usize {
350 self.engines.len()
351 }
352
353 pub fn engine(&self, r: usize) -> &'a Engine {
354 self.engines[r]
355 }
356
357 fn link(&self) -> Result<&'a PeerPullLink, Box<dyn std::error::Error>> {
358 self.link.ok_or_else(|| {
359 "glm5-tp transport: the peer-pull arm is armed without a publication link \
360 (runtime construction bug — refused rather than issuing an unordered peer read)"
361 .into()
362 })
363 }
364
365 /// Order `consumer`'s stream after everything `producer` has enqueued. One event record
366 /// plus one cross-context wait; NO host boundary.
367 ///
368 /// THE DESIGN DELTA vs the step seam, stated because it is the whole point of this lane.
369 /// `tp.rs`'s native-P2P pull sites fence the producer with a host
370 /// `engine.stream().synchronize()` — see the "PRODUCER FENCE (2026-08-20 flake fix)"
371 /// comments at `tp.rs:3543` and `tp.rs:2636`. That is correct and it is also a full
372 /// stream drain, i.e. it keeps the exact cost class this lane exists to delete: a
373 /// native-P2P arm built that way still pays one host sync per hop and would read as
374 /// "native P2P did not help". This link instead uses the `pp.rs` `BoundarySlot` ordering
375 /// contract (`ev_tx.record(s_tx)` then `s_rx.wait(&ev_tx)`, `pp.rs:2647`/`pp.rs:2678`),
376 /// which the hy3 PP-4 qualification closed at 50/50 fresh processes and 200/200 runtime
377 /// probes with zero byte mismatch on this exact card class — and which the tp2 lane
378 /// already cherry-picked onto this line (`tp2-20260831/LANE.md` stage 1). So: PULL
379 /// direction from `tp.rs`, EVENT ordering from `pp.rs`, host syncs from neither.
380 fn publish(&self, producer: usize, consumer: usize) -> Result<(), Box<dyn std::error::Error>> {
381 let link = self.link()?;
382 let ev = &link.pub_ev[producer];
383 {
384 let prod = self.engine(producer);
385 let _main = prod.gpu.enter_main()?;
386 ev.record(&prod.stream())?;
387 }
388 {
389 let cons = self.engine(consumer);
390 let _main = cons.gpu.enter_main()?;
391 cons.stream().wait(ev)?;
392 }
393 TP_PUB_EVENTS.fetch_add(2, Ordering::Relaxed);
394 Ok(())
395 }
396
397 /// The write-after-read half: order `producer`'s stream after `consumer` has finished
398 /// reading producer-owned memory, so the producer cannot recycle it under the reader.
399 /// Costs one event record plus one cross-context wait; no host boundary.
400 fn release(&self, producer: usize, consumer: usize) -> Result<(), Box<dyn std::error::Error>> {
401 let link = self.link()?;
402 let ev = &link.rel_ev[consumer];
403 {
404 let cons = self.engine(consumer);
405 let _main = cons.gpu.enter_main()?;
406 ev.record(&cons.stream())?;
407 }
408 {
409 let prod = self.engine(producer);
410 let _main = prod.gpu.enter_main()?;
411 prod.stream().wait(ev)?;
412 }
413 TP_PUB_EVENTS.fetch_add(2, Ordering::Relaxed);
414 Ok(())
415 }
416
417 /// The one cross-rank primitive: `consumer`'s stream copies `n` f32 from the producer's
418 /// `src[src_off..]` into its own `dst[dst_off..]`. A PEER READ by construction — the
419 /// copy is enqueued on the reading side.
420 #[allow(clippy::too_many_arguments)] // allow: (producer, src, src_off) x (consumer, dst, dst_off) x n IS the shape of a cross-rank ranged copy; collapsing it into a struct would hide which side owns which pointer, and that is the one thing a peer copy must never be vague about
421 fn pull_f32(
422 &self,
423 producer: usize,
424 src: &CudaSlice<f32>,
425 src_off: usize,
426 consumer: usize,
427 dst: &mut CudaSlice<f32>,
428 dst_off: usize,
429 n: usize,
430 ) -> Result<(), Box<dyn std::error::Error>> {
431 if src.len() < src_off + n || dst.len() < dst_off + n {
432 return Err(format!(
433 "glm5-tp peer pull geometry: src {}..{} of {} -> dst {}..{} of {}",
434 src_off,
435 src_off + n,
436 src.len(),
437 dst_off,
438 dst_off + n,
439 dst.len(),
440 )
441 .into());
442 }
443 {
444 let consumer_engine = self.engine(consumer);
445 // Rank-local CUDA scope: the consuming rank's context must be current for the copy
446 // to be enqueued on its stream and for the peer source pointer to resolve through
447 // that context's UVA mapping (the `Gpu::enter_main` contract in memra-runtime, and
448 // the discipline every `tp.rs` cross-context copy site follows). The scope CLOSES
449 // before the release fence below, which needs the producer's context current.
450 let _main = consumer_engine.gpu.enter_main()?;
451 let mut view = dst.slice_mut(dst_off..dst_off + n);
452 consumer_engine
453 .stream()
454 .memcpy_dtod(&src.slice(src_off..src_off + n), &mut view)?;
455 }
456 // WRITE-AFTER-READ FENCE. The read above runs on the CONSUMER's stream against memory
457 // the PRODUCER owns. Without this, the producer's stream is free to enqueue that
458 // buffer's async free (or any reuse of the recycled block) while the copy engine is
459 // still reading it — a silent cross-rank corruption that presents as run-to-run
460 // NON-DETERMINISM, which is how the rig gate found it (arm X1, decode
461 // self-consistency, step 17). See [`PeerPullLink`].
462 self.release(producer, consumer)?;
463 charge_peer_pull(n * std::mem::size_of::<f32>());
464 Ok(())
465 }
466}
467
468// ------------------------------------------------------------------------------------------
469// Hop shapes
470// ------------------------------------------------------------------------------------------
471//
472// Four shapes cover every cross-rank movement in the glm5 TP program. The call sites name
473// the shape; the arm lives here. Both arms move the SAME bytes by the same layout rules, so
474// swapping the arm cannot change a bit — which is why the gate bar stays decode BYTE
475// identity across the swap rather than a band.
476
477/// FAN-OUT: replicate root's `src` into a fresh buffer on EVERY peer rank. Returns the peer
478/// buffers indexed `[rank - 1]` (the `rt.peers` convention).
479///
480/// The mixer input `x`/`h` and the MoE activation `z` take this shape. Host-canonical pays
481/// ONE draining `dtoh` plus one `htod` per peer (identical to v1 at two ranks); peer-pull
482/// pays one publication chain and one peer read per peer.
483pub fn fanout_f32(
484 hop: &Hop<'_>,
485 src: &CudaSlice<f32>,
486 n: usize,
487) -> Result<Vec<CudaSlice<f32>>, Box<dyn std::error::Error>> {
488 let bytes = n * std::mem::size_of::<f32>();
489 let mut out = Vec::with_capacity(hop.ranks() - 1);
490 match hop.transport {
491 TpTransport::HostCanonical => {
492 let host = hop.engine(ROOT).dtoh_view(&src.slice(0..n))?;
493 charge_host_leg(bytes, true);
494 for to in 1..hop.ranks() {
495 out.push(hop.engine(to).htod(&host)?);
496 charge_host_leg(bytes, false);
497 }
498 }
499 TpTransport::PeerPull => {
500 for to in 1..hop.ranks() {
501 let mut buf = hop.engine(to).uninit(n)?;
502 hop.publish(ROOT, to)?;
503 hop.pull_f32(ROOT, src, 0, to, &mut buf, 0, n)?;
504 out.push(buf);
505 }
506 }
507 }
508 Ok(out)
509}
510
511/// FAN-OUT to ONE rank: replicate root's `src` into a fresh buffer on rank `to`. The EP
512/// diet's shape (a rank whose routing owns zero pairs receives zero activation bytes — the
513/// placement-map multiplier); at two ranks it is exactly the whole-group fan-out.
514pub fn fanout_f32_to(
515 hop: &Hop<'_>,
516 to: usize,
517 src: &CudaSlice<f32>,
518 n: usize,
519) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
520 let bytes = n * std::mem::size_of::<f32>();
521 match hop.transport {
522 TpTransport::HostCanonical => {
523 let host = hop.engine(ROOT).dtoh_view(&src.slice(0..n))?;
524 charge_host_leg(bytes, true);
525 let out = hop.engine(to).htod(&host)?;
526 charge_host_leg(bytes, false);
527 Ok(out)
528 }
529 TpTransport::PeerPull => {
530 let mut buf = hop.engine(to).uninit(n)?;
531 hop.publish(ROOT, to)?;
532 hop.pull_f32(ROOT, src, 0, to, &mut buf, 0, n)?;
533 Ok(buf)
534 }
535 }
536}
537
538/// FAN-OUT, i32 twin (the MLA position vector). Same shape, same accounting.
539pub fn fanout_i32(
540 hop: &Hop<'_>,
541 src: &CudaSlice<i32>,
542 n: usize,
543) -> Result<Vec<CudaSlice<i32>>, Box<dyn std::error::Error>> {
544 let bytes = n * std::mem::size_of::<i32>();
545 let mut out = Vec::with_capacity(hop.ranks() - 1);
546 match hop.transport {
547 TpTransport::HostCanonical => {
548 let host = hop.engine(ROOT).dtoh_i32(src)?;
549 charge_host_leg(bytes, true);
550 for to in 1..hop.ranks() {
551 out.push(hop.engine(to).htod_i32(&host)?);
552 charge_host_leg(bytes, false);
553 }
554 }
555 TpTransport::PeerPull => {
556 for to in 1..hop.ranks() {
557 let mut buf = hop.engine(to).uninit_i32(n)?;
558 hop.publish(ROOT, to)?;
559 {
560 let consumer = hop.engine(to);
561 let _main = consumer.gpu.enter_main()?;
562 let mut view = buf.slice_mut(0..n);
563 consumer.stream().memcpy_dtod(&src.slice(0..n), &mut view)?;
564 }
565 // The same write-after-read fence `Hop::pull_f32` applies; the i32 twin is
566 // not exempt, and an exemption here would be a hazard that only shows up
567 // under load.
568 hop.release(ROOT, to)?;
569 charge_peer_pull(bytes);
570 out.push(buf);
571 }
572 }
573 }
574 Ok(out)
575}
576
577/// GATHER: reconstruct the FULL token-major `[t, ranks * part]` tensor on EVERY rank from
578/// each rank's dense `[t, part]` shard (`parts[r]` resident on rank r, laid at columns
579/// `r*part..(r+1)*part`). Returns the full tensors indexed by rank.
580///
581/// This is the column-parallel-over-gather join: after it, each rank's `wo` column slice is
582/// a full-K matvec over identical bytes, which is what makes the join pure movement.
583///
584/// The peer-pull arm moves ONE dense block per (producer, consumer) direction and
585/// reconstructs the interleave with `place_rows_strided` — the kernel whose own doc says it
586/// "exists so multi-GPU collectives can move one dense shard per rank and reconstruct the
587/// canonical token-major matrix without issuing one peer copy per token". At `t == 1` the
588/// placement degenerates to a contiguous range and is skipped entirely: the pull lands
589/// straight at its column offset.
590pub fn gather_parts(
591 hop: &Hop<'_>,
592 parts: &[&CudaSlice<f32>],
593 t: usize,
594 part: usize,
595) -> Result<Vec<CudaSlice<f32>>, Box<dyn std::error::Error>> {
596 let ranks = hop.ranks();
597 if t == 0 || part == 0 {
598 return Err("glm5-tp gather: zero geometry".into());
599 }
600 if parts.len() != ranks {
601 return Err(format!("glm5-tp gather: {} parts for {ranks} ranks", parts.len()).into());
602 }
603 let full = ranks * part;
604 let span = t * part;
605 for (r, p) in parts.iter().enumerate() {
606 if p.len() < span {
607 return Err(format!(
608 "glm5-tp gather geometry: part[{r}] {} for t={t} part={part}",
609 p.len()
610 )
611 .into());
612 }
613 }
614 let bytes = span * std::mem::size_of::<f32>();
615 match hop.transport {
616 TpTransport::HostCanonical => {
617 // Drain every rank's part (peers first, root last — v1's order at two ranks),
618 // assemble the token-major full matrix once on host, upload to every rank.
619 let mut hosts: Vec<Option<Vec<f32>>> = (0..ranks).map(|_| None).collect();
620 for r in (0..ranks).rev() {
621 hosts[r] = Some(hop.engine(r).dtoh_view(&parts[r].slice(0..span))?);
622 charge_host_leg(bytes, true);
623 }
624 let mut full_host = vec![0f32; t * full];
625 for (r, h) in hosts.iter().enumerate() {
626 let h = h.as_ref().expect("drained above");
627 for tok in 0..t {
628 full_host[tok * full + r * part..tok * full + (r + 1) * part]
629 .copy_from_slice(&h[tok * part..(tok + 1) * part]);
630 }
631 }
632 let mut out = Vec::with_capacity(ranks);
633 for r in 0..ranks {
634 out.push(hop.engine(r).htod(&full_host)?);
635 charge_host_leg(t * full * std::mem::size_of::<f32>(), false);
636 }
637 Ok(out)
638 }
639 TpTransport::PeerPull => {
640 // Every producer publishes to every consumer (record-then-wait per ordered
641 // pair — the same primitive count as v1's two publish calls at two ranks),
642 // then every consumer pulls each foreign part.
643 for s in 0..ranks {
644 for d in 0..ranks {
645 if s != d {
646 hop.publish(s, d)?;
647 }
648 }
649 }
650 let mut out = Vec::with_capacity(ranks);
651 for d in 0..ranks {
652 let mut full_d = hop.engine(d).uninit(t * full)?;
653 if t == 1 {
654 // Decode: the strided placement IS a contiguous range, so land every
655 // part directly at its column offset and skip the placement kernels.
656 for s in 0..ranks {
657 if s == d {
658 hop.engine(d).copy_range_into(
659 &mut full_d,
660 s * part,
661 parts[s],
662 0,
663 part,
664 )?;
665 charge_local_copy();
666 } else {
667 hop.pull_f32(s, parts[s], 0, d, &mut full_d, s * part, part)?;
668 }
669 }
670 } else {
671 // Prime: one dense block per foreign direction, then byte-preserving
672 // placements.
673 for s in 0..ranks {
674 if s == d {
675 hop.engine(d).place_rows_strided(
676 parts[s],
677 &mut full_d,
678 part,
679 t,
680 full,
681 s * part,
682 )?;
683 charge_local_copy();
684 } else {
685 let mut foreign = hop.engine(d).uninit(span)?;
686 hop.pull_f32(s, parts[s], 0, d, &mut foreign, 0, span)?;
687 hop.engine(d).place_rows_strided(
688 &foreign,
689 &mut full_d,
690 part,
691 t,
692 full,
693 s * part,
694 )?;
695 charge_local_copy();
696 }
697 }
698 }
699 out.push(full_d);
700 }
701 Ok(out)
702 }
703 }
704}
705
706/// CONCAT-ON-ROOT: assemble the mixer output `[t, ranks * part]` on ROOT from the per-rank
707/// column-`wo` slices, rank r's rows at columns `r*part..(r+1)*part`.
708///
709/// Root is the only consumer (the residual stream and mHC are root-owned), so only the
710/// foreign directions cross. Same `t == 1` fast path as [`gather_parts`].
711pub fn concat_parts_on_root(
712 hop: &Hop<'_>,
713 parts: &[&CudaSlice<f32>],
714 t: usize,
715 part: usize,
716) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
717 let ranks = hop.ranks();
718 if t == 0 || part == 0 {
719 return Err("glm5-tp concat: zero geometry".into());
720 }
721 if parts.len() != ranks {
722 return Err(format!("glm5-tp concat: {} parts for {ranks} ranks", parts.len()).into());
723 }
724 let full = ranks * part;
725 let span = t * part;
726 for (r, p) in parts.iter().enumerate() {
727 if p.len() < span {
728 return Err(format!(
729 "glm5-tp concat geometry: part[{r}] {} for t={t} part={part}",
730 p.len()
731 )
732 .into());
733 }
734 }
735 let bytes = span * std::mem::size_of::<f32>();
736 match hop.transport {
737 TpTransport::HostCanonical => {
738 let mut hosts: Vec<Option<Vec<f32>>> = (0..ranks).map(|_| None).collect();
739 for r in (0..ranks).rev() {
740 hosts[r] = Some(hop.engine(r).dtoh_view(&parts[r].slice(0..span))?);
741 charge_host_leg(bytes, true);
742 }
743 let mut out_host = vec![0f32; t * full];
744 for (r, h) in hosts.iter().enumerate() {
745 let h = h.as_ref().expect("drained above");
746 for tok in 0..t {
747 out_host[tok * full + r * part..tok * full + (r + 1) * part]
748 .copy_from_slice(&h[tok * part..(tok + 1) * part]);
749 }
750 }
751 let out = hop.engine(ROOT).htod(&out_host)?;
752 charge_host_leg(t * full * std::mem::size_of::<f32>(), false);
753 Ok(out)
754 }
755 TpTransport::PeerPull => {
756 let mut out = hop.engine(ROOT).uninit(t * full)?;
757 for s in 1..ranks {
758 hop.publish(s, ROOT)?;
759 }
760 if t == 1 {
761 for s in 0..ranks {
762 if s == ROOT {
763 hop.engine(ROOT)
764 .copy_range_into(&mut out, s * part, parts[s], 0, part)?;
765 charge_local_copy();
766 } else {
767 hop.pull_f32(s, parts[s], 0, ROOT, &mut out, s * part, part)?;
768 }
769 }
770 } else {
771 for s in 0..ranks {
772 if s == ROOT {
773 hop.engine(ROOT).place_rows_strided(
774 parts[s],
775 &mut out,
776 part,
777 t,
778 full,
779 s * part,
780 )?;
781 charge_local_copy();
782 } else {
783 let mut foreign = hop.engine(ROOT).uninit(span)?;
784 hop.pull_f32(s, parts[s], 0, ROOT, &mut foreign, 0, span)?;
785 hop.engine(ROOT).place_rows_strided(
786 &foreign,
787 &mut out,
788 part,
789 t,
790 full,
791 s * part,
792 )?;
793 charge_local_copy();
794 }
795 }
796 }
797 Ok(out)
798 }
799 }
800}
801
802/// v1-SHAPE FAN-OUT, part 1: stage the producer's whole block to HOST in one draining leg.
803///
804/// Exists only so the `host-canonical` arm of the SEQUENTIAL EP walk keeps its v1 hop pattern
805/// exactly — one `dtoh` of `z` per layer-call, then one row `htod` per token per consuming
806/// rank. That pattern is what the banked 22.65 tok/s engine-twin number was measured on, and
807/// `MEMRA_GLM5_TP_TRANSPORT=0` has to reproduce it hop-for-hop, not just byte-for-byte, or the
808/// rollback arm silently becomes a different (faster) program and the A/B measures two changes.
809pub fn host_stage_block(
810 hop: &Hop<'_>,
811 from: usize,
812 src: &CudaSlice<f32>,
813 n: usize,
814) -> Result<Vec<f32>, Box<dyn std::error::Error>> {
815 let host = hop.engine(from).dtoh_view(&src.slice(0..n))?;
816 charge_host_leg(n * std::mem::size_of::<f32>(), true);
817 Ok(host)
818}
819
820/// v1-SHAPE FAN-OUT, part 2: upload one staged host row to rank `to` (one non-draining leg).
821pub fn host_row_to(
822 hop: &Hop<'_>,
823 to: usize,
824 row: &[f32],
825) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
826 let out = hop.engine(to).htod(row)?;
827 charge_host_leg(std::mem::size_of_val(row), false);
828 Ok(out)
829}
830
831/// BLOCK-RETURN: land rank `from`'s dense `[rows, width]` block into `dst[dst_off..]` on
832/// root. The EP walk's shape: the dieted arm returns one compact block per (layer-call,
833/// rank); the v1 sequential arm returns one expert row per foreign-owned slot. `n` is
834/// `rows * width`.
835pub fn return_block_to_root(
836 hop: &Hop<'_>,
837 from: usize,
838 blk: &CudaSlice<f32>,
839 dst: &mut CudaSlice<f32>,
840 dst_off: usize,
841 n: usize,
842) -> Result<(), Box<dyn std::error::Error>> {
843 let bytes = n * std::mem::size_of::<f32>();
844 match hop.transport {
845 TpTransport::HostCanonical => {
846 let host = hop.engine(from).dtoh_view(&blk.slice(0..n))?;
847 charge_host_leg(bytes, true);
848 hop.engine(ROOT).htod_f32_into_at(&host, dst, dst_off)?;
849 charge_host_leg(bytes, false);
850 Ok(())
851 }
852 TpTransport::PeerPull => {
853 hop.publish(from, ROOT)?;
854 hop.pull_f32(from, blk, 0, ROOT, dst, dst_off, n)
855 }
856 }
857}
858
859/// BLOCK-RETURN into a FRESH root buffer (the v1 per-slot EP shape, which hands the returned
860/// row straight to `axpy_into`).
861pub fn return_row_to_root(
862 hop: &Hop<'_>,
863 from: usize,
864 y: &CudaSlice<f32>,
865 n: usize,
866) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
867 match hop.transport {
868 TpTransport::HostCanonical => {
869 let host = hop.engine(from).dtoh_view(&y.slice(0..n))?;
870 charge_host_leg(n * std::mem::size_of::<f32>(), true);
871 let out = hop.engine(ROOT).htod(&host)?;
872 charge_host_leg(n * std::mem::size_of::<f32>(), false);
873 Ok(out)
874 }
875 TpTransport::PeerPull => {
876 let mut out = hop.engine(ROOT).uninit(n)?;
877 hop.publish(from, ROOT)?;
878 hop.pull_f32(from, y, 0, ROOT, &mut out, 0, n)?;
879 Ok(out)
880 }
881 }
882}
883
884// ------------------------------------------------------------------------------------------
885// Arming: the byte-integrity pull ladder
886// ------------------------------------------------------------------------------------------
887
888/// Payload sizes the arm-time ladder validates, mirroring the step seam's
889/// `NATIVE_P2P_PROBE_WORDS` (16 KiB .. 64 MiB) so a glm5 refusal and a step refusal name the
890/// same fabric with the same numbers. The bottom rung is deliberately BELOW our decode hop
891/// sizes (a decode gather part is <= 16 KiB at hidden 4096 f32) and the top rung above any
892/// prime chunk block, because RESEARCH.md §2.4 records that LL and Simple protocol paths
893/// "can fail differently" by size and §2.10 records phantom readings that only a byte check
894/// catches.
895const PULL_PROBE_WORDS: &[usize] = &[4_096, 16_384, 262_144, 16_777_216];
896
897static TRANSPORT_MARKED: AtomicBool = AtomicBool::new(false);
898
899/// Arm the transport for a TP load. On the peer-pull arm this validates the EXACT primitive
900/// the walk uses, in EVERY ordered rank pair, at every rung of [`PULL_PROBE_WORDS`], against
901/// a poisoned destination — and refuses the load on a single differing word.
902///
903/// `same_device_gate` is the one-card rig emulation (N contexts on one device): the pull
904/// primitive is exercised unchanged, the ladder still runs, and the announce says so, but no
905/// claim about a real fabric is made or implied.
906///
907/// What this DOES prove: our copy path moves the right bytes in every direction at four
908/// sizes. What it does NOT prove (RESEARCH.md §2.4's ladder, stated so nobody reads it
909/// wider): that a KERNEL dereference of a peer pointer works. On direct-attach (`NODE`)
910/// hosts the driver stages SM-issued peer access through system memory by default (§2.3b)
911/// and "`nvidia-smi topo -p2p r` returns OK while `cudaMemcpy` looks healthy, so neither
912/// detects it". Only a `simpleP2P`-class kernel peer read does; that lives in the lane's
913/// `peer-read-probe.cu` and is a HEALTH.sh item, not a serving-path dependency — this
914/// transport never dereferences a peer pointer from a kernel.
915pub fn arm_transport(
916 transport: TpTransport,
917 armed_flag: &str,
918 tag: &str,
919 engines: &[&Engine],
920 same_device_gate: bool,
921) -> Result<Option<PeerPullLink>, Box<dyn std::error::Error>> {
922 if transport == TpTransport::HostCanonical {
923 announce(
924 tag,
925 transport,
926 same_device_gate,
927 "host staging, no peer mapping",
928 );
929 return Ok(None);
930 }
931 if same_device_gate {
932 // N contexts on ONE device: there is no peer to grant (`cuDeviceCanAccessPeer`
933 // of a device with itself is not a peer relation) and no fabric to cross. The pull
934 // primitive still runs, unchanged, over the contexts' UVA mappings — which is
935 // what makes the rig gate a real test of the CODE and an explicit non-test of the
936 // FABRIC. Skipping the grant here is why the gate arm cannot silently pass on a box
937 // where the grant would have failed: on a real group the grants run and refuse.
938 eprintln!(
939 "[{tag}] same-device gate: peer-access grant SKIPPED (one device, {} \
940 contexts); the ladder below proves bit-preservation only, never fabric engagement",
941 engines.len(),
942 );
943 } else {
944 for (i, a) in engines.iter().enumerate() {
945 for (j, b) in engines.iter().enumerate() {
946 if i != j {
947 crate::tp::grant_peer_access(a, b, &format!("{armed_flag}=peer-pull"))?;
948 }
949 }
950 }
951 }
952 let link = PeerPullLink::new(engines)?;
953 let hop = Hop {
954 engines: engines.to_vec(),
955 transport,
956 link: Some(&link),
957 };
958 // MEASUREMENT HYGIENE. The ladder below drives the SAME instrumented primitive the walk
959 // does, so it charges the movement census — and it charges it HARD: 4 rungs x every
960 // ordered pair is >100 MiB of arm-time traffic against ~15 MiB per DECODE TOKEN of real
961 // walk traffic. The first gate run made this visible and unmissable: `xfer_bytes` on the
962 // peer-pull arm read 25x the host-canonical arm's, which is the opposite of the truth
963 // (peer-pull crosses PCIe HALF as much, because a host bounce crosses it twice). A box
964 // window reading `xfer_bytes` deltas would have derived a bytes/token figure that was
965 // ~97% arming noise. So: snapshot before, restore after. Arm-time validation traffic is
966 // not walk traffic and must not appear in a per-token census. The ladder's own receipt
967 // is its PASS line.
968 let census_before = (
969 TP_PEER_PULLS.load(Ordering::Relaxed),
970 TP_PUB_EVENTS.load(Ordering::Relaxed),
971 TP_XFER_BYTES.load(Ordering::Relaxed),
972 TP_HOST_LEGS.load(Ordering::Relaxed),
973 TP_HOST_SYNCS.load(Ordering::Relaxed),
974 TP_LOCAL_COPIES.load(Ordering::Relaxed),
975 );
976 let ranks = engines.len();
977 for &words in PULL_PROBE_WORDS {
978 for producer in 0..ranks {
979 for consumer in 0..ranks {
980 if consumer == producer {
981 continue;
982 }
983 let expected: Vec<f32> = (0..words)
984 .map(|i| {
985 f32::from_bits(
986 (i as u32)
987 .wrapping_mul(0x9e37_79b9)
988 .wrapping_add(((producer as u32) + 1) << 16)
989 // Keep every probe word a finite, non-NaN pattern so a
990 // comparison failure is a TRANSPORT fact and never a float
991 // identity artifact.
992 & 0x7f7f_ffff,
993 )
994 })
995 .collect();
996 let poison: Vec<f32> = expected.iter().map(|v| -*v - 1.0).collect();
997 let src = hop.engine(producer).htod(&expected)?;
998 let mut dst = hop.engine(consumer).htod(&poison)?;
999 hop.publish(producer, consumer)?;
1000 hop.pull_f32(producer, &src, 0, consumer, &mut dst, 0, words)?;
1001 let actual = hop.engine(consumer).dtoh(&dst)?;
1002 let mismatches = actual
1003 .iter()
1004 .zip(&expected)
1005 .filter(|(a, b)| a.to_bits() != b.to_bits())
1006 .count();
1007 if mismatches != 0 {
1008 return Err(format!(
1009 "{armed_flag}=peer-pull byte-integrity ladder FAILED: \
1010 rank{producer}->rank{consumer} at {} bytes, {mismatches}/{words} \
1011 words differ (refused before any layer was sharded; roll back \
1012 with: {})",
1013 words * std::mem::size_of::<f32>(),
1014 rollback_advice(),
1015 )
1016 .into());
1017 }
1018 }
1019 }
1020 }
1021 // Restore the census to its pre-ladder state (see the hygiene note above).
1022 let restore = |counter: &AtomicU64, before: u64| {
1023 let now = counter.load(Ordering::Relaxed);
1024 counter.fetch_sub(now.saturating_sub(before), Ordering::Relaxed);
1025 };
1026 restore(&TP_PEER_PULLS, census_before.0);
1027 restore(&TP_PUB_EVENTS, census_before.1);
1028 restore(&TP_XFER_BYTES, census_before.2);
1029 restore(&TP_HOST_LEGS, census_before.3);
1030 restore(&TP_HOST_SYNCS, census_before.4);
1031 restore(&TP_LOCAL_COPIES, census_before.5);
1032 eprintln!(
1033 "[{tag}] peer-pull byte-integrity ladder PASS: directions={} \
1034 byte_ladder={:?} mismatches=0 same_device_gate={same_device_gate} \
1035 census_excluded=arm-time-ladder-traffic",
1036 ranks * (ranks - 1),
1037 PULL_PROBE_WORDS
1038 .iter()
1039 .map(|w| w * std::mem::size_of::<f32>())
1040 .collect::<Vec<_>>(),
1041 );
1042 announce(
1043 tag,
1044 transport,
1045 same_device_gate,
1046 "consumer-issued cuMemcpyDtoDAsync, event-published, atomics-free",
1047 );
1048 Ok(Some(link))
1049}
1050
1051fn announce(tag: &str, transport: TpTransport, same_device_gate: bool, how: &str) {
1052 if TRANSPORT_MARKED.swap(true, Ordering::Relaxed) {
1053 return;
1054 }
1055 eprintln!(
1056 "[{tag}] armed transport={} shape={how} same_device_gate={same_device_gate} \
1057 performance_claim=false",
1058 transport.name(),
1059 );
1060}
1061
1062#[cfg(test)]
1063mod tests {
1064 use super::*;
1065
1066 #[test]
1067 fn transport_parse_is_literal_and_fail_closed() {
1068 for off in [None, Some(""), Some("0"), Some("host-canonical")] {
1069 assert_eq!(
1070 parse_transport(TRANSPORT_ENV, off).unwrap(),
1071 TpTransport::HostCanonical
1072 );
1073 }
1074 for on in [Some("1"), Some("peer-pull")] {
1075 assert_eq!(
1076 parse_transport(TRANSPORT_ENV, on).unwrap(),
1077 TpTransport::PeerPull
1078 );
1079 }
1080 // Every other spelling refuses BY NAME rather than silently picking an arm — a
1081 // typo'd transport must never serve the other one. Asserted under BOTH names, so a
1082 // banked script setting the alias reads its own name back.
1083 for bad in ["peer_pull", "peerpull", "p2p", "native", "2", "on", "true"] {
1084 for flag in [TRANSPORT_ENV, TRANSPORT_ENV_GLM5] {
1085 let err =
1086 parse_transport(flag, Some(bad)).expect_err("unknown transport must refuse");
1087 assert!(err.contains(flag), "{err}");
1088 assert!(err.contains(bad), "{err}");
1089 assert!(err.contains("host-canonical"), "{err}");
1090 assert!(err.contains("peer-pull"), "{err}");
1091 }
1092 }
1093 }
1094
1095 #[test]
1096 fn alias_resolution_honors_both_names_and_refuses_a_disagreeing_pair() {
1097 // unset/unset -> the default, cited under the general name
1098 assert_eq!(
1099 resolve_transport(None, None).unwrap(),
1100 (TpTransport::HostCanonical, TRANSPORT_ENV)
1101 );
1102 // either name alone arms, and the ARMED NAME comes back with it
1103 assert_eq!(
1104 resolve_transport(Some("peer-pull"), None).unwrap(),
1105 (TpTransport::PeerPull, TRANSPORT_ENV)
1106 );
1107 assert_eq!(
1108 resolve_transport(None, Some("peer-pull")).unwrap(),
1109 (TpTransport::PeerPull, TRANSPORT_ENV_GLM5)
1110 );
1111 // an agreeing pair resolves to the general name
1112 assert_eq!(
1113 resolve_transport(Some("1"), Some("1")).unwrap(),
1114 (TpTransport::PeerPull, TRANSPORT_ENV)
1115 );
1116 // a DISAGREEING pair refuses, naming both — arming happens before any session exists,
1117 // so refusing is safe and a silently-chosen transport is not
1118 for (g, a) in [("peer-pull", "0"), ("0", "1"), ("1", "host-canonical")] {
1119 let err = resolve_transport(Some(g), Some(a))
1120 .expect_err("a disagreeing pair must refuse the load");
1121 assert!(err.contains(TRANSPORT_ENV), "{err}");
1122 assert!(err.contains(TRANSPORT_ENV_GLM5), "{err}");
1123 assert!(err.contains("disagree"), "{err}");
1124 }
1125 // NOT a disagreement by meaning, but IS one by string: two spellings of the same arm.
1126 // Deliberately literal rather than normalize-then-compare, so an operator who typed
1127 // two different things is told instead of having one silently win.
1128 assert!(resolve_transport(Some("0"), Some("host-canonical")).is_err());
1129 }
1130
1131 #[test]
1132 fn default_is_the_v1_arm() {
1133 // The written default (docs/FLAGS.md): unmeasured behaviour does not default ON.
1134 assert_eq!(
1135 parse_transport(TRANSPORT_ENV, None).unwrap(),
1136 TpTransport::HostCanonical
1137 );
1138 assert_eq!(TpTransport::HostCanonical.name(), "host-canonical");
1139 assert_eq!(TpTransport::PeerPull.name(), "peer-pull");
1140 }
1141
1142 /// The census line is a receipt: every counter must be named in it, because a box window
1143 /// greps this line and a missing field reads as a zero.
1144 #[test]
1145 fn census_line_names_every_counter() {
1146 let line = transport_census_line("glm5-tp-transport", TpTransport::PeerPull);
1147 for field in [
1148 "transport=peer-pull",
1149 "host_legs=",
1150 "host_syncs=",
1151 "peer_pulls=",
1152 "pub_events=",
1153 "local_copies=",
1154 "xfer_bytes=",
1155 ] {
1156 assert!(line.contains(field), "{line} is missing {field}");
1157 }
1158 }
1159}