aisimulate_core/engine/generalized/
engine.rs1use std::num::NonZeroU32;
5
6use anyhow::{Context, Result, bail, ensure};
7
8use super::contracts::{
9 CommandContext, EngineEffects, EngineIdentity, EnginePassCompleted, EnginePassStarted,
10 GeneralizedEngineConfig, PassId, RankEffects, RankEngine, RankIdentity, RankPassStarted,
11 SameTimestampRetry, SchedulerCommand,
12};
13
14struct PendingRankPass<P> {
15 dp_rank: u32,
16 pending: P,
17}
18
19struct PendingGroupPass<P> {
20 pass_id: PassId,
21 started_at_ms: f64,
22 end_ms: f64,
23 by_rank: Vec<PendingRankPass<P>>,
24}
25
26pub struct GeneralizedMockerEngine<C: RankEngine> {
37 identity: EngineIdentity,
38 dp_size: NonZeroU32,
39 ranks: Vec<C>,
40 next_pass_id: u64,
41 pending_pass: Option<PendingGroupPass<C::PendingPass>>,
42 poisoned: Option<String>,
43}
44
45impl<C: RankEngine> GeneralizedMockerEngine<C> {
46 pub const fn identity(&self) -> EngineIdentity {
48 self.identity
49 }
50
51 pub const fn dp_size(&self) -> NonZeroU32 {
53 self.dp_size
54 }
55
56 pub fn rank_identities(&self) -> impl ExactSizeIterator<Item = RankIdentity> + '_ {
58 (0..self.dp_size.get()).map(|dp_rank| self.identity.rank(dp_rank, self.dp_size))
59 }
60
61 pub(crate) fn ranks_mut(&mut self) -> impl ExactSizeIterator<Item = &mut C> + '_ {
62 self.ranks.iter_mut()
63 }
64
65 pub fn new(
67 identity: EngineIdentity,
68 config: GeneralizedEngineConfig<C::Config>,
69 ) -> Result<Self> {
70 Self::new_with_rank_factory(identity, config.dp_size, |rank_identity| {
71 C::new(rank_identity, &config.rank)
72 })
73 }
74
75 pub fn new_with_rank_factory(
81 identity: EngineIdentity,
82 dp_size: NonZeroU32,
83 mut make_rank: impl FnMut(RankIdentity) -> Result<C>,
84 ) -> Result<Self> {
85 let mut ranks = Vec::with_capacity(dp_size.get() as usize);
86 for dp_rank in 0..dp_size.get() {
87 ranks.push(
88 make_rank(identity.rank(dp_rank, dp_size))
89 .with_context(|| format!("constructing attention-DP rank {dp_rank}"))?,
90 );
91 }
92 Ok(Self {
93 identity,
94 dp_size,
95 ranks,
96 next_pass_id: 0,
97 pending_pass: None,
98 poisoned: None,
99 })
100 }
101
102 pub fn apply_command_effects(
108 &mut self,
109 command: SchedulerCommand<C::Command>,
110 now_ms: f64,
111 ) -> Result<EngineEffects<C::CommandEffects>> {
112 self.ensure_healthy()?;
113 validate_time(now_ms, "command time")?;
114 let pass_in_flight = self.pending_pass.is_some();
115 let dp_rank = command.dp_rank;
116 ensure!(
117 (dp_rank as usize) < self.ranks.len(),
118 "attention-DP rank {dp_rank} is out of range for dp_size {}",
119 self.dp_size
120 );
121 if !pass_in_flight
122 && let Some(deadline_ms) = self.next_internal_deadline_ms()
123 && deadline_ms <= now_ms
124 {
125 bail!(
126 "engine internal work is due at {deadline_ms}ms by command time {now_ms}ms; process internal work and retry the command"
127 );
128 }
129 let effects = {
130 let rank = &mut self.ranks[dp_rank as usize];
131 let pending_pass = self.pending_pass.as_mut().and_then(|group| {
132 group
133 .by_rank
134 .iter_mut()
135 .find(|pending| pending.dp_rank == dp_rank)
136 .map(|pending| &mut pending.pending)
137 });
138 rank.apply_command_effects(
139 command.command,
140 CommandContext {
141 now_ms,
142 pass_in_flight,
143 },
144 pending_pass,
145 )
146 .with_context(|| format!("applying command to attention-DP rank {dp_rank}"))
147 };
148 let effects = effects?;
156 Ok(EngineEffects::one(dp_rank, effects))
157 }
158
159 pub fn is_ready(&self) -> bool {
167 self.poisoned.is_none()
168 && self.pending_pass.is_none()
169 && self
170 .ranks
171 .iter()
172 .any(|rank| rank.is_ready() && !rank.waiting_for_external_command())
173 }
174
175 pub fn waiting_for_external_command(&self) -> bool {
180 if self.poisoned.is_some() || self.pending_pass.is_some() {
181 return false;
182 }
183
184 let mut found_ready_rank = false;
185 for rank in &self.ranks {
186 if !rank.is_ready() {
187 continue;
188 }
189 found_ready_rank = true;
190 if !rank.waiting_for_external_command() {
191 return false;
192 }
193 }
194 found_ready_rank
195 }
196
197 pub fn execute_pass(
202 &mut self,
203 now_ms: f64,
204 ) -> Result<Option<EnginePassStarted<C::PassStartEffects>>> {
205 self.ensure_healthy()?;
206 validate_time(now_ms, "pass start time")?;
207 ensure!(
208 self.pending_pass.is_none(),
209 "engine {} already has a pass in flight",
210 self.identity.worker_id
211 );
212
213 if !self
214 .ranks
215 .iter()
216 .any(|rank| rank.is_ready() && !rank.waiting_for_external_command())
217 {
218 return Ok(None);
219 }
220
221 let pass_id = PassId(self.next_pass_id);
222 let next_pass_id = self
223 .next_pass_id
224 .checked_add(1)
225 .context("generalized engine pass ID overflow")?;
226
227 let mut end_ms = now_ms;
228 let mut same_timestamp_retry = SameTimestampRetry::NotApplicable;
229 let mut started = Vec::new();
230 let mut pending = Vec::new();
231 for dp_rank in 0..self.ranks.len() {
232 if !self.ranks[dp_rank].is_ready() || self.ranks[dp_rank].waiting_for_external_command()
233 {
234 continue;
235 }
236 let pass = {
237 let rank = &mut self.ranks[dp_rank];
238 rank.execute_pass(now_ms)
239 .with_context(|| format!("executing attention-DP rank {dp_rank}"))
240 .and_then(|pass| {
241 validate_time(pass.end_ms, "rank pass end time")?;
242 ensure!(
243 pass.end_ms >= now_ms,
244 "attention-DP rank {dp_rank} completed before its pass started"
245 );
246 Ok(pass)
247 })
248 };
249 let pass = pass.map_err(|error| self.poison(error))?;
250 end_ms = end_ms.max(pass.end_ms);
251 same_timestamp_retry = match (same_timestamp_retry, pass.same_timestamp_retry) {
252 (_, SameTimestampRetry::Retry) => SameTimestampRetry::Retry,
253 (SameTimestampRetry::Retry, _) => SameTimestampRetry::Retry,
254 (_, SameTimestampRetry::Exhausted) => SameTimestampRetry::Exhausted,
255 (status, SameTimestampRetry::NotApplicable) => status,
256 };
257 started.push(RankPassStarted {
258 dp_rank: dp_rank as u32,
259 rank_end_ms: pass.end_ms,
260 effects: pass.start_effects,
261 });
262 pending.push(PendingRankPass {
263 dp_rank: dp_rank as u32,
264 pending: pass.pending,
265 });
266 }
267
268 debug_assert!(!pending.is_empty());
269 self.next_pass_id = next_pass_id;
270 self.pending_pass = Some(PendingGroupPass {
271 pass_id,
272 started_at_ms: now_ms,
273 end_ms,
274 by_rank: pending,
275 });
276 Ok(Some(EnginePassStarted {
277 pass_id,
278 started_at_ms: now_ms,
279 end_ms,
280 participating_ranks: self.dp_size,
281 same_timestamp_retry,
282 by_rank: started,
283 }))
284 }
285
286 pub fn complete_pass(
291 &mut self,
292 pass_id: PassId,
293 end_ms: f64,
294 ) -> Result<EnginePassCompleted<C::PassCompletionEffects>> {
295 self.ensure_healthy()?;
296 validate_time(end_ms, "pass completion time")?;
297 let pending = self
298 .pending_pass
299 .as_ref()
300 .context("cannot complete a pass when none is in flight")?;
301 ensure!(
302 pending.pass_id == pass_id,
303 "pass ID mismatch: expected {}, got {}",
304 pending.pass_id.get(),
305 pass_id.get()
306 );
307 ensure!(
308 end_ms >= pending.end_ms,
309 "pass {} completed at {end_ms}ms before its modeled boundary {}ms",
310 pass_id.get(),
311 pending.end_ms
312 );
313
314 let pending = self
315 .pending_pass
316 .take()
317 .expect("pending pass was checked immediately before take");
318 let mut pending_by_rank = pending.by_rank.into_iter().peekable();
319 let mut effects = Vec::with_capacity(self.ranks.len());
320 for rank_index in 0..self.ranks.len() {
321 let dp_rank = rank_index as u32;
322 if pending_by_rank
323 .peek()
324 .is_some_and(|pending| pending.dp_rank == dp_rank)
325 {
326 let rank_pass = pending_by_rank
327 .next()
328 .expect("peeked pending rank pass must remain available");
329 let rank_effects = self.ranks[rank_index]
330 .complete_pass(rank_pass.pending, pending.end_ms)
334 .with_context(|| format!("completing attention-DP rank {dp_rank}"));
335 let rank_effects = rank_effects.map_err(|error| self.poison(error))?;
336 effects.push(RankEffects {
337 dp_rank,
338 effects: rank_effects,
339 });
340 continue;
341 }
342 let idle_effects = self.ranks[rank_index]
343 .complete_idle_group_pass(pending.started_at_ms, pending.end_ms)
346 .with_context(|| {
347 format!("completing idle attention-DP rank {dp_rank} group barrier")
348 });
349 let idle_effects = idle_effects.map_err(|error| self.poison(error))?;
350 if let Some(rank_effects) = idle_effects {
351 effects.push(RankEffects {
352 dp_rank,
353 effects: rank_effects,
354 });
355 }
356 }
357 debug_assert!(pending_by_rank.next().is_none());
358 Ok(EnginePassCompleted {
359 pass_id,
360 effects: EngineEffects { by_rank: effects },
361 })
362 }
363
364 pub fn next_internal_deadline_ms(&self) -> Option<f64> {
366 if self.poisoned.is_some() || self.pending_pass.is_some() {
367 return None;
368 }
369 self.ranks
370 .iter()
371 .filter_map(RankEngine::next_internal_deadline_ms)
372 .filter(|deadline| deadline.is_finite())
373 .min_by(f64::total_cmp)
374 }
375
376 pub fn process_internal_work(
378 &mut self,
379 now_ms: f64,
380 ) -> Result<EngineEffects<C::InternalEffects>> {
381 self.ensure_healthy()?;
382 validate_time(now_ms, "internal-work time")?;
383 if self.pending_pass.is_some() {
389 return Ok(EngineEffects::default());
390 }
391 let mut effects = Vec::new();
392 for dp_rank in 0..self.ranks.len() {
393 let is_due = self.ranks[dp_rank]
394 .next_internal_deadline_ms()
395 .is_some_and(|deadline| deadline.is_finite() && deadline <= now_ms);
396 if !is_due {
397 continue;
398 }
399 let rank_effects = self.ranks[dp_rank]
400 .process_internal_work(now_ms, false)
401 .with_context(|| {
402 format!("processing internal work for attention-DP rank {dp_rank}")
403 });
404 let rank_effects = rank_effects.map_err(|error| self.poison(error))?;
405 effects.push(RankEffects {
406 dp_rank: dp_rank as u32,
407 effects: rank_effects,
408 });
409 }
410 Ok(EngineEffects { by_rank: effects })
411 }
412
413 pub fn is_drained(&self) -> bool {
415 self.poisoned.is_none()
416 && self.pending_pass.is_none()
417 && self.ranks.iter().all(RankEngine::is_drained)
418 }
419
420 fn ensure_healthy(&self) -> Result<()> {
421 if let Some(reason) = &self.poisoned {
422 bail!(
423 "generalized engine worker {} is poisoned after a prior rank mutation failure: {reason}",
424 self.identity.worker_id
425 );
426 }
427 Ok(())
428 }
429
430 fn poison(&mut self, error: anyhow::Error) -> anyhow::Error {
431 let reason = format!("{error:#}");
432 self.poisoned.get_or_insert_with(|| reason.clone());
433 error.context(format!(
434 "generalized engine worker {} is now poisoned because a rank may have been partially mutated",
435 self.identity.worker_id
436 ))
437 }
438}
439
440fn validate_time(time_ms: f64, label: &str) -> Result<()> {
441 if !time_ms.is_finite() || time_ms < 0.0 {
442 bail!("{label} must be finite and non-negative, got {time_ms}");
443 }
444 Ok(())
445}