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