1use crate::circuit_breaker::CircuitBreaker;
7use crate::planning::LayerAssignment;
8use hmac::{Hmac, Mac};
9use sha2::Sha256;
10use std::io::{self, Read, Write};
11use std::io::{BufReader, BufWriter};
12use std::net::{Shutdown as TcpShutdown, SocketAddr, TcpListener, TcpStream};
13use std::path::PathBuf;
14use std::slice;
15use std::sync::atomic::{AtomicU64, Ordering};
16use std::sync::mpsc;
17use std::sync::Arc;
18use std::thread;
19use std::time::Duration;
20use std::time::Instant;
21
22#[cfg(unix)]
23#[cfg(unix)]
24use std::net::Shutdown as UnixSocketShutdown;
25#[cfg(unix)]
26use std::os::unix::net::{UnixListener, UnixStream};
27
28#[derive(Clone, Copy, Debug, PartialEq, Eq)]
30pub enum DeviceKind {
31 Npu,
32 Gpu,
33 Cpu,
34}
35
36impl DeviceKind {
37 pub const fn as_str(self) -> &'static str {
39 match self {
40 Self::Npu => "NPU",
41 Self::Gpu => "GPU",
42 Self::Cpu => "CPU",
43 }
44 }
45}
46
47#[derive(Clone, Debug, PartialEq)]
49pub struct StagePlacement {
50 pub node_id: String,
51 pub start_layer: usize,
52 pub end_layer: usize,
53 pub device: DeviceKind,
54 pub est_latency_ms: f32,
56}
57
58impl StagePlacement {
59 pub fn num_layers(&self) -> usize {
60 self.end_layer.saturating_sub(self.start_layer)
61 }
62}
63
64#[derive(Clone, Debug, Default, PartialEq)]
66pub struct PipelinePlan {
67 pub stages: Vec<StagePlacement>,
68}
69
70impl PipelinePlan {
71 pub fn from_assignments(
73 assignments: &[LayerAssignment],
74 device_by_node: &std::collections::HashMap<String, DeviceKind>,
75 ) -> Self {
76 Self::from_assignments_with_measured(assignments, device_by_node, &Default::default())
77 }
78
79 pub fn from_assignments_with_measured(
81 assignments: &[LayerAssignment],
82 device_by_node: &std::collections::HashMap<String, DeviceKind>,
83 cluster: &crate::cluster::ClusterState,
84 ) -> Self {
85 let stages = assignments
86 .iter()
87 .map(|assignment| {
88 let device = device_by_node
89 .get(&assignment.node_id)
90 .copied()
91 .unwrap_or(DeviceKind::Cpu);
92
93 let per_layer_cost_ms =
95 if let Some(metrics) = cluster.get_metrics(&assignment.node_id) {
96 if metrics.latency_samples > 0 && assignment.num_layers > 0 {
97 (metrics.avg_latency_us / 1000.0) / assignment.num_layers as f32
98 } else {
99 Self::default_cost_for_device(device)
100 }
101 } else {
102 Self::default_cost_for_device(device)
103 };
104
105 StagePlacement {
106 node_id: assignment.node_id.clone(),
107 start_layer: assignment.start_layer,
108 end_layer: assignment.end_layer,
109 device,
110 est_latency_ms: per_layer_cost_ms * assignment.num_layers as f32,
111 }
112 })
113 .collect();
114
115 Self { stages }
116 }
117
118 fn default_cost_for_device(device: DeviceKind) -> f32 {
119 match device {
120 DeviceKind::Npu => 0.42,
121 DeviceKind::Gpu => 0.55,
122 DeviceKind::Cpu => 1.25,
123 }
124 }
125
126 pub fn est_step_latency_ms(&self) -> f32 {
128 self.stages.iter().map(|s| s.est_latency_ms).sum()
129 }
130
131 pub fn summary(&self) -> String {
132 let mut out = String::from("Pipeline Plan\n");
133 out.push_str("============\n");
134
135 for (idx, stage) in self.stages.iter().enumerate() {
136 out.push_str(&format!(
137 "Stage {}: {} layers {}-{} on {} ({:.2} ms)\n",
138 idx,
139 stage.node_id,
140 stage.start_layer,
141 stage.end_layer,
142 stage.device.as_str(),
143 stage.est_latency_ms
144 ));
145 }
146
147 out.push_str(&format!(
148 "Estimated per-token step latency: {:.2} ms\n",
149 self.est_step_latency_ms()
150 ));
151 out
152 }
153}
154
155fn run_stage_compute(payload: &mut [f32], stage: &StagePlacement) {
156 let base_rounds = stage.num_layers().max(1) / 4 + 1;
157 let rounds = match stage.device {
158 DeviceKind::Npu => base_rounds,
159 DeviceKind::Gpu => base_rounds * 2,
160 DeviceKind::Cpu => base_rounds * 3,
161 };
162
163 let alpha = match stage.device {
164 DeviceKind::Npu => 1.001_f32,
165 DeviceKind::Gpu => 1.003_f32,
166 DeviceKind::Cpu => 1.005_f32,
167 };
168
169 for _ in 0..rounds {
170 for value in payload.iter_mut() {
171 *value = ((*value * alpha) + 0.125).sin();
172 }
173 }
174}
175
176#[derive(Clone, Debug, PartialEq)]
178pub struct StageExecutionStats {
179 pub stage_idx: usize,
180 pub processed_batches: usize,
181 pub avg_compute_ms: f32,
182 pub avg_recv_wait_ms: f32,
183 pub avg_send_wait_ms: f32,
184 pub avg_bridge_write_ms: f32,
185 pub avg_bridge_read_ms: f32,
186}
187
188#[derive(Clone, Debug, PartialEq)]
190pub struct ExecutionResult {
191 pub token_count: usize,
192 pub micro_batch: usize,
193 pub batch_count: usize,
194 pub stage_count: usize,
195 pub total_time_ms: f32,
196 pub throughput_tokens_per_sec: f32,
197 pub avg_token_latency_ms: f32,
198 pub p95_token_latency_ms: f32,
199 pub p99_token_latency_ms: f32,
200 pub token_latencies_ms: Vec<f32>,
206 pub stage_stats: Vec<StageExecutionStats>,
207}
208
209#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
211pub enum TransportKind {
212 #[default]
214 Tcp,
215 Unix,
218}
219
220#[derive(Debug)]
222pub enum BridgeListener {
223 Tcp(TcpListener),
224 #[cfg(unix)]
225 Unix(UnixListener),
226}
227
228impl BridgeListener {
229 pub fn accept(&self) -> io::Result<BridgeStream> {
231 match self {
232 BridgeListener::Tcp(l) => l.accept().map(|(s, _)| BridgeStream::Tcp(s)),
233 #[cfg(unix)]
234 BridgeListener::Unix(l) => l.accept().map(|(s, _)| BridgeStream::Unix(s)),
235 }
236 }
237}
238
239pub enum BridgeStream {
241 Tcp(TcpStream),
242 #[cfg(unix)]
243 Unix(UnixStream),
244}
245
246impl Read for BridgeStream {
247 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
248 match self {
249 BridgeStream::Tcp(s) => s.read(buf),
250 #[cfg(unix)]
251 BridgeStream::Unix(s) => s.read(buf),
252 }
253 }
254}
255
256impl Write for BridgeStream {
257 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
258 match self {
259 BridgeStream::Tcp(s) => s.write(buf),
260 #[cfg(unix)]
261 BridgeStream::Unix(s) => s.write(buf),
262 }
263 }
264 fn flush(&mut self) -> io::Result<()> {
265 match self {
266 BridgeStream::Tcp(s) => s.flush(),
267 #[cfg(unix)]
268 BridgeStream::Unix(s) => s.flush(),
269 }
270 }
271}
272
273impl BridgeStream {
274 pub fn set_nodelay(&self, nodelay: bool) -> io::Result<()> {
276 match self {
277 BridgeStream::Tcp(s) => s.set_nodelay(nodelay),
278 #[cfg(unix)]
279 BridgeStream::Unix(_) => Ok(()),
280 }
281 }
282
283 pub fn set_send_buffer_size(&self, _size: usize) -> io::Result<()> {
285 match self {
286 BridgeStream::Tcp(_s) => {
287 Ok(())
290 }
291 #[cfg(unix)]
292 BridgeStream::Unix(_) => Ok(()),
293 }
294 }
295
296 pub fn set_recv_buffer_size(&self, _size: usize) -> io::Result<()> {
298 match self {
299 BridgeStream::Tcp(_s) => {
300 Ok(())
303 }
304 #[cfg(unix)]
305 BridgeStream::Unix(_) => Ok(()),
306 }
307 }
308
309 pub fn shutdown_write(&self) -> io::Result<()> {
311 match self {
312 BridgeStream::Tcp(s) => s.shutdown(TcpShutdown::Write),
313 #[cfg(unix)]
314 BridgeStream::Unix(s) => s.shutdown(UnixSocketShutdown::Write),
315 }
316 }
317}
318
319#[derive(Clone, Debug)]
321pub enum BridgeAddr {
322 Tcp(SocketAddr),
323 Unix(PathBuf),
324}
325
326#[cfg(unix)]
335fn unique_bridge_socket_path(source_stage: usize) -> std::path::PathBuf {
336 use std::sync::atomic::{AtomicU64, Ordering as AtomicOrdering};
337 static BRIDGE_SOCKET_SEQ: AtomicU64 = AtomicU64::new(0);
338 let seq = BRIDGE_SOCKET_SEQ.fetch_add(1, AtomicOrdering::Relaxed);
339 let mut p = std::env::temp_dir();
340 p.push(format!(
341 "ghostlink-bridge-{}-{}-{}.sock",
342 std::process::id(),
343 seq,
344 source_stage
345 ));
346 p
347}
348
349impl BridgeAddr {
350 fn connect(&self, timeout: Duration) -> io::Result<BridgeStream> {
352 match self {
353 BridgeAddr::Tcp(addr) => {
354 TcpStream::connect_timeout(addr, timeout).map(BridgeStream::Tcp)
355 }
356 #[cfg(unix)]
357 BridgeAddr::Unix(path) => UnixStream::connect(path).map(BridgeStream::Unix),
358 #[cfg(not(unix))]
359 BridgeAddr::Unix(_) => Err(io::Error::new(
360 io::ErrorKind::Unsupported,
361 "Unix domain sockets require a Unix platform",
362 )),
363 }
364 }
365}
366
367#[derive(Clone, Debug, PartialEq, Eq)]
369pub struct TcpTransportConfig {
370 pub max_inflight_batches: usize,
371 pub reconnect_attempts: usize,
372 pub reconnect_backoff_ms: u64,
373 pub auth_token: Option<String>,
375 pub auth_session_token: Option<String>,
379 pub use_mtls: bool,
380 pub cert_chain_path: Option<String>,
381 pub transport: TransportKind,
383 pub send_buffer_size: usize,
385 pub recv_buffer_size: usize,
387 pub use_vectored_io: bool,
389 pub elems_per_token: usize,
398}
399
400impl Default for TcpTransportConfig {
401 fn default() -> Self {
402 Self {
403 max_inflight_batches: 512,
404 reconnect_attempts: 3,
405 reconnect_backoff_ms: 25,
406 auth_token: None,
407 auth_session_token: None,
408 use_mtls: false,
409 cert_chain_path: None,
410 transport: TransportKind::Tcp,
411 send_buffer_size: 256 * 1024, recv_buffer_size: 256 * 1024, use_vectored_io: true,
414 elems_per_token: 16,
415 }
416 }
417}
418
419#[derive(Clone, Debug)]
421struct SessionAuth {
422 session_id: u64,
423 seq: Arc<AtomicU64>,
424 peer_seq: Arc<AtomicU64>,
425}
426
427impl ExecutionResult {
428 pub fn summary(&self) -> String {
429 let mut out = String::from("Execution Runtime\n");
430 out.push_str("=================\n");
431 out.push_str(&format!(
432 "Tokens: {} | Micro-batch: {} | Batches: {} | Stages: {}\n",
433 self.token_count, self.micro_batch, self.batch_count, self.stage_count
434 ));
435 out.push_str(&format!(
436 "Measured wall-clock time: {:.2} ms\n",
437 self.total_time_ms
438 ));
439 out.push_str(&format!(
440 "Throughput: {:.2} tokens/sec\n",
441 self.throughput_tokens_per_sec
442 ));
443 out.push_str(&format!(
444 "Avg token latency: {:.2} ms | P95: {:.2} ms | P99: {:.2} ms\n",
445 self.avg_token_latency_ms, self.p95_token_latency_ms, self.p99_token_latency_ms
446 ));
447
448 for stage in &self.stage_stats {
449 out.push_str(&format!(
450 "Stage {} batches={} compute={:.2} ms recv-wait={:.2} ms send-wait={:.2} ms bridge-write={:.2} ms bridge-read={:.2} ms\n",
451 stage.stage_idx,
452 stage.processed_batches,
453 stage.avg_compute_ms,
454 stage.avg_recv_wait_ms,
455 stage.avg_send_wait_ms,
456 stage.avg_bridge_write_ms,
457 stage.avg_bridge_read_ms
458 ));
459 }
460
461 out
462 }
463}
464
465#[derive(Clone, Debug, PartialEq, Eq)]
467pub struct TokenStep {
468 pub token_idx: usize,
469 pub stage_idx: usize,
470}
471
472pub fn build_token_schedule(stage_count: usize, token_count: usize) -> Vec<TokenStep> {
477 let mut schedule = Vec::with_capacity(stage_count.saturating_mul(token_count));
478 for token_idx in 0..token_count {
479 for stage_idx in 0..stage_count {
480 schedule.push(TokenStep {
481 token_idx,
482 stage_idx,
483 });
484 }
485 }
486 schedule
487}
488
489pub fn execute_pipeline(
494 plan: &PipelinePlan,
495 token_count: usize,
496 micro_batch: usize,
497) -> Result<ExecutionResult, String> {
498 execute_pipeline_with_rebalance(plan, token_count, micro_batch, None)
499}
500
501pub fn execute_pipeline_with_rebalance(
502 plan: &PipelinePlan,
503 token_count: usize,
504 micro_batch: usize,
505 rebalance: Option<&crate::planning::RebalanceTrigger>,
506) -> Result<ExecutionResult, String> {
507 execute_pipeline_with_rebalance_and_measured(
508 plan,
509 token_count,
510 micro_batch,
511 rebalance,
512 None,
513 None,
514 )
515}
516
517pub fn execute_pipeline_with_rebalance_and_measured(
518 plan: &PipelinePlan,
519 token_count: usize,
520 micro_batch: usize,
521 rebalance: Option<&crate::planning::RebalanceTrigger>,
522 cluster: Option<&crate::cluster::ClusterState>,
523 placement_context: Option<&crate::planning::PlacementPlan>,
524) -> Result<ExecutionResult, String> {
525 let stage_count = plan.stages.len();
526 let micro_batch = micro_batch.max(1);
527 let batch_count = token_count.div_ceil(micro_batch);
528 if stage_count == 0 || token_count == 0 {
529 return Ok(ExecutionResult {
530 token_count,
531 micro_batch,
532 batch_count,
533 stage_count,
534 total_time_ms: 0.0,
535 throughput_tokens_per_sec: 0.0,
536 avg_token_latency_ms: 0.0,
537 p95_token_latency_ms: 0.0,
538 p99_token_latency_ms: 0.0,
539 token_latencies_ms: Vec::new(),
540 stage_stats: Vec::new(),
541 });
542 }
543
544 #[derive(Debug)]
545 struct BatchWork {
546 tokens_in_batch: usize,
547 started_at: Instant,
548 payload: Vec<f32>,
549 }
550
551 #[derive(Debug)]
552 struct StageAccumulator {
553 stage_idx: usize,
554 processed_batches: usize,
555 total_compute_ms: f32,
556 total_recv_wait_ms: f32,
557 total_send_wait_ms: f32,
558 }
559
560 use crate::ring::{RingConfig, SpscRingBuffer};
561 use std::sync::Arc;
562
563 let ring_cfg = RingConfig::default();
566
567 let mut rings = Vec::with_capacity(stage_count + 1);
568 for _ in 0..=stage_count {
569 rings.push(Arc::new(SpscRingBuffer::<BatchWork>::new(ring_cfg)));
570 }
571
572 let recycler_ring = Arc::new(SpscRingBuffer::<Vec<f32>>::new(ring_cfg));
574
575 let mut stage_handles = Vec::with_capacity(stage_count);
576 for stage_idx in 0..stage_count {
577 let stage = plan.stages[stage_idx].clone();
578 let rx_ring = Arc::clone(&rings[stage_idx]);
579 let tx_ring = Arc::clone(&rings[stage_idx + 1]);
580
581 let handle = thread::spawn(move || {
582 let mut total_compute_ms = 0.0_f32;
583 let mut total_recv_wait_ms = 0.0_f32;
584 let mut total_send_wait_ms = 0.0_f32;
585
586 for _ in 0..batch_count {
590 let recv_start = Instant::now();
591 rx_ring.wait_for_data();
592 let mut batch = rx_ring.pop().unwrap();
593 total_recv_wait_ms += recv_start.elapsed().as_secs_f32() * 1000.0;
594
595 let compute_start = Instant::now();
596 run_stage_compute(&mut batch.payload, &stage);
597 total_compute_ms += compute_start.elapsed().as_secs_f32() * 1000.0;
598
599 let send_start = Instant::now();
600 tx_ring.wait_for_space();
601 let _ = tx_ring.push(batch);
602 total_send_wait_ms += send_start.elapsed().as_secs_f32() * 1000.0;
603 }
604
605 StageAccumulator {
606 stage_idx,
607 processed_batches: batch_count,
608 total_compute_ms,
609 total_recv_wait_ms,
610 total_send_wait_ms,
611 }
612 });
613
614 stage_handles.push(handle);
615 }
616
617 let completion_ring = Arc::clone(&rings[stage_count]);
618 let entry_ring = Arc::clone(&rings[0]);
619 drop(rings);
621
622 let recycler_ring_c = Arc::clone(&recycler_ring);
623 let latencies_handle = thread::spawn(move || {
624 let mut latencies = Vec::with_capacity(token_count);
625 for _ in 0..batch_count {
626 completion_ring.wait_for_data();
627 if let Some(done_batch) = completion_ring.pop() {
628 let batch_latency_ms = done_batch.started_at.elapsed().as_secs_f32() * 1000.0;
629 for _ in 0..done_batch.tokens_in_batch {
630 latencies.push(batch_latency_ms);
631 }
632 recycler_ring_c.wait_for_space();
633 let _ = recycler_ring_c.push(done_batch.payload);
634 }
635 }
636 latencies
637 });
638
639 let exec_start = Instant::now();
640 for batch_idx in 0..batch_count {
641 let batch_start_token = batch_idx * micro_batch;
642 let tokens_in_batch = (token_count - batch_start_token).min(micro_batch);
643 let payload_len = (tokens_in_batch.max(1) * 16).max(32);
644
645 let mut payload = recycler_ring
646 .pop()
647 .unwrap_or_else(|| Vec::with_capacity(payload_len));
648 payload.resize(payload_len, 0.0);
649 for (idx, val) in payload.iter_mut().enumerate() {
650 *val = (batch_idx as f32 * 0.01) + (idx as f32 * 0.0001);
651 }
652
653 if let (Some(trigger), Some(cluster_state), Some(placement)) =
655 (rebalance, cluster, placement_context)
656 {
657 if batch_idx % 25 == 0 && batch_idx > 0 {
659 if let Some(migration) = trigger.evaluate(cluster_state, placement) {
660 tracing::info!(
661 "Dynamic Runtime: Rebalance Triggered! Moving layers {:?} from {} to {}",
662 migration.layers,
663 migration.source_node,
664 migration.target_node
665 );
666 for step in migration.generate_handoff_plan() {
667 tracing::info!(" -> {}", step);
668 }
669 }
670 }
671 }
672
673 entry_ring.wait_for_space();
674 let _ = entry_ring.push(BatchWork {
675 tokens_in_batch,
676 started_at: Instant::now(),
677 payload,
678 });
679 }
680 drop(entry_ring);
682
683 let token_latencies = latencies_handle.join().unwrap_or_default();
684 let total_time_ms = exec_start.elapsed().as_secs_f32() * 1000.0;
685
686 let mut stage_stats = Vec::with_capacity(stage_count);
687 for handle in stage_handles {
688 if let Ok(stats) = handle.join() {
689 let divisor = stats.processed_batches.max(1) as f32;
690 let avg_compute_ms = stats.total_compute_ms / divisor;
691
692 if let Some(cluster_state) = cluster {
694 if let Some(stage) = plan.stages.get(stats.stage_idx) {
695 cluster_state.get_metrics_mut(&stage.node_id, |m| {
696 m.record_latency(avg_compute_ms * 1000.0);
698 });
699 }
700 }
701
702 stage_stats.push(StageExecutionStats {
703 stage_idx: stats.stage_idx,
704 processed_batches: stats.processed_batches,
705 avg_compute_ms,
706 avg_recv_wait_ms: stats.total_recv_wait_ms / divisor,
707 avg_send_wait_ms: stats.total_send_wait_ms / divisor,
708 avg_bridge_write_ms: 0.0,
709 avg_bridge_read_ms: 0.0,
710 });
711 }
712 }
713 stage_stats.sort_by_key(|s| s.stage_idx);
714
715 let throughput_tokens_per_sec = if total_time_ms > 0.0 {
716 token_count as f32 / (total_time_ms / 1000.0)
717 } else {
718 0.0
719 };
720
721 let avg_token_latency_ms = if token_latencies.is_empty() {
722 0.0
723 } else {
724 token_latencies.iter().sum::<f32>() / token_latencies.len() as f32
725 };
726
727 let mut sorted = token_latencies.clone();
728 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
729 let p95_idx = (sorted.len().saturating_sub(1) as f32 * 0.95).round() as usize;
730 let p95_token_latency_ms = sorted.get(p95_idx).copied().unwrap_or(0.0);
731 let p99_idx = (sorted.len().saturating_sub(1) as f32 * 0.99).round() as usize;
732 let p99_token_latency_ms = sorted.get(p99_idx).copied().unwrap_or(0.0);
733
734 Ok(ExecutionResult {
735 token_count,
736 micro_batch,
737 batch_count,
738 stage_count,
739 total_time_ms,
740 throughput_tokens_per_sec,
741 avg_token_latency_ms,
742 p95_token_latency_ms,
743 p99_token_latency_ms,
744 token_latencies_ms: token_latencies,
745 stage_stats,
746 })
747}
748
749#[derive(Debug, Clone)]
750pub struct TransportBatch {
751 pub batch_id: usize,
752 pub tokens_in_batch: usize,
753 pub payload: Vec<f32>,
754}
755
756fn auth_tag(
757 source_stage: usize,
758 batch_id: usize,
759 tokens_in_batch: usize,
760 payload: &[f32],
761 token: &str,
762) -> [u8; 32] {
763 let mut mac = Hmac::<Sha256>::new_from_slice(token.as_bytes())
764 .expect("HMAC key setup for transport auth failed");
765 mac.update(&(source_stage as u32).to_le_bytes());
766 mac.update(&(batch_id as u64).to_le_bytes());
767 mac.update(&(tokens_in_batch as u32).to_le_bytes());
768 mac.update(&(payload.len() as u32).to_le_bytes());
769 let payload_bytes = payload_as_le_bytes(payload);
770 mac.update(payload_bytes.as_ref());
771 mac.finalize().into_bytes().into()
772}
773
774fn payload_as_le_bytes(payload: &[f32]) -> std::borrow::Cow<'_, [u8]> {
775 if cfg!(target_endian = "little") {
776 let bytes = unsafe {
778 slice::from_raw_parts(
779 payload.as_ptr() as *const u8,
780 std::mem::size_of_val(payload),
781 )
782 };
783 std::borrow::Cow::Borrowed(bytes)
784 } else {
785 let mut bytes = Vec::with_capacity(payload.len() * 4);
786 for value in payload {
787 bytes.extend_from_slice(&value.to_le_bytes());
788 }
789 std::borrow::Cow::Owned(bytes)
790 }
791}
792
793fn write_transport_batch(
794 writer: &mut impl Write,
795 batch: &TransportBatch,
796 source_stage: usize,
797 token: Option<&str>,
798 frame_buf: &mut Vec<u8>,
799) -> io::Result<()> {
800 _write_transport_batch_inner(writer, batch, source_stage, token, None, frame_buf)
801}
802
803fn write_transport_batch_session(
804 writer: &mut impl Write,
805 batch: &TransportBatch,
806 source_stage: usize,
807 session: &SessionAuth,
808 frame_buf: &mut Vec<u8>,
809) -> io::Result<()> {
810 _write_transport_batch_inner(writer, batch, source_stage, None, Some(session), frame_buf)
811}
812
813fn _write_transport_batch_inner(
814 writer: &mut impl Write,
815 batch: &TransportBatch,
816 source_stage: usize,
817 token: Option<&str>,
818 session: Option<&SessionAuth>,
819 frame_buf: &mut Vec<u8>,
820) -> io::Result<()> {
821 let batch_id = batch.batch_id as u64;
822 let tokens = batch.tokens_in_batch as u32;
823 let payload_len = batch.payload.len() as u32;
824 let source_stage_u16 = source_stage as u16;
825
826 frame_buf.clear();
827 let header_size = if session.is_some() {
828 2 + 8 + 4 + 4 + 1 + 8 + 8
830 } else if token.is_some() {
831 2 + 8 + 4 + 4 + 1 + 32
833 } else {
834 2 + 8 + 4 + 4 + 1
835 };
836 frame_buf.reserve(header_size);
838 frame_buf.extend_from_slice(&source_stage_u16.to_le_bytes());
839 frame_buf.extend_from_slice(&batch_id.to_le_bytes());
840 frame_buf.extend_from_slice(&tokens.to_le_bytes());
841 frame_buf.extend_from_slice(&payload_len.to_le_bytes());
842
843 if let Some(s) = session {
844 frame_buf.push(2); let gen = s.seq.fetch_add(1, Ordering::Relaxed);
846 frame_buf.extend_from_slice(&s.session_id.to_le_bytes());
847 frame_buf.extend_from_slice(&gen.to_le_bytes());
848 } else if let Some(t) = token {
849 frame_buf.push(1); let tag = auth_tag(
851 source_stage,
852 batch.batch_id,
853 batch.tokens_in_batch,
854 &batch.payload,
855 t,
856 );
857 frame_buf.extend_from_slice(&tag);
858 } else {
859 frame_buf.push(0); }
861
862 let payload_bytes = payload_as_le_bytes(&batch.payload);
867 writer.write_all(frame_buf)?;
868 writer.write_all(payload_bytes.as_ref())?;
869
870 Ok(())
871}
872
873fn read_transport_batch(
874 reader: &mut impl Read,
875 expected_source_stage: usize,
876 token: Option<&str>,
877 payload_buf: &mut Vec<f32>,
878) -> io::Result<Option<TransportBatch>> {
879 _read_transport_batch_inner(reader, expected_source_stage, token, None, payload_buf)
880}
881
882fn read_transport_batch_session(
883 reader: &mut impl Read,
884 expected_source_stage: usize,
885 session: &SessionAuth,
886 payload_buf: &mut Vec<f32>,
887) -> io::Result<Option<TransportBatch>> {
888 _read_transport_batch_inner(
889 reader,
890 expected_source_stage,
891 None,
892 Some(session),
893 payload_buf,
894 )
895}
896
897fn _read_transport_batch_inner(
898 reader: &mut impl Read,
899 expected_source_stage: usize,
900 token: Option<&str>,
901 session: Option<&SessionAuth>,
902 payload_buf: &mut Vec<f32>,
903) -> io::Result<Option<TransportBatch>> {
904 let mut source_stage_bytes = [0u8; 2];
905 match reader.read_exact(&mut source_stage_bytes) {
906 Ok(()) => {}
907 Err(err) if err.kind() == io::ErrorKind::UnexpectedEof => return Ok(None),
908 Err(err) => return Err(err),
909 }
910 let source_stage = u16::from_le_bytes(source_stage_bytes) as usize;
911 if source_stage != expected_source_stage {
912 return Err(io::Error::new(
913 io::ErrorKind::InvalidData,
914 "unexpected source stage in transport frame",
915 ));
916 }
917
918 let mut batch_id_bytes = [0u8; 8];
919 reader.read_exact(&mut batch_id_bytes)?;
920
921 let mut tokens_bytes = [0u8; 4];
922 reader.read_exact(&mut tokens_bytes)?;
923
924 let mut payload_len_bytes = [0u8; 4];
925 reader.read_exact(&mut payload_len_bytes)?;
926 let payload_len = u32::from_le_bytes(payload_len_bytes) as usize;
927
928 const MAX_WIRE_PAYLOAD_ELEMS: usize = 64 * 1024 * 1024; if payload_len > MAX_WIRE_PAYLOAD_ELEMS {
934 return Err(io::Error::new(
935 io::ErrorKind::InvalidData,
936 format!(
937 "wire payload length {} exceeds maximum {} (corrupted or cross-wired frame?)",
938 payload_len, MAX_WIRE_PAYLOAD_ELEMS
939 ),
940 ));
941 }
942
943 let mut tag_type_byte = [0u8; 1];
944 reader.read_exact(&mut tag_type_byte)?;
945 let tag_type = tag_type_byte[0];
946
947 match tag_type {
948 2 => {
949 let mut session_id_bytes = [0u8; 8];
951 reader.read_exact(&mut session_id_bytes)?;
952 let frame_session_id = u64::from_le_bytes(session_id_bytes);
953 let mut gen_bytes = [0u8; 8];
954 reader.read_exact(&mut gen_bytes)?;
955 let generation = u64::from_le_bytes(gen_bytes);
956
957 if session.is_none() {
958 return Err(io::Error::new(
959 io::ErrorKind::PermissionDenied,
960 "session auth required but no session established",
961 ));
962 }
963 let s = session.unwrap();
964 if frame_session_id != s.session_id {
965 return Err(io::Error::new(
966 io::ErrorKind::InvalidData,
967 "session auth: session_id mismatch",
968 ));
969 }
970 let prev = s.peer_seq.fetch_max(generation, Ordering::AcqRel);
971 if generation < prev {
972 return Err(io::Error::new(
973 io::ErrorKind::InvalidData,
974 "session auth: non-monotonic generation",
975 ));
976 }
977 }
978 1 => {
979 if token.is_none() {
981 return Err(io::Error::new(
982 io::ErrorKind::PermissionDenied,
983 "HMAC auth present but no token configured",
984 ));
985 }
986 let mut received_tag = [0u8; 32];
987 reader.read_exact(&mut received_tag)?;
988 payload_buf.resize(payload_len, 0.0);
989 _read_payload(reader, payload_buf, payload_len)?;
990
991 let batch_id = u64::from_le_bytes(batch_id_bytes) as usize;
992 let tokens_in_batch = u32::from_le_bytes(tokens_bytes) as usize;
993 let expected_tag = auth_tag(
994 source_stage,
995 batch_id,
996 tokens_in_batch,
997 payload_buf,
998 token.unwrap(),
999 );
1000 if received_tag != expected_tag {
1001 return Err(io::Error::new(
1002 io::ErrorKind::InvalidData,
1003 "transport auth tag mismatch",
1004 ));
1005 }
1006 return Ok(Some(TransportBatch {
1007 batch_id,
1008 tokens_in_batch,
1009 payload: payload_buf.clone(),
1010 }));
1011 }
1012 0 => {
1013 if token.is_some() || session.is_some() {
1015 return Err(io::Error::new(
1016 io::ErrorKind::PermissionDenied,
1017 "transport authentication required but no auth present",
1018 ));
1019 }
1020 }
1021 _ => {
1022 return Err(io::Error::new(
1023 io::ErrorKind::InvalidData,
1024 "unknown transport auth type",
1025 ));
1026 }
1027 }
1028
1029 payload_buf.resize(payload_len, 0.0);
1031 _read_payload(reader, payload_buf, payload_len)?;
1032
1033 let batch_id = u64::from_le_bytes(batch_id_bytes) as usize;
1034 let tokens_in_batch = u32::from_le_bytes(tokens_bytes) as usize;
1035
1036 Ok(Some(TransportBatch {
1037 batch_id,
1038 tokens_in_batch,
1039 payload: payload_buf.clone(),
1040 }))
1041}
1042
1043fn _read_payload(
1044 reader: &mut impl Read,
1045 payload_buf: &mut [f32],
1046 payload_len: usize,
1047) -> io::Result<()> {
1048 if payload_len == 0 {
1049 return Ok(());
1050 }
1051 if cfg!(target_endian = "little") {
1052 let payload_bytes = unsafe {
1053 slice::from_raw_parts_mut(
1054 payload_buf.as_mut_ptr() as *mut u8,
1055 payload_len * std::mem::size_of::<f32>(),
1056 )
1057 };
1058 reader.read_exact(payload_bytes)?;
1059 } else {
1060 let mut payload_bytes = vec![0u8; payload_len * 4];
1061 reader.read_exact(&mut payload_bytes)?;
1062 for (i, chunk) in payload_bytes.chunks_exact(4).enumerate() {
1063 payload_buf[i] = f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
1064 }
1065 }
1066 Ok(())
1067}
1068
1069pub fn spawn_tcp_bridge(
1083 source_stage: usize,
1084 input_rx: mpsc::Receiver<TransportBatch>,
1085 output_tx: mpsc::SyncSender<TransportBatch>,
1086 config: TcpTransportConfig,
1087 listener: BridgeListener,
1088 addr: BridgeAddr,
1089 circuit_breaker: Option<CircuitBreaker>,
1090) -> thread::JoinHandle<BridgeAccumulator> {
1091 thread::spawn(move || {
1092 let client_stream = {
1093 let breaker_open = circuit_breaker
1094 .as_ref()
1095 .is_some_and(|breaker| !breaker.should_attempt());
1096
1097 let mut connected = None;
1098 if breaker_open {
1099 tracing::warn!(
1100 "Bridge: circuit breaker open for {:?}; skipping connection attempts",
1101 addr
1102 );
1103 } else {
1104 let attempts = config.reconnect_attempts.max(1);
1105 for attempt in 0..attempts {
1106 match addr.connect(Duration::from_secs(1)) {
1107 Ok(stream) => {
1108 connected = Some(stream);
1109 break;
1110 }
1111 Err(e) => {
1112 tracing::debug!(
1113 "Bridge: Connection attempt {}/{} failed for {:?}: {}",
1114 attempt + 1,
1115 attempts,
1116 addr,
1117 e
1118 );
1119 thread::sleep(Duration::from_millis(config.reconnect_backoff_ms));
1120 }
1121 }
1122 }
1123 }
1124
1125 match connected {
1126 Some(stream) => {
1127 if let Some(breaker) = &circuit_breaker {
1128 breaker.record_success();
1129 }
1130 stream
1131 }
1132 None => {
1133 if let Some(breaker) = &circuit_breaker {
1134 breaker.record_failure();
1135 }
1136 tracing::error!(
1137 "Bridge: Exhausted retries for {:?}. Falling back to passthrough.",
1138 addr
1139 );
1140 for batch in input_rx {
1141 if output_tx.send(batch).is_err() {
1142 break;
1143 }
1144 }
1145 return BridgeAccumulator::default_with_stage(source_stage);
1146 }
1147 }
1148 };
1149
1150 if let Err(e) = client_stream.set_nodelay(true) {
1154 tracing::debug!("Bridge: failed to set TCP_NODELAY on client: {}", e);
1155 }
1156
1157 let server_stream = match listener.accept() {
1158 Ok(stream) => {
1159 #[cfg(unix)]
1163 if let BridgeAddr::Unix(path) = &addr {
1164 let _ = std::fs::remove_file(path);
1165 }
1166 stream
1167 }
1168 Err(_) => {
1169 #[cfg(unix)]
1170 if let BridgeAddr::Unix(path) = &addr {
1171 let _ = std::fs::remove_file(path);
1172 }
1173 for batch in input_rx {
1174 if output_tx.send(batch).is_err() {
1175 break;
1176 }
1177 }
1178 return BridgeAccumulator::default_with_stage(source_stage);
1179 }
1180 };
1181
1182 if let Err(e) = server_stream.set_nodelay(true) {
1183 tracing::debug!("Bridge: failed to set TCP_NODELAY on server: {}", e);
1184 }
1185
1186 if let Err(e) = client_stream.set_send_buffer_size(config.send_buffer_size) {
1188 tracing::debug!("Bridge: failed to set SO_SNDBUF on client: {}", e);
1189 }
1190 if let Err(e) = client_stream.set_recv_buffer_size(config.recv_buffer_size) {
1191 tracing::debug!("Bridge: failed to set SO_RCVBUF on client: {}", e);
1192 }
1193 if let Err(e) = server_stream.set_send_buffer_size(config.send_buffer_size) {
1194 tracing::debug!("Bridge: failed to set SO_SNDBUF on server: {}", e);
1195 }
1196 if let Err(e) = server_stream.set_recv_buffer_size(config.recv_buffer_size) {
1197 tracing::debug!("Bridge: failed to set SO_RCVBUF on server: {}", e);
1198 }
1199
1200 let session = config.auth_session_token.as_ref().map(|session_token| {
1201 let token_hash = {
1205 let mut mac = Hmac::<Sha256>::new_from_slice(session_token.as_bytes())
1206 .expect("HMAC key for session_id derivation");
1207 mac.update(b"session-id");
1208 mac.finalize().into_bytes()
1209 };
1210 let session_id = u64::from_le_bytes([
1211 token_hash[0],
1212 token_hash[1],
1213 token_hash[2],
1214 token_hash[3],
1215 token_hash[4],
1216 token_hash[5],
1217 token_hash[6],
1218 token_hash[7],
1219 ]);
1220 tracing::debug!(
1221 "TCP Bridge: session auth established, session_id={}",
1222 session_id
1223 );
1224 SessionAuth {
1225 session_id,
1226 seq: Arc::new(AtomicU64::new(0)),
1227 peer_seq: Arc::new(AtomicU64::new(0)),
1228 }
1229 });
1230
1231 let use_session = session.clone();
1232 let writer_use_session = use_session.clone();
1233 let reader_use_session = use_session.clone();
1234 let writer_auth_token = if writer_use_session.is_some() {
1235 None
1236 } else {
1237 config
1238 .auth_session_token
1239 .clone()
1240 .or_else(|| config.auth_token.clone())
1241 };
1242 let reader_auth_token = if reader_use_session.is_some() {
1243 None
1244 } else {
1245 config
1246 .auth_session_token
1247 .clone()
1248 .or_else(|| config.auth_token.clone())
1249 };
1250
1251 let writer = thread::spawn(move || {
1252 let mut writer = BufWriter::with_capacity(64 * 1024, client_stream);
1253 let mut processed_batches = 0usize;
1254 let mut total_write_ms = 0.0_f32;
1255 let mut frame_buf = Vec::with_capacity(64 * 1024);
1256 let write_result: Result<(), ()> = (|| {
1257 if let Some(ref s) = writer_use_session {
1258 for batch in input_rx {
1259 let write_start = Instant::now();
1260 write_transport_batch_session(
1261 &mut writer,
1262 &batch,
1263 source_stage,
1264 s,
1265 &mut frame_buf,
1266 )
1267 .map_err(|_| ())?;
1268 writer.flush().map_err(|_| ())?;
1278 total_write_ms += write_start.elapsed().as_secs_f32() * 1000.0;
1279 processed_batches += 1;
1280 }
1281 } else {
1282 for batch in input_rx {
1283 let write_start = Instant::now();
1284 write_transport_batch(
1285 &mut writer,
1286 &batch,
1287 source_stage,
1288 writer_auth_token.as_deref(),
1289 &mut frame_buf,
1290 )
1291 .map_err(|_| ())?;
1292 writer.flush().map_err(|_| ())?;
1293 total_write_ms += write_start.elapsed().as_secs_f32() * 1000.0;
1294 processed_batches += 1;
1295 }
1296 }
1297 Ok(())
1298 })();
1299 let _ = writer.flush();
1300 let _ = writer.get_ref().shutdown_write();
1301 let _ = write_result;
1302 (processed_batches, total_write_ms)
1303 });
1304
1305 let mut reader = BufReader::with_capacity(64 * 1024, server_stream);
1306 let mut read_batches = 0usize;
1307 let mut total_read_ms = 0.0_f32;
1308 let mut payload_buf = Vec::with_capacity(16 * 1024);
1309
1310 let _: Result<(), ()> = if let Some(ref s) = reader_use_session {
1311 loop {
1312 let read_start = Instant::now();
1313 match read_transport_batch_session(&mut reader, source_stage, s, &mut payload_buf) {
1314 Ok(Some(batch)) => {
1315 total_read_ms += read_start.elapsed().as_secs_f32() * 1000.0;
1316 if output_tx.send(batch).is_err() {
1317 break Ok(());
1318 }
1319 read_batches += 1;
1320 }
1321 Ok(None) => break Ok(()),
1322 Err(_) => break Err(()),
1323 }
1324 }
1325 } else {
1326 loop {
1327 let read_start = Instant::now();
1328 match read_transport_batch(
1329 &mut reader,
1330 source_stage,
1331 reader_auth_token.as_deref(),
1332 &mut payload_buf,
1333 ) {
1334 Ok(Some(batch)) => {
1335 total_read_ms += read_start.elapsed().as_secs_f32() * 1000.0;
1336 if output_tx.send(batch).is_err() {
1337 break Ok(());
1338 }
1339 read_batches += 1;
1340 }
1341 Ok(None) => break Ok(()),
1342 Err(_) => break Err(()),
1343 }
1344 }
1345 };
1346
1347 let (write_batches, total_write_ms) = writer.join().unwrap_or((0, 0.0));
1348 BridgeAccumulator {
1349 source_stage,
1350 processed_batches: read_batches.max(write_batches),
1351 total_write_ms,
1352 total_read_ms,
1353 }
1354 })
1355}
1356
1357#[derive(Debug)]
1358pub struct BridgeAccumulator {
1359 pub source_stage: usize,
1360 pub processed_batches: usize,
1361 pub total_write_ms: f32,
1362 pub total_read_ms: f32,
1363}
1364
1365impl BridgeAccumulator {
1366 fn default_with_stage(source_stage: usize) -> Self {
1367 Self {
1368 source_stage,
1369 processed_batches: 0,
1370 total_write_ms: 0.0,
1371 total_read_ms: 0.0,
1372 }
1373 }
1374}
1375
1376pub fn execute_pipeline_distributed(
1378 plan: &PipelinePlan,
1379 token_count: usize,
1380 micro_batch: usize,
1381 config: TcpTransportConfig,
1382 cluster: &crate::cluster::ClusterState,
1383 placement_context: Option<&crate::planning::PlacementPlan>,
1384 rebalance: Option<&crate::planning::RebalanceTrigger>,
1385) -> Result<ExecutionResult, String> {
1386 let stage_count = plan.stages.len();
1387 let micro_batch = micro_batch.max(1);
1388 let batch_count = token_count.div_ceil(micro_batch);
1389 if stage_count == 0 || token_count == 0 {
1390 return Ok(ExecutionResult {
1391 token_count,
1392 micro_batch,
1393 batch_count,
1394 stage_count,
1395 total_time_ms: 0.0,
1396 throughput_tokens_per_sec: 0.0,
1397 avg_token_latency_ms: 0.0,
1398 p95_token_latency_ms: 0.0,
1399 p99_token_latency_ms: 0.0,
1400 token_latencies_ms: Vec::new(),
1401 stage_stats: Vec::new(),
1402 });
1403 }
1404
1405 #[derive(Debug)]
1406 struct StageAccumulator {
1407 stage_idx: usize,
1408 processed_batches: usize,
1409 total_compute_ms: f32,
1410 total_recv_wait_ms: f32,
1411 total_send_wait_ms: f32,
1412 }
1413
1414 let (entry_tx, entry_rx) = mpsc::channel::<TransportBatch>();
1415 let mut stage_inputs: Vec<Option<mpsc::Receiver<TransportBatch>>> =
1416 Vec::with_capacity(stage_count);
1417 stage_inputs.push(Some(entry_rx));
1418 let mut stage_outputs = Vec::with_capacity(stage_count);
1419 let mut bridge_handles = Vec::new();
1420 let bridge_capacity = config.max_inflight_batches.max(1);
1421
1422 for source_stage_idx in 0..stage_count.saturating_sub(1) {
1423 let (bridge_in_tx, bridge_in_rx) = mpsc::sync_channel::<TransportBatch>(bridge_capacity);
1424 let (bridge_out_tx, bridge_out_rx) = mpsc::sync_channel::<TransportBatch>(bridge_capacity);
1425 stage_outputs.push(bridge_in_tx);
1426 stage_inputs.push(Some(bridge_out_rx));
1427
1428 let source_stage_p = &plan.stages[source_stage_idx];
1429 let target_stage_p = &plan.stages[source_stage_idx + 1];
1430 let same_node = source_stage_p.node_id == target_stage_p.node_id;
1431
1432 let (listener, addr) = if same_node && config.transport == TransportKind::Unix {
1433 #[cfg(unix)]
1434 {
1435 let sock_path = unique_bridge_socket_path(source_stage_idx);
1437 let _ = std::fs::remove_file(&sock_path);
1438 let ulistener = UnixListener::bind(&sock_path).map_err(|e| {
1439 format!("Failed to bind Unix socket at {}: {e}", sock_path.display())
1440 })?;
1441 let addr = BridgeAddr::Unix(sock_path);
1442 (BridgeListener::Unix(ulistener), addr)
1443 }
1444 #[cfg(not(unix))]
1445 return Err("Unix domain sockets require a Unix platform (Linux/macOS)".to_string());
1446 } else {
1447 let bind_ip = if let Some(m) = cluster.get_metrics(&target_stage_p.node_id) {
1449 m.ip_address
1450 .map(|sa| sa.ip())
1451 .unwrap_or_else(|| [127, 0, 0, 1].into())
1452 } else {
1453 [127, 0, 0, 1].into()
1454 };
1455
1456 let tcp_listener = TcpListener::bind(SocketAddr::new(bind_ip, 0))
1457 .map_err(|e| format!("Failed to bind TCP listener on {bind_ip}: {e}"))?;
1458 let actual_addr = tcp_listener
1459 .local_addr()
1460 .map_err(|e| format!("failed to get TCP local addr: {e}"))?;
1461
1462 let connect_ip = if same_node {
1464 [127, 0, 0, 1].into()
1465 } else {
1466 actual_addr.ip()
1467 };
1468 let connect_addr = SocketAddr::new(connect_ip, actual_addr.port());
1469 (
1470 BridgeListener::Tcp(tcp_listener),
1471 BridgeAddr::Tcp(connect_addr),
1472 )
1473 };
1474
1475 let breaker = Some(cluster.circuit_breaker_for(&target_stage_p.node_id));
1476 bridge_handles.push(spawn_tcp_bridge(
1477 source_stage_idx,
1478 bridge_in_rx,
1479 bridge_out_tx,
1480 config.clone(),
1481 listener,
1482 addr,
1483 breaker,
1484 ));
1485 }
1486
1487 let (completion_tx, completion_rx) = mpsc::sync_channel::<TransportBatch>(bridge_capacity);
1488 stage_outputs.push(completion_tx);
1489
1490 let mut stage_handles = Vec::with_capacity(stage_count);
1491 for stage_idx in 0..stage_count {
1492 let stage = plan.stages[stage_idx].clone();
1493 let rx = stage_inputs[stage_idx]
1494 .take()
1495 .expect("stage receiver should exist");
1496 let tx_next = stage_outputs[stage_idx].clone();
1497
1498 let handle = thread::spawn(move || {
1499 let mut processed_batches = 0usize;
1500 let mut total_compute_ms = 0.0_f32;
1501 let mut total_recv_wait_ms = 0.0_f32;
1502 let mut total_send_wait_ms = 0.0_f32;
1503
1504 loop {
1505 let recv_start = Instant::now();
1506 let Ok(mut batch) = rx.recv() else {
1507 break;
1508 };
1509 total_recv_wait_ms += recv_start.elapsed().as_secs_f32() * 1000.0;
1510
1511 let compute_start = Instant::now();
1512 run_stage_compute(&mut batch.payload, &stage);
1513 total_compute_ms += compute_start.elapsed().as_secs_f32() * 1000.0;
1514
1515 let send_start = Instant::now();
1516 if tx_next.send(batch).is_err() {
1517 break;
1518 }
1519 total_send_wait_ms += send_start.elapsed().as_secs_f32() * 1000.0;
1520 processed_batches += 1;
1521 }
1522
1523 StageAccumulator {
1524 stage_idx,
1525 processed_batches,
1526 total_compute_ms,
1527 total_recv_wait_ms,
1528 total_send_wait_ms,
1529 }
1530 });
1531
1532 stage_handles.push(handle);
1533 }
1534
1535 drop(stage_outputs);
1536
1537 let mut batch_started_at = vec![Instant::now(); batch_count];
1538 let exec_start = Instant::now();
1539 for (batch_idx, batch_started_slot) in batch_started_at.iter_mut().enumerate() {
1540 let batch_start_token = batch_idx * micro_batch;
1541 let tokens_in_batch = (token_count - batch_start_token).min(micro_batch);
1542 let payload_len = (tokens_in_batch.max(1) * config.elems_per_token.max(1)).max(32);
1543 let payload = (0..payload_len)
1544 .map(|idx| (batch_idx as f32 * 0.01) + (idx as f32 * 0.0001))
1545 .collect();
1546
1547 *batch_started_slot = Instant::now();
1548 let _ = entry_tx.send(TransportBatch {
1549 batch_id: batch_idx,
1550 tokens_in_batch,
1551 payload,
1552 });
1553 }
1554 drop(entry_tx);
1555
1556 let mut token_latencies = Vec::with_capacity(token_count);
1557 for _ in 0..batch_count {
1558 let Ok(done_batch) = completion_rx.recv() else {
1559 break;
1560 };
1561 let started_at = batch_started_at[done_batch.batch_id];
1562 let batch_latency_ms = started_at.elapsed().as_secs_f32() * 1000.0;
1563 for _ in 0..done_batch.tokens_in_batch {
1564 token_latencies.push(batch_latency_ms);
1565 }
1566 }
1567
1568 if let (Some(trigger), Some(placement)) = (rebalance, placement_context) {
1570 if let Some(migration) = trigger.evaluate(cluster, placement) {
1571 tracing::info!(
1572 "Distributed Runtime: Rebalance Triggered! Moving layers {:?} from {} to {}",
1573 migration.layers,
1574 migration.source_node,
1575 migration.target_node
1576 );
1577 }
1578 }
1579 let total_time_ms = exec_start.elapsed().as_secs_f32() * 1000.0;
1580
1581 let mut stage_stats = Vec::with_capacity(stage_count);
1582 for handle in stage_handles {
1583 if let Ok(stats) = handle.join() {
1584 let divisor = stats.processed_batches.max(1) as f32;
1585 let avg_compute_ms = stats.total_compute_ms / divisor;
1586
1587 if let Some(stage) = plan.stages.get(stats.stage_idx) {
1589 cluster.get_metrics_mut(&stage.node_id, |m| {
1590 m.record_latency(avg_compute_ms * 1000.0);
1591 });
1592 }
1593
1594 stage_stats.push(StageExecutionStats {
1595 stage_idx: stats.stage_idx,
1596 processed_batches: stats.processed_batches,
1597 avg_compute_ms,
1598 avg_recv_wait_ms: stats.total_recv_wait_ms / divisor,
1599 avg_send_wait_ms: stats.total_send_wait_ms / divisor,
1600 avg_bridge_write_ms: 0.0,
1601 avg_bridge_read_ms: 0.0,
1602 });
1603 }
1604 }
1605
1606 let mut bridge_stats = Vec::with_capacity(bridge_handles.len());
1607 for handle in bridge_handles {
1608 if let Ok(stats) = handle.join() {
1609 bridge_stats.push(stats);
1610 }
1611 }
1612
1613 for bridge in bridge_stats {
1614 if let Some(stage) = stage_stats
1615 .iter_mut()
1616 .find(|s| s.stage_idx == bridge.source_stage)
1617 {
1618 let divisor = bridge.processed_batches.max(1) as f32;
1619 stage.avg_bridge_write_ms = bridge.total_write_ms / divisor;
1620 stage.avg_bridge_read_ms = bridge.total_read_ms / divisor;
1621 }
1622 }
1623
1624 stage_stats.sort_by_key(|s| s.stage_idx);
1625
1626 let throughput_tokens_per_sec = if total_time_ms > 0.0 {
1627 token_count as f32 / (total_time_ms / 1000.0)
1628 } else {
1629 0.0
1630 };
1631
1632 let avg_token_latency_ms = if token_latencies.is_empty() {
1633 0.0
1634 } else {
1635 token_latencies.iter().sum::<f32>() / token_latencies.len() as f32
1636 };
1637
1638 let mut sorted = token_latencies.clone();
1639 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1640 let p95_idx = ((sorted.len().saturating_sub(1)) as f32 * 0.95).round() as usize;
1641 let p95_token_latency_ms = sorted.get(p95_idx).copied().unwrap_or(0.0);
1642 let p99_idx = ((sorted.len().saturating_sub(1)) as f32 * 0.99).round() as usize;
1643 let p99_token_latency_ms = sorted.get(p99_idx).copied().unwrap_or(0.0);
1644
1645 Ok(ExecutionResult {
1646 token_count,
1647 micro_batch,
1648 batch_count,
1649 stage_count,
1650 total_time_ms,
1651 throughput_tokens_per_sec,
1652 avg_token_latency_ms,
1653 p95_token_latency_ms,
1654 p99_token_latency_ms,
1655 token_latencies_ms: token_latencies,
1656 stage_stats,
1657 })
1658}
1659
1660pub fn execute_pipeline_tcp_loopback(
1661 plan: &PipelinePlan,
1662 token_count: usize,
1663 micro_batch: usize,
1664) -> Result<ExecutionResult, String> {
1665 execute_pipeline_tcp_loopback_with_config(
1666 plan,
1667 token_count,
1668 micro_batch,
1669 TcpTransportConfig::default(),
1670 )
1671}
1672
1673pub fn execute_pipeline_tcp_loopback_with_config(
1675 plan: &PipelinePlan,
1676 token_count: usize,
1677 micro_batch: usize,
1678 config: TcpTransportConfig,
1679) -> Result<ExecutionResult, String> {
1680 let stage_count = plan.stages.len();
1681 let micro_batch = micro_batch.max(1);
1682 let batch_count = token_count.div_ceil(micro_batch);
1683 if stage_count == 0 || token_count == 0 {
1684 return Ok(ExecutionResult {
1685 token_count,
1686 micro_batch,
1687 batch_count,
1688 stage_count,
1689 total_time_ms: 0.0,
1690 throughput_tokens_per_sec: 0.0,
1691 avg_token_latency_ms: 0.0,
1692 p95_token_latency_ms: 0.0,
1693 p99_token_latency_ms: 0.0,
1694 token_latencies_ms: Vec::new(),
1695 stage_stats: Vec::new(),
1696 });
1697 }
1698
1699 #[derive(Debug)]
1700 struct StageAccumulator {
1701 stage_idx: usize,
1702 processed_batches: usize,
1703 total_compute_ms: f32,
1704 total_recv_wait_ms: f32,
1705 total_send_wait_ms: f32,
1706 }
1707
1708 let (entry_tx, entry_rx) = mpsc::channel::<TransportBatch>();
1709 let mut stage_inputs: Vec<Option<mpsc::Receiver<TransportBatch>>> =
1710 Vec::with_capacity(stage_count);
1711 stage_inputs.push(Some(entry_rx));
1712 let mut stage_outputs = Vec::with_capacity(stage_count);
1713 let mut bridge_handles = Vec::new();
1714 let bridge_capacity = config.max_inflight_batches.max(1);
1715
1716 for source_stage in 0..stage_count.saturating_sub(1) {
1717 let (bridge_in_tx, bridge_in_rx) = mpsc::sync_channel::<TransportBatch>(bridge_capacity);
1718 let (bridge_out_tx, bridge_out_rx) = mpsc::sync_channel::<TransportBatch>(bridge_capacity);
1719 stage_outputs.push(bridge_in_tx);
1720 stage_inputs.push(Some(bridge_out_rx));
1721
1722 let (listener, addr) = match config.transport {
1723 TransportKind::Tcp => {
1724 let loopback = [127, 0, 0, 1].into();
1726 let bind_addr = SocketAddr::new(loopback, 0);
1727 let listener = TcpListener::bind(bind_addr)
1728 .map_err(|e| format!("failed to bind TCP loopback listener: {e}"))?;
1729 let actual_addr = listener
1730 .local_addr()
1731 .map_err(|e| format!("failed to get TCP loopback addr: {e}"))?;
1732 (BridgeListener::Tcp(listener), BridgeAddr::Tcp(actual_addr))
1733 }
1734 #[cfg(unix)]
1735 TransportKind::Unix => {
1736 let sock_path = unique_bridge_socket_path(source_stage);
1737 let _ = std::fs::remove_file(&sock_path);
1738 let listener = UnixListener::bind(&sock_path).map_err(|e| {
1739 format!("failed to bind Unix socket at {}: {e}", sock_path.display())
1740 })?;
1741 let addr = BridgeAddr::Unix(sock_path);
1742 (BridgeListener::Unix(listener), addr)
1743 }
1744 #[cfg(not(unix))]
1745 TransportKind::Unix => {
1746 return Err("Unix domain sockets require a Unix platform (Linux/macOS)".to_string());
1747 }
1748 };
1749
1750 bridge_handles.push(spawn_tcp_bridge(
1751 source_stage,
1752 bridge_in_rx,
1753 bridge_out_tx,
1754 config.clone(),
1755 listener,
1756 addr,
1757 None, ));
1759 }
1760
1761 let (completion_tx, completion_rx) = mpsc::sync_channel::<TransportBatch>(bridge_capacity);
1762 stage_outputs.push(completion_tx);
1763
1764 let mut stage_handles = Vec::with_capacity(stage_count);
1765 for stage_idx in 0..stage_count {
1766 let stage = plan.stages[stage_idx].clone();
1767 let rx = stage_inputs[stage_idx]
1768 .take()
1769 .expect("stage receiver should exist");
1770 let tx_next = stage_outputs[stage_idx].clone();
1771
1772 let handle = thread::spawn(move || {
1773 let mut processed_batches = 0usize;
1774 let mut total_compute_ms = 0.0_f32;
1775 let mut total_recv_wait_ms = 0.0_f32;
1776 let mut total_send_wait_ms = 0.0_f32;
1777
1778 loop {
1779 let recv_start = Instant::now();
1780 let Ok(mut batch) = rx.recv() else {
1781 break;
1782 };
1783 total_recv_wait_ms += recv_start.elapsed().as_secs_f32() * 1000.0;
1784
1785 let compute_start = Instant::now();
1786 run_stage_compute(&mut batch.payload, &stage);
1787 total_compute_ms += compute_start.elapsed().as_secs_f32() * 1000.0;
1788
1789 let send_start = Instant::now();
1790 if tx_next.send(batch).is_err() {
1791 break;
1792 }
1793 total_send_wait_ms += send_start.elapsed().as_secs_f32() * 1000.0;
1794 processed_batches += 1;
1795 }
1796
1797 StageAccumulator {
1798 stage_idx,
1799 processed_batches,
1800 total_compute_ms,
1801 total_recv_wait_ms,
1802 total_send_wait_ms,
1803 }
1804 });
1805
1806 stage_handles.push(handle);
1807 }
1808
1809 drop(stage_outputs);
1812
1813 let mut batch_started_at = vec![Instant::now(); batch_count];
1814 let exec_start = Instant::now();
1815 for (batch_idx, batch_started_slot) in batch_started_at.iter_mut().enumerate() {
1816 let batch_start_token = batch_idx * micro_batch;
1817 let tokens_in_batch = (token_count - batch_start_token).min(micro_batch);
1818 let payload_len = (tokens_in_batch.max(1) * config.elems_per_token.max(1)).max(32);
1819 let payload = (0..payload_len)
1820 .map(|idx| (batch_idx as f32 * 0.01) + (idx as f32 * 0.0001))
1821 .collect();
1822
1823 *batch_started_slot = Instant::now();
1824 if entry_tx
1825 .send(TransportBatch {
1826 batch_id: batch_idx,
1827 tokens_in_batch,
1828 payload,
1829 })
1830 .is_err()
1831 {
1832 break;
1833 }
1834 }
1835 drop(entry_tx);
1836
1837 let mut token_latencies = Vec::with_capacity(token_count);
1838 for _ in 0..batch_count {
1839 let Ok(done_batch) = completion_rx.recv() else {
1840 break;
1841 };
1842
1843 let started_at = batch_started_at
1844 .get(done_batch.batch_id)
1845 .copied()
1846 .unwrap_or(exec_start);
1847 let batch_latency_ms = started_at.elapsed().as_secs_f32() * 1000.0;
1848 for _ in 0..done_batch.tokens_in_batch {
1849 token_latencies.push(batch_latency_ms);
1850 }
1851 }
1852 let total_time_ms = exec_start.elapsed().as_secs_f32() * 1000.0;
1853
1854 let mut stage_stats = Vec::with_capacity(stage_count);
1855 for handle in stage_handles {
1856 if let Ok(stats) = handle.join() {
1857 let divisor = stats.processed_batches.max(1) as f32;
1858 stage_stats.push(StageExecutionStats {
1859 stage_idx: stats.stage_idx,
1860 processed_batches: stats.processed_batches,
1861 avg_compute_ms: stats.total_compute_ms / divisor,
1862 avg_recv_wait_ms: stats.total_recv_wait_ms / divisor,
1863 avg_send_wait_ms: stats.total_send_wait_ms / divisor,
1864 avg_bridge_write_ms: 0.0,
1865 avg_bridge_read_ms: 0.0,
1866 });
1867 }
1868 }
1869
1870 let mut bridge_stats = Vec::with_capacity(bridge_handles.len());
1871 for handle in bridge_handles {
1872 if let Ok(stats) = handle.join() {
1873 bridge_stats.push(stats);
1874 }
1875 }
1876
1877 for bridge in bridge_stats {
1878 if let Some(stage) = stage_stats
1879 .iter_mut()
1880 .find(|s| s.stage_idx == bridge.source_stage)
1881 {
1882 let divisor = bridge.processed_batches.max(1) as f32;
1883 stage.avg_bridge_write_ms = bridge.total_write_ms / divisor;
1884 stage.avg_bridge_read_ms = bridge.total_read_ms / divisor;
1885 }
1886 }
1887
1888 stage_stats.sort_by_key(|s| s.stage_idx);
1889
1890 let throughput_tokens_per_sec = if total_time_ms > 0.0 {
1891 token_count as f32 / (total_time_ms / 1000.0)
1892 } else {
1893 0.0
1894 };
1895
1896 let avg_token_latency_ms = if token_latencies.is_empty() {
1897 0.0
1898 } else {
1899 token_latencies.iter().sum::<f32>() / token_latencies.len() as f32
1900 };
1901
1902 let mut sorted = token_latencies.clone();
1903 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1904 let p95_idx = ((sorted.len().saturating_sub(1)) as f32 * 0.95).round() as usize;
1905 let p95_token_latency_ms = sorted.get(p95_idx).copied().unwrap_or(0.0);
1906 let p99_idx = ((sorted.len().saturating_sub(1)) as f32 * 0.99).round() as usize;
1907 let p99_token_latency_ms = sorted.get(p99_idx).copied().unwrap_or(0.0);
1908
1909 Ok(ExecutionResult {
1910 token_count,
1911 micro_batch,
1912 batch_count,
1913 stage_count,
1914 total_time_ms,
1915 throughput_tokens_per_sec,
1916 avg_token_latency_ms,
1917 p95_token_latency_ms,
1918 p99_token_latency_ms,
1919 token_latencies_ms: token_latencies,
1920 stage_stats,
1921 })
1922}
1923
1924fn write_stage_handshake(writer: &mut impl Write, stage: &StagePlacement) -> Result<(), String> {
1949 let num_layers = stage.num_layers() as u32;
1950 let device_byte: u8 = match stage.device {
1951 DeviceKind::Npu => 0,
1952 DeviceKind::Gpu => 1,
1953 DeviceKind::Cpu => 2,
1954 };
1955 writer
1956 .write_all(&num_layers.to_le_bytes())
1957 .and_then(|_| writer.write_all(&[device_byte]))
1958 .map_err(|e| format!("failed to write stage handshake: {e}"))
1959}
1960
1961fn read_stage_handshake(reader: &mut impl Read) -> Result<StagePlacement, String> {
1962 let mut layers_bytes = [0u8; 4];
1963 reader
1964 .read_exact(&mut layers_bytes)
1965 .map_err(|e| format!("failed to read stage handshake: {e}"))?;
1966 let num_layers = u32::from_le_bytes(layers_bytes) as usize;
1967
1968 let mut device_byte = [0u8; 1];
1969 reader
1970 .read_exact(&mut device_byte)
1971 .map_err(|e| format!("failed to read stage handshake: {e}"))?;
1972 let device = match device_byte[0] {
1973 0 => DeviceKind::Npu,
1974 1 => DeviceKind::Gpu,
1975 _ => DeviceKind::Cpu,
1976 };
1977
1978 Ok(StagePlacement {
1979 node_id: "remote".to_string(),
1980 start_layer: 0,
1981 end_layer: num_layers,
1982 device,
1983 est_latency_ms: 0.0,
1984 })
1985}
1986
1987#[derive(Clone, Debug, PartialEq)]
1991pub struct StageWorkerSummary {
1992 pub batches_processed: usize,
1993 pub avg_compute_ms: f32,
1994}
1995
1996pub fn run_stage_worker(
2008 listener: TcpListener,
2009 config: &TcpTransportConfig,
2010) -> Result<StageWorkerSummary, String> {
2011 let (mut stream, peer) = listener
2012 .accept()
2013 .map_err(|e| format!("failed to accept a connection: {e}"))?;
2014 let _ = stream.set_nodelay(true);
2015
2016 let stage = read_stage_handshake(&mut stream)?;
2017 eprintln!(
2018 "[stage-worker] connection from {peer}; assigned {} layers on {}",
2019 stage.num_layers(),
2020 stage.device.as_str()
2021 );
2022
2023 let mut frame_buf = Vec::new();
2024 let mut read_buf = Vec::new();
2025 let mut batches_processed = 0usize;
2026 let mut total_compute_ms = 0.0_f32;
2027
2028 loop {
2029 let batch =
2030 match read_transport_batch(&mut stream, 0, config.auth_token.as_deref(), &mut read_buf)
2031 {
2032 Ok(Some(batch)) => batch,
2033 Ok(None) => break, Err(e) => return Err(format!("failed to read batch from coordinator: {e}")),
2035 };
2036
2037 let mut payload = batch.payload;
2038 let compute_start = Instant::now();
2039 run_stage_compute(&mut payload, &stage);
2040 total_compute_ms += compute_start.elapsed().as_secs_f32() * 1000.0;
2041
2042 let result = TransportBatch {
2043 batch_id: batch.batch_id,
2044 tokens_in_batch: batch.tokens_in_batch,
2045 payload,
2046 };
2047 write_transport_batch(
2048 &mut stream,
2049 &result,
2050 0,
2051 config.auth_token.as_deref(),
2052 &mut frame_buf,
2053 )
2054 .map_err(|e| format!("failed to write result batch to coordinator: {e}"))?;
2055 batches_processed += 1;
2056 }
2057
2058 let avg_compute_ms = if batches_processed > 0 {
2059 total_compute_ms / batches_processed as f32
2060 } else {
2061 0.0
2062 };
2063 eprintln!(
2064 "[stage-worker] coordinator disconnected after {batches_processed} batch(es), avg compute {avg_compute_ms:.2} ms"
2065 );
2066 Ok(StageWorkerSummary {
2067 batches_processed,
2068 avg_compute_ms,
2069 })
2070}
2071
2072fn merge_stages_for_node(
2083 node_id: &str,
2084 stages: &[StagePlacement],
2085) -> Result<StagePlacement, String> {
2086 let matching: Vec<&StagePlacement> = stages.iter().filter(|s| s.node_id == node_id).collect();
2087 let Some(first) = matching.first() else {
2088 return Err(format!("no pipeline stages assigned to node '{node_id}'"));
2089 };
2090 let start_layer = matching.iter().map(|s| s.start_layer).min().unwrap_or(0);
2091 let end_layer = matching.iter().map(|s| s.end_layer).max().unwrap_or(0);
2092 Ok(StagePlacement {
2093 node_id: node_id.to_string(),
2094 start_layer,
2095 end_layer,
2096 device: first.device,
2097 est_latency_ms: matching.iter().map(|s| s.est_latency_ms).sum(),
2098 })
2099}
2100
2101pub fn execute_pipeline_with_remote_stage(
2112 plan: &PipelinePlan,
2113 local_node_id: &str,
2114 remote_node_id: &str,
2115 token_count: usize,
2116 micro_batch: usize,
2117 config: TcpTransportConfig,
2118 remote_addr: SocketAddr,
2119) -> Result<ExecutionResult, String> {
2120 let local_stage = merge_stages_for_node(local_node_id, &plan.stages)?;
2121 let remote_stage = merge_stages_for_node(remote_node_id, &plan.stages)?;
2122 let local_stage = &local_stage;
2123 let remote_stage = &remote_stage;
2124
2125 let micro_batch = micro_batch.max(1);
2126 let batch_count = token_count.div_ceil(micro_batch);
2127 if token_count == 0 {
2128 return Ok(ExecutionResult {
2129 token_count,
2130 micro_batch,
2131 batch_count,
2132 stage_count: 2,
2133 total_time_ms: 0.0,
2134 throughput_tokens_per_sec: 0.0,
2135 avg_token_latency_ms: 0.0,
2136 p95_token_latency_ms: 0.0,
2137 p99_token_latency_ms: 0.0,
2138 token_latencies_ms: Vec::new(),
2139 stage_stats: Vec::new(),
2140 });
2141 }
2142
2143 let mut stream = TcpStream::connect(remote_addr).map_err(|e| {
2144 format!(
2145 "failed to connect to remote stage worker at {remote_addr}: {e} \
2146 (is `ghost-link stage-worker --bind {remote_addr}` running there?)"
2147 )
2148 })?;
2149 let _ = stream.set_nodelay(true);
2150 write_stage_handshake(&mut stream, remote_stage)?;
2151
2152 let mut frame_buf = Vec::new();
2153 let mut read_buf = Vec::new();
2154 let mut token_latencies = Vec::with_capacity(token_count);
2155 let mut local_compute_ms = 0.0_f32;
2156 let mut remote_round_trip_ms = 0.0_f32;
2157
2158 let exec_start = Instant::now();
2159 for batch_idx in 0..batch_count {
2160 let batch_start_token = batch_idx * micro_batch;
2161 let tokens_in_batch = (token_count - batch_start_token).min(micro_batch);
2162 let payload_len = (tokens_in_batch.max(1) * config.elems_per_token.max(1)).max(32);
2163 let mut payload: Vec<f32> = (0..payload_len)
2164 .map(|idx| (batch_idx as f32 * 0.01) + (idx as f32 * 0.0001))
2165 .collect();
2166
2167 let batch_started = Instant::now();
2168
2169 let local_start = Instant::now();
2170 run_stage_compute(&mut payload, local_stage);
2171 local_compute_ms += local_start.elapsed().as_secs_f32() * 1000.0;
2172
2173 let outgoing = TransportBatch {
2174 batch_id: batch_idx,
2175 tokens_in_batch,
2176 payload,
2177 };
2178 let rt_start = Instant::now();
2179 write_transport_batch(
2180 &mut stream,
2181 &outgoing,
2182 0,
2183 config.auth_token.as_deref(),
2184 &mut frame_buf,
2185 )
2186 .map_err(|e| format!("failed to send batch {batch_idx} to remote worker: {e}"))?;
2187 let result = read_transport_batch(&mut stream, 0, config.auth_token.as_deref(), &mut read_buf)
2188 .map_err(|e| format!("failed to read batch {batch_idx} result from remote worker: {e}"))?
2189 .ok_or_else(|| {
2190 format!("remote worker at {remote_addr} closed the connection before batch {batch_idx} completed")
2191 })?;
2192 remote_round_trip_ms += rt_start.elapsed().as_secs_f32() * 1000.0;
2193
2194 let batch_latency_ms = batch_started.elapsed().as_secs_f32() * 1000.0;
2195 for _ in 0..result.tokens_in_batch {
2196 token_latencies.push(batch_latency_ms);
2197 }
2198 }
2199 let total_time_ms = exec_start.elapsed().as_secs_f32() * 1000.0;
2200 drop(stream); let divisor = batch_count.max(1) as f32;
2203 let throughput_tokens_per_sec = if total_time_ms > 0.0 {
2204 token_count as f32 / (total_time_ms / 1000.0)
2205 } else {
2206 0.0
2207 };
2208 let avg_token_latency_ms = if token_latencies.is_empty() {
2209 0.0
2210 } else {
2211 token_latencies.iter().sum::<f32>() / token_latencies.len() as f32
2212 };
2213 let mut sorted = token_latencies.clone();
2214 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
2215 let p95_idx = (sorted.len().saturating_sub(1) as f32 * 0.95).round() as usize;
2216 let p95_token_latency_ms = sorted.get(p95_idx).copied().unwrap_or(0.0);
2217 let p99_idx = (sorted.len().saturating_sub(1) as f32 * 0.99).round() as usize;
2218 let p99_token_latency_ms = sorted.get(p99_idx).copied().unwrap_or(0.0);
2219
2220 Ok(ExecutionResult {
2221 token_count,
2222 micro_batch,
2223 batch_count,
2224 stage_count: 2,
2225 total_time_ms,
2226 throughput_tokens_per_sec,
2227 avg_token_latency_ms,
2228 p95_token_latency_ms,
2229 p99_token_latency_ms,
2230 token_latencies_ms: token_latencies,
2231 stage_stats: vec![
2232 StageExecutionStats {
2233 stage_idx: 0,
2234 processed_batches: batch_count,
2235 avg_compute_ms: local_compute_ms / divisor,
2236 avg_recv_wait_ms: 0.0,
2237 avg_send_wait_ms: 0.0,
2238 avg_bridge_write_ms: 0.0,
2239 avg_bridge_read_ms: 0.0,
2240 },
2241 StageExecutionStats {
2242 stage_idx: 1,
2243 processed_batches: batch_count,
2244 avg_compute_ms: 0.0,
2250 avg_recv_wait_ms: 0.0,
2251 avg_send_wait_ms: 0.0,
2252 avg_bridge_write_ms: remote_round_trip_ms / divisor,
2253 avg_bridge_read_ms: 0.0,
2254 },
2255 ],
2256 })
2257}
2258
2259#[cfg(test)]
2260mod tests {
2261 use super::*;
2262 use crate::planning::LayerAssignment;
2263 use std::collections::HashMap;
2264 use std::io::Cursor;
2265
2266 #[test]
2267 fn pipeline_plan_uses_node_device_mapping() {
2268 let assignments = vec![
2269 LayerAssignment::new("node-a".to_string(), 0, 10, 5.0),
2270 LayerAssignment::new("node-b".to_string(), 10, 20, 5.0),
2271 ];
2272
2273 let mut devices = HashMap::new();
2274 devices.insert("node-a".to_string(), DeviceKind::Gpu);
2275 devices.insert("node-b".to_string(), DeviceKind::Cpu);
2276
2277 let plan = PipelinePlan::from_assignments(&assignments, &devices);
2278
2279 assert_eq!(plan.stages.len(), 2);
2280 assert_eq!(plan.stages[0].device, DeviceKind::Gpu);
2281 assert_eq!(plan.stages[1].device, DeviceKind::Cpu);
2282 assert!(plan.est_step_latency_ms() > 0.0);
2283 }
2284
2285 #[test]
2286 fn schedule_size_matches_tokens_times_stages() {
2287 let schedule = build_token_schedule(3, 4);
2288 assert_eq!(schedule.len(), 12);
2289 assert_eq!(
2290 schedule[0],
2291 TokenStep {
2292 token_idx: 0,
2293 stage_idx: 0
2294 }
2295 );
2296 assert_eq!(
2297 schedule[11],
2298 TokenStep {
2299 token_idx: 3,
2300 stage_idx: 2
2301 }
2302 );
2303 }
2304
2305 #[test]
2306 fn execution_reports_basic_stage_metrics() {
2307 let plan = PipelinePlan {
2308 stages: vec![
2309 StagePlacement {
2310 node_id: "node-a".to_string(),
2311 start_layer: 0,
2312 end_layer: 10,
2313 device: DeviceKind::Gpu,
2314 est_latency_ms: 1.0,
2315 },
2316 StagePlacement {
2317 node_id: "node-b".to_string(),
2318 start_layer: 10,
2319 end_layer: 20,
2320 device: DeviceKind::Gpu,
2321 est_latency_ms: 1.0,
2322 },
2323 ],
2324 };
2325
2326 let result = execute_pipeline(&plan, 8, 1).expect("failed");
2327 assert_eq!(result.stage_count, 2);
2328 assert_eq!(result.token_count, 8);
2329 assert_eq!(result.micro_batch, 1);
2330 assert_eq!(result.batch_count, 8);
2331 assert_eq!(result.stage_stats.len(), 2);
2332 assert!(result.total_time_ms > 0.0);
2333 assert!(result.avg_token_latency_ms > 0.0);
2334 assert!(result.throughput_tokens_per_sec > 0.0);
2335 }
2336
2337 #[test]
2338 fn execution_respects_micro_batch_count() {
2339 let plan = PipelinePlan {
2340 stages: vec![
2341 StagePlacement {
2342 node_id: "node-a".to_string(),
2343 start_layer: 0,
2344 end_layer: 10,
2345 device: DeviceKind::Gpu,
2346 est_latency_ms: 1.0,
2347 },
2348 StagePlacement {
2349 node_id: "node-b".to_string(),
2350 start_layer: 10,
2351 end_layer: 20,
2352 device: DeviceKind::Gpu,
2353 est_latency_ms: 1.0,
2354 },
2355 ],
2356 };
2357
2358 let mb1 = execute_pipeline(&plan, 32, 1).expect("failed");
2359 let mb4 = execute_pipeline(&plan, 32, 4).expect("failed");
2360
2361 assert_eq!(mb1.batch_count, 32);
2362 assert_eq!(mb4.batch_count, 8);
2363 assert!(mb4.total_time_ms > 0.0);
2364 }
2365
2366 #[test]
2367 fn execution_supports_npu_stage_metrics() {
2368 let plan = PipelinePlan {
2369 stages: vec![
2370 StagePlacement {
2371 node_id: "node-npu".to_string(),
2372 start_layer: 0,
2373 end_layer: 8,
2374 device: DeviceKind::Npu,
2375 est_latency_ms: 0.9,
2376 },
2377 StagePlacement {
2378 node_id: "node-gpu".to_string(),
2379 start_layer: 8,
2380 end_layer: 16,
2381 device: DeviceKind::Gpu,
2382 est_latency_ms: 1.1,
2383 },
2384 ],
2385 };
2386
2387 let result = execute_pipeline(&plan, 12, 3).expect("failed");
2388 assert_eq!(result.stage_count, 2);
2389 assert_eq!(result.batch_count, 4);
2390 assert_eq!(result.stage_stats.len(), 2);
2391 assert!(result.stage_stats.iter().any(|s| s.stage_idx == 0));
2392 assert!(result.total_time_ms > 0.0);
2393 assert!(result.throughput_tokens_per_sec > 0.0);
2394 }
2395
2396 #[test]
2397 fn tcp_loopback_execution_reports_metrics() {
2398 let plan = PipelinePlan {
2399 stages: vec![
2400 StagePlacement {
2401 node_id: "node-a".to_string(),
2402 start_layer: 0,
2403 end_layer: 10,
2404 device: DeviceKind::Gpu,
2405 est_latency_ms: 1.0,
2406 },
2407 StagePlacement {
2408 node_id: "node-b".to_string(),
2409 start_layer: 10,
2410 end_layer: 20,
2411 device: DeviceKind::Cpu,
2412 est_latency_ms: 2.0,
2413 },
2414 ],
2415 };
2416
2417 let result = execute_pipeline_tcp_loopback(&plan, 16, 2).expect("failed");
2418 assert_eq!(result.token_count, 16);
2419 assert_eq!(result.batch_count, 8);
2420 assert_eq!(result.stage_stats.len(), 2);
2421 assert!(result.total_time_ms > 0.0);
2422 assert!(result.throughput_tokens_per_sec > 0.0);
2423 assert!(
2424 result.p99_token_latency_ms >= result.p95_token_latency_ms,
2425 "p99 ({}) must be >= p95 ({})",
2426 result.p99_token_latency_ms,
2427 result.p95_token_latency_ms
2428 );
2429 }
2430
2431 #[test]
2432 fn elems_per_token_config_scales_payload_size_and_still_completes() {
2433 let plan = PipelinePlan {
2434 stages: vec![
2435 StagePlacement {
2436 node_id: "node-a".to_string(),
2437 start_layer: 0,
2438 end_layer: 10,
2439 device: DeviceKind::Gpu,
2440 est_latency_ms: 1.0,
2441 },
2442 StagePlacement {
2443 node_id: "node-b".to_string(),
2444 start_layer: 10,
2445 end_layer: 20,
2446 device: DeviceKind::Cpu,
2447 est_latency_ms: 2.0,
2448 },
2449 ],
2450 };
2451
2452 let config = TcpTransportConfig {
2456 elems_per_token: 2048,
2457 ..TcpTransportConfig::default()
2458 };
2459
2460 let result = execute_pipeline_tcp_loopback_with_config(&plan, 8, 2, config)
2461 .expect("large-payload loopback execution failed");
2462 assert_eq!(result.token_count, 8);
2463 assert!(result.total_time_ms > 0.0);
2464 assert!(
2465 result.p99_token_latency_ms >= result.p95_token_latency_ms,
2466 "p99 ({}) must be >= p95 ({})",
2467 result.p99_token_latency_ms,
2468 result.p95_token_latency_ms
2469 );
2470 }
2471
2472 #[test]
2473 fn tcp_loopback_execution_with_hardening_config_reports_metrics() {
2474 let plan = PipelinePlan {
2475 stages: vec![
2476 StagePlacement {
2477 node_id: "node-a".to_string(),
2478 start_layer: 0,
2479 end_layer: 12,
2480 device: DeviceKind::Gpu,
2481 est_latency_ms: 1.5,
2482 },
2483 StagePlacement {
2484 node_id: "node-b".to_string(),
2485 start_layer: 12,
2486 end_layer: 24,
2487 device: DeviceKind::Cpu,
2488 est_latency_ms: 2.5,
2489 },
2490 ],
2491 };
2492
2493 let result = execute_pipeline_tcp_loopback_with_config(
2494 &plan,
2495 24,
2496 3,
2497 TcpTransportConfig {
2498 max_inflight_batches: 4,
2499 reconnect_attempts: 4,
2500 reconnect_backoff_ms: 5,
2501 auth_token: Some("test-token".to_string()),
2502 ..Default::default()
2503 },
2504 )
2505 .expect("hardened loopback failed");
2506
2507 assert_eq!(result.token_count, 24);
2508 assert_eq!(result.batch_count, 8);
2509 assert_eq!(result.stage_stats.len(), 2);
2510 assert!(result.total_time_ms > 0.0);
2511 assert!(result.throughput_tokens_per_sec > 0.0);
2512 }
2513
2514 #[test]
2515 fn transport_rejects_auth_mismatch_frames() {
2516 let batch = TransportBatch {
2517 batch_id: 7,
2518 tokens_in_batch: 4,
2519 payload: vec![0.1, 0.2, 0.3, 0.4],
2520 };
2521 let mut encoded = Vec::new();
2522 let mut frame_buf = Vec::new();
2523 write_transport_batch(&mut encoded, &batch, 0, Some("token-a"), &mut frame_buf)
2524 .expect("encode frame");
2525
2526 let mut cursor = Cursor::new(encoded);
2527 let mut payload_buf = Vec::new();
2528 let err = read_transport_batch(&mut cursor, 0, Some("token-b"), &mut payload_buf)
2529 .expect_err("mismatched token should fail");
2530 assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
2531 }
2532
2533 #[test]
2534 fn transport_rejects_unexpected_source_stage() {
2535 let batch = TransportBatch {
2536 batch_id: 2,
2537 tokens_in_batch: 2,
2538 payload: vec![1.0, 2.0],
2539 };
2540 let mut encoded = Vec::new();
2541 let mut frame_buf = Vec::new();
2542 write_transport_batch(&mut encoded, &batch, 1, Some("token"), &mut frame_buf)
2543 .expect("encode frame");
2544
2545 let mut cursor = Cursor::new(encoded);
2546 let mut payload_buf = Vec::new();
2547 let err = read_transport_batch(&mut cursor, 0, Some("token"), &mut payload_buf)
2548 .expect_err("source-stage mismatch should fail");
2549 assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
2550 }
2551
2552 #[cfg(unix)]
2553 #[test]
2554 fn unix_socket_loopback_execution_reports_metrics() {
2555 let plan = PipelinePlan {
2556 stages: vec![
2557 StagePlacement {
2558 node_id: "node-a".to_string(),
2559 start_layer: 0,
2560 end_layer: 10,
2561 device: DeviceKind::Gpu,
2562 est_latency_ms: 1.0,
2563 },
2564 StagePlacement {
2565 node_id: "node-b".to_string(),
2566 start_layer: 10,
2567 end_layer: 20,
2568 device: DeviceKind::Cpu,
2569 est_latency_ms: 2.0,
2570 },
2571 ],
2572 };
2573
2574 let result = execute_pipeline_tcp_loopback_with_config(
2575 &plan,
2576 16,
2577 2,
2578 TcpTransportConfig {
2579 transport: TransportKind::Unix,
2580 ..Default::default()
2581 },
2582 )
2583 .expect("unix socket loopback failed");
2584
2585 assert_eq!(result.token_count, 16);
2586 assert_eq!(result.batch_count, 8);
2587 assert_eq!(result.stage_stats.len(), 2);
2588 assert!(result.total_time_ms > 0.0);
2589 assert!(result.throughput_tokens_per_sec > 0.0);
2590 }
2591
2592 #[test]
2593 fn stage_handshake_round_trips_layer_count_and_device() {
2594 let stage = StagePlacement {
2595 node_id: "coordinator-local".to_string(),
2596 start_layer: 5,
2597 end_layer: 37,
2598 device: DeviceKind::Gpu,
2599 est_latency_ms: 12.0,
2600 };
2601 let mut buf = Vec::new();
2602 write_stage_handshake(&mut buf, &stage).expect("handshake write failed");
2603
2604 let mut cursor = Cursor::new(buf);
2605 let decoded = read_stage_handshake(&mut cursor).expect("handshake read failed");
2606 assert_eq!(decoded.num_layers(), stage.num_layers());
2607 assert_eq!(decoded.device, DeviceKind::Gpu);
2608 }
2609
2610 #[test]
2611 fn execute_pipeline_with_remote_stage_rejects_when_remote_node_has_no_stages() {
2612 let plan = PipelinePlan {
2613 stages: vec![StagePlacement {
2614 node_id: "solo".to_string(),
2615 start_layer: 0,
2616 end_layer: 10,
2617 device: DeviceKind::Cpu,
2618 est_latency_ms: 1.0,
2619 }],
2620 };
2621 let err = execute_pipeline_with_remote_stage(
2622 &plan,
2623 "solo",
2624 "nowhere",
2625 8,
2626 2,
2627 TcpTransportConfig::default(),
2628 "127.0.0.1:1".parse().unwrap(),
2629 )
2630 .unwrap_err();
2631 assert!(
2632 err.contains("no pipeline stages assigned to node 'nowhere'"),
2633 "unexpected error: {err}"
2634 );
2635 }
2636
2637 #[test]
2638 fn merge_stages_for_node_collapses_multiple_chunks_into_one_placement() {
2639 let stages = vec![
2644 StagePlacement {
2645 node_id: "node-remote".to_string(),
2646 start_layer: 32,
2647 end_layer: 38,
2648 device: DeviceKind::Gpu,
2649 est_latency_ms: 1.0,
2650 },
2651 StagePlacement {
2652 node_id: "node-remote".to_string(),
2653 start_layer: 38,
2654 end_layer: 44,
2655 device: DeviceKind::Gpu,
2656 est_latency_ms: 1.5,
2657 },
2658 StagePlacement {
2659 node_id: "node-local".to_string(),
2660 start_layer: 0,
2661 end_layer: 32,
2662 device: DeviceKind::Cpu,
2663 est_latency_ms: 3.0,
2664 },
2665 ];
2666 let merged = merge_stages_for_node("node-remote", &stages).expect("merge failed");
2667 assert_eq!(merged.start_layer, 32);
2668 assert_eq!(merged.end_layer, 44);
2669 assert_eq!(merged.num_layers(), 12);
2670 assert_eq!(merged.device, DeviceKind::Gpu);
2671 assert!((merged.est_latency_ms - 2.5).abs() < 1e-6);
2672 }
2673
2674 #[test]
2683 fn execute_pipeline_with_remote_stage_does_a_real_tcp_round_trip() {
2684 let listener = TcpListener::bind("127.0.0.1:0").expect("failed to bind test listener");
2685 let worker_addr = listener.local_addr().expect("failed to read local_addr");
2686
2687 let worker_handle =
2688 thread::spawn(move || run_stage_worker(listener, &TcpTransportConfig::default()));
2689
2690 let plan = PipelinePlan {
2691 stages: vec![
2692 StagePlacement {
2693 node_id: "local".to_string(),
2694 start_layer: 0,
2695 end_layer: 10,
2696 device: DeviceKind::Cpu,
2697 est_latency_ms: 1.0,
2698 },
2699 StagePlacement {
2700 node_id: "remote".to_string(),
2701 start_layer: 10,
2702 end_layer: 20,
2703 device: DeviceKind::Cpu,
2704 est_latency_ms: 1.0,
2705 },
2706 ],
2707 };
2708
2709 let result = execute_pipeline_with_remote_stage(
2710 &plan,
2711 "local",
2712 "remote",
2713 16,
2714 2,
2715 TcpTransportConfig::default(),
2716 worker_addr,
2717 )
2718 .expect("real remote-stage execution failed");
2719
2720 let worker_summary = worker_handle
2721 .join()
2722 .expect("worker thread panicked")
2723 .expect("worker returned an error");
2724
2725 assert_eq!(result.token_count, 16);
2726 assert_eq!(result.batch_count, 8);
2727 assert_eq!(result.stage_stats.len(), 2);
2728 assert_eq!(worker_summary.batches_processed, 8);
2729 assert_eq!(
2730 result.stage_stats[1].processed_batches,
2731 worker_summary.batches_processed,
2732 "coordinator's batch count must agree with what the independent worker process actually processed"
2733 );
2734 assert!(
2735 result.stage_stats[1].avg_bridge_write_ms > 0.0,
2736 "remote round-trip time must be non-zero for a real socket hop, got {}",
2737 result.stage_stats[1].avg_bridge_write_ms
2738 );
2739 }
2740
2741 #[test]
2742 fn spawn_tcp_bridge_skips_connection_attempts_when_circuit_breaker_is_open() {
2743 use crate::circuit_breaker::{CircuitBreakerConfig, CircuitState};
2744
2745 let dead_addr = {
2747 let l = TcpListener::bind("127.0.0.1:0").unwrap();
2748 l.local_addr().unwrap()
2749 };
2750 let dummy_listener = BridgeListener::Tcp(TcpListener::bind("127.0.0.1:0").unwrap());
2751
2752 let breaker = CircuitBreaker::with_config(CircuitBreakerConfig {
2753 failure_threshold: 1,
2754 open_duration_secs: 3600,
2755 reset_timeout_secs: 3600,
2756 });
2757 breaker.record_failure();
2758 assert_eq!(breaker.current_state(), CircuitState::Open);
2759
2760 let (in_tx, in_rx) = mpsc::sync_channel::<TransportBatch>(1);
2761 let (out_tx, _out_rx) = mpsc::sync_channel::<TransportBatch>(1);
2762 drop(in_tx); let config = TcpTransportConfig {
2765 reconnect_attempts: 5,
2766 reconnect_backoff_ms: 2000,
2767 ..TcpTransportConfig::default()
2768 };
2769
2770 let start = Instant::now();
2771 let handle = spawn_tcp_bridge(
2772 0,
2773 in_rx,
2774 out_tx,
2775 config,
2776 dummy_listener,
2777 BridgeAddr::Tcp(dead_addr),
2778 Some(breaker),
2779 );
2780 handle.join().expect("bridge thread panicked");
2781
2782 assert!(
2785 start.elapsed() < Duration::from_millis(1500),
2786 "open circuit breaker should skip connection attempts entirely, took {:?}",
2787 start.elapsed()
2788 );
2789 }
2790}