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