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