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