Skip to main content

ghostlink_core/
runtime.rs

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