Skip to main content

ghostlink_core/
runtime.rs

1//! Distributed inference runtime scaffolding.
2//!
3//! This module provides lightweight execution planning primitives that bridge
4//! placement plans to token-step pipeline schedules across heterogeneous devices.
5
6use crate::circuit_breaker::CircuitBreaker;
7use crate::planning::LayerAssignment;
8use hmac::{Hmac, Mac};
9use sha2::Sha256;
10use std::io::{self, Read, Write};
11use std::io::{BufReader, BufWriter};
12use std::net::{Shutdown as TcpShutdown, SocketAddr, TcpListener, TcpStream};
13use std::path::PathBuf;
14use std::slice;
15use std::sync::atomic::{AtomicU64, Ordering};
16use std::sync::mpsc;
17use std::sync::Arc;
18use std::thread;
19use std::time::Duration;
20use std::time::Instant;
21
22#[cfg(unix)]
23#[cfg(unix)]
24use std::net::Shutdown as UnixSocketShutdown;
25#[cfg(unix)]
26use std::os::unix::net::{UnixListener, UnixStream};
27
28/// Execution target selected for a pipeline stage.
29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
30pub enum DeviceKind {
31    Npu,
32    Gpu,
33    Cpu,
34}
35
36impl DeviceKind {
37    /// Human-friendly label.
38    pub const fn as_str(self) -> &'static str {
39        match self {
40            Self::Npu => "NPU",
41            Self::Gpu => "GPU",
42            Self::Cpu => "CPU",
43        }
44    }
45}
46
47/// Placement of one contiguous layer range onto a node and device.
48#[derive(Clone, Debug, PartialEq)]
49pub struct StagePlacement {
50    pub node_id: String,
51    pub start_layer: usize,
52    pub end_layer: usize,
53    pub device: DeviceKind,
54    /// Estimated compute latency for one micro-batch token step.
55    pub est_latency_ms: f32,
56}
57
58impl StagePlacement {
59    pub fn num_layers(&self) -> usize {
60        self.end_layer.saturating_sub(self.start_layer)
61    }
62}
63
64/// Pipeline execution plan for one model.
65#[derive(Clone, Debug, Default, PartialEq)]
66pub struct PipelinePlan {
67    pub stages: Vec<StagePlacement>,
68}
69
70impl PipelinePlan {
71    /// Build a pipeline plan from layer assignments and a node/device map.
72    pub fn from_assignments(
73        assignments: &[LayerAssignment],
74        device_by_node: &std::collections::HashMap<String, DeviceKind>,
75    ) -> Self {
76        Self::from_assignments_with_measured(assignments, device_by_node, &Default::default())
77    }
78
79    /// Build a pipeline plan using measured latencies from cluster metrics if available.
80    pub fn from_assignments_with_measured(
81        assignments: &[LayerAssignment],
82        device_by_node: &std::collections::HashMap<String, DeviceKind>,
83        cluster: &crate::cluster::ClusterState,
84    ) -> Self {
85        let metrics_guard = cluster
86            .metrics
87            .lock()
88            .unwrap_or_else(|poison| poison.into_inner());
89
90        let stages = assignments
91            .iter()
92            .map(|assignment| {
93                let device = device_by_node
94                    .get(&assignment.node_id)
95                    .copied()
96                    .unwrap_or(DeviceKind::Cpu);
97
98                // Use measured latency if available (converted from us to ms per layer)
99                let per_layer_cost_ms =
100                    if let Some(metrics) = metrics_guard.get(&assignment.node_id) {
101                        if metrics.latency_samples > 0 && assignment.num_layers > 0 {
102                            (metrics.avg_latency_us / 1000.0) / assignment.num_layers as f32
103                        } else {
104                            Self::default_cost_for_device(device)
105                        }
106                    } else {
107                        Self::default_cost_for_device(device)
108                    };
109
110                StagePlacement {
111                    node_id: assignment.node_id.clone(),
112                    start_layer: assignment.start_layer,
113                    end_layer: assignment.end_layer,
114                    device,
115                    est_latency_ms: per_layer_cost_ms * assignment.num_layers as f32,
116                }
117            })
118            .collect();
119
120        Self { stages }
121    }
122
123    fn default_cost_for_device(device: DeviceKind) -> f32 {
124        match device {
125            DeviceKind::Npu => 0.42,
126            DeviceKind::Gpu => 0.55,
127            DeviceKind::Cpu => 1.25,
128        }
129    }
130
131    /// Return aggregate token-step latency estimate for one micro-batch.
132    pub fn est_step_latency_ms(&self) -> f32 {
133        self.stages.iter().map(|s| s.est_latency_ms).sum()
134    }
135
136    pub fn summary(&self) -> String {
137        let mut out = String::from("Pipeline Plan\n");
138        out.push_str("============\n");
139
140        for (idx, stage) in self.stages.iter().enumerate() {
141            out.push_str(&format!(
142                "Stage {}: {} layers {}-{} on {} ({:.2} ms)\n",
143                idx,
144                stage.node_id,
145                stage.start_layer,
146                stage.end_layer,
147                stage.device.as_str(),
148                stage.est_latency_ms
149            ));
150        }
151
152        out.push_str(&format!(
153            "Estimated per-token step latency: {:.2} ms\n",
154            self.est_step_latency_ms()
155        ));
156        out
157    }
158}
159
160fn run_stage_compute(payload: &mut [f32], stage: &StagePlacement) {
161    let base_rounds = stage.num_layers().max(1) / 4 + 1;
162    let rounds = match stage.device {
163        DeviceKind::Npu => base_rounds,
164        DeviceKind::Gpu => base_rounds * 2,
165        DeviceKind::Cpu => base_rounds * 3,
166    };
167
168    let alpha = match stage.device {
169        DeviceKind::Npu => 1.001_f32,
170        DeviceKind::Gpu => 1.003_f32,
171        DeviceKind::Cpu => 1.005_f32,
172    };
173
174    for _ in 0..rounds {
175        for value in payload.iter_mut() {
176            *value = ((*value * alpha) + 0.125).sin();
177        }
178    }
179}
180
181/// Per-stage runtime telemetry captured from real in-process execution.
182#[derive(Clone, Debug, PartialEq)]
183pub struct StageExecutionStats {
184    pub stage_idx: usize,
185    pub processed_batches: usize,
186    pub avg_compute_ms: f32,
187    pub avg_recv_wait_ms: f32,
188    pub avg_send_wait_ms: f32,
189    pub avg_bridge_write_ms: f32,
190    pub avg_bridge_read_ms: f32,
191}
192
193/// Aggregate runtime execution output.
194#[derive(Clone, Debug, PartialEq)]
195pub struct ExecutionResult {
196    pub token_count: usize,
197    pub micro_batch: usize,
198    pub batch_count: usize,
199    pub stage_count: usize,
200    pub total_time_ms: f32,
201    pub throughput_tokens_per_sec: f32,
202    pub avg_token_latency_ms: f32,
203    pub p95_token_latency_ms: f32,
204    pub p99_token_latency_ms: f32,
205    /// One entry per token, in completion order — the raw samples P95/P99
206    /// are computed from. Exposed (rather than kept purely internal) so
207    /// callers such as `ghost-link flow` can dump the real per-token
208    /// latency distribution for a histogram, not just precomputed
209    /// percentiles.
210    pub token_latencies_ms: Vec<f32>,
211    pub stage_stats: Vec<StageExecutionStats>,
212}
213
214/// Transport protocol selection for inter-stage bridges.
215#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
216pub enum TransportKind {
217    /// Standard TCP transport (works across machines).
218    #[default]
219    Tcp,
220    /// Unix domain socket transport (loopback/local only, lower overhead).
221    /// Supported on Linux, macOS, and Windows 10 build 1803+.
222    Unix,
223}
224
225/// Unified transport listener wrapping platform-specific socket types.
226#[derive(Debug)]
227pub enum BridgeListener {
228    Tcp(TcpListener),
229    #[cfg(unix)]
230    Unix(UnixListener),
231}
232
233impl BridgeListener {
234    /// Accept an incoming connection. Returns the connected stream (peer addr discarded).
235    pub fn accept(&self) -> io::Result<BridgeStream> {
236        match self {
237            BridgeListener::Tcp(l) => l.accept().map(|(s, _)| BridgeStream::Tcp(s)),
238            #[cfg(unix)]
239            BridgeListener::Unix(l) => l.accept().map(|(s, _)| BridgeStream::Unix(s)),
240        }
241    }
242}
243
244/// Unified transport stream wrapping platform-specific socket types.
245pub enum BridgeStream {
246    Tcp(TcpStream),
247    #[cfg(unix)]
248    Unix(UnixStream),
249}
250
251impl Read for BridgeStream {
252    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
253        match self {
254            BridgeStream::Tcp(s) => s.read(buf),
255            #[cfg(unix)]
256            BridgeStream::Unix(s) => s.read(buf),
257        }
258    }
259}
260
261impl Write for BridgeStream {
262    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
263        match self {
264            BridgeStream::Tcp(s) => s.write(buf),
265            #[cfg(unix)]
266            BridgeStream::Unix(s) => s.write(buf),
267        }
268    }
269    fn flush(&mut self) -> io::Result<()> {
270        match self {
271            BridgeStream::Tcp(s) => s.flush(),
272            #[cfg(unix)]
273            BridgeStream::Unix(s) => s.flush(),
274        }
275    }
276}
277
278impl BridgeStream {
279    /// Set TCP_NODELAY (no-op for Unix domain sockets).
280    pub fn set_nodelay(&self, nodelay: bool) -> io::Result<()> {
281        match self {
282            BridgeStream::Tcp(s) => s.set_nodelay(nodelay),
283            #[cfg(unix)]
284            BridgeStream::Unix(_) => Ok(()),
285        }
286    }
287
288    /// Set socket send buffer size (SO_SNDBUF). No-op on platforms without support.
289    pub fn set_send_buffer_size(&self, _size: usize) -> io::Result<()> {
290        match self {
291            BridgeStream::Tcp(_s) => {
292                // Platform-specific socket buffer tuning would go here
293                // Omitted for portability - most OSes auto-tune well
294                Ok(())
295            }
296            #[cfg(unix)]
297            BridgeStream::Unix(_) => Ok(()),
298        }
299    }
300
301    /// Set socket receive buffer size (SO_RCVBUF). No-op on platforms without support.
302    pub fn set_recv_buffer_size(&self, _size: usize) -> io::Result<()> {
303        match self {
304            BridgeStream::Tcp(_s) => {
305                // Platform-specific socket buffer tuning would go here
306                // Omitted for portability - most OSes auto-tune well
307                Ok(())
308            }
309            #[cfg(unix)]
310            BridgeStream::Unix(_) => Ok(()),
311        }
312    }
313
314    /// Shut down the write half of the connection.
315    pub fn shutdown_write(&self) -> io::Result<()> {
316        match self {
317            BridgeStream::Tcp(s) => s.shutdown(TcpShutdown::Write),
318            #[cfg(unix)]
319            BridgeStream::Unix(s) => s.shutdown(UnixSocketShutdown::Write),
320        }
321    }
322}
323
324/// Unified transport address for TCP or Unix domain sockets.
325#[derive(Clone, Debug)]
326pub enum BridgeAddr {
327    Tcp(SocketAddr),
328    Unix(PathBuf),
329}
330
331/// Build a collision-free Unix socket path for an inter-stage bridge.
332///
333/// Paths must be unique per process AND per pipeline: a fixed name like
334/// `ghostlink-bridge-0.sock` lets two concurrent pipelines delete each
335/// other's live sockets and cross-connect their streams, which surfaces
336/// as garbled frames (auth failures / bogus payload lengths) far from
337/// the cause. PID plus a process-wide counter guarantees uniqueness;
338/// callers still remove stale files defensively before binding.
339#[cfg(unix)]
340fn unique_bridge_socket_path(source_stage: usize) -> std::path::PathBuf {
341    use std::sync::atomic::{AtomicU64, Ordering as AtomicOrdering};
342    static BRIDGE_SOCKET_SEQ: AtomicU64 = AtomicU64::new(0);
343    let seq = BRIDGE_SOCKET_SEQ.fetch_add(1, AtomicOrdering::Relaxed);
344    let mut p = std::env::temp_dir();
345    p.push(format!(
346        "ghostlink-bridge-{}-{}-{}.sock",
347        std::process::id(),
348        seq,
349        source_stage
350    ));
351    p
352}
353
354impl BridgeAddr {
355    /// Connect to the address (with a timeout for TCP, instant for Unix).
356    fn connect(&self, timeout: Duration) -> io::Result<BridgeStream> {
357        match self {
358            BridgeAddr::Tcp(addr) => {
359                TcpStream::connect_timeout(addr, timeout).map(BridgeStream::Tcp)
360            }
361            #[cfg(unix)]
362            BridgeAddr::Unix(path) => UnixStream::connect(path).map(BridgeStream::Unix),
363            #[cfg(not(unix))]
364            BridgeAddr::Unix(_) => Err(io::Error::new(
365                io::ErrorKind::Unsupported,
366                "Unix domain sockets require a Unix platform",
367            )),
368        }
369    }
370}
371
372/// Runtime controls for TCP/Unix transport execution.
373#[derive(Clone, Debug, PartialEq, Eq)]
374pub struct TcpTransportConfig {
375    pub max_inflight_batches: usize,
376    pub reconnect_attempts: usize,
377    pub reconnect_backoff_ms: u64,
378    /// Per-batch HMAC-SHA256 auth token (legacy, use `auth_session_token` for performance).
379    pub auth_token: Option<String>,
380    /// Session-level auth token. Does a one-time HMAC handshake at connection setup,
381    /// then verifies a lightweight monotonic sequence counter per batch. Much faster
382    /// than per-batch HMAC while maintaining connection-level security.
383    pub auth_session_token: Option<String>,
384    pub use_mtls: bool,
385    pub cert_chain_path: Option<String>,
386    /// Transport protocol: `Tcp` (LAN) or `Unix` (loopback/local, lower overhead).
387    pub transport: TransportKind,
388    /// Socket send buffer size (SO_SNDBUF). 0 = system default.
389    pub send_buffer_size: usize,
390    /// Socket receive buffer size (SO_RCVBUF). 0 = system default.
391    pub recv_buffer_size: usize,
392    /// Use vectored I/O (writev/readv) for zero-copy header+payload writes.
393    pub use_vectored_io: bool,
394    /// Synthetic `f32` elements per token in the benchmark/demo payload these
395    /// pipeline-execution paths generate — they move a stand-in payload
396    /// through the real transport rather than real model activations (see
397    /// `ghost-link::main`'s `flow` command). Defaults to 16 elements (64
398    /// bytes/token), matching the historical hardcoded value so every
399    /// existing caller keeps identical behavior; the `flow` CLI raises this
400    /// to reflect a real FP16/BF16 hidden-state size (e.g. 4096 × 2 bytes ÷
401    /// 4 bytes/f32 = 2048 elements/token for a 7B-class model).
402    pub elems_per_token: usize,
403}
404
405impl Default for TcpTransportConfig {
406    fn default() -> Self {
407        Self {
408            max_inflight_batches: 512,
409            reconnect_attempts: 3,
410            reconnect_backoff_ms: 25,
411            auth_token: None,
412            auth_session_token: None,
413            use_mtls: false,
414            cert_chain_path: None,
415            transport: TransportKind::Tcp,
416            send_buffer_size: 256 * 1024, // 256 KB
417            recv_buffer_size: 256 * 1024, // 256 KB
418            use_vectored_io: true,
419            elems_per_token: 16,
420        }
421    }
422}
423
424/// Result of a one-time session auth handshake. Shared between reader and writer threads.
425#[derive(Clone, Debug)]
426struct SessionAuth {
427    session_id: u64,
428    seq: Arc<AtomicU64>,
429    peer_seq: Arc<AtomicU64>,
430}
431
432impl ExecutionResult {
433    pub fn summary(&self) -> String {
434        let mut out = String::from("Execution Runtime\n");
435        out.push_str("=================\n");
436        out.push_str(&format!(
437            "Tokens: {} | Micro-batch: {} | Batches: {} | Stages: {}\n",
438            self.token_count, self.micro_batch, self.batch_count, self.stage_count
439        ));
440        out.push_str(&format!(
441            "Measured wall-clock time: {:.2} ms\n",
442            self.total_time_ms
443        ));
444        out.push_str(&format!(
445            "Throughput: {:.2} tokens/sec\n",
446            self.throughput_tokens_per_sec
447        ));
448        out.push_str(&format!(
449            "Avg token latency: {:.2} ms | P95: {:.2} ms | P99: {:.2} ms\n",
450            self.avg_token_latency_ms, self.p95_token_latency_ms, self.p99_token_latency_ms
451        ));
452
453        for stage in &self.stage_stats {
454            out.push_str(&format!(
455                "Stage {} batches={} compute={:.2} ms recv-wait={:.2} ms send-wait={:.2} ms bridge-write={:.2} ms bridge-read={:.2} ms\n",
456                stage.stage_idx,
457                stage.processed_batches,
458                stage.avg_compute_ms,
459                stage.avg_recv_wait_ms,
460                stage.avg_send_wait_ms,
461                stage.avg_bridge_write_ms,
462                stage.avg_bridge_read_ms
463            ));
464        }
465
466        out
467    }
468}
469
470/// One scheduled operation in steady-state token generation.
471#[derive(Clone, Debug, PartialEq, Eq)]
472pub struct TokenStep {
473    pub token_idx: usize,
474    pub stage_idx: usize,
475}
476
477/// Generate a simple steady-state pipeline schedule preview.
478///
479/// For each token, all stages execute in order; this preview is useful for
480/// verifying stage count and queue depth wiring before integrating transport.
481pub fn build_token_schedule(stage_count: usize, token_count: usize) -> Vec<TokenStep> {
482    let mut schedule = Vec::with_capacity(stage_count.saturating_mul(token_count));
483    for token_idx in 0..token_count {
484        for stage_idx in 0..stage_count {
485            schedule.push(TokenStep {
486                token_idx,
487                stage_idx,
488            });
489        }
490    }
491    schedule
492}
493
494/// Execute pipeline stages with real channel/thread wiring for `token_count` tokens.
495///
496/// `micro_batch` controls how many tokens are packaged per batch. Values below
497/// 1 are clamped to 1.
498pub fn execute_pipeline(
499    plan: &PipelinePlan,
500    token_count: usize,
501    micro_batch: usize,
502) -> Result<ExecutionResult, String> {
503    execute_pipeline_with_rebalance(plan, token_count, micro_batch, None)
504}
505
506pub fn execute_pipeline_with_rebalance(
507    plan: &PipelinePlan,
508    token_count: usize,
509    micro_batch: usize,
510    rebalance: Option<&crate::planning::RebalanceTrigger>,
511) -> Result<ExecutionResult, String> {
512    execute_pipeline_with_rebalance_and_measured(
513        plan,
514        token_count,
515        micro_batch,
516        rebalance,
517        None,
518        None,
519    )
520}
521
522pub fn execute_pipeline_with_rebalance_and_measured(
523    plan: &PipelinePlan,
524    token_count: usize,
525    micro_batch: usize,
526    rebalance: Option<&crate::planning::RebalanceTrigger>,
527    cluster: Option<&crate::cluster::ClusterState>,
528    placement_context: Option<&crate::planning::PlacementPlan>,
529) -> Result<ExecutionResult, String> {
530    let stage_count = plan.stages.len();
531    let micro_batch = micro_batch.max(1);
532    let batch_count = token_count.div_ceil(micro_batch);
533    if stage_count == 0 || token_count == 0 {
534        return Ok(ExecutionResult {
535            token_count,
536            micro_batch,
537            batch_count,
538            stage_count,
539            total_time_ms: 0.0,
540            throughput_tokens_per_sec: 0.0,
541            avg_token_latency_ms: 0.0,
542            p95_token_latency_ms: 0.0,
543            p99_token_latency_ms: 0.0,
544            token_latencies_ms: Vec::new(),
545            stage_stats: Vec::new(),
546        });
547    }
548
549    #[derive(Debug)]
550    struct BatchWork {
551        tokens_in_batch: usize,
552        started_at: Instant,
553        payload: Vec<f32>,
554    }
555
556    #[derive(Debug)]
557    struct StageAccumulator {
558        stage_idx: usize,
559        processed_batches: usize,
560        total_compute_ms: f32,
561        total_recv_wait_ms: f32,
562        total_send_wait_ms: f32,
563    }
564
565    use crate::ring::{RingConfig, SpscRingBuffer};
566    use std::sync::Arc;
567
568    // Use zero-copy SPSC ring buffers for high-throughput in-memory execution.
569    // Use default RingConfig (4095 capacity, 2867 backpressure threshold) for maximum batching.
570    let ring_cfg = RingConfig::default();
571
572    let mut rings = Vec::with_capacity(stage_count + 1);
573    for _ in 0..=stage_count {
574        rings.push(Arc::new(SpscRingBuffer::<BatchWork>::new(ring_cfg)));
575    }
576
577    // Return ring for recycling payload vectors to reduce allocation churn.
578    let recycler_ring = Arc::new(SpscRingBuffer::<Vec<f32>>::new(ring_cfg));
579
580    let mut stage_handles = Vec::with_capacity(stage_count);
581    for stage_idx in 0..stage_count {
582        let stage = plan.stages[stage_idx].clone();
583        let rx_ring = Arc::clone(&rings[stage_idx]);
584        let tx_ring = Arc::clone(&rings[stage_idx + 1]);
585
586        let handle = thread::spawn(move || {
587            let mut total_compute_ms = 0.0_f32;
588            let mut total_recv_wait_ms = 0.0_f32;
589            let mut total_send_wait_ms = 0.0_f32;
590
591            // Fixed batch count avoids OS scheduler rendezvous — each stage spins
592            // on wait_for_data() / wait_for_space() with exponential backoff, staying
593            // hot on its core.
594            for _ in 0..batch_count {
595                let recv_start = Instant::now();
596                rx_ring.wait_for_data();
597                let mut batch = rx_ring.pop().unwrap();
598                total_recv_wait_ms += recv_start.elapsed().as_secs_f32() * 1000.0;
599
600                let compute_start = Instant::now();
601                run_stage_compute(&mut batch.payload, &stage);
602                total_compute_ms += compute_start.elapsed().as_secs_f32() * 1000.0;
603
604                let send_start = Instant::now();
605                tx_ring.wait_for_space();
606                let _ = tx_ring.push(batch);
607                total_send_wait_ms += send_start.elapsed().as_secs_f32() * 1000.0;
608            }
609
610            StageAccumulator {
611                stage_idx,
612                processed_batches: batch_count,
613                total_compute_ms,
614                total_recv_wait_ms,
615                total_send_wait_ms,
616            }
617        });
618
619        stage_handles.push(handle);
620    }
621
622    let completion_ring = Arc::clone(&rings[stage_count]);
623    let entry_ring = Arc::clone(&rings[0]);
624    // Clear rings vector so strong_count can drop once threads finish
625    drop(rings);
626
627    let recycler_ring_c = Arc::clone(&recycler_ring);
628    let latencies_handle = thread::spawn(move || {
629        let mut latencies = Vec::with_capacity(token_count);
630        for _ in 0..batch_count {
631            completion_ring.wait_for_data();
632            if let Some(done_batch) = completion_ring.pop() {
633                let batch_latency_ms = done_batch.started_at.elapsed().as_secs_f32() * 1000.0;
634                for _ in 0..done_batch.tokens_in_batch {
635                    latencies.push(batch_latency_ms);
636                }
637                recycler_ring_c.wait_for_space();
638                let _ = recycler_ring_c.push(done_batch.payload);
639            }
640        }
641        latencies
642    });
643
644    let exec_start = Instant::now();
645    for batch_idx in 0..batch_count {
646        let batch_start_token = batch_idx * micro_batch;
647        let tokens_in_batch = (token_count - batch_start_token).min(micro_batch);
648        let payload_len = (tokens_in_batch.max(1) * 16).max(32);
649
650        let mut payload = recycler_ring
651            .pop()
652            .unwrap_or_else(|| Vec::with_capacity(payload_len));
653        payload.resize(payload_len, 0.0);
654        for (idx, val) in payload.iter_mut().enumerate() {
655            *val = (batch_idx as f32 * 0.01) + (idx as f32 * 0.0001);
656        }
657
658        // Evaluate dynamic rebalance trigger during execution
659        if let (Some(trigger), Some(cluster_state), Some(placement)) =
660            (rebalance, cluster, placement_context)
661        {
662            // Check for rebalance every 25th batch to allow stability between checks
663            if batch_idx % 25 == 0 && batch_idx > 0 {
664                if let Some(migration) = trigger.evaluate(cluster_state, placement) {
665                    tracing::info!(
666                        "Dynamic Runtime: Rebalance Triggered! Moving layers {:?} from {} to {}",
667                        migration.layers,
668                        migration.source_node,
669                        migration.target_node
670                    );
671                    for step in migration.generate_handoff_plan() {
672                        tracing::info!("  -> {}", step);
673                    }
674                }
675            }
676        }
677
678        entry_ring.wait_for_space();
679        let _ = entry_ring.push(BatchWork {
680            tokens_in_batch,
681            started_at: Instant::now(),
682            payload,
683        });
684    }
685    // Drop entry ring to signal completion to stage threads
686    drop(entry_ring);
687
688    let token_latencies = latencies_handle.join().unwrap_or_default();
689    let total_time_ms = exec_start.elapsed().as_secs_f32() * 1000.0;
690
691    let mut stage_stats = Vec::with_capacity(stage_count);
692    for handle in stage_handles {
693        if let Ok(stats) = handle.join() {
694            let divisor = stats.processed_batches.max(1) as f32;
695            let avg_compute_ms = stats.total_compute_ms / divisor;
696
697            // Feedback loop: update cluster state with actual compute latency
698            if let Some(cluster_state) = cluster {
699                if let Some(stage) = plan.stages.get(stats.stage_idx) {
700                    cluster_state.get_metrics_mut(&stage.node_id, |m| {
701                        // Convert ms back to us for ClusterState (avg_latency_us)
702                        m.record_latency(avg_compute_ms * 1000.0);
703                    });
704                }
705            }
706
707            stage_stats.push(StageExecutionStats {
708                stage_idx: stats.stage_idx,
709                processed_batches: stats.processed_batches,
710                avg_compute_ms,
711                avg_recv_wait_ms: stats.total_recv_wait_ms / divisor,
712                avg_send_wait_ms: stats.total_send_wait_ms / divisor,
713                avg_bridge_write_ms: 0.0,
714                avg_bridge_read_ms: 0.0,
715            });
716        }
717    }
718    stage_stats.sort_by_key(|s| s.stage_idx);
719
720    let throughput_tokens_per_sec = if total_time_ms > 0.0 {
721        token_count as f32 / (total_time_ms / 1000.0)
722    } else {
723        0.0
724    };
725
726    let avg_token_latency_ms = if token_latencies.is_empty() {
727        0.0
728    } else {
729        token_latencies.iter().sum::<f32>() / token_latencies.len() as f32
730    };
731
732    let mut sorted = token_latencies.clone();
733    sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
734    let p95_idx = (sorted.len().saturating_sub(1) as f32 * 0.95).round() as usize;
735    let p95_token_latency_ms = sorted.get(p95_idx).copied().unwrap_or(0.0);
736    let p99_idx = (sorted.len().saturating_sub(1) as f32 * 0.99).round() as usize;
737    let p99_token_latency_ms = sorted.get(p99_idx).copied().unwrap_or(0.0);
738
739    Ok(ExecutionResult {
740        token_count,
741        micro_batch,
742        batch_count,
743        stage_count,
744        total_time_ms,
745        throughput_tokens_per_sec,
746        avg_token_latency_ms,
747        p95_token_latency_ms,
748        p99_token_latency_ms,
749        token_latencies_ms: token_latencies,
750        stage_stats,
751    })
752}
753
754#[derive(Debug, Clone)]
755pub struct TransportBatch {
756    pub batch_id: usize,
757    pub tokens_in_batch: usize,
758    pub payload: Vec<f32>,
759}
760
761fn auth_tag(
762    source_stage: usize,
763    batch_id: usize,
764    tokens_in_batch: usize,
765    payload: &[f32],
766    token: &str,
767) -> [u8; 32] {
768    let mut mac = Hmac::<Sha256>::new_from_slice(token.as_bytes())
769        .expect("HMAC key setup for transport auth failed");
770    mac.update(&(source_stage as u32).to_le_bytes());
771    mac.update(&(batch_id as u64).to_le_bytes());
772    mac.update(&(tokens_in_batch as u32).to_le_bytes());
773    mac.update(&(payload.len() as u32).to_le_bytes());
774    let payload_bytes = payload_as_le_bytes(payload);
775    mac.update(payload_bytes.as_ref());
776    mac.finalize().into_bytes().into()
777}
778
779fn payload_as_le_bytes(payload: &[f32]) -> std::borrow::Cow<'_, [u8]> {
780    if cfg!(target_endian = "little") {
781        // SAFETY: f32 is POD; casting [f32] to its contiguous byte representation is valid.
782        let bytes = unsafe {
783            slice::from_raw_parts(
784                payload.as_ptr() as *const u8,
785                std::mem::size_of_val(payload),
786            )
787        };
788        std::borrow::Cow::Borrowed(bytes)
789    } else {
790        let mut bytes = Vec::with_capacity(payload.len() * 4);
791        for value in payload {
792            bytes.extend_from_slice(&value.to_le_bytes());
793        }
794        std::borrow::Cow::Owned(bytes)
795    }
796}
797
798fn write_transport_batch(
799    writer: &mut impl Write,
800    batch: &TransportBatch,
801    source_stage: usize,
802    token: Option<&str>,
803    frame_buf: &mut Vec<u8>,
804) -> io::Result<()> {
805    _write_transport_batch_inner(writer, batch, source_stage, token, None, frame_buf)
806}
807
808fn write_transport_batch_session(
809    writer: &mut impl Write,
810    batch: &TransportBatch,
811    source_stage: usize,
812    session: &SessionAuth,
813    frame_buf: &mut Vec<u8>,
814) -> io::Result<()> {
815    _write_transport_batch_inner(writer, batch, source_stage, None, Some(session), frame_buf)
816}
817
818fn _write_transport_batch_inner(
819    writer: &mut impl Write,
820    batch: &TransportBatch,
821    source_stage: usize,
822    token: Option<&str>,
823    session: Option<&SessionAuth>,
824    frame_buf: &mut Vec<u8>,
825) -> io::Result<()> {
826    let batch_id = batch.batch_id as u64;
827    let tokens = batch.tokens_in_batch as u32;
828    let payload_len = batch.payload.len() as u32;
829    let source_stage_u16 = source_stage as u16;
830
831    frame_buf.clear();
832    let header_size = if session.is_some() {
833        // source_stage(2) + batch_id(8) + tokens(4) + payload_len(4) + tag_present(1) + session_id(8) + generation(8)
834        2 + 8 + 4 + 4 + 1 + 8 + 8
835    } else if token.is_some() {
836        // legacy HMAC: 2 + 8 + 4 + 4 + 1 + 32
837        2 + 8 + 4 + 4 + 1 + 32
838    } else {
839        2 + 8 + 4 + 4 + 1
840    };
841    // Reserve space ONLY for the header to avoid large intermediate allocation/capacity checks.
842    frame_buf.reserve(header_size);
843    frame_buf.extend_from_slice(&source_stage_u16.to_le_bytes());
844    frame_buf.extend_from_slice(&batch_id.to_le_bytes());
845    frame_buf.extend_from_slice(&tokens.to_le_bytes());
846    frame_buf.extend_from_slice(&payload_len.to_le_bytes());
847
848    if let Some(s) = session {
849        frame_buf.push(2); // session-level auth
850        let gen = s.seq.fetch_add(1, Ordering::Relaxed);
851        frame_buf.extend_from_slice(&s.session_id.to_le_bytes());
852        frame_buf.extend_from_slice(&gen.to_le_bytes());
853    } else if let Some(t) = token {
854        frame_buf.push(1); // legacy HMAC present
855        let tag = auth_tag(
856            source_stage,
857            batch.batch_id,
858            batch.tokens_in_batch,
859            &batch.payload,
860            t,
861        );
862        frame_buf.extend_from_slice(&tag);
863    } else {
864        frame_buf.push(0); // No auth
865    }
866
867    // Zero-Copy Transport Optimization: By writing the header and the float payload
868    // separately directly to the underlying BufWriter, we completely avoid copying
869    // the entire multi-kilobyte/megabyte float slice payload into the intermediate
870    // scratch frame_buf vector. This yields a massive reduction in memory bandwidth and CPU cycles.
871    let payload_bytes = payload_as_le_bytes(&batch.payload);
872    writer.write_all(frame_buf)?;
873    writer.write_all(payload_bytes.as_ref())?;
874
875    Ok(())
876}
877
878fn read_transport_batch(
879    reader: &mut impl Read,
880    expected_source_stage: usize,
881    token: Option<&str>,
882    payload_buf: &mut Vec<f32>,
883) -> io::Result<Option<TransportBatch>> {
884    _read_transport_batch_inner(reader, expected_source_stage, token, None, payload_buf)
885}
886
887fn read_transport_batch_session(
888    reader: &mut impl Read,
889    expected_source_stage: usize,
890    session: &SessionAuth,
891    payload_buf: &mut Vec<f32>,
892) -> io::Result<Option<TransportBatch>> {
893    _read_transport_batch_inner(
894        reader,
895        expected_source_stage,
896        None,
897        Some(session),
898        payload_buf,
899    )
900}
901
902fn _read_transport_batch_inner(
903    reader: &mut impl Read,
904    expected_source_stage: usize,
905    token: Option<&str>,
906    session: Option<&SessionAuth>,
907    payload_buf: &mut Vec<f32>,
908) -> io::Result<Option<TransportBatch>> {
909    // Coalesce multiple read_exact calls into a single 19-byte read of the static packet header.
910    // Length breakdown: source_stage(2) + batch_id(8) + tokens(4) + payload_len(4) + tag_type(1) = 19 bytes.
911    let mut header_buf = [0u8; 19];
912    match reader.read_exact(&mut header_buf) {
913        Ok(()) => {}
914        Err(err) if err.kind() == io::ErrorKind::UnexpectedEof => return Ok(None),
915        Err(err) => return Err(err),
916    }
917
918    let source_stage = u16::from_le_bytes(header_buf[0..2].try_into().unwrap()) as usize;
919    if source_stage != expected_source_stage {
920        return Err(io::Error::new(
921            io::ErrorKind::InvalidData,
922            "unexpected source stage in transport frame",
923        ));
924    }
925
926    let batch_id = u64::from_le_bytes(header_buf[2..10].try_into().unwrap()) as usize;
927    let tokens_in_batch = u32::from_le_bytes(header_buf[10..14].try_into().unwrap()) as usize;
928    let payload_len = u32::from_le_bytes(header_buf[14..18].try_into().unwrap()) as usize;
929
930    // Bound the wire-declared payload length BEFORE allocating. A corrupted or
931    // cross-wired frame can otherwise declare up to u32::MAX elements (~17 GB
932    // of f32), and the resulting failed allocation aborts the process (or gets
933    // the backend OOM-killed on Linux) instead of surfacing an error.
934    const MAX_WIRE_PAYLOAD_ELEMS: usize = 64 * 1024 * 1024; // 256 MB of f32
935    if payload_len > MAX_WIRE_PAYLOAD_ELEMS {
936        return Err(io::Error::new(
937            io::ErrorKind::InvalidData,
938            format!(
939                "wire payload length {} exceeds maximum {} (corrupted or cross-wired frame?)",
940                payload_len, MAX_WIRE_PAYLOAD_ELEMS
941            ),
942        ));
943    }
944
945    let tag_type = header_buf[18];
946
947    match tag_type {
948        2 => {
949            // Session-level auth: read session_id + generation in a single 16-byte read instead of two 8-byte reads.
950            let mut session_bytes = [0u8; 16];
951            reader.read_exact(&mut session_bytes)?;
952            let frame_session_id = u64::from_le_bytes(session_bytes[0..8].try_into().unwrap());
953            let generation = u64::from_le_bytes(session_bytes[8..16].try_into().unwrap());
954
955            if session.is_none() {
956                return Err(io::Error::new(
957                    io::ErrorKind::PermissionDenied,
958                    "session auth required but no session established",
959                ));
960            }
961            let s = session.unwrap();
962            if frame_session_id != s.session_id {
963                return Err(io::Error::new(
964                    io::ErrorKind::InvalidData,
965                    "session auth: session_id mismatch",
966                ));
967            }
968            let prev = s.peer_seq.fetch_max(generation, Ordering::AcqRel);
969            if generation < prev {
970                return Err(io::Error::new(
971                    io::ErrorKind::InvalidData,
972                    "session auth: non-monotonic generation",
973                ));
974            }
975        }
976        1 => {
977            // Legacy HMAC auth
978            if token.is_none() {
979                return Err(io::Error::new(
980                    io::ErrorKind::PermissionDenied,
981                    "HMAC auth present but no token configured",
982                ));
983            }
984            let mut received_tag = [0u8; 32];
985            reader.read_exact(&mut received_tag)?;
986            payload_buf.resize(payload_len, 0.0);
987            _read_payload(reader, payload_buf, payload_len)?;
988
989            let expected_tag = auth_tag(
990                source_stage,
991                batch_id,
992                tokens_in_batch,
993                payload_buf,
994                token.unwrap(),
995            );
996            if received_tag != expected_tag {
997                return Err(io::Error::new(
998                    io::ErrorKind::InvalidData,
999                    "transport auth tag mismatch",
1000                ));
1001            }
1002            return Ok(Some(TransportBatch {
1003                batch_id,
1004                tokens_in_batch,
1005                payload: payload_buf.clone(),
1006            }));
1007        }
1008        0 => {
1009            // No auth
1010            if token.is_some() || session.is_some() {
1011                return Err(io::Error::new(
1012                    io::ErrorKind::PermissionDenied,
1013                    "transport authentication required but no auth present",
1014                ));
1015            }
1016        }
1017        _ => {
1018            return Err(io::Error::new(
1019                io::ErrorKind::InvalidData,
1020                "unknown transport auth type",
1021            ));
1022        }
1023    }
1024
1025    // Common path: read payload after auth verification
1026    payload_buf.resize(payload_len, 0.0);
1027    _read_payload(reader, payload_buf, payload_len)?;
1028
1029    Ok(Some(TransportBatch {
1030        batch_id,
1031        tokens_in_batch,
1032        payload: payload_buf.clone(),
1033    }))
1034}
1035
1036fn _read_payload(
1037    reader: &mut impl Read,
1038    payload_buf: &mut [f32],
1039    payload_len: usize,
1040) -> io::Result<()> {
1041    if payload_len == 0 {
1042        return Ok(());
1043    }
1044    if cfg!(target_endian = "little") {
1045        let payload_bytes = unsafe {
1046            slice::from_raw_parts_mut(
1047                payload_buf.as_mut_ptr() as *mut u8,
1048                payload_len * std::mem::size_of::<f32>(),
1049            )
1050        };
1051        reader.read_exact(payload_bytes)?;
1052    } else {
1053        let mut payload_bytes = vec![0u8; payload_len * 4];
1054        reader.read_exact(&mut payload_bytes)?;
1055        for (i, chunk) in payload_bytes.chunks_exact(4).enumerate() {
1056            payload_buf[i] = f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
1057        }
1058    }
1059    Ok(())
1060}
1061
1062/// Spawn a TCP transport bridge between pipeline stages.
1063///
1064/// In a multi-node deployment, the `bind_addr` and `connect_addr` can point to
1065/// different physical interfaces or remote hosts.
1066///
1067/// `circuit_breaker` is optional and persists failure history *across* calls
1068/// (unlike `config.reconnect_attempts`, which only bounds retries within this
1069/// one call) — pass `ClusterState::circuit_breaker_for(node_id)` for a real
1070/// remote node so a chronically-unreachable node fails fast on subsequent
1071/// pipeline runs instead of repeating the full connect/backoff sequence every
1072/// time. `None` (e.g. loopback benchmarking, where there's no real remote
1073/// failure mode to protect against) reproduces the prior always-retry behavior
1074/// exactly.
1075pub fn spawn_tcp_bridge(
1076    source_stage: usize,
1077    input_rx: mpsc::Receiver<TransportBatch>,
1078    output_tx: mpsc::SyncSender<TransportBatch>,
1079    config: TcpTransportConfig,
1080    listener: BridgeListener,
1081    addr: BridgeAddr,
1082    circuit_breaker: Option<CircuitBreaker>,
1083) -> thread::JoinHandle<BridgeAccumulator> {
1084    thread::spawn(move || {
1085        let client_stream = {
1086            let breaker_open = circuit_breaker
1087                .as_ref()
1088                .is_some_and(|breaker| !breaker.should_attempt());
1089
1090            let mut connected = None;
1091            if breaker_open {
1092                tracing::warn!(
1093                    "Bridge: circuit breaker open for {:?}; skipping connection attempts",
1094                    addr
1095                );
1096            } else {
1097                let attempts = config.reconnect_attempts.max(1);
1098                for attempt in 0..attempts {
1099                    match addr.connect(Duration::from_secs(1)) {
1100                        Ok(stream) => {
1101                            connected = Some(stream);
1102                            break;
1103                        }
1104                        Err(e) => {
1105                            tracing::debug!(
1106                                "Bridge: Connection attempt {}/{} failed for {:?}: {}",
1107                                attempt + 1,
1108                                attempts,
1109                                addr,
1110                                e
1111                            );
1112                            thread::sleep(Duration::from_millis(config.reconnect_backoff_ms));
1113                        }
1114                    }
1115                }
1116            }
1117
1118            match connected {
1119                Some(stream) => {
1120                    if let Some(breaker) = &circuit_breaker {
1121                        breaker.record_success();
1122                    }
1123                    stream
1124                }
1125                None => {
1126                    if let Some(breaker) = &circuit_breaker {
1127                        breaker.record_failure();
1128                    }
1129                    tracing::error!(
1130                        "Bridge: Exhausted retries for {:?}. Falling back to passthrough.",
1131                        addr
1132                    );
1133                    for batch in input_rx {
1134                        if output_tx.send(batch).is_err() {
1135                            break;
1136                        }
1137                    }
1138                    return BridgeAccumulator::default_with_stage(source_stage);
1139                }
1140            }
1141        };
1142
1143        // Inter-stage frames are small and latency-sensitive. For TCP, Nagle's
1144        // algorithm would coalesce these and stall on peer delayed-ACKs (~40ms),
1145        // which compounds across hops and dominates multi-stage paths.
1146        if let Err(e) = client_stream.set_nodelay(true) {
1147            tracing::debug!("Bridge: failed to set TCP_NODELAY on client: {}", e);
1148        }
1149
1150        let server_stream = match listener.accept() {
1151            Ok(stream) => {
1152                // Once the peer is connected the Unix socket file is no longer
1153                // needed; unlink immediately so unique per-pipeline socket
1154                // paths do not accumulate in the temp directory.
1155                #[cfg(unix)]
1156                if let BridgeAddr::Unix(path) = &addr {
1157                    let _ = std::fs::remove_file(path);
1158                }
1159                stream
1160            }
1161            Err(_) => {
1162                #[cfg(unix)]
1163                if let BridgeAddr::Unix(path) = &addr {
1164                    let _ = std::fs::remove_file(path);
1165                }
1166                for batch in input_rx {
1167                    if output_tx.send(batch).is_err() {
1168                        break;
1169                    }
1170                }
1171                return BridgeAccumulator::default_with_stage(source_stage);
1172            }
1173        };
1174
1175        if let Err(e) = server_stream.set_nodelay(true) {
1176            tracing::debug!("Bridge: failed to set TCP_NODELAY on server: {}", e);
1177        }
1178
1179        // Set socket buffer sizes for better throughput
1180        if let Err(e) = client_stream.set_send_buffer_size(config.send_buffer_size) {
1181            tracing::debug!("Bridge: failed to set SO_SNDBUF on client: {}", e);
1182        }
1183        if let Err(e) = client_stream.set_recv_buffer_size(config.recv_buffer_size) {
1184            tracing::debug!("Bridge: failed to set SO_RCVBUF on client: {}", e);
1185        }
1186        if let Err(e) = server_stream.set_send_buffer_size(config.send_buffer_size) {
1187            tracing::debug!("Bridge: failed to set SO_SNDBUF on server: {}", e);
1188        }
1189        if let Err(e) = server_stream.set_recv_buffer_size(config.recv_buffer_size) {
1190            tracing::debug!("Bridge: failed to set SO_RCVBUF on server: {}", e);
1191        }
1192
1193        let session = config.auth_session_token.as_ref().map(|session_token| {
1194            // Derive a deterministic session_id from the token (no handshake round-trip).
1195            // The TCP connect/accept already authenticates this connection.
1196            // Use SHA-256 of the token (first 8 bytes) for cross-platform determinism.
1197            let token_hash = {
1198                let mut mac = Hmac::<Sha256>::new_from_slice(session_token.as_bytes())
1199                    .expect("HMAC key for session_id derivation");
1200                mac.update(b"session-id");
1201                mac.finalize().into_bytes()
1202            };
1203            let session_id = u64::from_le_bytes([
1204                token_hash[0],
1205                token_hash[1],
1206                token_hash[2],
1207                token_hash[3],
1208                token_hash[4],
1209                token_hash[5],
1210                token_hash[6],
1211                token_hash[7],
1212            ]);
1213            tracing::debug!(
1214                "TCP Bridge: session auth established, session_id={}",
1215                session_id
1216            );
1217            SessionAuth {
1218                session_id,
1219                seq: Arc::new(AtomicU64::new(0)),
1220                peer_seq: Arc::new(AtomicU64::new(0)),
1221            }
1222        });
1223
1224        let use_session = session.clone();
1225        let writer_use_session = use_session.clone();
1226        let reader_use_session = use_session.clone();
1227        let writer_auth_token = if writer_use_session.is_some() {
1228            None
1229        } else {
1230            config
1231                .auth_session_token
1232                .clone()
1233                .or_else(|| config.auth_token.clone())
1234        };
1235        let reader_auth_token = if reader_use_session.is_some() {
1236            None
1237        } else {
1238            config
1239                .auth_session_token
1240                .clone()
1241                .or_else(|| config.auth_token.clone())
1242        };
1243
1244        let writer = thread::spawn(move || {
1245            let mut writer = BufWriter::with_capacity(64 * 1024, client_stream);
1246            let mut processed_batches = 0usize;
1247            let mut total_write_ms = 0.0_f32;
1248            let mut frame_buf = Vec::with_capacity(64 * 1024);
1249            let write_result: Result<(), ()> = (|| {
1250                if let Some(ref s) = writer_use_session {
1251                    for batch in input_rx {
1252                        let write_start = Instant::now();
1253                        write_transport_batch_session(
1254                            &mut writer,
1255                            &batch,
1256                            source_stage,
1257                            s,
1258                            &mut frame_buf,
1259                        )
1260                        .map_err(|_| ())?;
1261                        // BufWriter only spills to the socket once its 64KB
1262                        // buffer fills — for the small, sub-buffer-size frames
1263                        // this bridge carries, that meant every batch sat in
1264                        // userspace until the whole input channel drained, so
1265                        // the receiver saw nothing until one final burst at
1266                        // stream end instead of the pipelined, per-batch
1267                        // delivery TCP_NODELAY above was set up to provide.
1268                        // Flushing per frame is what actually puts NODELAY to
1269                        // work for this latency-sensitive path.
1270                        writer.flush().map_err(|_| ())?;
1271                        total_write_ms += write_start.elapsed().as_secs_f32() * 1000.0;
1272                        processed_batches += 1;
1273                    }
1274                } else {
1275                    for batch in input_rx {
1276                        let write_start = Instant::now();
1277                        write_transport_batch(
1278                            &mut writer,
1279                            &batch,
1280                            source_stage,
1281                            writer_auth_token.as_deref(),
1282                            &mut frame_buf,
1283                        )
1284                        .map_err(|_| ())?;
1285                        writer.flush().map_err(|_| ())?;
1286                        total_write_ms += write_start.elapsed().as_secs_f32() * 1000.0;
1287                        processed_batches += 1;
1288                    }
1289                }
1290                Ok(())
1291            })();
1292            let _ = writer.flush();
1293            let _ = writer.get_ref().shutdown_write();
1294            let _ = write_result;
1295            (processed_batches, total_write_ms)
1296        });
1297
1298        let mut reader = BufReader::with_capacity(64 * 1024, server_stream);
1299        let mut read_batches = 0usize;
1300        let mut total_read_ms = 0.0_f32;
1301        let mut payload_buf = Vec::with_capacity(16 * 1024);
1302
1303        let _: Result<(), ()> = if let Some(ref s) = reader_use_session {
1304            loop {
1305                let read_start = Instant::now();
1306                match read_transport_batch_session(&mut reader, source_stage, s, &mut payload_buf) {
1307                    Ok(Some(batch)) => {
1308                        total_read_ms += read_start.elapsed().as_secs_f32() * 1000.0;
1309                        if output_tx.send(batch).is_err() {
1310                            break Ok(());
1311                        }
1312                        read_batches += 1;
1313                    }
1314                    Ok(None) => break Ok(()),
1315                    Err(_) => break Err(()),
1316                }
1317            }
1318        } else {
1319            loop {
1320                let read_start = Instant::now();
1321                match read_transport_batch(
1322                    &mut reader,
1323                    source_stage,
1324                    reader_auth_token.as_deref(),
1325                    &mut payload_buf,
1326                ) {
1327                    Ok(Some(batch)) => {
1328                        total_read_ms += read_start.elapsed().as_secs_f32() * 1000.0;
1329                        if output_tx.send(batch).is_err() {
1330                            break Ok(());
1331                        }
1332                        read_batches += 1;
1333                    }
1334                    Ok(None) => break Ok(()),
1335                    Err(_) => break Err(()),
1336                }
1337            }
1338        };
1339
1340        let (write_batches, total_write_ms) = writer.join().unwrap_or((0, 0.0));
1341        BridgeAccumulator {
1342            source_stage,
1343            processed_batches: read_batches.max(write_batches),
1344            total_write_ms,
1345            total_read_ms,
1346        }
1347    })
1348}
1349
1350#[derive(Debug)]
1351pub struct BridgeAccumulator {
1352    pub source_stage: usize,
1353    pub processed_batches: usize,
1354    pub total_write_ms: f32,
1355    pub total_read_ms: f32,
1356}
1357
1358impl BridgeAccumulator {
1359    fn default_with_stage(source_stage: usize) -> Self {
1360        Self {
1361            source_stage,
1362            processed_batches: 0,
1363            total_write_ms: 0.0,
1364            total_read_ms: 0.0,
1365        }
1366    }
1367}
1368
1369/// Execute pipeline stages distributed across the LAN using TCP transport.
1370pub fn execute_pipeline_distributed(
1371    plan: &PipelinePlan,
1372    token_count: usize,
1373    micro_batch: usize,
1374    config: TcpTransportConfig,
1375    cluster: &crate::cluster::ClusterState,
1376    placement_context: Option<&crate::planning::PlacementPlan>,
1377    rebalance: Option<&crate::planning::RebalanceTrigger>,
1378) -> Result<ExecutionResult, String> {
1379    let stage_count = plan.stages.len();
1380    let micro_batch = micro_batch.max(1);
1381    let batch_count = token_count.div_ceil(micro_batch);
1382    if stage_count == 0 || token_count == 0 {
1383        return Ok(ExecutionResult {
1384            token_count,
1385            micro_batch,
1386            batch_count,
1387            stage_count,
1388            total_time_ms: 0.0,
1389            throughput_tokens_per_sec: 0.0,
1390            avg_token_latency_ms: 0.0,
1391            p95_token_latency_ms: 0.0,
1392            p99_token_latency_ms: 0.0,
1393            token_latencies_ms: Vec::new(),
1394            stage_stats: Vec::new(),
1395        });
1396    }
1397
1398    #[derive(Debug)]
1399    struct StageAccumulator {
1400        stage_idx: usize,
1401        processed_batches: usize,
1402        total_compute_ms: f32,
1403        total_recv_wait_ms: f32,
1404        total_send_wait_ms: f32,
1405    }
1406
1407    let (entry_tx, entry_rx) = mpsc::channel::<TransportBatch>();
1408    let mut stage_inputs: Vec<Option<mpsc::Receiver<TransportBatch>>> =
1409        Vec::with_capacity(stage_count);
1410    stage_inputs.push(Some(entry_rx));
1411    let mut stage_outputs = Vec::with_capacity(stage_count);
1412    let mut bridge_handles = Vec::new();
1413    let bridge_capacity = config.max_inflight_batches.max(1);
1414
1415    for source_stage_idx in 0..stage_count.saturating_sub(1) {
1416        let (bridge_in_tx, bridge_in_rx) = mpsc::sync_channel::<TransportBatch>(bridge_capacity);
1417        let (bridge_out_tx, bridge_out_rx) = mpsc::sync_channel::<TransportBatch>(bridge_capacity);
1418        stage_outputs.push(bridge_in_tx);
1419        stage_inputs.push(Some(bridge_out_rx));
1420
1421        let source_stage_p = &plan.stages[source_stage_idx];
1422        let target_stage_p = &plan.stages[source_stage_idx + 1];
1423        let same_node = source_stage_p.node_id == target_stage_p.node_id;
1424
1425        let (listener, addr) = if same_node && config.transport == TransportKind::Unix {
1426            #[cfg(unix)]
1427            {
1428                // Same-node with Unix transport: use Unix domain socket.
1429                let sock_path = unique_bridge_socket_path(source_stage_idx);
1430                let _ = std::fs::remove_file(&sock_path);
1431                let ulistener = UnixListener::bind(&sock_path).map_err(|e| {
1432                    format!("Failed to bind Unix socket at {}: {e}", sock_path.display())
1433                })?;
1434                let addr = BridgeAddr::Unix(sock_path);
1435                (BridgeListener::Unix(ulistener), addr)
1436            }
1437            #[cfg(not(unix))]
1438            return Err("Unix domain sockets require a Unix platform (Linux/macOS)".to_string());
1439        } else {
1440            // TCP transport for cross-node or when configured explicitly.
1441            let bind_ip = if let Some(m) = cluster.get_metrics(&target_stage_p.node_id) {
1442                m.ip_address
1443                    .map(|sa| sa.ip())
1444                    .unwrap_or_else(|| [127, 0, 0, 1].into())
1445            } else {
1446                [127, 0, 0, 1].into()
1447            };
1448
1449            let tcp_listener = TcpListener::bind(SocketAddr::new(bind_ip, 0))
1450                .map_err(|e| format!("Failed to bind TCP listener on {bind_ip}: {e}"))?;
1451            let actual_addr = tcp_listener
1452                .local_addr()
1453                .map_err(|e| format!("failed to get TCP local addr: {e}"))?;
1454
1455            // For connection, if it's the same node, use loopback.
1456            let connect_ip = if same_node {
1457                [127, 0, 0, 1].into()
1458            } else {
1459                actual_addr.ip()
1460            };
1461            let connect_addr = SocketAddr::new(connect_ip, actual_addr.port());
1462            (
1463                BridgeListener::Tcp(tcp_listener),
1464                BridgeAddr::Tcp(connect_addr),
1465            )
1466        };
1467
1468        let breaker = Some(cluster.circuit_breaker_for(&target_stage_p.node_id));
1469        bridge_handles.push(spawn_tcp_bridge(
1470            source_stage_idx,
1471            bridge_in_rx,
1472            bridge_out_tx,
1473            config.clone(),
1474            listener,
1475            addr,
1476            breaker,
1477        ));
1478    }
1479
1480    let (completion_tx, completion_rx) = mpsc::sync_channel::<TransportBatch>(bridge_capacity);
1481    stage_outputs.push(completion_tx);
1482
1483    let mut stage_handles = Vec::with_capacity(stage_count);
1484    for stage_idx in 0..stage_count {
1485        let stage = plan.stages[stage_idx].clone();
1486        let rx = stage_inputs[stage_idx]
1487            .take()
1488            .expect("stage receiver should exist");
1489        let tx_next = stage_outputs[stage_idx].clone();
1490
1491        let handle = thread::spawn(move || {
1492            let mut processed_batches = 0usize;
1493            let mut total_compute_ms = 0.0_f32;
1494            let mut total_recv_wait_ms = 0.0_f32;
1495            let mut total_send_wait_ms = 0.0_f32;
1496
1497            loop {
1498                let recv_start = Instant::now();
1499                let Ok(mut batch) = rx.recv() else {
1500                    break;
1501                };
1502                total_recv_wait_ms += recv_start.elapsed().as_secs_f32() * 1000.0;
1503
1504                let compute_start = Instant::now();
1505                run_stage_compute(&mut batch.payload, &stage);
1506                total_compute_ms += compute_start.elapsed().as_secs_f32() * 1000.0;
1507
1508                let send_start = Instant::now();
1509                if tx_next.send(batch).is_err() {
1510                    break;
1511                }
1512                total_send_wait_ms += send_start.elapsed().as_secs_f32() * 1000.0;
1513                processed_batches += 1;
1514            }
1515
1516            StageAccumulator {
1517                stage_idx,
1518                processed_batches,
1519                total_compute_ms,
1520                total_recv_wait_ms,
1521                total_send_wait_ms,
1522            }
1523        });
1524
1525        stage_handles.push(handle);
1526    }
1527
1528    drop(stage_outputs);
1529
1530    let mut batch_started_at = vec![Instant::now(); batch_count];
1531    let exec_start = Instant::now();
1532    for (batch_idx, batch_started_slot) in batch_started_at.iter_mut().enumerate() {
1533        let batch_start_token = batch_idx * micro_batch;
1534        let tokens_in_batch = (token_count - batch_start_token).min(micro_batch);
1535        let payload_len = (tokens_in_batch.max(1) * config.elems_per_token.max(1)).max(32);
1536        let payload = (0..payload_len)
1537            .map(|idx| (batch_idx as f32 * 0.01) + (idx as f32 * 0.0001))
1538            .collect();
1539
1540        *batch_started_slot = Instant::now();
1541        let _ = entry_tx.send(TransportBatch {
1542            batch_id: batch_idx,
1543            tokens_in_batch,
1544            payload,
1545        });
1546    }
1547    drop(entry_tx);
1548
1549    let mut token_latencies = Vec::with_capacity(token_count);
1550    for _ in 0..batch_count {
1551        let Ok(done_batch) = completion_rx.recv() else {
1552            break;
1553        };
1554        let started_at = batch_started_at[done_batch.batch_id];
1555        let batch_latency_ms = started_at.elapsed().as_secs_f32() * 1000.0;
1556        for _ in 0..done_batch.tokens_in_batch {
1557            token_latencies.push(batch_latency_ms);
1558        }
1559    }
1560
1561    // Evaluate dynamic rebalance trigger during execution
1562    if let (Some(trigger), Some(placement)) = (rebalance, placement_context) {
1563        if let Some(migration) = trigger.evaluate(cluster, placement) {
1564            tracing::info!(
1565                "Distributed Runtime: Rebalance Triggered! Moving layers {:?} from {} to {}",
1566                migration.layers,
1567                migration.source_node,
1568                migration.target_node
1569            );
1570        }
1571    }
1572    let total_time_ms = exec_start.elapsed().as_secs_f32() * 1000.0;
1573
1574    let mut stage_stats = Vec::with_capacity(stage_count);
1575    for handle in stage_handles {
1576        if let Ok(stats) = handle.join() {
1577            let divisor = stats.processed_batches.max(1) as f32;
1578            let avg_compute_ms = stats.total_compute_ms / divisor;
1579
1580            // Feedback loop: update cluster state with actual compute latency.
1581            if let Some(stage) = plan.stages.get(stats.stage_idx) {
1582                cluster.get_metrics_mut(&stage.node_id, |m| {
1583                    m.record_latency(avg_compute_ms * 1000.0);
1584                });
1585            }
1586
1587            stage_stats.push(StageExecutionStats {
1588                stage_idx: stats.stage_idx,
1589                processed_batches: stats.processed_batches,
1590                avg_compute_ms,
1591                avg_recv_wait_ms: stats.total_recv_wait_ms / divisor,
1592                avg_send_wait_ms: stats.total_send_wait_ms / divisor,
1593                avg_bridge_write_ms: 0.0,
1594                avg_bridge_read_ms: 0.0,
1595            });
1596        }
1597    }
1598
1599    let mut bridge_stats = Vec::with_capacity(bridge_handles.len());
1600    for handle in bridge_handles {
1601        if let Ok(stats) = handle.join() {
1602            bridge_stats.push(stats);
1603        }
1604    }
1605
1606    for bridge in bridge_stats {
1607        if let Some(stage) = stage_stats
1608            .iter_mut()
1609            .find(|s| s.stage_idx == bridge.source_stage)
1610        {
1611            let divisor = bridge.processed_batches.max(1) as f32;
1612            stage.avg_bridge_write_ms = bridge.total_write_ms / divisor;
1613            stage.avg_bridge_read_ms = bridge.total_read_ms / divisor;
1614        }
1615    }
1616
1617    stage_stats.sort_by_key(|s| s.stage_idx);
1618
1619    let throughput_tokens_per_sec = if total_time_ms > 0.0 {
1620        token_count as f32 / (total_time_ms / 1000.0)
1621    } else {
1622        0.0
1623    };
1624
1625    let avg_token_latency_ms = if token_latencies.is_empty() {
1626        0.0
1627    } else {
1628        token_latencies.iter().sum::<f32>() / token_latencies.len() as f32
1629    };
1630
1631    let mut sorted = token_latencies.clone();
1632    sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1633    let p95_idx = ((sorted.len().saturating_sub(1)) as f32 * 0.95).round() as usize;
1634    let p95_token_latency_ms = sorted.get(p95_idx).copied().unwrap_or(0.0);
1635    let p99_idx = ((sorted.len().saturating_sub(1)) as f32 * 0.99).round() as usize;
1636    let p99_token_latency_ms = sorted.get(p99_idx).copied().unwrap_or(0.0);
1637
1638    Ok(ExecutionResult {
1639        token_count,
1640        micro_batch,
1641        batch_count,
1642        stage_count,
1643        total_time_ms,
1644        throughput_tokens_per_sec,
1645        avg_token_latency_ms,
1646        p95_token_latency_ms,
1647        p99_token_latency_ms,
1648        token_latencies_ms: token_latencies,
1649        stage_stats,
1650    })
1651}
1652
1653pub fn execute_pipeline_tcp_loopback(
1654    plan: &PipelinePlan,
1655    token_count: usize,
1656    micro_batch: usize,
1657) -> Result<ExecutionResult, String> {
1658    execute_pipeline_tcp_loopback_with_config(
1659        plan,
1660        token_count,
1661        micro_batch,
1662        TcpTransportConfig::default(),
1663    )
1664}
1665
1666/// Execute pipeline stages with real TCP loopback transport bridges and explicit transport config.
1667pub fn execute_pipeline_tcp_loopback_with_config(
1668    plan: &PipelinePlan,
1669    token_count: usize,
1670    micro_batch: usize,
1671    config: TcpTransportConfig,
1672) -> Result<ExecutionResult, String> {
1673    let stage_count = plan.stages.len();
1674    let micro_batch = micro_batch.max(1);
1675    let batch_count = token_count.div_ceil(micro_batch);
1676    if stage_count == 0 || token_count == 0 {
1677        return Ok(ExecutionResult {
1678            token_count,
1679            micro_batch,
1680            batch_count,
1681            stage_count,
1682            total_time_ms: 0.0,
1683            throughput_tokens_per_sec: 0.0,
1684            avg_token_latency_ms: 0.0,
1685            p95_token_latency_ms: 0.0,
1686            p99_token_latency_ms: 0.0,
1687            token_latencies_ms: Vec::new(),
1688            stage_stats: Vec::new(),
1689        });
1690    }
1691
1692    #[derive(Debug)]
1693    struct StageAccumulator {
1694        stage_idx: usize,
1695        processed_batches: usize,
1696        total_compute_ms: f32,
1697        total_recv_wait_ms: f32,
1698        total_send_wait_ms: f32,
1699    }
1700
1701    let (entry_tx, entry_rx) = mpsc::channel::<TransportBatch>();
1702    let mut stage_inputs: Vec<Option<mpsc::Receiver<TransportBatch>>> =
1703        Vec::with_capacity(stage_count);
1704    stage_inputs.push(Some(entry_rx));
1705    let mut stage_outputs = Vec::with_capacity(stage_count);
1706    let mut bridge_handles = Vec::new();
1707    let bridge_capacity = config.max_inflight_batches.max(1);
1708
1709    for source_stage in 0..stage_count.saturating_sub(1) {
1710        let (bridge_in_tx, bridge_in_rx) = mpsc::sync_channel::<TransportBatch>(bridge_capacity);
1711        let (bridge_out_tx, bridge_out_rx) = mpsc::sync_channel::<TransportBatch>(bridge_capacity);
1712        stage_outputs.push(bridge_in_tx);
1713        stage_inputs.push(Some(bridge_out_rx));
1714
1715        let (listener, addr) = match config.transport {
1716            TransportKind::Tcp => {
1717                // For loopback execution, use distinct ports for each inter-stage bridge.
1718                let loopback = [127, 0, 0, 1].into();
1719                let bind_addr = SocketAddr::new(loopback, 0);
1720                let listener = TcpListener::bind(bind_addr)
1721                    .map_err(|e| format!("failed to bind TCP loopback listener: {e}"))?;
1722                let actual_addr = listener
1723                    .local_addr()
1724                    .map_err(|e| format!("failed to get TCP loopback addr: {e}"))?;
1725                (BridgeListener::Tcp(listener), BridgeAddr::Tcp(actual_addr))
1726            }
1727            #[cfg(unix)]
1728            TransportKind::Unix => {
1729                let sock_path = unique_bridge_socket_path(source_stage);
1730                let _ = std::fs::remove_file(&sock_path);
1731                let listener = UnixListener::bind(&sock_path).map_err(|e| {
1732                    format!("failed to bind Unix socket at {}: {e}", sock_path.display())
1733                })?;
1734                let addr = BridgeAddr::Unix(sock_path);
1735                (BridgeListener::Unix(listener), addr)
1736            }
1737            #[cfg(not(unix))]
1738            TransportKind::Unix => {
1739                return Err("Unix domain sockets require a Unix platform (Linux/macOS)".to_string());
1740            }
1741        };
1742
1743        bridge_handles.push(spawn_tcp_bridge(
1744            source_stage,
1745            bridge_in_rx,
1746            bridge_out_tx,
1747            config.clone(),
1748            listener,
1749            addr,
1750            None, // loopback benchmarking: no real remote-node failure mode to track
1751        ));
1752    }
1753
1754    let (completion_tx, completion_rx) = mpsc::sync_channel::<TransportBatch>(bridge_capacity);
1755    stage_outputs.push(completion_tx);
1756
1757    let mut stage_handles = Vec::with_capacity(stage_count);
1758    for stage_idx in 0..stage_count {
1759        let stage = plan.stages[stage_idx].clone();
1760        let rx = stage_inputs[stage_idx]
1761            .take()
1762            .expect("stage receiver should exist");
1763        let tx_next = stage_outputs[stage_idx].clone();
1764
1765        let handle = thread::spawn(move || {
1766            let mut processed_batches = 0usize;
1767            let mut total_compute_ms = 0.0_f32;
1768            let mut total_recv_wait_ms = 0.0_f32;
1769            let mut total_send_wait_ms = 0.0_f32;
1770
1771            loop {
1772                let recv_start = Instant::now();
1773                let Ok(mut batch) = rx.recv() else {
1774                    break;
1775                };
1776                total_recv_wait_ms += recv_start.elapsed().as_secs_f32() * 1000.0;
1777
1778                let compute_start = Instant::now();
1779                run_stage_compute(&mut batch.payload, &stage);
1780                total_compute_ms += compute_start.elapsed().as_secs_f32() * 1000.0;
1781
1782                let send_start = Instant::now();
1783                if tx_next.send(batch).is_err() {
1784                    break;
1785                }
1786                total_send_wait_ms += send_start.elapsed().as_secs_f32() * 1000.0;
1787                processed_batches += 1;
1788            }
1789
1790            StageAccumulator {
1791                stage_idx,
1792                processed_batches,
1793                total_compute_ms,
1794                total_recv_wait_ms,
1795                total_send_wait_ms,
1796            }
1797        });
1798
1799        stage_handles.push(handle);
1800    }
1801
1802    // Drop setup-time sender clones so channel closure can propagate and
1803    // stage/bridge threads can terminate once work is drained.
1804    drop(stage_outputs);
1805
1806    let mut batch_started_at = vec![Instant::now(); batch_count];
1807    let exec_start = Instant::now();
1808    for (batch_idx, batch_started_slot) in batch_started_at.iter_mut().enumerate() {
1809        let batch_start_token = batch_idx * micro_batch;
1810        let tokens_in_batch = (token_count - batch_start_token).min(micro_batch);
1811        let payload_len = (tokens_in_batch.max(1) * config.elems_per_token.max(1)).max(32);
1812        let payload = (0..payload_len)
1813            .map(|idx| (batch_idx as f32 * 0.01) + (idx as f32 * 0.0001))
1814            .collect();
1815
1816        *batch_started_slot = Instant::now();
1817        if entry_tx
1818            .send(TransportBatch {
1819                batch_id: batch_idx,
1820                tokens_in_batch,
1821                payload,
1822            })
1823            .is_err()
1824        {
1825            break;
1826        }
1827    }
1828    drop(entry_tx);
1829
1830    let mut token_latencies = Vec::with_capacity(token_count);
1831    for _ in 0..batch_count {
1832        let Ok(done_batch) = completion_rx.recv() else {
1833            break;
1834        };
1835
1836        let started_at = batch_started_at
1837            .get(done_batch.batch_id)
1838            .copied()
1839            .unwrap_or(exec_start);
1840        let batch_latency_ms = started_at.elapsed().as_secs_f32() * 1000.0;
1841        for _ in 0..done_batch.tokens_in_batch {
1842            token_latencies.push(batch_latency_ms);
1843        }
1844    }
1845    let total_time_ms = exec_start.elapsed().as_secs_f32() * 1000.0;
1846
1847    let mut stage_stats = Vec::with_capacity(stage_count);
1848    for handle in stage_handles {
1849        if let Ok(stats) = handle.join() {
1850            let divisor = stats.processed_batches.max(1) as f32;
1851            stage_stats.push(StageExecutionStats {
1852                stage_idx: stats.stage_idx,
1853                processed_batches: stats.processed_batches,
1854                avg_compute_ms: stats.total_compute_ms / divisor,
1855                avg_recv_wait_ms: stats.total_recv_wait_ms / divisor,
1856                avg_send_wait_ms: stats.total_send_wait_ms / divisor,
1857                avg_bridge_write_ms: 0.0,
1858                avg_bridge_read_ms: 0.0,
1859            });
1860        }
1861    }
1862
1863    let mut bridge_stats = Vec::with_capacity(bridge_handles.len());
1864    for handle in bridge_handles {
1865        if let Ok(stats) = handle.join() {
1866            bridge_stats.push(stats);
1867        }
1868    }
1869
1870    for bridge in bridge_stats {
1871        if let Some(stage) = stage_stats
1872            .iter_mut()
1873            .find(|s| s.stage_idx == bridge.source_stage)
1874        {
1875            let divisor = bridge.processed_batches.max(1) as f32;
1876            stage.avg_bridge_write_ms = bridge.total_write_ms / divisor;
1877            stage.avg_bridge_read_ms = bridge.total_read_ms / divisor;
1878        }
1879    }
1880
1881    stage_stats.sort_by_key(|s| s.stage_idx);
1882
1883    let throughput_tokens_per_sec = if total_time_ms > 0.0 {
1884        token_count as f32 / (total_time_ms / 1000.0)
1885    } else {
1886        0.0
1887    };
1888
1889    let avg_token_latency_ms = if token_latencies.is_empty() {
1890        0.0
1891    } else {
1892        token_latencies.iter().sum::<f32>() / token_latencies.len() as f32
1893    };
1894
1895    let mut sorted = token_latencies.clone();
1896    sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1897    let p95_idx = ((sorted.len().saturating_sub(1)) as f32 * 0.95).round() as usize;
1898    let p95_token_latency_ms = sorted.get(p95_idx).copied().unwrap_or(0.0);
1899    let p99_idx = ((sorted.len().saturating_sub(1)) as f32 * 0.99).round() as usize;
1900    let p99_token_latency_ms = sorted.get(p99_idx).copied().unwrap_or(0.0);
1901
1902    Ok(ExecutionResult {
1903        token_count,
1904        micro_batch,
1905        batch_count,
1906        stage_count,
1907        total_time_ms,
1908        throughput_tokens_per_sec,
1909        avg_token_latency_ms,
1910        p95_token_latency_ms,
1911        p99_token_latency_ms,
1912        token_latencies_ms: token_latencies,
1913        stage_stats,
1914    })
1915}
1916
1917// --- Real remote-worker execution -----------------------------------------
1918//
1919// Everything above this point (`execute_pipeline_distributed`,
1920// `execute_pipeline_tcp_loopback_with_config`, ...) runs entirely within one
1921// process: every stage's TCP bridge both binds *and* connects to a local
1922// interface, so even in "TCP transport" mode there is no way to reach a
1923// genuinely separate machine — `bind()` only ever succeeds for addresses
1924// this host actually owns. The two functions below are the real thing: a
1925// worker process that binds and accepts on a second machine, and a
1926// coordinator function that *connects out* to it (`TcpStream::connect`, not
1927// `TcpListener::bind`) instead of simulating both ends locally.
1928//
1929// Scope is intentionally narrow (exactly one local stage + one remote
1930// stage, one connection, one-shot) to match `flow`'s existing local+remote
1931// 2-node model rather than building a general N-worker mesh. The "compute"
1932// on both ends is still `run_stage_compute`'s synthetic proxy workload
1933// (real CPU time, not real LLM inference) — what's genuinely new here is
1934// the *transport*: a real socket between two real OS processes, optionally
1935// on two real machines, not the numbers `run_stage_compute` produces.
1936
1937/// A minimal wire handshake sent once when a coordinator connects to a
1938/// `run_stage_worker`: just enough for the worker to reconstruct the
1939/// `StagePlacement` it needs to call the same `run_stage_compute` the local
1940/// path uses, so timing is produced by identical code on both ends.
1941fn write_stage_handshake(writer: &mut impl Write, stage: &StagePlacement) -> Result<(), String> {
1942    let num_layers = stage.num_layers() as u32;
1943    let device_byte: u8 = match stage.device {
1944        DeviceKind::Npu => 0,
1945        DeviceKind::Gpu => 1,
1946        DeviceKind::Cpu => 2,
1947    };
1948    writer
1949        .write_all(&num_layers.to_le_bytes())
1950        .and_then(|_| writer.write_all(&[device_byte]))
1951        .map_err(|e| format!("failed to write stage handshake: {e}"))
1952}
1953
1954fn read_stage_handshake(reader: &mut impl Read) -> Result<StagePlacement, String> {
1955    let mut layers_bytes = [0u8; 4];
1956    reader
1957        .read_exact(&mut layers_bytes)
1958        .map_err(|e| format!("failed to read stage handshake: {e}"))?;
1959    let num_layers = u32::from_le_bytes(layers_bytes) as usize;
1960
1961    let mut device_byte = [0u8; 1];
1962    reader
1963        .read_exact(&mut device_byte)
1964        .map_err(|e| format!("failed to read stage handshake: {e}"))?;
1965    let device = match device_byte[0] {
1966        0 => DeviceKind::Npu,
1967        1 => DeviceKind::Gpu,
1968        _ => DeviceKind::Cpu,
1969    };
1970
1971    Ok(StagePlacement {
1972        node_id: "remote".to_string(),
1973        start_layer: 0,
1974        end_layer: num_layers,
1975        device,
1976        est_latency_ms: 0.0,
1977    })
1978}
1979
1980/// Summary a `stage-worker` process reports (to its own stdout, and back to
1981/// any caller driving it directly) once its one coordinator connection
1982/// closes.
1983#[derive(Clone, Debug, PartialEq)]
1984pub struct StageWorkerSummary {
1985    pub batches_processed: usize,
1986    pub avg_compute_ms: f32,
1987}
1988
1989/// Runs as a real, separate OS process (see `ghost-link stage-worker`):
1990/// accepts exactly one coordinator connection on `listener`, reads the
1991/// stage handshake, then loops read-batch → compute → write-result until
1992/// the coordinator closes the connection. One-shot, matching how
1993/// `cluster-start`'s child `listen` processes already behave.
1994///
1995/// Takes an already-bound `TcpListener` rather than binding internally so
1996/// the caller controls exactly when/how the port is chosen — the CLI binds
1997/// the address the user passed on `--bind`, while tests bind `:0` to get a
1998/// real OS-assigned free port and can read it back via `local_addr()`
1999/// before this function (which blocks until a peer connects) is called.
2000pub fn run_stage_worker(
2001    listener: TcpListener,
2002    config: &TcpTransportConfig,
2003) -> Result<StageWorkerSummary, String> {
2004    let (mut stream, peer) = listener
2005        .accept()
2006        .map_err(|e| format!("failed to accept a connection: {e}"))?;
2007    let _ = stream.set_nodelay(true);
2008
2009    let stage = read_stage_handshake(&mut stream)?;
2010    eprintln!(
2011        "[stage-worker] connection from {peer}; assigned {} layers on {}",
2012        stage.num_layers(),
2013        stage.device.as_str()
2014    );
2015
2016    let mut frame_buf = Vec::new();
2017    let mut read_buf = Vec::new();
2018    let mut batches_processed = 0usize;
2019    let mut total_compute_ms = 0.0_f32;
2020
2021    loop {
2022        let batch =
2023            match read_transport_batch(&mut stream, 0, config.auth_token.as_deref(), &mut read_buf)
2024            {
2025                Ok(Some(batch)) => batch,
2026                Ok(None) => break, // coordinator closed the connection cleanly
2027                Err(e) => return Err(format!("failed to read batch from coordinator: {e}")),
2028            };
2029
2030        let mut payload = batch.payload;
2031        let compute_start = Instant::now();
2032        run_stage_compute(&mut payload, &stage);
2033        total_compute_ms += compute_start.elapsed().as_secs_f32() * 1000.0;
2034
2035        let result = TransportBatch {
2036            batch_id: batch.batch_id,
2037            tokens_in_batch: batch.tokens_in_batch,
2038            payload,
2039        };
2040        write_transport_batch(
2041            &mut stream,
2042            &result,
2043            0,
2044            config.auth_token.as_deref(),
2045            &mut frame_buf,
2046        )
2047        .map_err(|e| format!("failed to write result batch to coordinator: {e}"))?;
2048        batches_processed += 1;
2049    }
2050
2051    let avg_compute_ms = if batches_processed > 0 {
2052        total_compute_ms / batches_processed as f32
2053    } else {
2054        0.0
2055    };
2056    eprintln!(
2057        "[stage-worker] coordinator disconnected after {batches_processed} batch(es), avg compute {avg_compute_ms:.2} ms"
2058    );
2059    Ok(StageWorkerSummary {
2060        batches_processed,
2061        avg_compute_ms,
2062    })
2063}
2064
2065/// Collapses every stage assigned to one node into a single logical
2066/// `StagePlacement` spanning that node's full layer range. The real layer
2067/// assignment algorithm doesn't hand each node one contiguous chunk — a
2068/// 60-layer/2-node `flow` plan produces ~11 raw stages (verified: 6 small
2069/// chunks for the local node, 5 for the remote one, each capped at roughly
2070/// 3GB), not the 2 this function originally assumed. The chunks for a given
2071/// node ARE always contiguous relative to each other, so merging them into
2072/// one placement per node is behaviorally sound: `run_stage_compute`'s
2073/// synthetic cost model only depends on total layer count and device kind,
2074/// not on how finely a node's range happens to be subdivided.
2075fn merge_stages_for_node(
2076    node_id: &str,
2077    stages: &[StagePlacement],
2078) -> Result<StagePlacement, String> {
2079    let matching: Vec<&StagePlacement> = stages.iter().filter(|s| s.node_id == node_id).collect();
2080    let Some(first) = matching.first() else {
2081        return Err(format!("no pipeline stages assigned to node '{node_id}'"));
2082    };
2083    let start_layer = matching.iter().map(|s| s.start_layer).min().unwrap_or(0);
2084    let end_layer = matching.iter().map(|s| s.end_layer).max().unwrap_or(0);
2085    Ok(StagePlacement {
2086        node_id: node_id.to_string(),
2087        start_layer,
2088        end_layer,
2089        device: first.device,
2090        est_latency_ms: matching.iter().map(|s| s.est_latency_ms).sum(),
2091    })
2092}
2093
2094/// Coordinator side of real remote execution. `plan` must have at least one
2095/// stage assigned to `local_node_id` and at least one assigned to
2096/// `remote_node_id` — matching `flow`'s existing local+remote 2-*node*
2097/// model, though each node's stages get merged (see `merge_stages_for_node`)
2098/// since the real assignment algorithm splits one node's layers into
2099/// several raw stages, not necessarily one. The merged local stage runs
2100/// in-process as usual; the merged remote stage's batches are sent over a
2101/// real outbound connection (`TcpStream::connect(remote_addr)`) to a
2102/// separate `run_stage_worker` process, so the round-trip time recorded
2103/// here is genuine network + remote-compute time, not a local simulation.
2104pub fn execute_pipeline_with_remote_stage(
2105    plan: &PipelinePlan,
2106    local_node_id: &str,
2107    remote_node_id: &str,
2108    token_count: usize,
2109    micro_batch: usize,
2110    config: TcpTransportConfig,
2111    remote_addr: SocketAddr,
2112) -> Result<ExecutionResult, String> {
2113    let local_stage = merge_stages_for_node(local_node_id, &plan.stages)?;
2114    let remote_stage = merge_stages_for_node(remote_node_id, &plan.stages)?;
2115    let local_stage = &local_stage;
2116    let remote_stage = &remote_stage;
2117
2118    let micro_batch = micro_batch.max(1);
2119    let batch_count = token_count.div_ceil(micro_batch);
2120    if token_count == 0 {
2121        return Ok(ExecutionResult {
2122            token_count,
2123            micro_batch,
2124            batch_count,
2125            stage_count: 2,
2126            total_time_ms: 0.0,
2127            throughput_tokens_per_sec: 0.0,
2128            avg_token_latency_ms: 0.0,
2129            p95_token_latency_ms: 0.0,
2130            p99_token_latency_ms: 0.0,
2131            token_latencies_ms: Vec::new(),
2132            stage_stats: Vec::new(),
2133        });
2134    }
2135
2136    let mut stream = TcpStream::connect(remote_addr).map_err(|e| {
2137        format!(
2138            "failed to connect to remote stage worker at {remote_addr}: {e} \
2139             (is `ghost-link stage-worker --bind {remote_addr}` running there?)"
2140        )
2141    })?;
2142    let _ = stream.set_nodelay(true);
2143    write_stage_handshake(&mut stream, remote_stage)?;
2144
2145    let mut frame_buf = Vec::new();
2146    let mut read_buf = Vec::new();
2147    let mut token_latencies = Vec::with_capacity(token_count);
2148    let mut local_compute_ms = 0.0_f32;
2149    let mut remote_round_trip_ms = 0.0_f32;
2150
2151    let exec_start = Instant::now();
2152    for batch_idx in 0..batch_count {
2153        let batch_start_token = batch_idx * micro_batch;
2154        let tokens_in_batch = (token_count - batch_start_token).min(micro_batch);
2155        let payload_len = (tokens_in_batch.max(1) * config.elems_per_token.max(1)).max(32);
2156        let mut payload: Vec<f32> = (0..payload_len)
2157            .map(|idx| (batch_idx as f32 * 0.01) + (idx as f32 * 0.0001))
2158            .collect();
2159
2160        let batch_started = Instant::now();
2161
2162        let local_start = Instant::now();
2163        run_stage_compute(&mut payload, local_stage);
2164        local_compute_ms += local_start.elapsed().as_secs_f32() * 1000.0;
2165
2166        let outgoing = TransportBatch {
2167            batch_id: batch_idx,
2168            tokens_in_batch,
2169            payload,
2170        };
2171        let rt_start = Instant::now();
2172        write_transport_batch(
2173            &mut stream,
2174            &outgoing,
2175            0,
2176            config.auth_token.as_deref(),
2177            &mut frame_buf,
2178        )
2179        .map_err(|e| format!("failed to send batch {batch_idx} to remote worker: {e}"))?;
2180        let result = read_transport_batch(&mut stream, 0, config.auth_token.as_deref(), &mut read_buf)
2181            .map_err(|e| format!("failed to read batch {batch_idx} result from remote worker: {e}"))?
2182            .ok_or_else(|| {
2183                format!("remote worker at {remote_addr} closed the connection before batch {batch_idx} completed")
2184            })?;
2185        remote_round_trip_ms += rt_start.elapsed().as_secs_f32() * 1000.0;
2186
2187        let batch_latency_ms = batch_started.elapsed().as_secs_f32() * 1000.0;
2188        for _ in 0..result.tokens_in_batch {
2189            token_latencies.push(batch_latency_ms);
2190        }
2191    }
2192    let total_time_ms = exec_start.elapsed().as_secs_f32() * 1000.0;
2193    drop(stream); // signals the worker's read loop to exit cleanly
2194
2195    let divisor = batch_count.max(1) as f32;
2196    let throughput_tokens_per_sec = if total_time_ms > 0.0 {
2197        token_count as f32 / (total_time_ms / 1000.0)
2198    } else {
2199        0.0
2200    };
2201    let avg_token_latency_ms = if token_latencies.is_empty() {
2202        0.0
2203    } else {
2204        token_latencies.iter().sum::<f32>() / token_latencies.len() as f32
2205    };
2206    let mut sorted = token_latencies.clone();
2207    sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
2208    let p95_idx = (sorted.len().saturating_sub(1) as f32 * 0.95).round() as usize;
2209    let p95_token_latency_ms = sorted.get(p95_idx).copied().unwrap_or(0.0);
2210    let p99_idx = (sorted.len().saturating_sub(1) as f32 * 0.99).round() as usize;
2211    let p99_token_latency_ms = sorted.get(p99_idx).copied().unwrap_or(0.0);
2212
2213    Ok(ExecutionResult {
2214        token_count,
2215        micro_batch,
2216        batch_count,
2217        stage_count: 2,
2218        total_time_ms,
2219        throughput_tokens_per_sec,
2220        avg_token_latency_ms,
2221        p95_token_latency_ms,
2222        p99_token_latency_ms,
2223        token_latencies_ms: token_latencies,
2224        stage_stats: vec![
2225            StageExecutionStats {
2226                stage_idx: 0,
2227                processed_batches: batch_count,
2228                avg_compute_ms: local_compute_ms / divisor,
2229                avg_recv_wait_ms: 0.0,
2230                avg_send_wait_ms: 0.0,
2231                avg_bridge_write_ms: 0.0,
2232                avg_bridge_read_ms: 0.0,
2233            },
2234            StageExecutionStats {
2235                stage_idx: 1,
2236                processed_batches: batch_count,
2237                // The remote side's own compute time isn't observable from
2238                // here without a second telemetry channel — what IS real
2239                // and observable is the round trip, reported as bridge
2240                // write time so it's visible in `ExecutionResult::summary()`
2241                // without inventing a number for compute we didn't measure.
2242                avg_compute_ms: 0.0,
2243                avg_recv_wait_ms: 0.0,
2244                avg_send_wait_ms: 0.0,
2245                avg_bridge_write_ms: remote_round_trip_ms / divisor,
2246                avg_bridge_read_ms: 0.0,
2247            },
2248        ],
2249    })
2250}
2251
2252#[cfg(test)]
2253mod tests {
2254    use super::*;
2255    use crate::planning::LayerAssignment;
2256    use std::collections::HashMap;
2257    use std::io::Cursor;
2258
2259    #[test]
2260    fn pipeline_plan_uses_node_device_mapping() {
2261        let assignments = vec![
2262            LayerAssignment::new("node-a".to_string(), 0, 10, 5.0),
2263            LayerAssignment::new("node-b".to_string(), 10, 20, 5.0),
2264        ];
2265
2266        let mut devices = HashMap::new();
2267        devices.insert("node-a".to_string(), DeviceKind::Gpu);
2268        devices.insert("node-b".to_string(), DeviceKind::Cpu);
2269
2270        let plan = PipelinePlan::from_assignments(&assignments, &devices);
2271
2272        assert_eq!(plan.stages.len(), 2);
2273        assert_eq!(plan.stages[0].device, DeviceKind::Gpu);
2274        assert_eq!(plan.stages[1].device, DeviceKind::Cpu);
2275        assert!(plan.est_step_latency_ms() > 0.0);
2276    }
2277
2278    #[test]
2279    fn schedule_size_matches_tokens_times_stages() {
2280        let schedule = build_token_schedule(3, 4);
2281        assert_eq!(schedule.len(), 12);
2282        assert_eq!(
2283            schedule[0],
2284            TokenStep {
2285                token_idx: 0,
2286                stage_idx: 0
2287            }
2288        );
2289        assert_eq!(
2290            schedule[11],
2291            TokenStep {
2292                token_idx: 3,
2293                stage_idx: 2
2294            }
2295        );
2296    }
2297
2298    #[test]
2299    fn execution_reports_basic_stage_metrics() {
2300        let plan = PipelinePlan {
2301            stages: vec![
2302                StagePlacement {
2303                    node_id: "node-a".to_string(),
2304                    start_layer: 0,
2305                    end_layer: 10,
2306                    device: DeviceKind::Gpu,
2307                    est_latency_ms: 1.0,
2308                },
2309                StagePlacement {
2310                    node_id: "node-b".to_string(),
2311                    start_layer: 10,
2312                    end_layer: 20,
2313                    device: DeviceKind::Gpu,
2314                    est_latency_ms: 1.0,
2315                },
2316            ],
2317        };
2318
2319        let result = execute_pipeline(&plan, 8, 1).expect("failed");
2320        assert_eq!(result.stage_count, 2);
2321        assert_eq!(result.token_count, 8);
2322        assert_eq!(result.micro_batch, 1);
2323        assert_eq!(result.batch_count, 8);
2324        assert_eq!(result.stage_stats.len(), 2);
2325        assert!(result.total_time_ms > 0.0);
2326        assert!(result.avg_token_latency_ms > 0.0);
2327        assert!(result.throughput_tokens_per_sec > 0.0);
2328    }
2329
2330    #[test]
2331    fn execution_respects_micro_batch_count() {
2332        let plan = PipelinePlan {
2333            stages: vec![
2334                StagePlacement {
2335                    node_id: "node-a".to_string(),
2336                    start_layer: 0,
2337                    end_layer: 10,
2338                    device: DeviceKind::Gpu,
2339                    est_latency_ms: 1.0,
2340                },
2341                StagePlacement {
2342                    node_id: "node-b".to_string(),
2343                    start_layer: 10,
2344                    end_layer: 20,
2345                    device: DeviceKind::Gpu,
2346                    est_latency_ms: 1.0,
2347                },
2348            ],
2349        };
2350
2351        let mb1 = execute_pipeline(&plan, 32, 1).expect("failed");
2352        let mb4 = execute_pipeline(&plan, 32, 4).expect("failed");
2353
2354        assert_eq!(mb1.batch_count, 32);
2355        assert_eq!(mb4.batch_count, 8);
2356        assert!(mb4.total_time_ms > 0.0);
2357    }
2358
2359    #[test]
2360    fn execution_supports_npu_stage_metrics() {
2361        let plan = PipelinePlan {
2362            stages: vec![
2363                StagePlacement {
2364                    node_id: "node-npu".to_string(),
2365                    start_layer: 0,
2366                    end_layer: 8,
2367                    device: DeviceKind::Npu,
2368                    est_latency_ms: 0.9,
2369                },
2370                StagePlacement {
2371                    node_id: "node-gpu".to_string(),
2372                    start_layer: 8,
2373                    end_layer: 16,
2374                    device: DeviceKind::Gpu,
2375                    est_latency_ms: 1.1,
2376                },
2377            ],
2378        };
2379
2380        let result = execute_pipeline(&plan, 12, 3).expect("failed");
2381        assert_eq!(result.stage_count, 2);
2382        assert_eq!(result.batch_count, 4);
2383        assert_eq!(result.stage_stats.len(), 2);
2384        assert!(result.stage_stats.iter().any(|s| s.stage_idx == 0));
2385        assert!(result.total_time_ms > 0.0);
2386        assert!(result.throughput_tokens_per_sec > 0.0);
2387    }
2388
2389    #[test]
2390    fn tcp_loopback_execution_reports_metrics() {
2391        let plan = PipelinePlan {
2392            stages: vec![
2393                StagePlacement {
2394                    node_id: "node-a".to_string(),
2395                    start_layer: 0,
2396                    end_layer: 10,
2397                    device: DeviceKind::Gpu,
2398                    est_latency_ms: 1.0,
2399                },
2400                StagePlacement {
2401                    node_id: "node-b".to_string(),
2402                    start_layer: 10,
2403                    end_layer: 20,
2404                    device: DeviceKind::Cpu,
2405                    est_latency_ms: 2.0,
2406                },
2407            ],
2408        };
2409
2410        let result = execute_pipeline_tcp_loopback(&plan, 16, 2).expect("failed");
2411        assert_eq!(result.token_count, 16);
2412        assert_eq!(result.batch_count, 8);
2413        assert_eq!(result.stage_stats.len(), 2);
2414        assert!(result.total_time_ms > 0.0);
2415        assert!(result.throughput_tokens_per_sec > 0.0);
2416        assert!(
2417            result.p99_token_latency_ms >= result.p95_token_latency_ms,
2418            "p99 ({}) must be >= p95 ({})",
2419            result.p99_token_latency_ms,
2420            result.p95_token_latency_ms
2421        );
2422    }
2423
2424    #[test]
2425    fn elems_per_token_config_scales_payload_size_and_still_completes() {
2426        let plan = PipelinePlan {
2427            stages: vec![
2428                StagePlacement {
2429                    node_id: "node-a".to_string(),
2430                    start_layer: 0,
2431                    end_layer: 10,
2432                    device: DeviceKind::Gpu,
2433                    est_latency_ms: 1.0,
2434                },
2435                StagePlacement {
2436                    node_id: "node-b".to_string(),
2437                    start_layer: 10,
2438                    end_layer: 20,
2439                    device: DeviceKind::Cpu,
2440                    est_latency_ms: 2.0,
2441                },
2442            ],
2443        };
2444
2445        // 4096 hidden_dim * 2 bytes (fp16/bf16) / 4 bytes per wire f32 = 2048
2446        // elements/token — a real 7B-class model's per-token activation size,
2447        // instead of the default 16-element synthetic stand-in.
2448        let config = TcpTransportConfig {
2449            elems_per_token: 2048,
2450            ..TcpTransportConfig::default()
2451        };
2452
2453        let result = execute_pipeline_tcp_loopback_with_config(&plan, 8, 2, config)
2454            .expect("large-payload loopback execution failed");
2455        assert_eq!(result.token_count, 8);
2456        assert!(result.total_time_ms > 0.0);
2457        assert!(
2458            result.p99_token_latency_ms >= result.p95_token_latency_ms,
2459            "p99 ({}) must be >= p95 ({})",
2460            result.p99_token_latency_ms,
2461            result.p95_token_latency_ms
2462        );
2463    }
2464
2465    #[test]
2466    fn tcp_loopback_execution_with_hardening_config_reports_metrics() {
2467        let plan = PipelinePlan {
2468            stages: vec![
2469                StagePlacement {
2470                    node_id: "node-a".to_string(),
2471                    start_layer: 0,
2472                    end_layer: 12,
2473                    device: DeviceKind::Gpu,
2474                    est_latency_ms: 1.5,
2475                },
2476                StagePlacement {
2477                    node_id: "node-b".to_string(),
2478                    start_layer: 12,
2479                    end_layer: 24,
2480                    device: DeviceKind::Cpu,
2481                    est_latency_ms: 2.5,
2482                },
2483            ],
2484        };
2485
2486        let result = execute_pipeline_tcp_loopback_with_config(
2487            &plan,
2488            24,
2489            3,
2490            TcpTransportConfig {
2491                max_inflight_batches: 4,
2492                reconnect_attempts: 4,
2493                reconnect_backoff_ms: 5,
2494                auth_token: Some("test-token".to_string()),
2495                ..Default::default()
2496            },
2497        )
2498        .expect("hardened loopback failed");
2499
2500        assert_eq!(result.token_count, 24);
2501        assert_eq!(result.batch_count, 8);
2502        assert_eq!(result.stage_stats.len(), 2);
2503        assert!(result.total_time_ms > 0.0);
2504        assert!(result.throughput_tokens_per_sec > 0.0);
2505    }
2506
2507    #[test]
2508    fn transport_rejects_auth_mismatch_frames() {
2509        let batch = TransportBatch {
2510            batch_id: 7,
2511            tokens_in_batch: 4,
2512            payload: vec![0.1, 0.2, 0.3, 0.4],
2513        };
2514        let mut encoded = Vec::new();
2515        let mut frame_buf = Vec::new();
2516        write_transport_batch(&mut encoded, &batch, 0, Some("token-a"), &mut frame_buf)
2517            .expect("encode frame");
2518
2519        let mut cursor = Cursor::new(encoded);
2520        let mut payload_buf = Vec::new();
2521        let err = read_transport_batch(&mut cursor, 0, Some("token-b"), &mut payload_buf)
2522            .expect_err("mismatched token should fail");
2523        assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
2524    }
2525
2526    #[test]
2527    fn transport_rejects_unexpected_source_stage() {
2528        let batch = TransportBatch {
2529            batch_id: 2,
2530            tokens_in_batch: 2,
2531            payload: vec![1.0, 2.0],
2532        };
2533        let mut encoded = Vec::new();
2534        let mut frame_buf = Vec::new();
2535        write_transport_batch(&mut encoded, &batch, 1, Some("token"), &mut frame_buf)
2536            .expect("encode frame");
2537
2538        let mut cursor = Cursor::new(encoded);
2539        let mut payload_buf = Vec::new();
2540        let err = read_transport_batch(&mut cursor, 0, Some("token"), &mut payload_buf)
2541            .expect_err("source-stage mismatch should fail");
2542        assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
2543    }
2544
2545    #[cfg(unix)]
2546    #[test]
2547    fn unix_socket_loopback_execution_reports_metrics() {
2548        let plan = PipelinePlan {
2549            stages: vec![
2550                StagePlacement {
2551                    node_id: "node-a".to_string(),
2552                    start_layer: 0,
2553                    end_layer: 10,
2554                    device: DeviceKind::Gpu,
2555                    est_latency_ms: 1.0,
2556                },
2557                StagePlacement {
2558                    node_id: "node-b".to_string(),
2559                    start_layer: 10,
2560                    end_layer: 20,
2561                    device: DeviceKind::Cpu,
2562                    est_latency_ms: 2.0,
2563                },
2564            ],
2565        };
2566
2567        let result = execute_pipeline_tcp_loopback_with_config(
2568            &plan,
2569            16,
2570            2,
2571            TcpTransportConfig {
2572                transport: TransportKind::Unix,
2573                ..Default::default()
2574            },
2575        )
2576        .expect("unix socket loopback failed");
2577
2578        assert_eq!(result.token_count, 16);
2579        assert_eq!(result.batch_count, 8);
2580        assert_eq!(result.stage_stats.len(), 2);
2581        assert!(result.total_time_ms > 0.0);
2582        assert!(result.throughput_tokens_per_sec > 0.0);
2583    }
2584
2585    #[test]
2586    fn stage_handshake_round_trips_layer_count_and_device() {
2587        let stage = StagePlacement {
2588            node_id: "coordinator-local".to_string(),
2589            start_layer: 5,
2590            end_layer: 37,
2591            device: DeviceKind::Gpu,
2592            est_latency_ms: 12.0,
2593        };
2594        let mut buf = Vec::new();
2595        write_stage_handshake(&mut buf, &stage).expect("handshake write failed");
2596
2597        let mut cursor = Cursor::new(buf);
2598        let decoded = read_stage_handshake(&mut cursor).expect("handshake read failed");
2599        assert_eq!(decoded.num_layers(), stage.num_layers());
2600        assert_eq!(decoded.device, DeviceKind::Gpu);
2601    }
2602
2603    #[test]
2604    fn execute_pipeline_with_remote_stage_rejects_when_remote_node_has_no_stages() {
2605        let plan = PipelinePlan {
2606            stages: vec![StagePlacement {
2607                node_id: "solo".to_string(),
2608                start_layer: 0,
2609                end_layer: 10,
2610                device: DeviceKind::Cpu,
2611                est_latency_ms: 1.0,
2612            }],
2613        };
2614        let err = execute_pipeline_with_remote_stage(
2615            &plan,
2616            "solo",
2617            "nowhere",
2618            8,
2619            2,
2620            TcpTransportConfig::default(),
2621            "127.0.0.1:1".parse().unwrap(),
2622        )
2623        .unwrap_err();
2624        assert!(
2625            err.contains("no pipeline stages assigned to node 'nowhere'"),
2626            "unexpected error: {err}"
2627        );
2628    }
2629
2630    #[test]
2631    fn merge_stages_for_node_collapses_multiple_chunks_into_one_placement() {
2632        // Regression: real layer assignment splits one node's range into
2633        // several raw stages (verified manually: 60 layers / 2 nodes
2634        // produced 11 raw stages, not 2) — merging must still produce the
2635        // node's FULL range, not just its first chunk.
2636        let stages = vec![
2637            StagePlacement {
2638                node_id: "node-remote".to_string(),
2639                start_layer: 32,
2640                end_layer: 38,
2641                device: DeviceKind::Gpu,
2642                est_latency_ms: 1.0,
2643            },
2644            StagePlacement {
2645                node_id: "node-remote".to_string(),
2646                start_layer: 38,
2647                end_layer: 44,
2648                device: DeviceKind::Gpu,
2649                est_latency_ms: 1.5,
2650            },
2651            StagePlacement {
2652                node_id: "node-local".to_string(),
2653                start_layer: 0,
2654                end_layer: 32,
2655                device: DeviceKind::Cpu,
2656                est_latency_ms: 3.0,
2657            },
2658        ];
2659        let merged = merge_stages_for_node("node-remote", &stages).expect("merge failed");
2660        assert_eq!(merged.start_layer, 32);
2661        assert_eq!(merged.end_layer, 44);
2662        assert_eq!(merged.num_layers(), 12);
2663        assert_eq!(merged.device, DeviceKind::Gpu);
2664        assert!((merged.est_latency_ms - 2.5).abs() < 1e-6);
2665    }
2666
2667    /// The real thing, end to end: a genuine second OS thread (standing in
2668    /// for a genuine second OS *process*, which the CLI-level integration
2669    /// test in the ghost-link crate covers) binds a real TCP listener on an
2670    /// OS-assigned port, and the coordinator function connects out to it —
2671    /// not the local-bind-on-both-ends simulation every other function in
2672    /// this file uses. Proves the round trip is real by asserting non-zero
2673    /// bridge time was actually measured, and that the worker's own
2674    /// independently-tracked batch count agrees with the coordinator's.
2675    #[test]
2676    fn execute_pipeline_with_remote_stage_does_a_real_tcp_round_trip() {
2677        let listener = TcpListener::bind("127.0.0.1:0").expect("failed to bind test listener");
2678        let worker_addr = listener.local_addr().expect("failed to read local_addr");
2679
2680        let worker_handle =
2681            thread::spawn(move || run_stage_worker(listener, &TcpTransportConfig::default()));
2682
2683        let plan = PipelinePlan {
2684            stages: vec![
2685                StagePlacement {
2686                    node_id: "local".to_string(),
2687                    start_layer: 0,
2688                    end_layer: 10,
2689                    device: DeviceKind::Cpu,
2690                    est_latency_ms: 1.0,
2691                },
2692                StagePlacement {
2693                    node_id: "remote".to_string(),
2694                    start_layer: 10,
2695                    end_layer: 20,
2696                    device: DeviceKind::Cpu,
2697                    est_latency_ms: 1.0,
2698                },
2699            ],
2700        };
2701
2702        let result = execute_pipeline_with_remote_stage(
2703            &plan,
2704            "local",
2705            "remote",
2706            16,
2707            2,
2708            TcpTransportConfig::default(),
2709            worker_addr,
2710        )
2711        .expect("real remote-stage execution failed");
2712
2713        let worker_summary = worker_handle
2714            .join()
2715            .expect("worker thread panicked")
2716            .expect("worker returned an error");
2717
2718        assert_eq!(result.token_count, 16);
2719        assert_eq!(result.batch_count, 8);
2720        assert_eq!(result.stage_stats.len(), 2);
2721        assert_eq!(worker_summary.batches_processed, 8);
2722        assert_eq!(
2723            result.stage_stats[1].processed_batches,
2724            worker_summary.batches_processed,
2725            "coordinator's batch count must agree with what the independent worker process actually processed"
2726        );
2727        assert!(
2728            result.stage_stats[1].avg_bridge_write_ms > 0.0,
2729            "remote round-trip time must be non-zero for a real socket hop, got {}",
2730            result.stage_stats[1].avg_bridge_write_ms
2731        );
2732    }
2733
2734    #[test]
2735    fn spawn_tcp_bridge_skips_connection_attempts_when_circuit_breaker_is_open() {
2736        use crate::circuit_breaker::{CircuitBreakerConfig, CircuitState};
2737
2738        // Bind-then-drop reliably yields an address nothing is listening on.
2739        let dead_addr = {
2740            let l = TcpListener::bind("127.0.0.1:0").unwrap();
2741            l.local_addr().unwrap()
2742        };
2743        let dummy_listener = BridgeListener::Tcp(TcpListener::bind("127.0.0.1:0").unwrap());
2744
2745        let breaker = CircuitBreaker::with_config(CircuitBreakerConfig {
2746            failure_threshold: 1,
2747            open_duration_secs: 3600,
2748            reset_timeout_secs: 3600,
2749        });
2750        breaker.record_failure();
2751        assert_eq!(breaker.current_state(), CircuitState::Open);
2752
2753        let (in_tx, in_rx) = mpsc::sync_channel::<TransportBatch>(1);
2754        let (out_tx, _out_rx) = mpsc::sync_channel::<TransportBatch>(1);
2755        drop(in_tx); // closes the passthrough loop's input immediately
2756
2757        let config = TcpTransportConfig {
2758            reconnect_attempts: 5,
2759            reconnect_backoff_ms: 2000,
2760            ..TcpTransportConfig::default()
2761        };
2762
2763        let start = Instant::now();
2764        let handle = spawn_tcp_bridge(
2765            0,
2766            in_rx,
2767            out_tx,
2768            config,
2769            dummy_listener,
2770            BridgeAddr::Tcp(dead_addr),
2771            Some(breaker),
2772        );
2773        handle.join().expect("bridge thread panicked");
2774
2775        // 5 attempts * 2s backoff would take >=10s if the open breaker weren't
2776        // honored; a healthy short-circuit finishes in well under a second.
2777        assert!(
2778            start.elapsed() < Duration::from_millis(1500),
2779            "open circuit breaker should skip connection attempts entirely, took {:?}",
2780            start.elapsed()
2781        );
2782    }
2783}