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