1use std::{
4 num::NonZeroUsize,
5 sync::atomic::{AtomicU64, Ordering},
6 time::Instant,
7};
8
9use eredu_core::{
10 consensus::{validate_ranked_identity_bounded, BoundedConsensusTransport},
11 scheduler::{
12 RequestId, RequestStatus, Scheduler, SchedulerCapabilities, SchedulerError,
13 SchedulerLimits, SchedulerProgress, SchedulerReport, SemanticStateTransaction,
14 TransitionOutput, WorkId,
15 },
16 BoundedCompletionWait, Completion, ParallelTopology, RealtimeInputFrame, RealtimeSampling,
17 RealtimeSpeechConfig,
18};
19use sha2::{Digest, Sha256};
20
21use crate::{
22 CommunicationCompletionPolicy, LayerWeightResidency, RealtimeGenerationBranch,
23 RealtimeGenerationState, RealtimeGenerationTransactionError, RealtimeIdentity,
24 RealtimeIngressContract, RealtimePayloadContract, RealtimePayloadGeneration,
25 RealtimePayloadOwnerIdentity, SelectedRealtimeRealization,
26};
27
28static NEXT_REALTIME_INCARNATION: AtomicU64 = AtomicU64::new(1);
29
30#[derive(Debug, Clone, Eq, PartialEq)]
32pub struct RealtimeModelSessionIdentity {
33 selected: Option<Box<SelectedRealtimeRealization>>,
34 architecture: RealtimeIdentity,
35 source: RealtimeIdentity,
36 execution: RealtimeIdentity,
37 schedule_identity: RealtimeIdentity,
38 schedule: RealtimeSpeechConfig,
39 state_layout: RealtimeIdentity,
40 topology_policy: RealtimeIdentity,
41 topology: ParallelTopology,
42 rank: usize,
43 residency: LayerWeightResidency,
44 completion: CommunicationCompletionPolicy,
45}
46
47impl RealtimeModelSessionIdentity {
48 pub fn from_selected(selected: &SelectedRealtimeRealization) -> Self {
50 Self {
51 selected: Some(Box::new(selected.clone())),
52 architecture: selected.requirements().architecture().clone(),
53 source: selected.source().clone(),
54 execution: selected.execution().clone(),
55 schedule_identity: selected.requirements().speech_schedule_identity().clone(),
56 schedule: selected.requirements().speech_schedule().clone(),
57 state_layout: selected.requirements().state_layout_identity().clone(),
58 topology_policy: selected.requirements().topology().identity().clone(),
59 topology: selected.topology(),
60 rank: selected.rank(),
61 residency: selected.residency(),
62 completion: selected.completion(),
63 }
64 }
65
66 #[cfg(test)]
67 #[allow(clippy::too_many_arguments)]
68 fn from_parts(
69 architecture: RealtimeIdentity,
70 source: RealtimeIdentity,
71 execution: RealtimeIdentity,
72 schedule_identity: RealtimeIdentity,
73 schedule: RealtimeSpeechConfig,
74 state_layout: RealtimeIdentity,
75 topology_policy: RealtimeIdentity,
76 topology: ParallelTopology,
77 rank: usize,
78 residency: LayerWeightResidency,
79 completion: CommunicationCompletionPolicy,
80 ) -> Self {
81 Self {
82 selected: None,
83 architecture,
84 source,
85 execution,
86 schedule_identity,
87 schedule,
88 state_layout,
89 topology_policy,
90 topology,
91 rank,
92 residency,
93 completion,
94 }
95 }
96
97 pub const fn schedule(&self) -> &RealtimeSpeechConfig {
99 &self.schedule
100 }
101
102 pub const fn topology(&self) -> ParallelTopology {
104 self.topology
105 }
106
107 pub const fn completion(&self) -> CommunicationCompletionPolicy {
109 self.completion
110 }
111
112 pub fn model_owner(&self) -> RealtimeModelOwnerIdentity {
114 RealtimeModelOwnerIdentity(self.clone())
115 }
116
117 fn distributed_consensus_identity(&self) -> [u32; 8] {
118 let mut digest = Sha256::new();
119 for identity in [
120 &self.architecture,
121 &self.source,
122 &self.execution,
123 &self.schedule_identity,
124 &self.topology_policy,
125 ] {
126 let bytes = identity.as_str().as_bytes();
127 digest.update(
128 u64::try_from(bytes.len())
129 .expect("identity byte length fits u64")
130 .to_le_bytes(),
131 );
132 digest.update(bytes);
133 }
134 for value in [
135 format!("{:?}", self.schedule),
136 format!("{:?}", self.topology),
137 format!("{:?}", self.residency),
138 format!("{:?}", self.completion),
139 ] {
140 digest.update(
141 u64::try_from(value.len())
142 .expect("identity component length fits u64")
143 .to_le_bytes(),
144 );
145 digest.update(value.as_bytes());
146 }
147 if let Some(selected) = &self.selected {
148 for value in [
149 format!("{:?}", selected.state()),
150 format!("{:?}", selected.observations()),
151 ] {
152 digest.update(
153 u64::try_from(value.len())
154 .expect("selected identity component length fits u64")
155 .to_le_bytes(),
156 );
157 digest.update(value.as_bytes());
158 }
159 }
160 let digest = digest.finalize();
161 std::array::from_fn(|index| {
162 let start = index * 4;
163 u32::from_le_bytes(
164 digest[start..start + 4]
165 .try_into()
166 .expect("SHA-256 has eight complete u32 words"),
167 )
168 })
169 }
170}
171
172#[derive(Debug, Clone, Eq, PartialEq)]
174pub struct RealtimeModelOwnerIdentity(RealtimeModelSessionIdentity);
175
176impl RealtimeModelOwnerIdentity {
177 pub const fn session_identity(&self) -> &RealtimeModelSessionIdentity {
179 &self.0
180 }
181}
182
183#[derive(Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
185pub struct RealtimeSessionIncarnation(u64);
186
187impl RealtimeSessionIncarnation {
188 pub const fn value(self) -> u64 {
190 self.0
191 }
192}
193
194#[derive(Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
196pub struct RealtimeHistoryGeneration(u64);
197
198impl RealtimeHistoryGeneration {
199 pub const fn value(self) -> u64 {
201 self.0
202 }
203}
204
205pub struct RealtimeSessionState<M, S, R, C> {
207 model: RealtimeModelSessionIdentity,
208 owner: RealtimeModelOwnerIdentity,
209 incarnation: RealtimeSessionIncarnation,
210 history_generation: RealtimeHistoryGeneration,
211 committed_batch: Option<NonZeroUsize>,
212 generation: RealtimeGenerationState<M, S, R, C>,
213}
214
215impl<M, S, R, C> RealtimeSessionState<M, S, R, C> {
216 pub const fn model_identity(&self) -> &RealtimeModelSessionIdentity {
218 &self.model
219 }
220
221 pub const fn model_owner(&self) -> &RealtimeModelOwnerIdentity {
223 &self.owner
224 }
225
226 pub const fn incarnation(&self) -> RealtimeSessionIncarnation {
228 self.incarnation
229 }
230
231 pub const fn history_generation(&self) -> RealtimeHistoryGeneration {
233 self.history_generation
234 }
235
236 pub const fn committed_batch(&self) -> Option<NonZeroUsize> {
238 self.committed_batch
239 }
240
241 pub fn payload_contract(
243 &self,
244 ingress: &RealtimeIngressContract,
245 ) -> Result<RealtimePayloadContract, RealtimeSessionExecutionError> {
246 if ingress.schedule() != self.model.schedule() {
247 return Err(RealtimeSessionExecutionError::IngressScheduleMismatch);
248 }
249 let batch = self
250 .committed_batch
251 .ok_or(RealtimeSessionExecutionError::BatchNotAdmitted)?;
252 Ok(RealtimePayloadContract::new(
253 ingress.schedule().clone(),
254 batch.get(),
255 ingress.text_domain(),
256 ingress.audio_domain(),
257 RealtimePayloadGeneration::new(self.history_generation.value())
258 .expect("scheduler history generations are nonzero"),
259 RealtimePayloadOwnerIdentity::new(self.incarnation.value())
260 .expect("scheduler session incarnations are nonzero"),
261 )
262 .expect("scheduler-admitted payload contract has a positive batch"))
263 }
264
265 pub const fn generation(&self) -> &RealtimeGenerationState<M, S, R, C> {
267 &self.generation
268 }
269
270 pub fn generation_mut(&mut self) -> &mut RealtimeGenerationState<M, S, R, C> {
272 &mut self.generation
273 }
274}
275
276pub struct RealtimeSessionBranch<MB, S, R, C> {
278 model: RealtimeModelSessionIdentity,
279 owner: RealtimeModelOwnerIdentity,
280 incarnation: RealtimeSessionIncarnation,
281 history_generation: RealtimeHistoryGeneration,
282 committed_batch: Option<NonZeroUsize>,
283 generation: RealtimeGenerationBranch<MB, S, R, C>,
284}
285
286impl<MB, S, R, C> RealtimeSessionBranch<MB, S, R, C> {
287 pub const fn model_identity(&self) -> &RealtimeModelSessionIdentity {
289 &self.model
290 }
291
292 pub const fn model_owner(&self) -> &RealtimeModelOwnerIdentity {
294 &self.owner
295 }
296
297 pub const fn incarnation(&self) -> RealtimeSessionIncarnation {
299 self.incarnation
300 }
301
302 pub const fn history_generation(&self) -> RealtimeHistoryGeneration {
304 self.history_generation
305 }
306
307 pub const fn committed_batch(&self) -> Option<NonZeroUsize> {
309 self.committed_batch
310 }
311
312 pub fn payload_contract(
314 &self,
315 ingress: &RealtimeIngressContract,
316 ) -> Result<RealtimePayloadContract, RealtimeSessionExecutionError> {
317 if ingress.schedule() != self.model.schedule() {
318 return Err(RealtimeSessionExecutionError::IngressScheduleMismatch);
319 }
320 let batch = self
321 .committed_batch
322 .ok_or(RealtimeSessionExecutionError::BatchNotAdmitted)?;
323 Ok(RealtimePayloadContract::new(
324 ingress.schedule().clone(),
325 batch.get(),
326 ingress.text_domain(),
327 ingress.audio_domain(),
328 RealtimePayloadGeneration::new(self.history_generation.value())
329 .expect("scheduler history generations are nonzero"),
330 RealtimePayloadOwnerIdentity::new(self.incarnation.value())
331 .expect("scheduler session incarnations are nonzero"),
332 )
333 .expect("scheduler-admitted payload contract has a positive batch"))
334 }
335
336 pub fn generation_mut(&mut self) -> &mut RealtimeGenerationBranch<MB, S, R, C> {
338 &mut self.generation
339 }
340
341 fn admit_batch(&mut self, batch: usize) -> Result<(), RealtimeSessionExecutionError> {
342 let batch = NonZeroUsize::new(batch).ok_or(RealtimeSessionExecutionError::EmptyBatch)?;
343 match self.committed_batch {
344 Some(committed) if committed != batch => Err(RealtimeSessionExecutionError::Batch {
345 committed: committed.get(),
346 submitted: batch.get(),
347 }),
348 Some(_) => Ok(()),
349 None => {
350 self.committed_batch = Some(batch);
351 Ok(())
352 }
353 }
354 }
355}
356
357impl<M, S, R, C> SemanticStateTransaction for RealtimeSessionState<M, S, R, C>
358where
359 M: SemanticStateTransaction,
360 M::Error: 'static,
361 S: Clone,
362 R: Clone,
363 C: Completion,
364{
365 type Branch = RealtimeSessionBranch<M::Branch, S, R, C>;
366 type Error = RealtimeSessionTransactionError<M::Error, C::Error>;
367
368 fn branch(&self) -> Result<Self::Branch, Self::Error> {
369 Ok(RealtimeSessionBranch {
370 model: self.model.clone(),
371 owner: self.owner.clone(),
372 incarnation: self.incarnation,
373 history_generation: self.history_generation,
374 committed_batch: self.committed_batch,
375 generation: self
376 .generation
377 .branch()
378 .map_err(RealtimeSessionTransactionError::Generation)?,
379 })
380 }
381
382 fn commit_branch(&mut self, branch: Self::Branch) -> Result<(), Self::Error> {
383 if self.model != branch.model
384 || self.owner != branch.owner
385 || self.incarnation != branch.incarnation
386 || self.history_generation != branch.history_generation
387 {
388 Self::discard_branch(branch)?;
389 return Err(RealtimeSessionTransactionError::IdentityMismatch);
390 }
391 if let (Some(committed), Some(submitted)) = (self.committed_batch, branch.committed_batch) {
392 if committed != submitted {
393 Self::discard_branch(branch)?;
394 return Err(RealtimeSessionTransactionError::BatchMismatch);
395 }
396 }
397 self.generation
398 .commit_branch(branch.generation)
399 .map_err(RealtimeSessionTransactionError::Generation)?;
400 self.committed_batch = branch.committed_batch;
401 Ok(())
402 }
403
404 fn discard_branch(branch: Self::Branch) -> Result<(), Self::Error> {
405 RealtimeGenerationState::<M, S, R, C>::discard_branch(branch.generation)
406 .map_err(RealtimeSessionTransactionError::Generation)
407 }
408
409 fn permits_parallel_branches(&self) -> bool {
410 false
411 }
412}
413
414pub struct ReleasedRealtimeSession<M, S, R, C> {
416 state: RealtimeSessionState<M, S, R, C>,
417}
418
419impl<M, S, R, C> ReleasedRealtimeSession<M, S, R, C> {
420 pub const fn model_identity(&self) -> &RealtimeModelSessionIdentity {
422 self.state.model_identity()
423 }
424
425 pub const fn incarnation(&self) -> RealtimeSessionIncarnation {
427 self.state.incarnation()
428 }
429
430 pub const fn committed_batch(&self) -> Option<NonZeroUsize> {
432 self.state.committed_batch()
433 }
434}
435
436pub struct RealtimeSessionScheduler<M, S, R, C, O>
438where
439 M: SemanticStateTransaction,
440 M::Error: 'static,
441 S: Clone,
442 R: Clone,
443 C: Completion,
444 O: TransitionOutput,
445{
446 model: RealtimeModelSessionIdentity,
447 scheduler: Scheduler<RealtimeInputFrame, RealtimeSessionState<M, S, R, C>, O>,
448}
449
450impl<M, S, R, C, O> RealtimeSessionScheduler<M, S, R, C, O>
451where
452 M: SemanticStateTransaction,
453 M::Error: 'static,
454 S: Clone,
455 R: Clone,
456 C: Completion,
457 O: TransitionOutput,
458{
459 pub const fn model_identity(&self) -> &RealtimeModelSessionIdentity {
461 &self.model
462 }
463
464 pub fn new(
466 model: RealtimeModelSessionIdentity,
467 limits: SchedulerLimits,
468 ) -> Result<Self, SchedulerError> {
469 Ok(Self {
470 model,
471 scheduler: Scheduler::new(limits)?,
472 })
473 }
474
475 pub fn register(
477 &mut self,
478 request: RequestId,
479 generation: RealtimeGenerationState<M, S, R, C>,
480 ) -> Result<RealtimeSessionIncarnation, RealtimeSessionError> {
481 if generation.schedule_state().schedule() != self.model.schedule() {
482 return Err(RealtimeSessionError::ScheduleMismatch);
483 }
484 self.scheduler.validate_registration(request)?;
485 let incarnation = allocate_incarnation()?;
486 let state = RealtimeSessionState {
487 model: self.model.clone(),
488 owner: self.model.model_owner(),
489 incarnation,
490 history_generation: RealtimeHistoryGeneration(incarnation.0),
491 committed_batch: None,
492 generation,
493 };
494 self.scheduler.register(request, state)?;
495 Ok(incarnation)
496 }
497
498 pub fn resume(
500 &mut self,
501 request: RequestId,
502 released: ReleasedRealtimeSession<M, S, R, C>,
503 ) -> Result<(), RealtimeSessionResumeError<M, S, R, C>> {
504 if released.state.model != self.model {
505 return Err(RealtimeSessionResumeError {
506 reason: RealtimeSessionError::ModelIdentityMismatch,
507 released: Box::new(released),
508 });
509 }
510 if let Err(error) = self.scheduler.validate_registration(request) {
511 return Err(RealtimeSessionResumeError {
512 reason: RealtimeSessionError::Scheduler(error),
513 released: Box::new(released),
514 });
515 }
516 self.scheduler
517 .register(request, released.state)
518 .expect("prevalidated realtime resumption cannot fail registration");
519 Ok(())
520 }
521
522 pub fn enqueue(
524 &mut self,
525 request: RequestId,
526 frame: RealtimeInputFrame,
527 ) -> Result<WorkId, SchedulerError> {
528 self.scheduler.enqueue(request, frame)
529 }
530
531 pub fn enqueue_with_deadline(
533 &mut self,
534 request: RequestId,
535 frame: RealtimeInputFrame,
536 deadline: Option<Instant>,
537 ) -> Result<WorkId, SchedulerError> {
538 self.scheduler
539 .enqueue_with_deadline(request, frame, deadline)
540 }
541
542 pub fn enqueue_batch(
544 &mut self,
545 request: RequestId,
546 frames: Vec<RealtimeInputFrame>,
547 ) -> Result<Vec<WorkId>, SchedulerError> {
548 self.scheduler.enqueue_batch(request, frames)
549 }
550
551 pub fn run_local_turn<E>(
553 &mut self,
554 now: Instant,
555 mut execute: impl FnMut(
556 WorkId,
557 &RealtimeInputFrame,
558 &mut RealtimeSessionBranch<M::Branch, S, R, C>,
559 ) -> Result<O, E>,
560 ) -> Result<SchedulerProgress<RealtimeInputFrame, O>, SchedulerError>
561 where
562 E: std::error::Error,
563 {
564 self.ensure_local_topology()?;
565 self.scheduler.run_local_turn(now, |id, frame, branch| {
566 branch
567 .admit_batch(frame.batch())
568 .map_err(RealtimeSessionSubmissionError::<E>::Session)?;
569 execute(id, frame, branch).map_err(RealtimeSessionSubmissionError::Execution)
570 })
571 }
572
573 pub fn run_local_bounded<E>(
575 &mut self,
576 now: Instant,
577 maximum_frames: usize,
578 mut execute: impl FnMut(
579 WorkId,
580 &RealtimeInputFrame,
581 &mut RealtimeSessionBranch<M::Branch, S, R, C>,
582 ) -> Result<O, E>,
583 ) -> Result<SchedulerProgress<RealtimeInputFrame, O>, SchedulerError>
584 where
585 E: std::error::Error,
586 {
587 self.ensure_local_topology()?;
588 let mut progress = self.scheduler.poll_completions(now);
589 self.scheduler.prepare_bounded(maximum_frames, now)?;
590 progress.newly_submitted = self.scheduler.submit_prepared(now, |id, frame, branch| {
591 branch
592 .admit_batch(frame.batch())
593 .map_err(RealtimeSessionSubmissionError::<E>::Session)?;
594 execute(id, frame, branch).map_err(RealtimeSessionSubmissionError::Execution)
595 })?;
596 let completed = self.scheduler.poll_completions(now);
597 progress.committed.extend(completed.committed);
598 progress.failed.extend(completed.failed);
599 Ok(progress)
600 }
601
602 pub fn run_distributed_turn<T, E>(
604 &mut self,
605 protocol: u64,
606 transport: &T,
607 now: Instant,
608 mut execute: impl FnMut(
609 WorkId,
610 &RealtimeInputFrame,
611 &mut RealtimeSessionBranch<M::Branch, S, R, C>,
612 ) -> Result<O, E>,
613 ) -> Result<SchedulerProgress<RealtimeInputFrame, O>, SchedulerError>
614 where
615 T: BoundedConsensusTransport,
616 <T::Completion as Completion>::Error: std::fmt::Display,
617 E: std::error::Error,
618 O: eredu_core::scheduler::DistributedTransitionOutput,
619 {
620 if self.model.topology().is_replicated() {
621 return Err(SchedulerError::Consensus(
622 "distributed realtime turns require a non-replicated selected topology".into(),
623 ));
624 }
625 let participants = self.model.topology().world_size();
626 if transport.participant_count() != participants {
627 return Err(SchedulerError::Consensus(format!(
628 "distributed realtime transport has {} participants; selected topology requires {participants}",
629 transport.participant_count(),
630 )));
631 }
632 let wait: BoundedCompletionWait = self.model.completion().bounded_wait();
633 validate_ranked_identity_bounded(
634 transport,
635 protocol,
636 &self.model.distributed_consensus_identity(),
637 self.model.rank,
638 wait,
639 )
640 .map_err(|error| SchedulerError::Consensus(error.to_string()))?;
641 self.scheduler
642 .run_distributed_turn(protocol, transport, wait, now, |id, frame, branch| {
643 branch
644 .admit_batch(frame.batch())
645 .map_err(RealtimeSessionSubmissionError::<E>::Session)?;
646 execute(id, frame, branch).map_err(RealtimeSessionSubmissionError::Execution)
647 })
648 }
649
650 fn ensure_local_topology(&self) -> Result<(), SchedulerError> {
651 if self.model.topology().is_replicated() {
652 Ok(())
653 } else {
654 Err(SchedulerError::Consensus(
655 "rank-local realtime turns are forbidden for a non-replicated selected topology"
656 .into(),
657 ))
658 }
659 }
660
661 pub fn replace_sampling<E>(
663 &mut self,
664 request: RequestId,
665 sampling: RealtimeSampling,
666 realize: impl FnOnce(RealtimeSampling) -> Result<(Vec<S>, Option<R>), E>,
667 ) -> Result<(), RealtimeSamplingUpdateError<E, M::Error, C::Error>> {
668 if self.scheduler.queued_for_request(request) != 0 {
669 return Err(RealtimeSamplingReplacementError::QueuedWork);
670 }
671 let state = self
672 .scheduler
673 .request_state_mut(request)
674 .map_err(RealtimeSamplingReplacementError::Scheduler)?;
675 let (samplers, random) =
676 realize(sampling).map_err(RealtimeSamplingReplacementError::Realization)?;
677 state
678 .generation_mut()
679 .replace_sampling(sampling, samplers, random)
680 .map_err(RealtimeSamplingReplacementError::Generation)
681 }
682
683 pub fn cancel(&mut self, request: RequestId) -> Result<(), SchedulerError> {
685 self.scheduler.cancel(request)
686 }
687
688 pub fn finish(&mut self, request: RequestId) -> Result<(), SchedulerError> {
690 self.scheduler.finish(request)
691 }
692
693 pub fn release(
695 &mut self,
696 request: RequestId,
697 ) -> Result<ReleasedRealtimeSession<M, S, R, C>, SchedulerError> {
698 self.scheduler
699 .release(request)
700 .map(|state| ReleasedRealtimeSession { state })
701 }
702
703 pub fn request_status(&self, request: RequestId) -> Option<RequestStatus> {
705 self.scheduler.request_status(request)
706 }
707
708 pub fn queued_for_request(&self, request: RequestId) -> usize {
710 self.scheduler.queued_for_request(request)
711 }
712
713 pub fn forget_terminal(&mut self, request: RequestId) -> Result<RequestStatus, SchedulerError> {
715 self.scheduler.forget_terminal(request)
716 }
717
718 pub fn request_state(&self, request: RequestId) -> Option<&RealtimeSessionState<M, S, R, C>> {
720 self.scheduler.request_state(request)
721 }
722
723 pub fn report(&self) -> SchedulerReport {
725 self.scheduler.report()
726 }
727
728 pub fn capabilities(&self) -> SchedulerCapabilities {
730 self.scheduler.capabilities()
731 }
732}
733
734fn allocate_incarnation() -> Result<RealtimeSessionIncarnation, RealtimeSessionError> {
735 NEXT_REALTIME_INCARNATION
736 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |value| {
737 value.checked_add(1)
738 })
739 .map(RealtimeSessionIncarnation)
740 .map_err(|_| RealtimeSessionError::IncarnationExhausted)
741}
742
743#[derive(Debug, thiserror::Error)]
745#[non_exhaustive]
746pub enum RealtimeSessionError {
747 #[error(transparent)]
749 Scheduler(#[from] SchedulerError),
750 #[error("realtime generation schedule differs from selected model identity")]
752 ScheduleMismatch,
753 #[error("released realtime session model identity does not match")]
755 ModelIdentityMismatch,
756 #[error("realtime session incarnation identity space is exhausted")]
758 IncarnationExhausted,
759}
760
761#[derive(Debug, Clone, Copy, Eq, PartialEq, thiserror::Error)]
763#[non_exhaustive]
764pub enum RealtimeSessionExecutionError {
765 #[error("realtime session frame batch has not been admitted")]
767 BatchNotAdmitted,
768 #[error("realtime session frame batch must be positive")]
770 EmptyBatch,
771 #[error("realtime session batch is {submitted}, committed batch is {committed}")]
773 Batch {
774 committed: usize,
776 submitted: usize,
778 },
779 #[error("realtime ingress schedule differs from selected session schedule")]
781 IngressScheduleMismatch,
782}
783
784pub struct RealtimeSessionResumeError<M, S, R, C> {
786 reason: RealtimeSessionError,
787 released: Box<ReleasedRealtimeSession<M, S, R, C>>,
788}
789
790impl<M, S, R, C> RealtimeSessionResumeError<M, S, R, C> {
791 pub const fn reason(&self) -> &RealtimeSessionError {
793 &self.reason
794 }
795
796 pub fn into_released(self) -> ReleasedRealtimeSession<M, S, R, C> {
798 *self.released
799 }
800}
801
802impl<M, S, R, C> std::fmt::Debug for RealtimeSessionResumeError<M, S, R, C> {
803 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
804 formatter
805 .debug_struct("RealtimeSessionResumeError")
806 .field("reason", &self.reason)
807 .finish_non_exhaustive()
808 }
809}
810
811impl<M, S, R, C> std::fmt::Display for RealtimeSessionResumeError<M, S, R, C> {
812 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
813 self.reason.fmt(formatter)
814 }
815}
816
817impl<M, S, R, C> std::error::Error for RealtimeSessionResumeError<M, S, R, C> {}
818
819#[derive(Debug, thiserror::Error)]
820enum RealtimeSessionSubmissionError<E: std::error::Error> {
821 #[error(transparent)]
822 Session(RealtimeSessionExecutionError),
823 #[error(transparent)]
824 Execution(E),
825}
826
827#[derive(Debug, thiserror::Error)]
829pub enum RealtimeSessionTransactionError<M, C>
830where
831 M: std::error::Error,
832 C: std::error::Error,
833{
834 #[error(transparent)]
836 Generation(RealtimeGenerationTransactionError<M, C>),
837 #[error("realtime session branch identity does not match canonical state")]
839 IdentityMismatch,
840 #[error("realtime session branch batch does not match canonical state")]
842 BatchMismatch,
843}
844
845#[derive(Debug, thiserror::Error)]
847pub enum RealtimeSamplingReplacementError<E, G> {
848 #[error("cannot replace realtime sampling while frames are queued")]
850 QueuedWork,
851 #[error(transparent)]
853 Scheduler(SchedulerError),
854 #[error("realtime sampling realization failed")]
856 Realization(E),
857 #[error("realtime generation rejected replacement sampling")]
859 Generation(G),
860}
861
862pub type RealtimeSamplingUpdateError<E, M, C> =
864 RealtimeSamplingReplacementError<E, RealtimeGenerationTransactionError<M, C>>;
865
866#[cfg(test)]
867mod tests {
868 use std::{cell::Cell, convert::Infallible, rc::Rc, time::Duration};
869
870 use eredu_core::{
871 consensus::{BoundedConsensusTransport, ConsensusTransport},
872 scheduler::{RequestStatus, TransitionOutput},
873 BoundedCompletion, BoundedCompletionOutcome, BoundedCompletionWait,
874 CompletionCancellationMode, RealtimeFrameConvention, Submission,
875 };
876
877 use super::*;
878 use crate::TokenDomain;
879
880 #[derive(Debug, Clone, Eq, PartialEq)]
881 struct ModelState(usize);
882
883 #[derive(Debug, Clone, Copy, thiserror::Error)]
884 #[error("model transaction failed")]
885 struct ModelError;
886
887 impl SemanticStateTransaction for ModelState {
888 type Branch = Self;
889 type Error = ModelError;
890
891 fn branch(&self) -> Result<Self::Branch, Self::Error> {
892 Ok(self.clone())
893 }
894
895 fn commit_branch(&mut self, branch: Self::Branch) -> Result<(), Self::Error> {
896 *self = branch;
897 Ok(())
898 }
899 }
900
901 #[derive(Debug, Clone)]
902 struct MockCompletion(Rc<Cell<bool>>);
903
904 #[derive(Debug, Clone, Copy, thiserror::Error)]
905 #[error("completion failed")]
906 struct CompletionError;
907
908 impl Completion for MockCompletion {
909 type Error = CompletionError;
910
911 fn is_complete(&self) -> Result<bool, Self::Error> {
912 Ok(self.0.get())
913 }
914
915 fn wait(&self) -> Result<(), Self::Error> {
916 self.0.get().then_some(()).ok_or(CompletionError)
917 }
918 }
919
920 #[derive(Debug)]
921 struct MockOutput {
922 completion: MockCompletion,
923 }
924
925 impl TransitionOutput for MockOutput {
926 type Error = CompletionError;
927
928 fn is_complete(&self) -> Result<bool, Self::Error> {
929 self.completion.is_complete()
930 }
931
932 fn retained_resources(&self) -> usize {
933 1
934 }
935 }
936
937 impl eredu_core::scheduler::DistributedTransitionOutput for MockOutput {
938 fn encode_distributed_output(&self, output: &mut Vec<u32>) -> Result<(), String> {
939 output.push(0);
940 Ok(())
941 }
942 }
943
944 type Sessions = RealtimeSessionScheduler<ModelState, (), (), MockCompletion, MockOutput>;
945
946 fn schedule() -> RealtimeSpeechConfig {
947 RealtimeSpeechConfig::new(
948 2,
949 1,
950 1,
951 1,
952 9,
953 8,
954 RealtimeFrameConvention::FeedbackAlignedHistory,
955 vec![0, 0, 1],
956 )
957 .unwrap()
958 }
959
960 fn identity(suffix: &str) -> RealtimeModelSessionIdentity {
961 identity_with_topology(suffix, ParallelTopology::new(1, 1, 1, 1).unwrap())
962 }
963
964 fn identity_with_topology(
965 suffix: &str,
966 topology: ParallelTopology,
967 ) -> RealtimeModelSessionIdentity {
968 let id = |prefix: &str| RealtimeIdentity::new(format!("{prefix}-{suffix}")).unwrap();
969 RealtimeModelSessionIdentity::from_parts(
970 id("architecture"),
971 id("source"),
972 id("execution"),
973 id("schedule"),
974 schedule(),
975 id("state"),
976 id("topology"),
977 topology,
978 0,
979 LayerWeightResidency::FullyResident,
980 CommunicationCompletionPolicy::new(
981 Duration::from_secs(1),
982 CompletionCancellationMode::QuarantineUntilComplete,
983 )
984 .unwrap(),
985 )
986 }
987
988 #[derive(Debug, Clone, Copy)]
989 struct ReadyConsensusCompletion;
990
991 impl Completion for ReadyConsensusCompletion {
992 type Error = Infallible;
993
994 fn is_complete(&self) -> Result<bool, Self::Error> {
995 Ok(true)
996 }
997
998 fn wait(&self) -> Result<(), Self::Error> {
999 Ok(())
1000 }
1001 }
1002
1003 impl BoundedCompletion for ReadyConsensusCompletion {
1004 fn wait_bounded(
1005 self,
1006 _: BoundedCompletionWait,
1007 ) -> Result<BoundedCompletionOutcome, Self::Error> {
1008 Ok(BoundedCompletionOutcome::Completed)
1009 }
1010 }
1011
1012 struct DisagreeingTransport {
1013 calls: Cell<usize>,
1014 disagree_on: usize,
1015 }
1016
1017 impl DisagreeingTransport {
1018 fn model_identity() -> Self {
1019 Self {
1020 calls: Cell::new(0),
1021 disagree_on: 1,
1022 }
1023 }
1024
1025 fn schedule() -> Self {
1026 Self {
1027 calls: Cell::new(0),
1028 disagree_on: 4,
1029 }
1030 }
1031 }
1032
1033 impl ConsensusTransport for DisagreeingTransport {
1034 type Error = Infallible;
1035
1036 fn participant_count(&self) -> usize {
1037 2
1038 }
1039
1040 fn all_gather_words(&self, local: &[u32]) -> Result<Vec<u32>, Self::Error> {
1041 let call = self.calls.get() + 1;
1042 self.calls.set(call);
1043 let mut gathered = local.repeat(2);
1044 if call == 1 {
1045 *gathered.last_mut().expect("identity frame has a rank word") = 1;
1046 }
1047 if call == self.disagree_on {
1048 gathered[local.len()] ^= 1;
1049 }
1050 Ok(gathered)
1051 }
1052 }
1053
1054 impl BoundedConsensusTransport for DisagreeingTransport {
1055 type Completion = ReadyConsensusCompletion;
1056 type GatherOutput = Vec<u32>;
1057
1058 fn submit_all_gather_words(
1059 &self,
1060 local: &[u32],
1061 ) -> Result<Submission<Self::GatherOutput, Self::Completion>, Self::Error> {
1062 Ok(Submission {
1063 output: self.all_gather_words(local)?,
1064 completion: ReadyConsensusCompletion,
1065 })
1066 }
1067
1068 fn resolve_all_gather_words(
1069 &self,
1070 output: Self::GatherOutput,
1071 ) -> Result<Vec<u32>, Self::Error> {
1072 Ok(output)
1073 }
1074 }
1075
1076 fn generation() -> RealtimeGenerationState<ModelState, (), (), MockCompletion> {
1077 RealtimeGenerationState::new(
1078 ModelState(0),
1079 schedule(),
1080 RealtimeSampling::greedy(),
1081 vec![(), ()],
1082 None,
1083 )
1084 .unwrap()
1085 }
1086
1087 fn ingress() -> RealtimeIngressContract {
1088 RealtimeIngressContract::new(schedule(), TokenDomain::new(32), TokenDomain::new(16))
1089 .unwrap()
1090 }
1091
1092 fn frame(batch: usize) -> RealtimeInputFrame {
1093 RealtimeInputFrame::new(batch, vec![1; batch])
1094 }
1095
1096 fn limits(submissions: usize) -> SchedulerLimits {
1097 SchedulerLimits::with_execution_bounds(8, 32, submissions, 8, 1, usize::MAX).unwrap()
1098 }
1099
1100 fn execute_immediately(
1101 _id: WorkId,
1102 _frame: &RealtimeInputFrame,
1103 branch: &mut RealtimeSessionBranch<ModelState, (), (), MockCompletion>,
1104 ) -> Result<MockOutput, Infallible> {
1105 branch.generation_mut().model_state_mut().0 += 1;
1106 let completion = MockCompletion(Rc::new(Cell::new(true)));
1107 branch
1108 .generation_mut()
1109 .attach_submission_completion(completion.clone())
1110 .unwrap();
1111 Ok(MockOutput { completion })
1112 }
1113
1114 #[test]
1115 fn one_scheduler_round_robins_single_lane_sessions() {
1116 let mut sessions = Sessions::new(identity("a"), limits(2)).unwrap();
1117 let first = RequestId::new(1);
1118 let second = RequestId::new(2);
1119 let first_incarnation = sessions.register(first, generation()).unwrap();
1120 let second_incarnation = sessions.register(second, generation()).unwrap();
1121 assert_ne!(first_incarnation, second_incarnation);
1122 sessions
1123 .enqueue_batch(first, vec![frame(1), frame(1)])
1124 .unwrap();
1125 sessions
1126 .enqueue_batch(second, vec![frame(1), frame(1)])
1127 .unwrap();
1128
1129 let progress = sessions
1130 .run_local_turn(Instant::now(), execute_immediately)
1131 .unwrap();
1132 assert_eq!(progress.committed.len(), 2);
1133 assert_eq!(progress.committed[0].0.request(), first);
1134 assert_eq!(progress.committed[1].0.request(), second);
1135 assert_eq!(
1136 sessions
1137 .request_state(first)
1138 .unwrap()
1139 .generation()
1140 .model_state()
1141 .0,
1142 1
1143 );
1144 assert_eq!(
1145 sessions
1146 .request_state(second)
1147 .unwrap()
1148 .generation()
1149 .model_state()
1150 .0,
1151 1
1152 );
1153 }
1154
1155 #[test]
1156 fn non_replicated_identity_rejects_local_turn_before_submission() {
1157 let topology = ParallelTopology::new(2, 1, 1, 1).unwrap();
1158 let mut sessions =
1159 Sessions::new(identity_with_topology("tp", topology), limits(1)).unwrap();
1160 let request = RequestId::new(90);
1161 sessions.register(request, generation()).unwrap();
1162 sessions.enqueue(request, frame(1)).unwrap();
1163 let calls = Rc::new(Cell::new(0));
1164 let observed = calls.clone();
1165 let error = sessions
1166 .run_local_turn(Instant::now(), move |id, frame, branch| {
1167 observed.set(observed.get() + 1);
1168 execute_immediately(id, frame, branch)
1169 })
1170 .unwrap_err();
1171 assert!(matches!(error, SchedulerError::Consensus(_)));
1172 assert_eq!(calls.get(), 0);
1173 assert_eq!(sessions.queued_for_request(request), 1);
1174
1175 let error = sessions
1176 .run_local_bounded(Instant::now(), 1, |id, frame, branch| {
1177 calls.set(calls.get() + 1);
1178 execute_immediately(id, frame, branch)
1179 })
1180 .unwrap_err();
1181 assert!(matches!(error, SchedulerError::Consensus(_)));
1182 assert_eq!(calls.get(), 0);
1183 assert_eq!(sessions.queued_for_request(request), 1);
1184 }
1185
1186 #[test]
1187 fn distributed_model_identity_disagreement_submits_and_publishes_nothing() {
1188 let topology = ParallelTopology::new(2, 1, 1, 1).unwrap();
1189 let mut sessions = Sessions::new(
1190 identity_with_topology("tp-disagreement", topology),
1191 limits(1),
1192 )
1193 .unwrap();
1194 let request = RequestId::new(91);
1195 sessions.register(request, generation()).unwrap();
1196 sessions.enqueue(request, frame(1)).unwrap();
1197 let calls = Rc::new(Cell::new(0));
1198 let observed = calls.clone();
1199 let error = sessions
1200 .run_distributed_turn(
1201 17,
1202 &DisagreeingTransport::model_identity(),
1203 Instant::now(),
1204 move |id, frame, branch| {
1205 observed.set(observed.get() + 1);
1206 execute_immediately(id, frame, branch)
1207 },
1208 )
1209 .unwrap_err();
1210 assert!(matches!(error, SchedulerError::Consensus(_)));
1211 assert_eq!(calls.get(), 0);
1212 assert_eq!(
1213 sessions
1214 .request_state(request)
1215 .unwrap()
1216 .generation()
1217 .model_state()
1218 .0,
1219 0
1220 );
1221 assert_eq!(sessions.report().completed_work, 0);
1222 }
1223
1224 #[test]
1225 fn distributed_schedule_disagreement_submits_and_publishes_nothing() {
1226 let topology = ParallelTopology::new(2, 1, 1, 1).unwrap();
1227 let mut sessions =
1228 Sessions::new(identity_with_topology("tp-schedule", topology), limits(1)).unwrap();
1229 let request = RequestId::new(92);
1230 sessions.register(request, generation()).unwrap();
1231 sessions.enqueue(request, frame(1)).unwrap();
1232 let calls = Rc::new(Cell::new(0));
1233 let observed = calls.clone();
1234 let error = sessions
1235 .run_distributed_turn(
1236 18,
1237 &DisagreeingTransport::schedule(),
1238 Instant::now(),
1239 move |id, frame, branch| {
1240 observed.set(observed.get() + 1);
1241 execute_immediately(id, frame, branch)
1242 },
1243 )
1244 .unwrap_err();
1245 assert!(matches!(error, SchedulerError::Consensus(_)));
1246 assert_eq!(calls.get(), 0);
1247 assert_eq!(sessions.report().completed_work, 0);
1248 }
1249
1250 #[test]
1251 fn scheduler_branch_derives_payload_identity_before_native_execution() {
1252 let mut sessions = Sessions::new(identity("payload"), limits(1)).unwrap();
1253 let request = RequestId::new(20);
1254 let incarnation = sessions.register(request, generation()).unwrap();
1255 sessions.enqueue(request, frame(2)).unwrap();
1256
1257 sessions
1258 .run_local_turn(Instant::now(), |id, submitted, branch| {
1259 let contract = branch.payload_contract(&ingress()).unwrap();
1260 assert_eq!(contract.batch().get(), 2);
1261 assert_eq!(contract.owner().value(), incarnation.value());
1262 assert_eq!(
1263 contract.generation().value(),
1264 branch.history_generation().value()
1265 );
1266 execute_immediately(id, submitted, branch)
1267 })
1268 .unwrap();
1269 }
1270
1271 #[test]
1272 fn committed_batch_rejects_a_different_batch_before_execution() {
1273 let mut sessions = Sessions::new(identity("a"), limits(1)).unwrap();
1274 let request = RequestId::new(3);
1275 sessions.register(request, generation()).unwrap();
1276 sessions.enqueue(request, frame(1)).unwrap();
1277 sessions
1278 .run_local_turn(Instant::now(), execute_immediately)
1279 .unwrap();
1280 assert_eq!(
1281 sessions
1282 .request_state(request)
1283 .unwrap()
1284 .committed_batch()
1285 .unwrap()
1286 .get(),
1287 1
1288 );
1289
1290 sessions.enqueue(request, frame(2)).unwrap();
1291 let calls = Rc::new(Cell::new(0));
1292 let observed = calls.clone();
1293 assert!(sessions
1294 .run_local_turn(Instant::now(), move |id, frame, branch| {
1295 observed.set(observed.get() + 1);
1296 execute_immediately(id, frame, branch)
1297 })
1298 .is_err());
1299 assert_eq!(calls.get(), 0);
1300 assert_eq!(
1301 sessions.request_status(request),
1302 Some(RequestStatus::Failed)
1303 );
1304 }
1305
1306 #[test]
1307 fn release_resume_preserves_incarnation_and_wrong_model_keeps_state_recoverable() {
1308 let request = RequestId::new(4);
1309 let mut first = Sessions::new(identity("a"), limits(1)).unwrap();
1310 let incarnation = first.register(request, generation()).unwrap();
1311 let released = first.release(request).unwrap();
1312 assert_eq!(released.incarnation(), incarnation);
1313
1314 let mut wrong = Sessions::new(identity("b"), limits(1)).unwrap();
1315 let error = wrong.resume(request, released).unwrap_err();
1316 assert!(matches!(
1317 error.reason(),
1318 RealtimeSessionError::ModelIdentityMismatch
1319 ));
1320 let released = error.into_released();
1321 first.resume(request, released).unwrap();
1322 assert_eq!(
1323 first.request_state(request).unwrap().incarnation(),
1324 incarnation
1325 );
1326 }
1327
1328 #[test]
1329 fn queued_work_blocks_sampling_replacement_before_realization() {
1330 let request = RequestId::new(5);
1331 let mut sessions = Sessions::new(identity("a"), limits(1)).unwrap();
1332 sessions.register(request, generation()).unwrap();
1333 sessions.enqueue(request, frame(1)).unwrap();
1334 let realized = Rc::new(Cell::new(false));
1335 let observed = realized.clone();
1336 let result = sessions.replace_sampling(
1337 request,
1338 RealtimeSampling::greedy(),
1339 move |_| -> Result<_, Infallible> {
1340 observed.set(true);
1341 Ok((vec![(), ()], None))
1342 },
1343 );
1344 assert!(matches!(
1345 result,
1346 Err(RealtimeSamplingReplacementError::QueuedWork)
1347 ));
1348 assert!(!realized.get());
1349 }
1350
1351 #[test]
1352 fn cancellation_and_deadline_use_core_terminal_semantics() {
1353 let mut sessions = Sessions::new(identity("a"), limits(1)).unwrap();
1354 let cancelled = RequestId::new(6);
1355 sessions.register(cancelled, generation()).unwrap();
1356 sessions.enqueue(cancelled, frame(1)).unwrap();
1357 sessions.cancel(cancelled).unwrap();
1358 assert_eq!(
1359 sessions.request_status(cancelled),
1360 Some(RequestStatus::Cancelled)
1361 );
1362
1363 let expired = RequestId::new(7);
1364 sessions.register(expired, generation()).unwrap();
1365 sessions
1366 .enqueue_with_deadline(
1367 expired,
1368 frame(1),
1369 Some(Instant::now() - Duration::from_secs(1)),
1370 )
1371 .unwrap();
1372 let calls = Rc::new(Cell::new(0));
1373 let observed = calls.clone();
1374 sessions
1375 .run_local_turn(Instant::now(), move |id, frame, branch| {
1376 observed.set(observed.get() + 1);
1377 execute_immediately(id, frame, branch)
1378 })
1379 .unwrap();
1380 assert_eq!(calls.get(), 0);
1381 assert_eq!(
1382 sessions.request_status(expired),
1383 Some(RequestStatus::DeadlineExceeded)
1384 );
1385 }
1386
1387 #[test]
1388 fn cancelling_submitted_work_retains_it_until_exact_completion() {
1389 let request = RequestId::new(8);
1390 let mut sessions = Sessions::new(identity("a"), limits(1)).unwrap();
1391 sessions.register(request, generation()).unwrap();
1392 sessions.enqueue(request, frame(1)).unwrap();
1393 let completion = Rc::new(Cell::new(false));
1394 let submitted = completion.clone();
1395 sessions
1396 .run_local_turn(Instant::now(), move |_, _, branch| {
1397 let completion = MockCompletion(submitted.clone());
1398 branch
1399 .generation_mut()
1400 .attach_submission_completion(completion.clone())
1401 .unwrap();
1402 Ok::<_, Infallible>(MockOutput { completion })
1403 })
1404 .unwrap();
1405 sessions.cancel(request).unwrap();
1406 assert_eq!(sessions.report().abandoned_in_flight_work, 1);
1407 completion.set(true);
1408 sessions
1409 .run_local_turn(Instant::now(), execute_immediately)
1410 .unwrap();
1411 assert_eq!(sessions.report().abandoned_in_flight_work, 0);
1412 }
1413}