aisimulate_core/engine/
handoff.rs1use serde::{Deserialize, Serialize};
11use uuid::Uuid;
12
13use crate::engine::config::WorkerType;
14
15#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
21#[serde(transparent)]
22pub struct HandoffId(Uuid);
23
24impl HandoffId {
25 pub const fn new(value: Uuid) -> Self {
27 Self(value)
28 }
29
30 pub const fn get(self) -> Uuid {
32 self.0
33 }
34}
35
36impl From<Uuid> for HandoffId {
37 fn from(value: Uuid) -> Self {
38 Self(value)
39 }
40}
41
42impl From<HandoffId> for Uuid {
43 fn from(value: HandoffId) -> Self {
44 value.0
45 }
46}
47
48#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
50#[serde(rename_all = "snake_case")]
51pub enum TransferTimingMode {
52 #[default]
54 FullPrompt,
55 DestinationMissing,
57}
58
59#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
65pub struct HandoffTransferTiming {
66 pub mode: TransferTimingMode,
68 pub full_prompt_tokens: usize,
70 pub kv_bytes_per_token: Option<usize>,
72 pub bandwidth_gb_s: Option<f64>,
74}
75
76impl HandoffTransferTiming {
77 pub fn delay_ms(self, destination_missing_tokens: usize) -> Option<f64> {
83 let tokens = match self.mode {
84 TransferTimingMode::FullPrompt => self.full_prompt_tokens,
85 TransferTimingMode::DestinationMissing => destination_missing_tokens,
86 };
87 let (Some(bytes_per_token), Some(bandwidth_gb_s)) =
88 (self.kv_bytes_per_token, self.bandwidth_gb_s)
89 else {
90 return None;
91 };
92 if bandwidth_gb_s <= 0.0 {
93 return None;
94 }
95
96 Some(tokens as f64 * bytes_per_token as f64 / (bandwidth_gb_s * 1e9) * 1000.0)
97 }
98
99 pub fn full_prompt_delay_ms(self) -> Option<f64> {
101 Self {
102 mode: TransferTimingMode::FullPrompt,
103 ..self
104 }
105 .delay_ms(0)
106 }
107}
108
109pub fn prefill_handoff_delay_ms(
114 worker_type: WorkerType,
115 completed: bool,
116 num_input_tokens: usize,
117 bandwidth_gb_s: Option<f64>,
118 kv_bytes_per_token: Option<usize>,
119) -> Option<f64> {
120 if worker_type != WorkerType::Prefill || !completed {
121 return None;
122 }
123 HandoffTransferTiming {
124 mode: TransferTimingMode::FullPrompt,
125 full_prompt_tokens: num_input_tokens,
126 kv_bytes_per_token,
127 bandwidth_gb_s,
128 }
129 .full_prompt_delay_ms()
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135
136 #[test]
137 fn handoff_id_round_trips_caller_owned_value() {
138 let value = Uuid::from_u128(17);
139 let handoff_id = HandoffId::new(value);
140 assert_eq!(handoff_id.get(), value);
141 }
142
143 #[test]
144 fn transfer_delay_uses_selected_prompt_footprint() {
145 let timing = HandoffTransferTiming {
146 mode: TransferTimingMode::DestinationMissing,
147 full_prompt_tokens: 100,
148 kv_bytes_per_token: Some(1_000),
149 bandwidth_gb_s: Some(1.0),
150 };
151
152 assert_eq!(timing.delay_ms(20), Some(0.02));
153 assert_eq!(timing.full_prompt_delay_ms(), Some(0.1));
154 }
155
156 #[test]
157 fn incomplete_or_non_positive_timing_model_has_no_delay() {
158 let timing = HandoffTransferTiming {
159 mode: TransferTimingMode::FullPrompt,
160 full_prompt_tokens: 100,
161 kv_bytes_per_token: None,
162 bandwidth_gb_s: Some(1.0),
163 };
164 assert_eq!(timing.delay_ms(0), None);
165
166 let timing = HandoffTransferTiming {
167 kv_bytes_per_token: Some(1_000),
168 bandwidth_gb_s: Some(0.0),
169 ..timing
170 };
171 assert_eq!(timing.delay_ms(0), None);
172 }
173
174 #[test]
175 fn prefill_handoff_delay_requires_completed_prefill_work() {
176 let args = (128, Some(1.0), Some(1_000_000));
177 assert_eq!(
178 prefill_handoff_delay_ms(WorkerType::Prefill, true, args.0, args.1, args.2,),
179 Some(128.0)
180 );
181 assert_eq!(
182 prefill_handoff_delay_ms(WorkerType::Prefill, false, args.0, args.1, args.2,),
183 None
184 );
185 assert_eq!(
186 prefill_handoff_delay_ms(WorkerType::Decode, true, args.0, args.1, args.2,),
187 None
188 );
189 }
190}