1use std::collections::BTreeMap;
2use std::fmt;
3use std::num::NonZeroU64;
4use std::sync::atomic::{AtomicU64, Ordering};
5use std::sync::{Arc, RwLock};
6
7use tenferro_tensor::{Tensor, TensorValue};
8
9use crate::graph::CompiledGraph;
10use crate::program::FrozenProgram;
11
12use super::cache::{PreparedPlanCacheLimits, RuntimeCacheSet};
13use super::cache_owner::{FrozenCacheOwner, FrozenCacheOwnerKind};
14use super::engine_registration::{CandidateRegistrationToken, EngineRegistrationState};
15use super::execution;
16#[cfg(test)]
17use super::extension::ExtensionSlotFullForTest;
18use super::extension::{
19 bind_candidate_module, configure_module, freeze_extension_slots, BoundCandidateModuleRecord,
20 CandidateModuleRecord, CandidateRegistrationIdentity, ExtensionEngineSnapshotView,
21 ExtensionFamilyId, FrozenExtensionSlots,
22};
23use super::preparation::{PreparedEntryKey, PreparedProgram, PreparedProgramResult};
24use super::schedule::EventDomainId;
25use super::{
26 CacheOwnerId, CoreCapabilityBundle, EngineId, EngineRegistration, ExecutionContextIdentity,
27 ExecutionPolicy, ExtensionModule, ExtensionModuleError, ExtensionModuleId,
28 FrozenTransferRegistry, HardwareClassId, InputSignature, PrepareCapability, PrepareOptions,
29 ProviderDeviceIdentity, RegistrationIdentity, RegistrationKey, ResolvedTransferEndpoint,
30 ResolvedTransferRoute, RuntimeCacheError, RuntimeCacheStats, RuntimeConfigError, RuntimeEpoch,
31 RuntimeId, RuntimeReconfigureError, RuntimeStateError, StorageClass, TransferEndpoint,
32 TransferProvider, TransferRoute,
33};
34use crate::{Error, ErrorPhase};
35
36static NEXT_RUNTIME_ID: AtomicU64 = AtomicU64::new(1);
37static NEXT_REGISTRATION_ISSUER: AtomicU64 = AtomicU64::new(1);
38const INITIAL_REGISTRATION_ORDINAL: NonZeroU64 = NonZeroU64::MIN;
39
40#[derive(Clone, Debug)]
41struct CandidateEngineRecord {
42 registration: EngineRegistration,
43 identity: CandidateRegistrationIdentity,
44}
45
46#[derive(Clone, Debug)]
47struct BoundCandidateEngineRecord {
48 registration: EngineRegistration,
49 identity: RegistrationIdentity,
50}
51
52#[derive(Clone, Debug)]
53enum CandidateTransferBinding {
54 New,
56 Preserved {
58 source: ProviderDeviceIdentity,
59 destination: ProviderDeviceIdentity,
60 },
61}
62
63#[derive(Clone, Debug)]
64struct CandidateTransferRecord {
65 provider: Arc<dyn TransferProvider>,
66 binding: CandidateTransferBinding,
67}
68
69struct BoundCandidateTransferRecord {
70 provider: Arc<dyn TransferProvider>,
71 source: ProviderDeviceIdentity,
72 destination: ProviderDeviceIdentity,
73}
74
75#[derive(Clone, Debug)]
76struct CandidateConfig {
77 policy: ExecutionPolicy,
78 engines: BTreeMap<EngineId, CandidateEngineRecord>,
79 modules: BTreeMap<ExtensionModuleId, CandidateModuleRecord>,
80 transfers: BTreeMap<TransferRoute, CandidateTransferRecord>,
81}
82
83struct BoundCandidateConfig {
84 policy: ExecutionPolicy,
85 engines: BTreeMap<EngineId, BoundCandidateEngineRecord>,
86 modules: BTreeMap<ExtensionModuleId, BoundCandidateModuleRecord>,
87 transfers: BTreeMap<TransferRoute, BoundCandidateTransferRecord>,
88}
89
90impl CandidateConfig {
91 fn empty() -> Self {
92 Self {
93 policy: default_execution_policy(),
94 engines: BTreeMap::new(),
95 modules: BTreeMap::new(),
96 transfers: BTreeMap::new(),
97 }
98 }
99
100 fn from_snapshot(snapshot: &RuntimeConfigSnapshot) -> Result<Self, RuntimeConfigError> {
101 let engines = snapshot
102 .engines
103 .iter()
104 .map(|slot| {
105 let registration = slot.to_registration()?;
106 Ok((
107 registration.engine_id().clone(),
108 CandidateEngineRecord {
109 registration,
110 identity: CandidateRegistrationIdentity::Preserved(
111 slot.metadata().identity,
112 ),
113 },
114 ))
115 })
116 .collect::<Result<BTreeMap<_, _>, RuntimeConfigError>>()?;
117 Ok(Self {
118 policy: snapshot.policy.clone(),
119 engines,
120 modules: snapshot.extensions.to_candidate_modules(),
121 transfers: snapshot
122 .transfers
123 .iter()
124 .map(|(resolved_route, provider)| {
125 (
126 TransferRoute::new(
127 resolved_route.source().logical().clone(),
128 resolved_route.destination().logical().clone(),
129 ),
130 CandidateTransferRecord {
131 provider: Arc::clone(provider),
132 binding: CandidateTransferBinding::Preserved {
133 source: resolved_route.source().provider_device_identity().clone(),
134 destination: resolved_route
135 .destination()
136 .provider_device_identity()
137 .clone(),
138 },
139 },
140 )
141 })
142 .collect(),
143 })
144 }
145}
146
147#[derive(Clone, Debug)]
148struct FrozenEngineMetadata {
149 candidate_token: Arc<CandidateRegistrationToken>,
150 identity: RegistrationIdentity,
151 event_domain_id: EventDomainId,
152}
153
154#[derive(Clone)]
155struct PreparationOnlyEngineSnapshot {
156 metadata: FrozenEngineMetadata,
157 binding: super::ProviderPreparationBinding,
158}
159
160#[derive(Clone, Debug)]
161pub(super) struct ExecutableEngineSnapshot {
162 metadata: FrozenEngineMetadata,
163 binding: super::ProviderExecutableBinding,
164}
165
166#[derive(Clone)]
167enum FrozenEngineSlot {
168 PreparationOnly(Arc<PreparationOnlyEngineSnapshot>),
169 Executable(Arc<ExecutableEngineSnapshot>),
170}
171
172impl FrozenEngineSlot {
173 fn metadata(&self) -> &FrozenEngineMetadata {
174 match self {
175 Self::PreparationOnly(snapshot) => &snapshot.metadata,
176 Self::Executable(snapshot) => &snapshot.metadata,
177 }
178 }
179
180 fn provider_device_identity(&self) -> &ProviderDeviceIdentity {
181 match self {
182 Self::PreparationOnly(snapshot) => snapshot.binding.provider_device_identity(),
183 Self::Executable(snapshot) => snapshot.binding.contract().provider_device_identity(),
184 }
185 }
186
187 fn engine_id(&self) -> &EngineId {
188 match self {
189 Self::PreparationOnly(snapshot) => snapshot.binding.engine_id(),
190 Self::Executable(snapshot) => snapshot.binding.engine_id(),
191 }
192 }
193
194 fn hardware_class(&self) -> &HardwareClassId {
195 match self {
196 Self::PreparationOnly(snapshot) => snapshot.binding.hardware_class(),
197 Self::Executable(snapshot) => snapshot.binding.hardware_class(),
198 }
199 }
200
201 fn storage_classes(&self) -> &[StorageClass] {
202 match self {
203 Self::PreparationOnly(snapshot) => snapshot.binding.storage_classes(),
204 Self::Executable(snapshot) => snapshot.binding.storage_classes(),
205 }
206 }
207
208 fn default_storage_class(&self) -> &StorageClass {
209 match self {
210 Self::PreparationOnly(snapshot) => snapshot.binding.default_storage_class(),
211 Self::Executable(snapshot) => snapshot.binding.default_storage_class(),
212 }
213 }
214
215 fn context_identity(&self) -> ExecutionContextIdentity {
216 match self {
217 Self::PreparationOnly(snapshot) => snapshot.binding.context_identity(),
218 Self::Executable(snapshot) => snapshot.binding.contract().context_identity(),
219 }
220 }
221
222 fn capabilities(&self) -> &CoreCapabilityBundle {
223 match self {
224 Self::PreparationOnly(snapshot) => snapshot.binding.capabilities(),
225 Self::Executable(snapshot) => snapshot.binding.contract().capabilities(),
226 }
227 }
228
229 fn executable(&self) -> Option<&Arc<ExecutableEngineSnapshot>> {
230 match self {
231 Self::PreparationOnly(_) => None,
232 Self::Executable(snapshot) => Some(snapshot),
233 }
234 }
235}
236
237impl ExecutableEngineSnapshot {
238 pub(super) fn engine_id(&self) -> &EngineId {
239 self.binding.engine_id()
240 }
241
242 pub(super) fn event_domain_id(&self) -> EventDomainId {
243 self.metadata.event_domain_id
244 }
245
246 pub(super) fn provider_device_identity(&self) -> &ProviderDeviceIdentity {
247 self.binding.contract().provider_device_identity()
248 }
249
250 #[cfg(test)]
251 pub(super) fn context_identity(&self) -> ExecutionContextIdentity {
252 self.binding.contract().context_identity()
253 }
254
255 pub(super) fn max_fused_region_inputs(&self) -> Option<usize> {
258 self.binding
259 .contract()
260 .capabilities()
261 .elementwise()
262 .and_then(|elementwise| elementwise.max_fused_region_inputs())
263 }
264
265 pub(super) fn executor(&self) -> &Arc<dyn super::execution::ErasedTensorBackendExecutor> {
266 self.binding.contract().executor()
267 }
268
269 pub(super) fn event_domain_driver(&self) -> &Arc<dyn super::EventDomainDriver> {
270 self.binding.contract().event_domain_driver()
271 }
272
273 #[cfg(test)]
274 pub(super) fn has_executor(&self) -> bool {
275 true
276 }
277
278 #[cfg(test)]
279 pub(super) fn has_event_domain_driver(&self) -> bool {
280 true
281 }
282
283 pub(super) fn accepts_input_placement(
284 &self,
285 placement: &tenferro_tensor::Placement,
286 storage_class: &StorageClass,
287 ) -> bool {
288 self.binding.storage_classes().contains(storage_class)
289 && self
290 .binding
291 .contract()
292 .accepts_input_placement(placement, storage_class)
293 }
294
295 pub(super) fn accepts_input_signature(
296 &self,
297 input: &super::InputSignatureEntry,
298 storage_class: &StorageClass,
299 ) -> bool {
300 self.binding.storage_classes().contains(storage_class)
301 && self
302 .binding
303 .contract()
304 .accepts_input_signature(input, storage_class)
305 }
306
307 pub(super) fn accepts_runtime_input(
308 &self,
309 input: &tenferro_tensor::TensorRead<'_>,
310 storage_class: &StorageClass,
311 ) -> bool {
312 self.binding.storage_classes().contains(storage_class)
313 && self
314 .binding
315 .contract()
316 .accepts_runtime_input(input, storage_class)
317 }
318
319 pub(super) fn owns_resident_tensor(
320 &self,
321 input: &tenferro_tensor::TensorRead<'_>,
322 storage_class: &StorageClass,
323 ) -> bool {
324 self.binding.storage_classes().contains(storage_class)
325 && self
326 .binding
327 .contract()
328 .owns_resident_tensor(input, storage_class)
329 }
330
331 #[cfg(test)]
332 pub(super) fn for_test(
333 engine_id: EngineId,
334 provider_device_identity: ProviderDeviceIdentity,
335 event_domain_id: EventDomainId,
336 storage_class: StorageClass,
337 ) -> Arc<Self> {
338 Self::for_test_with_driver(
339 engine_id,
340 provider_device_identity,
341 event_domain_id,
342 storage_class,
343 Arc::new(super::ImmediateEventDomainDriver::new()),
344 )
345 }
346
347 #[cfg(test)]
348 pub(super) fn for_test_with_driver(
349 engine_id: EngineId,
350 provider_device_identity: ProviderDeviceIdentity,
351 event_domain_id: EventDomainId,
352 storage_class: StorageClass,
353 event_domain_driver: Arc<dyn super::EventDomainDriver>,
354 ) -> Arc<Self> {
355 let ingress = super::InputIngressContract::new(
356 super::InputPlacementContract::new(|_, _| true),
357 super::InputSignatureContract::new(|_, _, _, _| true),
358 super::RuntimeInputContract::new(|_, _| true),
359 super::ResidentOutputContract::new(|_, _| true),
360 );
361 let contract = super::ExecutableEngineContract::new(
362 provider_device_identity,
363 CoreCapabilityBundle::default(),
364 tenferro_cpu::CpuBackend::new(),
365 event_domain_driver,
366 ingress,
367 None,
368 );
369 let binding = super::ProviderExecutableBinding::new(
370 engine_id,
371 HardwareClassId::new("tenferro.test.schedule.hardware").expect("test hardware class"),
372 Arc::from(vec![storage_class.clone()]),
373 storage_class,
374 contract,
375 )
376 .expect("test executable binding");
377 Arc::new(Self {
378 metadata: FrozenEngineMetadata {
379 candidate_token: Arc::new(CandidateRegistrationToken),
380 identity: event_domain_id.registration_identity(),
381 event_domain_id,
382 },
383 binding,
384 })
385 }
386}
387
388impl FrozenEngineSlot {
389 fn to_registration(&self) -> Result<EngineRegistration, RuntimeConfigError> {
390 let metadata = self.metadata();
391 let registration = match self {
392 Self::PreparationOnly(snapshot) => {
393 EngineRegistration::from_state(EngineRegistrationState::PreparationOnly {
394 binding: snapshot.binding.clone(),
395 })
396 }
397 Self::Executable(snapshot) => EngineRegistration::from_state(
398 EngineRegistrationState::Executable(snapshot.binding.clone()),
399 ),
400 };
401 Ok(registration.with_candidate_token(Arc::clone(&metadata.candidate_token)))
402 }
403}
404
405impl fmt::Debug for FrozenEngineSlot {
406 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
407 let metadata = self.metadata();
408 formatter
409 .debug_struct("FrozenEngineSlot")
410 .field("engine_id", self.engine_id())
411 .field("registration_identity", &metadata.identity)
412 .field("event_domain_id", &metadata.event_domain_id)
413 .field("context_identity", &self.context_identity())
414 .field("hardware_class", self.hardware_class())
415 .field(
416 "state",
417 &match self {
418 Self::PreparationOnly(_) => "preparation-only",
419 Self::Executable(_) => "executable",
420 },
421 )
422 .finish_non_exhaustive()
423 }
424}
425
426#[derive(Clone)]
444pub struct RuntimeConfigSnapshot {
445 runtime_id: RuntimeId,
446 epoch: RuntimeEpoch,
447 policy: ExecutionPolicy,
448 engines: Arc<[FrozenEngineSlot]>,
449 engine_indices: BTreeMap<EngineId, usize>,
450 extensions: FrozenExtensionSlots,
451 transfers: FrozenTransferRegistry,
452 cache_owners: Arc<[FrozenCacheOwner]>,
453}
454
455impl RuntimeConfigSnapshot {
456 pub fn runtime_id(&self) -> RuntimeId {
458 self.runtime_id
459 }
460
461 pub fn epoch(&self) -> RuntimeEpoch {
463 self.epoch
464 }
465
466 pub fn execution_policy(&self) -> &ExecutionPolicy {
468 &self.policy
469 }
470
471 pub fn engine_count(&self) -> usize {
473 self.engines.len()
474 }
475
476 pub fn extension_module_count(&self) -> usize {
478 self.extensions.module_count()
479 }
480
481 pub fn transfer_provider_count(&self) -> usize {
483 self.transfers.len()
484 }
485
486 #[doc(hidden)]
488 pub fn has_extension_family(&self, family_id: &'static str) -> bool {
489 self.extensions.has_family(family_id)
490 }
491
492 #[doc(hidden)]
511 pub fn has_extension_engine(&self, family_id: &'static str, engine_id: &EngineId) -> bool {
512 self.extensions.has_engine(family_id, engine_id)
513 }
514
515 #[doc(hidden)]
522 pub fn has_extension_module_identical(&self, module: &Arc<dyn ExtensionModule>) -> bool {
523 self.extensions.has_module_identical(module)
524 }
525
526 #[doc(hidden)]
534 pub fn has_extension_module_engine(
535 &self,
536 module_id: &ExtensionModuleId,
537 family_id: &'static str,
538 engine_id: &EngineId,
539 ) -> bool {
540 self.extensions
541 .has_module_engine(module_id, family_id, engine_id)
542 }
543
544 pub fn engine(&self, id: &EngineId) -> Option<EngineSnapshotView<'_>> {
546 self.engine_indices
547 .get(id)
548 .map(|&index| EngineSnapshotView {
549 slot: &self.engines[index],
550 })
551 }
552
553 #[cfg(test)]
554 pub(crate) fn engine_ids_for_test(&self) -> impl Iterator<Item = &EngineId> {
555 self.engines.iter().map(FrozenEngineSlot::engine_id)
556 }
557
558 #[cfg(test)]
559 pub(crate) fn transfer_routes_for_test(&self) -> impl Iterator<Item = &ResolvedTransferRoute> {
560 self.transfers.iter().map(|(route, _)| route)
561 }
562
563 pub(super) fn engine_views_for_preparation(
564 &self,
565 ) -> impl Iterator<Item = EngineSnapshotView<'_>> + '_ {
566 self.engines.iter().map(|slot| EngineSnapshotView { slot })
567 }
568
569 pub(super) fn extension_slot_for_preparation(
570 &self,
571 family_id: ExtensionFamilyId,
572 engine_id: &EngineId,
573 ) -> Option<ExtensionEngineSnapshotView<'_>> {
574 self.extensions.slot_for_preparation(family_id, engine_id)
575 }
576
577 pub(super) fn transfer_registry_for_preparation(&self) -> FrozenTransferRegistry {
578 self.transfers.clone()
579 }
580
581 #[cfg(test)]
582 pub(crate) fn extension_slots_for_test(
583 &self,
584 ) -> impl Iterator<
585 Item = (
586 &ExtensionModuleId,
587 ExtensionFamilyId,
588 &EngineId,
589 RegistrationIdentity,
590 ),
591 > {
592 self.extensions.slots_for_test()
593 }
594
595 #[cfg(test)]
596 pub(crate) fn extension_slot_identity_for_test(
597 &self,
598 family_id: ExtensionFamilyId,
599 engine_id: &EngineId,
600 ) -> Option<RegistrationIdentity> {
601 self.extensions.slot_identity_for_test(family_id, engine_id)
602 }
603
604 #[cfg(test)]
605 pub(crate) fn extension_slot_full_for_test(
606 &self,
607 family_id: ExtensionFamilyId,
608 engine_id: &EngineId,
609 ) -> Option<ExtensionSlotFullForTest<'_>> {
610 self.extensions.slot_full_for_test(family_id, engine_id)
611 }
612
613 #[cfg(test)]
614 pub(super) fn cache_owners_for_test(&self) -> &[FrozenCacheOwner] {
615 &self.cache_owners
616 }
617
618 pub(super) fn cache_owners_for_runtime(&self) -> &[FrozenCacheOwner] {
619 &self.cache_owners
620 }
621}
622
623impl fmt::Debug for RuntimeConfigSnapshot {
624 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
625 formatter
626 .debug_struct("RuntimeConfigSnapshot")
627 .field("runtime_id", &self.runtime_id)
628 .field("epoch", &self.epoch)
629 .field("execution_policy", &self.policy)
630 .field("engine_count", &self.engines.len())
631 .field("extension_module_count", &self.extensions.module_count())
632 .field("extension_engine_count", &self.extensions.engine_count())
633 .field("transfer_provider_count", &self.transfers.len())
634 .field("cache_owner_count", &self.cache_owners.len())
635 .finish_non_exhaustive()
636 }
637}
638
639struct RuntimeState {
640 runtime_id: RuntimeId,
641 issuer: NonZeroU64,
642 next_registration_ordinal: AtomicU64,
643 active: RwLock<Arc<RuntimeConfigSnapshot>>,
644 published_epoch: AtomicU64,
645 caches: RuntimeCacheSet<PreparedEntryKey, PreparedProgram>,
646}
647
648#[derive(Clone)]
662pub struct Runtime(Arc<RuntimeState>);
663
664impl Runtime {
665 pub fn builder() -> RuntimeConfigBuilder {
667 RuntimeConfigBuilder::new()
668 }
669
670 pub fn id(&self) -> RuntimeId {
672 self.0.runtime_id
673 }
674
675 pub fn snapshot(&self) -> Result<Arc<RuntimeConfigSnapshot>, RuntimeStateError> {
682 self.0
683 .active
684 .read()
685 .map(|snapshot| Arc::clone(&snapshot))
686 .map_err(|_| RuntimeStateError::Poisoned {
687 lock: "runtime.active",
688 })
689 }
690
691 pub fn epoch(&self) -> Result<RuntimeEpoch, RuntimeStateError> {
699 match NonZeroU64::new(self.0.published_epoch.load(Ordering::Acquire)) {
700 Some(value) => Ok(RuntimeEpoch::from_nonzero(value)),
701 None => Err(RuntimeStateError::Poisoned {
702 lock: "runtime.published_epoch",
703 }),
704 }
705 }
706
707 pub fn prepared_cache_limits(&self) -> Result<PreparedPlanCacheLimits, RuntimeStateError> {
726 self.0.caches.prepared().limits()
727 }
728
729 pub fn set_prepared_cache_limits(
756 &self,
757 limits: PreparedPlanCacheLimits,
758 ) -> Result<(), RuntimeStateError> {
759 self.0.caches.prepared().set_limits(limits)
760 }
761
762 pub fn clear_prepared_cache(&self) -> Result<(), RuntimeStateError> {
781 self.0.caches.prepared().clear()
782 }
783
784 pub fn cache_stats(&self) -> Result<RuntimeCacheStats, RuntimeCacheError> {
804 super::preparation::cache_stats(self, &self.0.caches)
805 }
806
807 pub fn clear_caches(&self) -> Result<(), RuntimeCacheError> {
827 super::preparation::clear_caches(self, &self.0.caches)
828 }
829
830 #[allow(
831 dead_code,
832 reason = "Phase 5 graph execution consumes crate-private prepared programs"
833 )]
834 pub(crate) fn prepare_for(
835 &self,
836 frozen: &FrozenProgram,
837 signature: &InputSignature,
838 options: &PrepareOptions,
839 ) -> PreparedProgramResult<Arc<PreparedProgram>> {
840 super::preparation::prepare_for(self, &self.0.caches, frozen, signature, options)
841 }
842
843 pub(crate) fn prepare_compiled_for(
844 &self,
845 program: &CompiledGraph,
846 signature: &InputSignature,
847 options: &PrepareOptions,
848 ) -> PreparedProgramResult<Arc<PreparedProgram>> {
849 super::preparation::prepare_compiled_for(self, &self.0.caches, program, signature, options)
850 }
851
852 pub fn prepare_extension_immediate(
957 &self,
958 engine_id: &EngineId,
959 op: &dyn tenferro_ops::ext_op::ExtensionOp,
960 signature: &InputSignature,
961 ) -> crate::Result<PrepareCapability> {
962 super::preparation::prepare_extension_immediate(self, engine_id, op, signature).map_err(
963 |source| {
964 Error::runtime_state_source(
965 "Runtime::prepare_extension_immediate",
966 ErrorPhase::Execution,
967 source,
968 )
969 },
970 )
971 }
972
973 pub fn run_compiled(
1015 &self,
1016 program: &CompiledGraph,
1017 inputs: &[&Tensor],
1018 ) -> crate::Result<Vec<Tensor>> {
1019 super::execution::run_compiled(self, program, inputs)
1020 }
1021
1022 pub fn execute_scoped_read_only<'env>(
1036 &self,
1037 program: &CompiledGraph,
1038 inputs: super::execution::ScopedReadInputs<'env>,
1039 ) -> std::result::Result<
1040 super::execution::ScopedExecutionOutcome<'env>,
1041 super::execution::ScopedSubmitRejected<'env>,
1042 > {
1043 super::execution::execute_scoped_read_only(self, program, inputs)
1044 }
1045
1046 pub fn prepare_compiled(
1071 &self,
1072 program: &CompiledGraph,
1073 inputs: &[&Tensor],
1074 ) -> crate::Result<super::execution::PreparedCompiledGraph> {
1075 super::execution::prepare_compiled(self, program, inputs)
1076 }
1077
1078 pub fn run_prepared(
1088 &self,
1089 prepared: &super::execution::PreparedCompiledGraph,
1090 inputs: &[&Tensor],
1091 ) -> crate::Result<Vec<Tensor>> {
1092 super::execution::run_prepared(self, prepared, inputs)
1093 }
1094
1095 pub fn submit(
1111 &self,
1112 program: &CompiledGraph,
1113 inputs: super::execution::ExecutionInputs,
1114 ) -> std::result::Result<super::execution::ExecutionHandle, super::execution::SubmitError> {
1115 super::execution::submit(self, program, inputs)
1116 }
1117
1118 pub fn run_compiled_values(
1156 &self,
1157 program: &CompiledGraph,
1158 inputs: &[&Tensor],
1159 ) -> crate::Result<Vec<TensorValue>> {
1160 super::execution::run_compiled_values(self, program, inputs)
1161 }
1162
1163 pub fn reconfigure(
1179 &self,
1180 edit: impl FnOnce(&mut RuntimeReconfiguration<'_>) -> Result<(), RuntimeConfigError>,
1181 ) -> Result<RuntimeEpoch, RuntimeReconfigureError> {
1182 let base = self
1183 .snapshot()
1184 .map_err(|source| RuntimeReconfigureError::State { source })?;
1185 let mut candidate = CandidateConfig::from_snapshot(&base)
1186 .map_err(|source| RuntimeReconfigureError::Edit { source })?;
1187 let mut changed = false;
1188 {
1189 let mut reconfiguration = RuntimeReconfiguration {
1190 candidate: &mut candidate,
1191 changed: &mut changed,
1192 };
1193 edit(&mut reconfiguration)
1194 .map_err(|source| RuntimeReconfigureError::Edit { source })?;
1195 }
1196
1197 if !changed {
1198 return Ok(base.epoch());
1199 }
1200 let next_identity_ordinal = NonZeroU64::new(
1201 self.0.next_registration_ordinal.load(Ordering::SeqCst),
1202 )
1203 .ok_or(RuntimeReconfigureError::Edit {
1204 source: RuntimeConfigError::IdentityExhausted,
1205 })?;
1206 let (bound_candidate, post_ordinal) =
1207 validate_candidate(candidate, self.0.issuer, next_identity_ordinal)
1208 .map_err(|source| RuntimeReconfigureError::Edit { source })?;
1209
1210 let next_epoch =
1211 base.epoch()
1212 .checked_next()
1213 .ok_or(RuntimeReconfigureError::EpochExhausted {
1214 current: base.epoch(),
1215 })?;
1216
1217 let mut guard = self
1218 .0
1219 .active
1220 .write()
1221 .map_err(|_| RuntimeReconfigureError::State {
1222 source: RuntimeStateError::Poisoned {
1223 lock: "runtime.active",
1224 },
1225 })?;
1226 if !Arc::ptr_eq(&*guard, &base) {
1227 return Err(RuntimeReconfigureError::ConcurrentReconfiguration {
1228 base: base.epoch(),
1229 current: guard.epoch(),
1230 });
1231 }
1232
1233 let next_snapshot = Arc::new(
1234 freeze_candidate(self.0.runtime_id, next_epoch, bound_candidate)
1235 .map_err(|source| RuntimeReconfigureError::Edit { source })?,
1236 );
1237
1238 self.0
1239 .next_registration_ordinal
1240 .store(post_ordinal.get(), Ordering::SeqCst);
1241 *guard = next_snapshot;
1242 self.0
1243 .published_epoch
1244 .store(next_epoch.get().get(), Ordering::Release);
1245 Ok(next_epoch)
1246 }
1247
1248 #[cfg(test)]
1249 pub(crate) fn force_epoch_for_test(&self, epoch: RuntimeEpoch) {
1250 let mut guard = self.0.active.write().expect("test runtime lock");
1251 let mut replacement = (**guard).clone();
1252 replacement.epoch = epoch;
1253 *guard = Arc::new(replacement);
1254 self.0
1255 .published_epoch
1256 .store(epoch.get().get(), Ordering::Release);
1257 }
1258
1259 #[cfg(test)]
1260 pub(crate) fn force_next_registration_ordinal_for_test(&self, next: NonZeroU64) {
1261 self.0
1262 .next_registration_ordinal
1263 .store(next.get(), Ordering::SeqCst);
1264 }
1265
1266 #[cfg(test)]
1267 pub(crate) fn poison_active_lock_for_test(&self) {
1268 let state = Arc::clone(&self.0);
1269 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(move || {
1270 let _guard = state.active.write().expect("test runtime lock");
1271 panic!("poison runtime.active for test");
1272 }));
1273 }
1274}
1275
1276impl fmt::Debug for Runtime {
1277 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1278 formatter
1279 .debug_struct("Runtime")
1280 .field("runtime_id", &self.0.runtime_id)
1281 .field("published_epoch", &self.epoch().ok())
1282 .finish_non_exhaustive()
1283 }
1284}
1285
1286pub struct RuntimeConfigBuilder {
1300 candidate: CandidateConfig,
1301}
1302
1303impl RuntimeConfigBuilder {
1304 pub fn new() -> Self {
1306 Self {
1307 candidate: CandidateConfig::empty(),
1308 }
1309 }
1310
1311 pub fn execution_policy(&mut self, value: ExecutionPolicy) -> &mut Self {
1313 self.candidate.policy = value;
1314 self
1315 }
1316
1317 pub fn register_engine(
1324 &mut self,
1325 value: EngineRegistration,
1326 ) -> Result<&mut Self, RuntimeConfigError> {
1327 let mut changed = false;
1328 register_engine_candidate(&mut self.candidate, value, &mut changed)?;
1329 Ok(self)
1330 }
1331
1332 pub fn replace_engine(
1338 &mut self,
1339 value: EngineRegistration,
1340 ) -> Result<&mut Self, RuntimeConfigError> {
1341 let mut changed = false;
1342 replace_engine_candidate(&mut self.candidate, value, &mut changed)?;
1343 Ok(self)
1344 }
1345
1346 pub fn remove_engine(&mut self, id: &EngineId) -> Result<&mut Self, RuntimeConfigError> {
1352 let mut changed = false;
1353 remove_engine_candidate(&mut self.candidate, id, &mut changed)?;
1354 Ok(self)
1355 }
1356
1357 pub fn install_extension_module(
1364 &mut self,
1365 value: Arc<dyn ExtensionModule>,
1366 ) -> Result<&mut Self, RuntimeConfigError> {
1367 let mut changed = false;
1368 install_extension_module_candidate(&mut self.candidate, value, &mut changed)?;
1369 Ok(self)
1370 }
1371
1372 pub fn register_transfer_provider(
1452 &mut self,
1453 source: TransferEndpoint,
1454 destination: TransferEndpoint,
1455 provider: Arc<dyn TransferProvider>,
1456 ) -> Result<&mut Self, RuntimeConfigError> {
1457 let mut changed = false;
1458 register_transfer_provider_candidate(
1459 &mut self.candidate,
1460 source,
1461 destination,
1462 provider,
1463 &mut changed,
1464 )?;
1465 Ok(self)
1466 }
1467
1468 pub fn remove_transfer_provider(
1479 &mut self,
1480 source: TransferEndpoint,
1481 destination: TransferEndpoint,
1482 ) -> Result<&mut Self, RuntimeConfigError> {
1483 let mut changed = false;
1484 remove_transfer_provider_candidate(&mut self.candidate, source, destination, &mut changed)?;
1485 Ok(self)
1486 }
1487
1488 pub fn replace_extension_module(
1495 &mut self,
1496 value: Arc<dyn ExtensionModule>,
1497 ) -> Result<&mut Self, RuntimeConfigError> {
1498 let mut changed = false;
1499 replace_extension_module_candidate(&mut self.candidate, value, &mut changed)?;
1500 Ok(self)
1501 }
1502
1503 pub fn remove_extension_module(
1510 &mut self,
1511 id: &ExtensionModuleId,
1512 ) -> Result<&mut Self, RuntimeConfigError> {
1513 let mut changed = false;
1514 remove_extension_module_candidate(&mut self.candidate, id, &mut changed)?;
1515 Ok(self)
1516 }
1517
1518 pub fn build(self) -> Result<Runtime, RuntimeConfigError> {
1528 let runtime_id = RuntimeId::from_nonzero(allocate_nonzero(&NEXT_RUNTIME_ID)?);
1529 let issuer = allocate_nonzero(&NEXT_REGISTRATION_ISSUER)?;
1530 let (bound_candidate, post_ordinal) =
1531 validate_candidate(self.candidate, issuer, INITIAL_REGISTRATION_ORDINAL)?;
1532 let epoch = RuntimeEpoch::one();
1533 let snapshot = Arc::new(freeze_candidate(runtime_id, epoch, bound_candidate)?);
1534 let state = RuntimeState {
1535 runtime_id,
1536 issuer,
1537 next_registration_ordinal: AtomicU64::new(post_ordinal.get()),
1538 active: RwLock::new(snapshot),
1539 published_epoch: AtomicU64::new(epoch.get().get()),
1540 caches: RuntimeCacheSet::new(PreparedPlanCacheLimits::default()),
1541 };
1542 Ok(Runtime(Arc::new(state)))
1543 }
1544}
1545
1546impl Default for RuntimeConfigBuilder {
1547 fn default() -> Self {
1548 Self::new()
1549 }
1550}
1551
1552impl fmt::Debug for RuntimeConfigBuilder {
1553 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1554 formatter
1555 .debug_struct("RuntimeConfigBuilder")
1556 .field("execution_policy", &self.candidate.policy)
1557 .field("engine_count", &self.candidate.engines.len())
1558 .field("extension_module_count", &self.candidate.modules.len())
1559 .field("transfer_provider_count", &self.candidate.transfers.len())
1560 .finish_non_exhaustive()
1561 }
1562}
1563
1564pub struct RuntimeReconfiguration<'a> {
1566 candidate: &'a mut CandidateConfig,
1567 changed: &'a mut bool,
1568}
1569
1570impl RuntimeReconfiguration<'_> {
1571 pub fn execution_policy(&mut self, policy: ExecutionPolicy) -> &mut Self {
1573 if self.candidate.policy != policy {
1574 self.candidate.policy = policy;
1575 *self.changed = true;
1576 }
1577 self
1578 }
1579
1580 pub fn register_engine(
1587 &mut self,
1588 value: EngineRegistration,
1589 ) -> Result<&mut Self, RuntimeConfigError> {
1590 register_engine_candidate(self.candidate, value, self.changed)?;
1591 Ok(self)
1592 }
1593
1594 pub fn replace_engine(
1600 &mut self,
1601 value: EngineRegistration,
1602 ) -> Result<&mut Self, RuntimeConfigError> {
1603 replace_engine_candidate(self.candidate, value, self.changed)?;
1604 Ok(self)
1605 }
1606
1607 pub fn remove_engine(&mut self, id: &EngineId) -> Result<&mut Self, RuntimeConfigError> {
1613 remove_engine_candidate(self.candidate, id, self.changed)?;
1614 Ok(self)
1615 }
1616
1617 pub fn install_extension_module(
1624 &mut self,
1625 value: Arc<dyn ExtensionModule>,
1626 ) -> Result<&mut Self, RuntimeConfigError> {
1627 install_extension_module_candidate(self.candidate, value, self.changed)?;
1628 Ok(self)
1629 }
1630
1631 pub fn register_transfer_provider(
1706 &mut self,
1707 source: TransferEndpoint,
1708 destination: TransferEndpoint,
1709 provider: Arc<dyn TransferProvider>,
1710 ) -> Result<&mut Self, RuntimeConfigError> {
1711 register_transfer_provider_candidate(
1712 self.candidate,
1713 source,
1714 destination,
1715 provider,
1716 self.changed,
1717 )?;
1718 Ok(self)
1719 }
1720
1721 pub fn remove_transfer_provider(
1729 &mut self,
1730 source: TransferEndpoint,
1731 destination: TransferEndpoint,
1732 ) -> Result<&mut Self, RuntimeConfigError> {
1733 remove_transfer_provider_candidate(self.candidate, source, destination, self.changed)?;
1734 Ok(self)
1735 }
1736
1737 pub fn replace_extension_module(
1745 &mut self,
1746 value: Arc<dyn ExtensionModule>,
1747 ) -> Result<&mut Self, RuntimeConfigError> {
1748 replace_extension_module_candidate(self.candidate, value, self.changed)?;
1749 Ok(self)
1750 }
1751
1752 #[doc(hidden)]
1815 pub fn ensure_extension_module_for_engine(
1816 &mut self,
1817 value: Arc<dyn ExtensionModule>,
1818 family_id: &'static str,
1819 engine_id: &EngineId,
1820 ) -> Result<&mut Self, RuntimeConfigError> {
1821 let module_id = value.module_id().clone();
1822 if let Some(existing) = self.candidate.modules.get(&module_id)
1823 && existing
1824 .engines
1825 .contains_key(&(family_id, engine_id.clone()))
1826 {
1827 return Ok(self);
1828 }
1829
1830 let record = configure_module(Arc::clone(&value))
1831 .map_err(|source| RuntimeConfigError::ExtensionModule { source })?;
1832 if !record.engines.contains_key(&(family_id, engine_id.clone())) {
1833 return Err(RuntimeConfigError::MissingExtensionEngine {
1834 module_id,
1835 family_id,
1836 engine_id: engine_id.clone(),
1837 });
1838 }
1839 self.candidate.modules.insert(module_id, record);
1840 *self.changed = true;
1841 Ok(self)
1842 }
1843
1844 #[doc(hidden)]
1904 pub fn replace_extension_module_for_engine(
1905 &mut self,
1906 value: Arc<dyn ExtensionModule>,
1907 family_id: &'static str,
1908 engine_id: &EngineId,
1909 ) -> Result<&mut Self, RuntimeConfigError> {
1910 let module_id = value.module_id().clone();
1911 if let Some(existing) = self.candidate.modules.get(&module_id)
1912 && existing.module_identical(&value)
1913 {
1914 if existing
1915 .engines
1916 .contains_key(&(family_id, engine_id.clone()))
1917 {
1918 return Ok(self);
1919 }
1920 return Err(RuntimeConfigError::MissingExtensionEngine {
1921 module_id,
1922 family_id,
1923 engine_id: engine_id.clone(),
1924 });
1925 }
1926
1927 let record = configure_module(Arc::clone(&value))
1928 .map_err(|source| RuntimeConfigError::ExtensionModule { source })?;
1929 if !record.engines.contains_key(&(family_id, engine_id.clone())) {
1930 return Err(RuntimeConfigError::MissingExtensionEngine {
1931 module_id,
1932 family_id,
1933 engine_id: engine_id.clone(),
1934 });
1935 }
1936 self.candidate.modules.insert(module_id, record);
1937 *self.changed = true;
1938 Ok(self)
1939 }
1940
1941 pub fn remove_extension_module(
1948 &mut self,
1949 id: &ExtensionModuleId,
1950 ) -> Result<&mut Self, RuntimeConfigError> {
1951 remove_extension_module_candidate(self.candidate, id, self.changed)?;
1952 Ok(self)
1953 }
1954}
1955
1956impl fmt::Debug for RuntimeReconfiguration<'_> {
1957 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1958 formatter
1959 .debug_struct("RuntimeReconfiguration")
1960 .field("engine_count", &self.candidate.engines.len())
1961 .field("extension_module_count", &self.candidate.modules.len())
1962 .field("transfer_provider_count", &self.candidate.transfers.len())
1963 .field("changed", &*self.changed)
1964 .finish_non_exhaustive()
1965 }
1966}
1967
1968#[derive(Clone, Copy)]
1980pub struct EngineSnapshotView<'a> {
1981 slot: &'a FrozenEngineSlot,
1982}
1983
1984impl<'a> EngineSnapshotView<'a> {
1985 pub fn engine_id(&self) -> &'a EngineId {
1987 self.slot.engine_id()
1988 }
1989
1990 pub fn provider_device_identity(&self) -> &'a super::ProviderDeviceIdentity {
2000 self.slot.provider_device_identity()
2001 }
2002
2003 pub fn registration_identity(&self) -> RegistrationIdentity {
2005 self.slot.metadata().identity
2006 }
2007
2008 pub fn context_identity(&self) -> ExecutionContextIdentity {
2010 self.slot.context_identity()
2011 }
2012
2013 pub fn event_domain_id(&self) -> EventDomainId {
2015 self.slot.metadata().event_domain_id
2016 }
2017
2018 pub(super) fn executable_witness(&self) -> Option<&'a Arc<ExecutableEngineSnapshot>> {
2019 self.slot.executable()
2020 }
2021
2022 pub fn hardware_class(&self) -> &'a HardwareClassId {
2024 self.slot.hardware_class()
2025 }
2026
2027 pub fn capabilities(&self) -> &'a CoreCapabilityBundle {
2029 self.slot.capabilities()
2030 }
2031
2032 pub(super) fn storage_classes(&self) -> &'a [StorageClass] {
2033 self.slot.storage_classes()
2034 }
2035
2036 pub(super) fn default_storage_class(&self) -> &'a StorageClass {
2037 self.slot.default_storage_class()
2038 }
2039
2040 #[doc(hidden)]
2067 pub fn accepts_input_signature(&self, input: &super::InputSignatureEntry) -> bool {
2068 self.slot
2069 .storage_classes()
2070 .iter()
2071 .any(|storage_class| self.accepts_input_signature_for_storage(input, storage_class))
2072 }
2073
2074 pub(super) fn accepts_input_signature_for_storage(
2075 &self,
2076 input: &super::InputSignatureEntry,
2077 storage_class: &StorageClass,
2078 ) -> bool {
2079 self.slot
2080 .executable()
2081 .is_some_and(|snapshot| snapshot.accepts_input_signature(input, storage_class))
2082 }
2083
2084 #[cfg(test)]
2085 pub(crate) fn has_execution_engine_for_test(&self) -> bool {
2086 self.slot.executable().is_some()
2087 }
2088}
2089
2090impl fmt::Debug for EngineSnapshotView<'_> {
2091 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
2092 formatter
2093 .debug_struct("EngineSnapshotView")
2094 .field("engine_id", self.engine_id())
2095 .field("registration_identity", &self.registration_identity())
2096 .field("context_identity", &self.context_identity())
2097 .field("hardware_class", self.hardware_class())
2098 .field("capabilities", self.capabilities())
2099 .finish()
2100 }
2101}
2102
2103fn default_execution_policy() -> ExecutionPolicy {
2104 ExecutionPolicy::new(super::Determinism::Fast, None, 0)
2105}
2106
2107fn register_engine_candidate(
2108 candidate: &mut CandidateConfig,
2109 registration: EngineRegistration,
2110 changed: &mut bool,
2111) -> Result<(), RuntimeConfigError> {
2112 let engine_id = registration.engine_id().clone();
2113 match candidate.engines.get(&engine_id) {
2114 Some(existing) if existing.registration.candidate_identical(®istration) => Ok(()),
2115 Some(_) => Err(RuntimeConfigError::DuplicateEngine { engine_id }),
2116 None => {
2117 ensure_unique_provider_device_target(candidate, ®istration)?;
2118 candidate.engines.insert(
2119 engine_id,
2120 CandidateEngineRecord {
2121 registration,
2122 identity: CandidateRegistrationIdentity::New,
2123 },
2124 );
2125 *changed = true;
2126 Ok(())
2127 }
2128 }
2129}
2130
2131fn replace_engine_candidate(
2132 candidate: &mut CandidateConfig,
2133 registration: EngineRegistration,
2134 changed: &mut bool,
2135) -> Result<(), RuntimeConfigError> {
2136 let engine_id = registration.engine_id().clone();
2137 let Some(existing) = candidate.engines.get(&engine_id) else {
2138 return Err(RuntimeConfigError::MissingEngine { engine_id });
2139 };
2140 if existing.registration.candidate_identical(®istration) {
2141 return Ok(());
2142 }
2143 if existing.registration.provider_device_identity() != registration.provider_device_identity() {
2144 return Err(RuntimeConfigError::EngineTargetRebind {
2145 engine_id,
2146 current: existing.registration.provider_device_identity().clone(),
2147 replacement: registration.provider_device_identity().clone(),
2148 });
2149 }
2150 ensure_unique_provider_device_target_except(candidate, ®istration, &engine_id)?;
2151 candidate.engines.insert(
2152 engine_id,
2153 CandidateEngineRecord {
2154 registration,
2155 identity: CandidateRegistrationIdentity::New,
2156 },
2157 );
2158 *changed = true;
2159 Ok(())
2160}
2161
2162fn remove_engine_candidate(
2163 candidate: &mut CandidateConfig,
2164 id: &EngineId,
2165 changed: &mut bool,
2166) -> Result<(), RuntimeConfigError> {
2167 match candidate.engines.remove(id) {
2168 Some(_) => {
2169 *changed = true;
2170 Ok(())
2171 }
2172 None => Err(RuntimeConfigError::MissingEngine {
2173 engine_id: id.clone(),
2174 }),
2175 }
2176}
2177
2178fn install_extension_module_candidate(
2179 candidate: &mut CandidateConfig,
2180 module: Arc<dyn ExtensionModule>,
2181 changed: &mut bool,
2182) -> Result<(), RuntimeConfigError> {
2183 let module_id = module.module_id().clone();
2184 match candidate.modules.get(&module_id) {
2185 Some(existing) if existing.module_identical(&module) => Ok(()),
2186 Some(_) => Err(RuntimeConfigError::ExtensionModule {
2187 source: ExtensionModuleError::ConflictingModule { module_id },
2188 }),
2189 None => {
2190 let record = configure_module(module)
2191 .map_err(|source| RuntimeConfigError::ExtensionModule { source })?;
2192 candidate.modules.insert(module_id, record);
2193 *changed = true;
2194 Ok(())
2195 }
2196 }
2197}
2198
2199fn replace_extension_module_candidate(
2200 candidate: &mut CandidateConfig,
2201 module: Arc<dyn ExtensionModule>,
2202 changed: &mut bool,
2203) -> Result<(), RuntimeConfigError> {
2204 let module_id = module.module_id().clone();
2205 match candidate.modules.get(&module_id) {
2206 Some(existing) if existing.module_identical(&module) => Ok(()),
2207 _ => {
2208 let record = configure_module(module)
2209 .map_err(|source| RuntimeConfigError::ExtensionModule { source })?;
2210 candidate.modules.insert(module_id, record);
2211 *changed = true;
2212 Ok(())
2213 }
2214 }
2215}
2216
2217fn remove_extension_module_candidate(
2218 candidate: &mut CandidateConfig,
2219 id: &ExtensionModuleId,
2220 changed: &mut bool,
2221) -> Result<(), RuntimeConfigError> {
2222 if candidate.modules.remove(id).is_some() {
2223 *changed = true;
2224 }
2225 Ok(())
2226}
2227
2228fn register_transfer_provider_candidate(
2229 candidate: &mut CandidateConfig,
2230 source: TransferEndpoint,
2231 destination: TransferEndpoint,
2232 provider: Arc<dyn TransferProvider>,
2233 changed: &mut bool,
2234) -> Result<(), RuntimeConfigError> {
2235 let key = TransferRoute::new(source, destination);
2236 match candidate.transfers.get(&key) {
2237 Some(existing) if Arc::ptr_eq(&existing.provider, &provider) => Ok(()),
2238 Some(_) => Err(RuntimeConfigError::ConflictingRegistration {
2239 key: RegistrationKey::TransferProvider {
2240 source: key.source().clone(),
2241 destination: key.destination().clone(),
2242 },
2243 }),
2244 None => {
2245 candidate.transfers.insert(
2246 key,
2247 CandidateTransferRecord {
2248 provider,
2249 binding: CandidateTransferBinding::New,
2250 },
2251 );
2252 *changed = true;
2253 Ok(())
2254 }
2255 }
2256}
2257
2258fn remove_transfer_provider_candidate(
2259 candidate: &mut CandidateConfig,
2260 source: TransferEndpoint,
2261 destination: TransferEndpoint,
2262 changed: &mut bool,
2263) -> Result<(), RuntimeConfigError> {
2264 let key = TransferRoute::new(source, destination);
2265 if candidate.transfers.remove(&key).is_none() {
2266 return Err(RuntimeConfigError::MissingTransferProvider {
2267 source_endpoint: key.source().clone(),
2268 destination: key.destination().clone(),
2269 });
2270 }
2271 *changed = true;
2272 Ok(())
2273}
2274
2275fn ensure_unique_provider_device_target(
2276 candidate: &CandidateConfig,
2277 registration: &EngineRegistration,
2278) -> Result<(), RuntimeConfigError> {
2279 ensure_unique_provider_device_target_except(candidate, registration, registration.engine_id())
2280}
2281
2282fn ensure_unique_provider_device_target_except(
2283 candidate: &CandidateConfig,
2284 registration: &EngineRegistration,
2285 ignored_engine_id: &EngineId,
2286) -> Result<(), RuntimeConfigError> {
2287 if let Some((first_engine_id, _)) = candidate.engines.iter().find(|(engine_id, record)| {
2288 *engine_id != ignored_engine_id
2289 && record.registration.provider_device_identity()
2290 == registration.provider_device_identity()
2291 }) {
2292 return Err(RuntimeConfigError::DuplicateProviderDeviceTarget {
2293 provider_device_identity: registration.provider_device_identity().clone(),
2294 first_engine_id: first_engine_id.clone(),
2295 duplicate_engine_id: registration.engine_id().clone(),
2296 });
2297 }
2298 Ok(())
2299}
2300
2301fn validate_candidate(
2302 candidate: CandidateConfig,
2303 issuer: NonZeroU64,
2304 next_ordinal: NonZeroU64,
2305) -> Result<(BoundCandidateConfig, NonZeroU64), RuntimeConfigError> {
2306 let mut seen_targets = BTreeMap::<ProviderDeviceIdentity, EngineId>::new();
2307 for (engine_id, record) in &candidate.engines {
2308 if let Some(first_engine_id) = seen_targets.insert(
2309 record.registration.provider_device_identity().clone(),
2310 engine_id.clone(),
2311 ) {
2312 return Err(RuntimeConfigError::DuplicateProviderDeviceTarget {
2313 provider_device_identity: record.registration.provider_device_identity().clone(),
2314 first_engine_id,
2315 duplicate_engine_id: engine_id.clone(),
2316 });
2317 }
2318 }
2319
2320 let mut bound_transfers = BTreeMap::new();
2321 for (route, record) in &candidate.transfers {
2322 let source_binding = validate_transfer_endpoint(&candidate, route.source())?;
2323 let destination_binding = validate_transfer_endpoint(&candidate, route.destination())?;
2324 let preserved = match &record.binding {
2325 CandidateTransferBinding::New => None,
2326 CandidateTransferBinding::Preserved {
2327 source,
2328 destination,
2329 } => Some((source, destination)),
2330 };
2331 if let Some((registered_source, registered_destination)) = preserved {
2332 if registered_source != &source_binding {
2333 return Err(RuntimeConfigError::StaleTransferRoute {
2334 source_endpoint: route.source().clone(),
2335 destination: route.destination().clone(),
2336 endpoint: route.source().clone(),
2337 registered: Box::new(registered_source.clone()),
2338 current: Box::new(source_binding.clone()),
2339 });
2340 }
2341 if registered_destination != &destination_binding {
2342 return Err(RuntimeConfigError::StaleTransferRoute {
2343 source_endpoint: route.source().clone(),
2344 destination: route.destination().clone(),
2345 endpoint: route.destination().clone(),
2346 registered: Box::new(registered_destination.clone()),
2347 current: Box::new(destination_binding.clone()),
2348 });
2349 }
2350 }
2351 bound_transfers.insert(
2352 route.clone(),
2353 BoundCandidateTransferRecord {
2354 provider: Arc::clone(&record.provider),
2355 source: source_binding,
2356 destination: destination_binding,
2357 },
2358 );
2359 }
2360 let mut seen = BTreeMap::<(ExtensionFamilyId, EngineId), ExtensionModuleId>::new();
2361 for (module_id, module) in &candidate.modules {
2362 for family_engine in module.engines.keys() {
2363 if seen
2364 .insert(
2365 (family_engine.0, family_engine.1.clone()),
2366 module_id.clone(),
2367 )
2368 .is_some()
2369 {
2370 return Err(RuntimeConfigError::ConflictingRegistration {
2371 key: RegistrationKey::ExtensionEngine {
2372 family: family_engine.0,
2373 engine: family_engine.1.clone(),
2374 },
2375 });
2376 }
2377 }
2378 }
2379
2380 let CandidateConfig {
2381 policy,
2382 engines,
2383 modules,
2384 transfers: _,
2385 } = candidate;
2386 let mut allocator = RegistrationIdentityAllocator::new(issuer, next_ordinal);
2387 let engines = engines
2388 .into_iter()
2389 .map(|(engine_id, record)| {
2390 let identity = match record.identity {
2391 CandidateRegistrationIdentity::New => allocator.allocate()?,
2392 CandidateRegistrationIdentity::Preserved(identity) => identity,
2393 };
2394 Ok((
2395 engine_id,
2396 BoundCandidateEngineRecord {
2397 registration: record.registration,
2398 identity,
2399 },
2400 ))
2401 })
2402 .collect::<Result<BTreeMap<_, _>, RuntimeConfigError>>()?;
2403 let modules = modules
2404 .into_iter()
2405 .map(|(module_id, module)| {
2406 let mut allocate = || allocator.allocate();
2407 Ok((module_id, bind_candidate_module(module, &mut allocate)?))
2408 })
2409 .collect::<Result<BTreeMap<_, _>, RuntimeConfigError>>()?;
2410 Ok((
2411 BoundCandidateConfig {
2412 policy,
2413 engines,
2414 modules,
2415 transfers: bound_transfers,
2416 },
2417 allocator.next_ordinal(),
2418 ))
2419}
2420
2421fn validate_transfer_endpoint(
2422 candidate: &CandidateConfig,
2423 endpoint: &TransferEndpoint,
2424) -> Result<ProviderDeviceIdentity, RuntimeConfigError> {
2425 let Some(engine) = candidate.engines.get(endpoint.engine_id()) else {
2426 return Err(RuntimeConfigError::UnknownTransferEndpointEngine {
2427 endpoint: endpoint.clone(),
2428 });
2429 };
2430 if !engine
2431 .registration
2432 .storage_classes()
2433 .contains(endpoint.storage_class())
2434 {
2435 return Err(RuntimeConfigError::UnsupportedTransferEndpointStorage {
2436 endpoint: endpoint.clone(),
2437 });
2438 }
2439 Ok(engine.registration.provider_device_identity().clone())
2440}
2441
2442struct RegistrationIdentityAllocator {
2443 issuer: NonZeroU64,
2444 next: NonZeroU64,
2445}
2446
2447impl RegistrationIdentityAllocator {
2448 fn new(issuer: NonZeroU64, next: NonZeroU64) -> Self {
2449 Self { issuer, next }
2450 }
2451
2452 fn allocate(&mut self) -> Result<RegistrationIdentity, RuntimeConfigError> {
2453 let identity = RegistrationIdentity::new(self.issuer, self.next);
2454 let next = self
2455 .next
2456 .get()
2457 .checked_add(1)
2458 .and_then(NonZeroU64::new)
2459 .ok_or(RuntimeConfigError::IdentityExhausted)?;
2460 self.next = next;
2461 Ok(identity)
2462 }
2463
2464 fn next_ordinal(&self) -> NonZeroU64 {
2465 self.next
2466 }
2467}
2468
2469fn freeze_candidate(
2470 runtime_id: RuntimeId,
2471 epoch: RuntimeEpoch,
2472 candidate: BoundCandidateConfig,
2473) -> Result<RuntimeConfigSnapshot, RuntimeConfigError> {
2474 let mut engines = Vec::with_capacity(candidate.engines.len());
2475 let mut engine_indices = BTreeMap::new();
2476 let mut engine_locations = BTreeMap::new();
2477 let mut cache_owners = Vec::new();
2478 for (index, (engine_id, record)) in candidate.engines.into_iter().enumerate() {
2479 let BoundCandidateEngineRecord {
2480 registration,
2481 identity,
2482 } = record;
2483 let event_domain_id = EventDomainId::new(runtime_id, epoch, identity);
2484 let (state, candidate_token) = registration.into_state_and_token();
2485 let provider_device_identity = state.provider_device_identity().clone();
2486 let metadata = FrozenEngineMetadata {
2487 candidate_token,
2488 identity,
2489 event_domain_id,
2490 };
2491 let frozen = match state {
2492 EngineRegistrationState::PreparationOnly { binding } => {
2493 FrozenEngineSlot::PreparationOnly(Arc::new(PreparationOnlyEngineSnapshot {
2494 metadata,
2495 binding,
2496 }))
2497 }
2498 EngineRegistrationState::Executable(binding) => {
2499 if let Some(owner) = binding.contract().cache_owner().cloned() {
2500 cache_owners.push(FrozenCacheOwner {
2501 id: engine_cache_owner_id(&engine_id),
2502 kind: FrozenCacheOwnerKind::Engine,
2503 owner,
2504 });
2505 }
2506 cache_owners.push(FrozenCacheOwner {
2507 id: engine_extension_cache_owner_id(&engine_id),
2508 kind: FrozenCacheOwnerKind::Extension,
2509 owner: execution::extension_cache_owner(binding.contract().executor().clone()),
2510 });
2511 FrozenEngineSlot::Executable(Arc::new(ExecutableEngineSnapshot {
2512 metadata,
2513 binding,
2514 }))
2515 }
2516 };
2517 engine_locations.insert(
2518 engine_id.clone(),
2519 (provider_device_identity, event_domain_id),
2520 );
2521 engine_indices.insert(engine_id, index);
2522 engines.push(frozen);
2523 }
2524 let extensions = freeze_extension_slots(candidate.modules)?;
2525 for (id, owner) in extensions.cache_owner_records() {
2526 cache_owners.push(FrozenCacheOwner {
2527 id,
2528 kind: FrozenCacheOwnerKind::Extension,
2529 owner,
2530 });
2531 }
2532 let mut transfers = BTreeMap::new();
2533 for (route, record) in candidate.transfers {
2534 let BoundCandidateTransferRecord {
2535 provider,
2536 source: source_binding,
2537 destination: destination_binding,
2538 } = record;
2539 let (_, source_event_domain_id) = bound_engine_location(&engine_locations, route.source())?;
2540 let (_, destination_event_domain_id) =
2541 bound_engine_location(&engine_locations, route.destination())?;
2542 let resolved_route = ResolvedTransferRoute::new(
2543 ResolvedTransferEndpoint::new(
2544 route.source().clone(),
2545 source_binding,
2546 *source_event_domain_id,
2547 ),
2548 ResolvedTransferEndpoint::new(
2549 route.destination().clone(),
2550 destination_binding,
2551 *destination_event_domain_id,
2552 ),
2553 );
2554 transfers.insert(resolved_route, provider);
2555 }
2556 Ok(RuntimeConfigSnapshot {
2557 runtime_id,
2558 epoch,
2559 policy: candidate.policy,
2560 engines: engines.into(),
2561 engine_indices,
2562 extensions,
2563 transfers: FrozenTransferRegistry::new(transfers),
2564 cache_owners: cache_owners.into(),
2565 })
2566}
2567
2568fn bound_engine_location<'a>(
2569 locations: &'a BTreeMap<EngineId, (ProviderDeviceIdentity, EventDomainId)>,
2570 endpoint: &TransferEndpoint,
2571) -> Result<&'a (ProviderDeviceIdentity, EventDomainId), RuntimeConfigError> {
2572 locations
2573 .get(endpoint.engine_id())
2574 .ok_or_else(|| RuntimeConfigError::BoundCandidateInvariant {
2575 endpoint: endpoint.clone(),
2576 })
2577}
2578
2579fn engine_cache_owner_id(engine_id: &EngineId) -> CacheOwnerId {
2580 let id = engine_id.as_str();
2581 CacheOwnerId::from_canonical_owner_id(Arc::<str>::from(format!("engine[{}]:{id}", id.len())))
2582}
2583
2584fn engine_extension_cache_owner_id(engine_id: &EngineId) -> CacheOwnerId {
2585 let id = engine_id.as_str();
2586 CacheOwnerId::from_canonical_owner_id(Arc::<str>::from(format!(
2587 "extension-executor[{}]:{id}",
2588 id.len()
2589 )))
2590}
2591
2592fn allocate_nonzero(counter: &AtomicU64) -> Result<NonZeroU64, RuntimeConfigError> {
2593 let value = counter
2594 .try_update(Ordering::SeqCst, Ordering::SeqCst, |next| {
2595 next.checked_add(1)
2596 })
2597 .map_err(|_| RuntimeConfigError::IdentityExhausted)?;
2598 NonZeroU64::new(value).ok_or(RuntimeConfigError::IdentityExhausted)
2599}
2600
2601#[cfg(test)]
2602mod freeze_tests {
2603 use crate::{ProviderId, TransferRequest};
2604
2605 use super::*;
2606
2607 #[derive(Debug)]
2608 struct FreezeTestContext;
2609
2610 #[derive(Debug)]
2611 struct FreezeTestProvider;
2612
2613 impl TransferProvider for FreezeTestProvider {
2614 fn transfer_blocking(
2615 &self,
2616 _request: TransferRequest<'_>,
2617 ) -> crate::Result<tenferro_tensor::Tensor> {
2618 Err(crate::Error::Internal("freeze test provider".into()))
2619 }
2620 }
2621
2622 fn registration(
2623 engine_id: &str,
2624 target: &str,
2625 ) -> Result<EngineRegistration, RuntimeConfigError> {
2626 let engine_id = EngineId::new(engine_id).map_err(RuntimeConfigError::from)?;
2627 let storage =
2628 StorageClass::new("tenferro.test.freeze.storage").map_err(RuntimeConfigError::from)?;
2629 Ok(EngineRegistration::preparation_only(
2630 super::super::ProviderPreparationBinding::new(
2631 engine_id,
2632 ProviderDeviceIdentity::new(
2633 ProviderId::new("tenferro.test.freeze.provider")
2634 .map_err(RuntimeConfigError::from)?,
2635 target,
2636 )
2637 .map_err(RuntimeConfigError::from)?,
2638 ExecutionContextIdentity::of::<FreezeTestContext>(),
2639 HardwareClassId::new("tenferro.test.freeze.hardware")
2640 .map_err(RuntimeConfigError::from)?,
2641 Arc::from(vec![storage.clone()]),
2642 storage,
2643 CoreCapabilityBundle::default(),
2644 )?,
2645 ))
2646 }
2647
2648 fn candidate(binding: CandidateTransferBinding) -> Result<CandidateConfig, RuntimeConfigError> {
2649 let source_id =
2650 EngineId::new("tenferro.test.freeze.source").map_err(RuntimeConfigError::from)?;
2651 let destination_id =
2652 EngineId::new("tenferro.test.freeze.destination").map_err(RuntimeConfigError::from)?;
2653 let storage =
2654 StorageClass::new("tenferro.test.freeze.storage").map_err(RuntimeConfigError::from)?;
2655 let source_endpoint = TransferEndpoint::new(source_id.clone(), storage.clone());
2656 let destination_endpoint = TransferEndpoint::new(destination_id.clone(), storage);
2657 let mut candidate = CandidateConfig::empty();
2658 let mut changed = false;
2659 register_engine_candidate(
2660 &mut candidate,
2661 registration(source_id.as_str(), "freeze-source")?,
2662 &mut changed,
2663 )?;
2664 register_engine_candidate(
2665 &mut candidate,
2666 registration(destination_id.as_str(), "freeze-destination")?,
2667 &mut changed,
2668 )?;
2669 register_transfer_provider_candidate(
2670 &mut candidate,
2671 source_endpoint.clone(),
2672 destination_endpoint.clone(),
2673 Arc::new(FreezeTestProvider),
2674 &mut changed,
2675 )?;
2676 candidate
2677 .transfers
2678 .get_mut(&TransferRoute::new(source_endpoint, destination_endpoint))
2679 .expect("registered route")
2680 .binding = binding;
2681 Ok(candidate)
2682 }
2683
2684 #[test]
2685 fn validation_owns_stale_route_rejection_and_bound_freeze_is_total() {
2686 let wrong_source = ProviderDeviceIdentity::new(
2687 ProviderId::new("tenferro.test.freeze.provider").unwrap(),
2688 "different-source",
2689 )
2690 .unwrap();
2691 let preserved = CandidateTransferBinding::Preserved {
2692 source: wrong_source,
2693 destination: ProviderDeviceIdentity::new(
2694 ProviderId::new("tenferro.test.freeze.provider").unwrap(),
2695 "freeze-destination",
2696 )
2697 .unwrap(),
2698 };
2699 let result = validate_candidate(
2700 candidate(preserved).unwrap(),
2701 NonZeroU64::new(1).unwrap(),
2702 NonZeroU64::new(1).unwrap(),
2703 );
2704 let error = match result {
2705 Ok(_) => panic!("candidate validation must reject stale preserved bindings"),
2706 Err(error) => error,
2707 };
2708 assert!(matches!(
2709 error,
2710 RuntimeConfigError::StaleTransferRoute { .. }
2711 ));
2712
2713 let (bound, _) = validate_candidate(
2714 candidate(CandidateTransferBinding::New).unwrap(),
2715 NonZeroU64::new(1).unwrap(),
2716 NonZeroU64::new(1).unwrap(),
2717 )
2718 .expect("validation must produce a complete bound candidate");
2719 freeze_candidate(
2720 RuntimeId::from_nonzero(NonZeroU64::new(1).unwrap()),
2721 RuntimeEpoch::one(),
2722 bound,
2723 )
2724 .expect("a bound candidate must freeze without semantic route revalidation");
2725 }
2726
2727 #[test]
2728 fn frozen_engine_slots_are_arc_sized() {
2729 let slot_size = std::mem::size_of::<FrozenEngineSlot>();
2730 let arc_size = std::mem::size_of::<Arc<()>>();
2731
2732 assert!(
2733 slot_size <= 2 * arc_size,
2734 "frozen engine slots should keep immutable snapshot payloads behind Arc: slot_size={slot_size}, arc_size={arc_size}",
2735 );
2736 }
2737}