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