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