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