Skip to main content

aisimulate_core/engine/generalized/
engine.rs

1// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4use 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
26/// Single-rank or attention-DP grouped generalized mock engine.
27///
28/// `dp_size == 1` is the single-rank layer. Larger values compose independent
29/// rank cores behind one logical-worker barrier.
30///
31/// Multi-rank mutations are not transactional across a group. If a grouped
32/// operation fails after an earlier rank may have changed state, the logical
33/// engine becomes poisoned and rejects later mutations. A targeted command is
34/// delegated to exactly one rank; its implementation owns command atomicity,
35/// and ordinary command rejections remain recoverable.
36pub 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    /// Stable identity of this logical worker.
47    pub const fn identity(&self) -> EngineIdentity {
48        self.identity
49    }
50
51    /// Number of scheduler ranks composed behind the group barrier.
52    pub const fn dp_size(&self) -> NonZeroU32 {
53        self.dp_size
54    }
55
56    /// Stable identities of the scheduler ranks in DP-rank order.
57    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    /// Construct every rank in a logical engine.
66    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    /// Construct every rank with a caller-supplied factory.
76    ///
77    /// This is the runtime-provider seam for rank implementations whose
78    /// serialized configuration names a provider but cannot itself contain a
79    /// process-local callback, such as an AIC latency model.
80    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    /// Apply a command to one rank.
103    ///
104    /// When the engine is idle and internal work is due at or before
105    /// `now_ms`, this returns a retryable error without mutation. The caller
106    /// must call [`Self::process_internal_work`] and then retry the command.
107    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        // A command targets exactly one rank, and schedulers use command
149        // errors for recoverable admission rejections (for example, a prompt
150        // larger than the destination KV pool). Poisoning the whole logical
151        // worker here would convert that normal handoff failure into a replay
152        // dead end. Rank implementations must therefore keep command errors
153        // atomic; fail-stop poisoning is reserved for the grouped operations
154        // below, where an earlier sibling may already have committed.
155        let effects = effects?;
156        Ok(EngineEffects::one(dp_rank, effects))
157    }
158
159    /// Whether the logical engine can commit a grouped pass.
160    ///
161    /// No sibling may start a new pass while an earlier grouped pass is
162    /// awaiting completion. Otherwise, one ready rank that is not blocked on
163    /// externally commanded ownership makes the group ready. A held source or
164    /// reserved destination remains owned by the engine, but must not create
165    /// effect-free passes while it waits for release/activation.
166    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    /// Whether every currently ready rank is blocked on an external command.
176    ///
177    /// Attention-DP uses `all`, not `any`: an unrelated ready sibling must
178    /// still be allowed to expose a scheduler livelock or make progress.
179    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    /// Eagerly commit one pass on every currently ready rank.
198    ///
199    /// Returns `None` when no rank has work. Starting a second pass before
200    /// completing the first is a caller error.
201    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    /// Complete the committed pass and release pass-end effects.
287    ///
288    /// `end_ms` may be later than the modeled boundary (for example when a
289    /// wall-clock driver wakes late), but never earlier.
290    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                    // Rank-local FPM is normalized to the modeled shared
331                    // barrier. A wall-clock driver's late wakeup is accepted
332                    // above, but must not inflate modeled execution time.
333                    .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                // Idle-rank FPM represents the modeled shared barrier, not a
344                // live driver's scheduling delay after that barrier elapsed.
345                .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    /// Earliest valid internal-work deadline across all ranks.
365    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    /// Process internal work whose rank deadline is due.
377    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        // A rank may model a physical deadline inside an eagerly committed
384        // model step, but the framework cannot consume that completion until
385        // the shared pass boundary. Besides preserving visibility ordering,
386        // returning before consulting any rank makes this a true no-op: no
387        // residency activation, observer event, or rank-local clock advance.
388        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    /// Whether every rank is drained and no grouped pass remains in flight.
414    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}