1use serde::{Deserialize, Serialize};
2use sha2::{Digest, Sha256};
3use std::collections::{BTreeMap, BTreeSet, VecDeque};
4use std::error::Error;
5use std::num::NonZeroU64;
6use std::panic::{catch_unwind, AssertUnwindSafe};
7use std::sync::atomic::{AtomicU64, Ordering};
8use std::sync::{Arc, Mutex, MutexGuard, OnceLock};
9use std::time::Duration;
10
11use ferrum_types::AttentionExecutionPolicy;
12
13use super::{
14 CapabilityId, DeviceAllocationPermit, DeviceId, DynamicStorageProfile, ElementType,
15 ExecutionIdentityEnvelope, FailureDomain, FailureEnvelope, IdentifiedFailure, PlanHash,
16 ReusableExecutionBucketId, ReusableExecutionCatalogLifetime, StaticWeightTransformPlan,
17 VNextError, WeightComponentPayload, WeightComponentSegments, WeightComponentSpec,
18};
19
20pub const DEVICE_REUSABLE_EXECUTION_CAPABILITY_ID: &str = "capability.device.reusable_execution.v1";
23pub const DEVICE_ON_DEMAND_REUSABLE_EXECUTION_CAPABILITY_ID: &str =
26 "capability.device.reusable_execution.on_demand.v1";
27pub const DEVICE_NATIVE_ADAPTIVE_ATTENTION_CAPABILITY_ID: &str =
30 "capability.device.native_adaptive_attention.v1";
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
33#[serde(transparent)]
34pub struct ExecutionLaneId(NonZeroU64);
35
36impl ExecutionLaneId {
37 pub(crate) fn mint() -> Result<Self, VNextError> {
38 static NEXT_LANE_ID: AtomicU64 = AtomicU64::new(1);
39 let raw = NEXT_LANE_ID
40 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
41 current.checked_add(1)
42 })
43 .map_err(|_| VNextError::InvalidExecutionPlan {
44 reason: "execution lane identity space is exhausted".to_owned(),
45 })?;
46 NonZeroU64::new(raw)
47 .map(Self)
48 .ok_or_else(|| VNextError::InvalidExecutionPlan {
49 reason: "execution lane identity must be non-zero".to_owned(),
50 })
51 }
52
53 pub const fn get(self) -> u64 {
54 self.0.get()
55 }
56}
57
58pub const MAX_DEFERRED_DEVICE_CLEANUP_TASKS: usize = 64;
63pub const MAX_DEFERRED_DEVICE_CLEANUP_MAINTENANCE_TASKS: usize = 64;
64
65const _: () = assert!(
66 MAX_DEFERRED_DEVICE_CLEANUP_TASKS > 0
67 && MAX_DEFERRED_DEVICE_CLEANUP_TASKS <= 64
68 && MAX_DEFERRED_DEVICE_CLEANUP_MAINTENANCE_TASKS > 0
69 && MAX_DEFERRED_DEVICE_CLEANUP_MAINTENANCE_TASKS <= 64
70);
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
73pub(crate) struct DeferredDeviceCleanupDomainId(NonZeroU64);
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76pub(crate) enum DeferredDeviceCleanupDisposition {
77 Completed,
78 Retryable,
79 Quarantined,
80}
81
82pub(crate) trait DeferredDeviceCleanupTask: Send + 'static {
83 fn try_cleanup(&mut self) -> DeferredDeviceCleanupDisposition;
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87enum DeferredDeviceCleanupTaskState {
88 Pending,
89 Retryable,
90 Quarantined,
91 Panicked,
92}
93
94struct DeferredDeviceCleanupEntry {
95 task_id: NonZeroU64,
96 task: Box<dyn DeferredDeviceCleanupTask>,
97 state: DeferredDeviceCleanupTaskState,
98}
99
100#[derive(Default)]
101struct DeferredDeviceCleanupDomain {
102 queued: VecDeque<DeferredDeviceCleanupEntry>,
103 in_progress: usize,
104 submitted_total: u64,
105 attempted_total: u64,
106 completed_total: u64,
107 panicked_total: u64,
108}
109
110#[derive(Default)]
111struct DeferredDeviceCleanupRegistry {
112 domains: BTreeMap<DeferredDeviceCleanupDomainId, DeferredDeviceCleanupDomain>,
113}
114
115#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
116pub struct DeferredDeviceCleanupStatus {
117 queued: usize,
118 in_progress: usize,
119 retryable: usize,
120 quarantined: usize,
121 panicked: usize,
122 submitted_total: u64,
123 attempted_total: u64,
124 completed_total: u64,
125 panicked_total: u64,
126}
127
128impl DeferredDeviceCleanupStatus {
129 pub const fn queued(&self) -> usize {
130 self.queued
131 }
132
133 pub const fn in_progress(&self) -> usize {
134 self.in_progress
135 }
136
137 pub const fn pending(&self) -> usize {
138 self.queued + self.in_progress
139 }
140
141 pub const fn retryable(&self) -> usize {
142 self.retryable
143 }
144
145 pub const fn quarantined(&self) -> usize {
146 self.quarantined
147 }
148
149 pub const fn panicked(&self) -> usize {
150 self.panicked
151 }
152
153 pub const fn submitted_total(&self) -> u64 {
154 self.submitted_total
155 }
156
157 pub const fn attempted_total(&self) -> u64 {
158 self.attempted_total
159 }
160
161 pub const fn completed_total(&self) -> u64 {
162 self.completed_total
163 }
164
165 pub const fn panicked_total(&self) -> u64 {
166 self.panicked_total
167 }
168
169 pub const fn is_saturated(&self) -> bool {
170 self.pending() >= MAX_DEFERRED_DEVICE_CLEANUP_TASKS
171 }
172}
173
174#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
175pub struct DeferredDeviceCleanupMaintenanceReceipt {
176 attempted: usize,
177 completed: usize,
178 retryable: usize,
179 quarantined: usize,
180 panicked: usize,
181 status_after: DeferredDeviceCleanupStatus,
182}
183
184impl DeferredDeviceCleanupMaintenanceReceipt {
185 pub const fn attempted(&self) -> usize {
186 self.attempted
187 }
188
189 pub const fn completed(&self) -> usize {
190 self.completed
191 }
192
193 pub const fn retryable(&self) -> usize {
194 self.retryable
195 }
196
197 pub const fn quarantined(&self) -> usize {
198 self.quarantined
199 }
200
201 pub const fn panicked(&self) -> usize {
202 self.panicked
203 }
204
205 pub const fn status_after(&self) -> &DeferredDeviceCleanupStatus {
206 &self.status_after
207 }
208}
209
210static NEXT_DEFERRED_DEVICE_CLEANUP_DOMAIN_ID: AtomicU64 = AtomicU64::new(1);
211static NEXT_DEFERRED_DEVICE_CLEANUP_TASK_ID: AtomicU64 = AtomicU64::new(1);
212static DEFERRED_DEVICE_CLEANUP_REGISTRY: OnceLock<Mutex<DeferredDeviceCleanupRegistry>> =
213 OnceLock::new();
214
215fn deferred_device_cleanup_registry() -> &'static Mutex<DeferredDeviceCleanupRegistry> {
216 DEFERRED_DEVICE_CLEANUP_REGISTRY
217 .get_or_init(|| Mutex::new(DeferredDeviceCleanupRegistry::default()))
218}
219
220fn lock_deferred_device_cleanup_registry() -> MutexGuard<'static, DeferredDeviceCleanupRegistry> {
221 deferred_device_cleanup_registry()
222 .lock()
223 .unwrap_or_else(std::sync::PoisonError::into_inner)
224}
225
226pub(crate) fn new_deferred_device_cleanup_domain() -> DeferredDeviceCleanupDomainId {
227 let raw = NEXT_DEFERRED_DEVICE_CLEANUP_DOMAIN_ID
228 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
229 current.checked_add(1)
230 })
231 .expect("deferred device cleanup domain identity space is exhausted");
232 DeferredDeviceCleanupDomainId(
233 NonZeroU64::new(raw).expect("deferred device cleanup domain ids start at one"),
234 )
235}
236
237pub(crate) fn defer_device_cleanup<T>(domain_id: DeferredDeviceCleanupDomainId, task: T)
242where
243 T: DeferredDeviceCleanupTask,
244{
245 let task_id = NEXT_DEFERRED_DEVICE_CLEANUP_TASK_ID
246 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
247 current.checked_add(1)
248 })
249 .expect("deferred device cleanup task identity space is exhausted");
250 let mut registry = lock_deferred_device_cleanup_registry();
251 let domain = registry.domains.entry(domain_id).or_default();
252 domain.queued.push_back(DeferredDeviceCleanupEntry {
253 task_id: NonZeroU64::new(task_id).expect("deferred device cleanup task ids start at one"),
254 task: Box::new(task),
255 state: DeferredDeviceCleanupTaskState::Pending,
256 });
257 domain.submitted_total = domain.submitted_total.saturating_add(1);
258}
259
260pub(crate) fn deferred_device_cleanup_status(
261 domain_id: DeferredDeviceCleanupDomainId,
262) -> DeferredDeviceCleanupStatus {
263 let registry = lock_deferred_device_cleanup_registry();
264 registry
265 .domains
266 .get(&domain_id)
267 .map(deferred_device_cleanup_domain_status)
268 .unwrap_or_else(empty_deferred_device_cleanup_status)
269}
270
271pub(crate) fn maintain_deferred_device_cleanups(
272 domain_id: DeferredDeviceCleanupDomainId,
273 maximum_tasks: usize,
274) -> DeferredDeviceCleanupMaintenanceReceipt {
275 debug_assert!(
276 maximum_tasks > 0 && maximum_tasks <= MAX_DEFERRED_DEVICE_CLEANUP_MAINTENANCE_TASKS
277 );
278 let selected = {
279 let registry = lock_deferred_device_cleanup_registry();
280 registry
281 .domains
282 .get(&domain_id)
283 .map(|domain| {
284 domain
285 .queued
286 .iter()
287 .take(maximum_tasks)
288 .map(|entry| entry.task_id)
289 .collect::<Vec<_>>()
290 })
291 .unwrap_or_default()
292 };
293
294 let mut attempted = 0;
295 let mut completed = 0;
296 let mut retryable = 0;
297 let mut quarantined = 0;
298 let mut panicked = 0;
299 for task_id in selected {
300 let Some(mut entry) = ({
301 let mut registry = lock_deferred_device_cleanup_registry();
302 let domain = registry.domains.entry(domain_id).or_default();
303 let entry = domain
304 .queued
305 .iter()
306 .position(|entry| entry.task_id == task_id)
307 .and_then(|position| domain.queued.remove(position));
308 if entry.is_some() {
309 domain.in_progress = domain.in_progress.saturating_add(1);
310 }
311 entry
312 }) else {
313 continue;
314 };
315 attempted += 1;
316 let outcome = catch_unwind(AssertUnwindSafe(|| entry.task.try_cleanup()));
317 let mut registry = lock_deferred_device_cleanup_registry();
318 let domain = registry.domains.entry(domain_id).or_default();
319 domain.in_progress = domain.in_progress.saturating_sub(1);
320 domain.attempted_total = domain.attempted_total.saturating_add(1);
321 match outcome {
322 Ok(DeferredDeviceCleanupDisposition::Completed) => {
323 domain.completed_total = domain.completed_total.saturating_add(1);
324 completed += 1;
325 }
326 Ok(DeferredDeviceCleanupDisposition::Retryable) => {
327 entry.state = DeferredDeviceCleanupTaskState::Retryable;
328 domain.queued.push_back(entry);
329 retryable += 1;
330 }
331 Ok(DeferredDeviceCleanupDisposition::Quarantined) => {
332 entry.state = DeferredDeviceCleanupTaskState::Quarantined;
333 domain.queued.push_back(entry);
334 quarantined += 1;
335 }
336 Err(_) => {
337 entry.state = DeferredDeviceCleanupTaskState::Panicked;
338 domain.queued.push_back(entry);
339 domain.panicked_total = domain.panicked_total.saturating_add(1);
340 panicked += 1;
341 }
342 }
343 }
344
345 let status_after = deferred_device_cleanup_status(domain_id);
346 DeferredDeviceCleanupMaintenanceReceipt {
347 attempted,
348 completed,
349 retryable,
350 quarantined,
351 panicked,
352 status_after,
353 }
354}
355
356pub(crate) fn retire_deferred_device_cleanup_domain(
357 domain_id: DeferredDeviceCleanupDomainId,
358) -> bool {
359 let mut registry = lock_deferred_device_cleanup_registry();
360 if registry
361 .domains
362 .get(&domain_id)
363 .is_some_and(|domain| !domain.queued.is_empty() || domain.in_progress != 0)
364 {
365 return false;
366 }
367 registry.domains.remove(&domain_id);
368 true
369}
370
371fn deferred_device_cleanup_domain_status(
372 domain: &DeferredDeviceCleanupDomain,
373) -> DeferredDeviceCleanupStatus {
374 DeferredDeviceCleanupStatus {
375 queued: domain.queued.len(),
376 in_progress: domain.in_progress,
377 retryable: domain
378 .queued
379 .iter()
380 .filter(|entry| entry.state == DeferredDeviceCleanupTaskState::Retryable)
381 .count(),
382 quarantined: domain
383 .queued
384 .iter()
385 .filter(|entry| entry.state == DeferredDeviceCleanupTaskState::Quarantined)
386 .count(),
387 panicked: domain
388 .queued
389 .iter()
390 .filter(|entry| entry.state == DeferredDeviceCleanupTaskState::Panicked)
391 .count(),
392 submitted_total: domain.submitted_total,
393 attempted_total: domain.attempted_total,
394 completed_total: domain.completed_total,
395 panicked_total: domain.panicked_total,
396 }
397}
398
399const fn empty_deferred_device_cleanup_status() -> DeferredDeviceCleanupStatus {
400 DeferredDeviceCleanupStatus {
401 queued: 0,
402 in_progress: 0,
403 retryable: 0,
404 quarantined: 0,
405 panicked: 0,
406 submitted_total: 0,
407 attempted_total: 0,
408 completed_total: 0,
409 panicked_total: 0,
410 }
411}
412
413#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
415#[serde(rename_all = "snake_case")]
416pub enum DeviceClass {
417 Host,
418 Accelerator,
419 Reference,
420}
421
422#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
423pub struct DeviceDescriptor {
424 pub id: DeviceId,
425 pub class: DeviceClass,
426 pub ordinal: u32,
427 pub total_memory_bytes: u64,
428 pub runtime_implementation_fingerprint: String,
429 pub capabilities: BTreeSet<CapabilityId>,
430 pub dynamic_storage_profiles: BTreeSet<DynamicStorageProfile>,
431}
432
433impl DeviceDescriptor {
434 pub fn validate(&self) -> Result<(), VNextError> {
435 if self.total_memory_bytes == 0
436 || self.dynamic_storage_profiles.is_empty()
437 || self.runtime_implementation_fingerprint.len() != 64
438 || !self
439 .runtime_implementation_fingerprint
440 .bytes()
441 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
442 {
443 return Err(VNextError::InvalidExecutionPlan {
444 reason: format!(
445 "device `{}` has invalid capacity or runtime implementation fingerprint",
446 self.id
447 ),
448 });
449 }
450 Ok(())
451 }
452}
453
454#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
455pub struct BufferRequest {
456 resource_id: super::ResourceId,
457 size_bytes: u64,
458 alignment_bytes: u64,
459 usage: BufferUsage,
460 element_type: ElementType,
461}
462
463impl BufferRequest {
464 pub fn new(
465 resource_id: super::ResourceId,
466 size_bytes: u64,
467 alignment_bytes: u64,
468 usage: BufferUsage,
469 element_type: ElementType,
470 ) -> Result<Self, super::VNextError> {
471 if size_bytes == 0 || alignment_bytes == 0 || !alignment_bytes.is_power_of_two() {
472 return Err(super::VNextError::InvalidExecutionPlan {
473 reason: "buffer request has invalid size or alignment".to_owned(),
474 });
475 }
476 Ok(Self {
477 resource_id,
478 size_bytes,
479 alignment_bytes,
480 usage,
481 element_type,
482 })
483 }
484
485 pub fn resource_id(&self) -> &super::ResourceId {
486 &self.resource_id
487 }
488
489 pub fn size_bytes(&self) -> u64 {
490 self.size_bytes
491 }
492
493 pub fn alignment_bytes(&self) -> u64 {
494 self.alignment_bytes
495 }
496
497 pub fn usage(&self) -> BufferUsage {
498 self.usage
499 }
500
501 pub fn element_type(&self) -> ElementType {
502 self.element_type
503 }
504}
505
506#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
507pub struct BufferDescriptor {
508 pub resource_id: super::ResourceId,
509 pub size_bytes: u64,
510 pub alignment_bytes: u64,
511 pub usage: BufferUsage,
512 pub element_type: ElementType,
513}
514
515#[derive(Clone)]
519pub struct DeviceBufferRetention {
520 _primary_owner: Arc<dyn Send + Sync + 'static>,
521 _secondary_owner: Option<Arc<dyn Send + Sync + 'static>>,
522 reusable_address_scope: Option<DeviceReusableAddressScope>,
523}
524
525#[derive(Debug, Clone, Copy, PartialEq, Eq)]
526pub enum DeviceReusableAddressScope {
527 Plan,
528 ExecutionLane(ExecutionLaneId),
529}
530
531impl DeviceBufferRetention {
532 pub(crate) fn plan<T>(owner: Arc<T>) -> Self
533 where
534 T: Send + Sync + 'static,
535 {
536 Self {
537 _primary_owner: owner,
538 _secondary_owner: None,
539 reusable_address_scope: Some(DeviceReusableAddressScope::Plan),
540 }
541 }
542
543 pub(crate) fn pair<T, U>(primary_owner: Arc<T>, secondary_owner: Arc<U>) -> Self
544 where
545 T: Send + Sync + 'static,
546 U: Send + Sync + 'static,
547 {
548 Self {
549 _primary_owner: primary_owner,
550 _secondary_owner: Some(secondary_owner),
551 reusable_address_scope: None,
552 }
553 }
554
555 pub(crate) fn lane_pair<T, U>(
556 lane_id: ExecutionLaneId,
557 primary_owner: Arc<T>,
558 secondary_owner: Arc<U>,
559 ) -> Self
560 where
561 T: Send + Sync + 'static,
562 U: Send + Sync + 'static,
563 {
564 Self {
565 _primary_owner: primary_owner,
566 _secondary_owner: Some(secondary_owner),
567 reusable_address_scope: Some(DeviceReusableAddressScope::ExecutionLane(lane_id)),
568 }
569 }
570
571 pub const fn reusable_address_scope(&self) -> Option<DeviceReusableAddressScope> {
572 self.reusable_address_scope
573 }
574}
575
576#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
577#[serde(rename_all = "snake_case")]
578pub enum BufferUsage {
579 Weights,
580 Activations,
581 State,
582 Persistent,
585 Binding,
587 Scratch,
588 Transfer,
589}
590
591#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
592pub struct CopyRegion {
593 source_offset_bytes: u64,
594 destination_offset_bytes: u64,
595 length_bytes: u64,
596}
597
598impl CopyRegion {
599 pub fn new(
600 source_offset_bytes: u64,
601 destination_offset_bytes: u64,
602 length_bytes: u64,
603 ) -> Result<Self, super::VNextError> {
604 if length_bytes == 0
605 || source_offset_bytes.checked_add(length_bytes).is_none()
606 || destination_offset_bytes.checked_add(length_bytes).is_none()
607 {
608 return Err(super::VNextError::InvalidExecutionPlan {
609 reason: "copy region is empty or overflows u64".to_owned(),
610 });
611 }
612 Ok(Self {
613 source_offset_bytes,
614 destination_offset_bytes,
615 length_bytes,
616 })
617 }
618
619 pub fn validate_bounds(
620 &self,
621 source: &BufferDescriptor,
622 destination: &BufferDescriptor,
623 ) -> Result<(), super::VNextError> {
624 let source_end = self
625 .source_offset_bytes
626 .checked_add(self.length_bytes)
627 .ok_or_else(|| super::VNextError::InvalidExecutionPlan {
628 reason: "source copy range overflows u64".to_owned(),
629 })?;
630 let destination_end = self
631 .destination_offset_bytes
632 .checked_add(self.length_bytes)
633 .ok_or_else(|| super::VNextError::InvalidExecutionPlan {
634 reason: "destination copy range overflows u64".to_owned(),
635 })?;
636 if source_end > source.size_bytes || destination_end > destination.size_bytes {
637 return Err(super::VNextError::InvalidExecutionPlan {
638 reason: "copy region exceeds a buffer boundary".to_owned(),
639 });
640 }
641 Ok(())
642 }
643
644 pub fn source_offset_bytes(self) -> u64 {
645 self.source_offset_bytes
646 }
647
648 pub fn destination_offset_bytes(self) -> u64 {
649 self.destination_offset_bytes
650 }
651
652 pub fn length_bytes(self) -> u64 {
653 self.length_bytes
654 }
655}
656
657#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
658#[serde(rename_all = "snake_case")]
659pub enum StreamState {
660 Ready,
661 Recording,
662 Submitted,
663 Failed,
664}
665
666#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
676#[repr(u8)]
677#[serde(rename_all = "snake_case")]
678pub enum DeviceTimingMode {
679 #[default]
680 Off = 0,
681 Completion = 1,
682 Replay = 2,
683 Kernel = 3,
684 Verification = 4,
685}
686
687impl DeviceTimingMode {
688 pub const fn completion_enabled(self) -> bool {
689 !matches!(self, Self::Off)
690 }
691
692 pub const fn physical_span_attribution_enabled(self) -> bool {
693 matches!(self, Self::Replay | Self::Kernel | Self::Verification)
694 }
695
696 pub const fn kernel_attribution_enabled(self) -> bool {
697 matches!(self, Self::Kernel | Self::Verification)
698 }
699
700 pub const fn direct_reusable_execution_allowed(self) -> bool {
701 !matches!(self, Self::Kernel | Self::Verification)
702 }
703}
704
705#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
706#[serde(rename_all = "snake_case")]
707pub enum DeviceExecutionPath {
708 Eager,
709 Replayed,
710}
711
712impl DeviceExecutionPath {
713 pub const fn as_str(self) -> &'static str {
714 match self {
715 Self::Eager => "eager",
716 Self::Replayed => "replayed",
717 }
718 }
719}
720
721#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
727#[serde(rename_all = "snake_case")]
728pub enum DeviceComputePathRequirement {
729 #[default]
730 Adaptive,
731 EagerOnly,
732 ReplayedOnly,
733 ReplayedWithDeclaredEagerBoundaries,
736}
737
738#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
744#[serde(rename_all = "snake_case")]
745pub enum DeviceSubmissionAttributionRequirement {
746 #[default]
747 None,
748 LogicalExecutionPath,
749}
750
751impl DeviceSubmissionAttributionRequirement {
752 pub const fn logical_execution_path_required(self) -> bool {
753 matches!(self, Self::LogicalExecutionPath)
754 }
755}
756
757#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
758#[serde(rename_all = "snake_case")]
759pub enum DeviceBatchingForm {
760 Scalar,
761 Packed,
762 ParticipantLoop,
763}
764
765impl DeviceBatchingForm {
766 pub const fn as_str(self) -> &'static str {
767 match self {
768 Self::Scalar => "scalar",
769 Self::Packed => "packed",
770 Self::ParticipantLoop => "participant_loop",
771 }
772 }
773}
774
775#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
782pub struct DeviceCommandLogicalWork {
783 batching_form: DeviceBatchingForm,
784 participant_start: u32,
785 participant_count: u32,
786 token_count: u64,
787}
788
789impl DeviceCommandLogicalWork {
790 pub fn new(
791 batching_form: DeviceBatchingForm,
792 participant_count: u32,
793 token_count: u64,
794 ) -> Result<Self, super::VNextError> {
795 Self::for_participant_range(batching_form, 0, participant_count, token_count)
796 }
797
798 pub fn for_participant_range(
799 batching_form: DeviceBatchingForm,
800 participant_start: u32,
801 participant_count: u32,
802 token_count: u64,
803 ) -> Result<Self, super::VNextError> {
804 if participant_count == 0 || participant_start.checked_add(participant_count).is_none() {
805 return Err(super::VNextError::InvalidExecutionPlan {
806 reason: "node-scoped device command has an empty or overflowing logical participant range"
807 .to_owned(),
808 });
809 }
810 Ok(Self {
811 batching_form,
812 participant_start,
813 participant_count,
814 token_count,
815 })
816 }
817
818 pub const fn batching_form(self) -> DeviceBatchingForm {
819 self.batching_form
820 }
821
822 pub const fn participant_start(self) -> u32 {
823 self.participant_start
824 }
825
826 pub const fn participant_count(self) -> u32 {
827 self.participant_count
828 }
829
830 pub const fn participant_end(self) -> u32 {
831 self.participant_start + self.participant_count
832 }
833
834 pub const fn token_count(self) -> u64 {
835 self.token_count
836 }
837}
838
839#[derive(Debug, Clone, Copy, PartialEq, Eq)]
842pub enum DeviceSubmissionStage {
843 ValidateAndPrepare,
844 BeginTiming,
845 EnqueueCommands,
846 RecordFenceAndAccount,
847}
848
849#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
855pub struct DeviceReusableExecutionObservation {
856 candidate_segments: u64,
857 captured_segments: u64,
858 uploaded_segments: u64,
859 cache_hit_segments: u64,
860 cached_rejected_segments: u64,
861 capture_rejected_segments: u64,
862 warmup_required_segments: u64,
863 quiescence_deferred_segments: u64,
864 capacity_deferred_segments: u64,
865 outside_preparation_segments: u64,
866 evicted_segments: u64,
867 replayed_segments: u64,
868 replayed_commands: u64,
869 eager_commands: u64,
870}
871
872impl DeviceReusableExecutionObservation {
873 pub fn observe_candidate_segment(&mut self) {
874 self.candidate_segments = self.candidate_segments.saturating_add(1);
875 }
876
877 pub fn observe_captured_segment(&mut self) {
878 self.captured_segments = self.captured_segments.saturating_add(1);
879 }
880
881 pub fn observe_uploaded_segment(&mut self) {
882 self.uploaded_segments = self.uploaded_segments.saturating_add(1);
883 }
884
885 pub fn observe_cache_hit_segment(&mut self) {
886 self.cache_hit_segments = self.cache_hit_segments.saturating_add(1);
887 }
888
889 pub fn observe_cached_rejected_segment(&mut self) {
890 self.cached_rejected_segments = self.cached_rejected_segments.saturating_add(1);
891 }
892
893 pub fn observe_capture_rejection(&mut self) {
894 self.capture_rejected_segments = self.capture_rejected_segments.saturating_add(1);
895 }
896
897 pub fn observe_quiescence_deferred_segment(&mut self) {
898 self.quiescence_deferred_segments = self.quiescence_deferred_segments.saturating_add(1);
899 }
900
901 pub fn observe_warmup_required_segment(&mut self) {
902 self.warmup_required_segments = self.warmup_required_segments.saturating_add(1);
903 }
904
905 pub fn observe_capacity_deferred_segment(&mut self) {
906 self.capacity_deferred_segments = self.capacity_deferred_segments.saturating_add(1);
907 }
908
909 pub fn observe_outside_preparation_segment(&mut self) {
910 self.outside_preparation_segments = self.outside_preparation_segments.saturating_add(1);
911 }
912
913 pub fn observe_evicted_segment(&mut self) {
914 self.evicted_segments = self.evicted_segments.saturating_add(1);
915 }
916
917 pub fn observe_replayed_segment(&mut self, command_count: usize) {
918 self.replayed_segments = self.replayed_segments.saturating_add(1);
919 self.replayed_commands = self
920 .replayed_commands
921 .saturating_add(u64::try_from(command_count).unwrap_or(u64::MAX));
922 }
923
924 pub fn observe_eager_command(&mut self) {
925 self.eager_commands = self.eager_commands.saturating_add(1);
926 }
927
928 pub const fn candidate_segments(self) -> u64 {
929 self.candidate_segments
930 }
931
932 pub const fn captured_segments(self) -> u64 {
933 self.captured_segments
934 }
935
936 pub const fn uploaded_segments(self) -> u64 {
937 self.uploaded_segments
938 }
939
940 pub const fn cache_hit_segments(self) -> u64 {
941 self.cache_hit_segments
942 }
943
944 pub const fn cached_rejected_segments(self) -> u64 {
945 self.cached_rejected_segments
946 }
947
948 pub const fn capture_rejected_segments(self) -> u64 {
949 self.capture_rejected_segments
950 }
951
952 pub const fn quiescence_deferred_segments(self) -> u64 {
953 self.quiescence_deferred_segments
954 }
955
956 pub const fn warmup_required_segments(self) -> u64 {
957 self.warmup_required_segments
958 }
959
960 pub const fn capacity_deferred_segments(self) -> u64 {
961 self.capacity_deferred_segments
962 }
963
964 pub const fn outside_preparation_segments(self) -> u64 {
965 self.outside_preparation_segments
966 }
967
968 pub const fn evicted_segments(self) -> u64 {
969 self.evicted_segments
970 }
971
972 pub const fn replayed_segments(self) -> u64 {
973 self.replayed_segments
974 }
975
976 pub const fn replayed_commands(self) -> u64 {
977 self.replayed_commands
978 }
979
980 pub const fn eager_commands(self) -> u64 {
981 self.eager_commands
982 }
983}
984
985#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
988pub struct DeviceReusableExecutionTrim {
989 released_executables: u64,
990 released_rejections: u64,
991}
992
993#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1000pub struct DeviceReusableExecutionPlan {
1001 maximum_executables: usize,
1002 #[serde(skip_serializing_if = "ReusableExecutionCatalogLifetime::is_startup_sealed")]
1003 catalog_lifetime: ReusableExecutionCatalogLifetime,
1004}
1005
1006impl DeviceReusableExecutionPlan {
1007 pub fn new(maximum_executables: usize) -> Result<Self, super::VNextError> {
1008 if maximum_executables == 0 {
1009 return Err(super::VNextError::InvalidExecutionPlan {
1010 reason: "reusable execution plan requires non-zero capacity".to_owned(),
1011 });
1012 }
1013 Ok(Self {
1014 maximum_executables,
1015 catalog_lifetime: ReusableExecutionCatalogLifetime::StartupSealed,
1016 })
1017 }
1018
1019 pub fn on_demand(maximum_executables: usize) -> Result<Self, super::VNextError> {
1020 let mut plan = Self::new(maximum_executables)?;
1021 plan.catalog_lifetime = ReusableExecutionCatalogLifetime::OnDemandBounded;
1022 Ok(plan)
1023 }
1024
1025 pub const fn catalog_lifetime(self) -> ReusableExecutionCatalogLifetime {
1026 self.catalog_lifetime
1027 }
1028
1029 pub const fn maximum_executables(self) -> usize {
1030 self.maximum_executables
1031 }
1032}
1033
1034#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
1040#[serde(transparent)]
1041pub struct DeviceReusableExecutionTopologyFingerprint([u8; 32]);
1042
1043impl DeviceReusableExecutionTopologyFingerprint {
1044 pub const fn from_sha256(bytes: [u8; 32]) -> Self {
1045 Self(bytes)
1046 }
1047
1048 pub const fn static_program() -> Self {
1049 Self([0; 32])
1050 }
1051
1052 pub const fn as_bytes(&self) -> &[u8; 32] {
1053 &self.0
1054 }
1055}
1056
1057#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
1064pub struct DeviceReusableExecutionProgramId {
1065 plan_hash: PlanHash,
1066 runtime_implementation_fingerprint: String,
1067 lane_id: ExecutionLaneId,
1068 bucket_id: ReusableExecutionBucketId,
1069 program_binding_layout_fingerprint: String,
1070 lane_stable_layout_fingerprint: String,
1071 lane_slot_id: u64,
1072 immediate_sequences: u32,
1073 immediate_tokens: u64,
1074 immediate_pages: u64,
1075 topology_fingerprint: DeviceReusableExecutionTopologyFingerprint,
1076}
1077
1078impl DeviceReusableExecutionProgramId {
1079 pub fn new(
1080 plan_hash: PlanHash,
1081 runtime_implementation_fingerprint: String,
1082 lane_id: ExecutionLaneId,
1083 bucket_id: ReusableExecutionBucketId,
1084 program_binding_layout_fingerprint: String,
1085 lane_stable_layout_fingerprint: String,
1086 lane_slot_id: u64,
1087 immediate_sequences: u32,
1088 immediate_tokens: u64,
1089 immediate_pages: u64,
1090 ) -> Result<Self, VNextError> {
1091 let is_sha256 = |value: &str| {
1092 let digest = value.strip_prefix("sha256/").unwrap_or(value);
1093 digest.len() == 64
1094 && digest
1095 .bytes()
1096 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
1097 };
1098 if !is_sha256(&runtime_implementation_fingerprint)
1099 || !is_sha256(&program_binding_layout_fingerprint)
1100 || !is_sha256(&lane_stable_layout_fingerprint)
1101 || immediate_sequences == 0
1102 || immediate_tokens == 0
1103 {
1104 return Err(VNextError::InvalidExecutionPlan {
1105 reason: "reusable execution program identity is incomplete or non-canonical"
1106 .to_owned(),
1107 });
1108 }
1109 Ok(Self {
1110 plan_hash,
1111 runtime_implementation_fingerprint,
1112 lane_id,
1113 bucket_id,
1114 program_binding_layout_fingerprint,
1115 lane_stable_layout_fingerprint,
1116 lane_slot_id,
1117 immediate_sequences,
1118 immediate_tokens,
1119 immediate_pages,
1120 topology_fingerprint: DeviceReusableExecutionTopologyFingerprint::static_program(),
1121 })
1122 }
1123
1124 pub fn with_topology_fingerprint(
1125 mut self,
1126 topology_fingerprint: DeviceReusableExecutionTopologyFingerprint,
1127 ) -> Self {
1128 self.topology_fingerprint = topology_fingerprint;
1129 self
1130 }
1131
1132 pub fn plan_hash(&self) -> &PlanHash {
1133 &self.plan_hash
1134 }
1135
1136 pub fn runtime_implementation_fingerprint(&self) -> &str {
1137 &self.runtime_implementation_fingerprint
1138 }
1139
1140 pub const fn lane_id(&self) -> ExecutionLaneId {
1141 self.lane_id
1142 }
1143
1144 pub fn bucket_id(&self) -> &ReusableExecutionBucketId {
1145 &self.bucket_id
1146 }
1147
1148 pub fn program_binding_layout_fingerprint(&self) -> &str {
1149 &self.program_binding_layout_fingerprint
1150 }
1151
1152 pub fn lane_stable_layout_fingerprint(&self) -> &str {
1153 &self.lane_stable_layout_fingerprint
1154 }
1155
1156 pub const fn lane_slot_id(&self) -> u64 {
1157 self.lane_slot_id
1158 }
1159
1160 pub const fn immediate_sequences(&self) -> u32 {
1161 self.immediate_sequences
1162 }
1163
1164 pub const fn immediate_tokens(&self) -> u64 {
1165 self.immediate_tokens
1166 }
1167
1168 pub const fn immediate_pages(&self) -> u64 {
1169 self.immediate_pages
1170 }
1171
1172 pub const fn topology_fingerprint(&self) -> DeviceReusableExecutionTopologyFingerprint {
1173 self.topology_fingerprint
1174 }
1175
1176 pub fn fingerprint(&self) -> String {
1180 const DOMAIN: &[u8] = b"ferrum.runtime-vnext.reusable-program-id.v1\0";
1181 let mut digest = Sha256::new();
1182 digest.update(DOMAIN);
1183 for value in [
1184 self.plan_hash.as_str(),
1185 self.runtime_implementation_fingerprint.as_str(),
1186 self.bucket_id.as_str(),
1187 self.program_binding_layout_fingerprint.as_str(),
1188 self.lane_stable_layout_fingerprint.as_str(),
1189 ] {
1190 digest.update(
1191 u64::try_from(value.len())
1192 .expect("validated reusable program identity strings fit u64")
1193 .to_le_bytes(),
1194 );
1195 digest.update(value.as_bytes());
1196 }
1197 digest.update(self.lane_id.get().to_le_bytes());
1198 digest.update(self.lane_slot_id.to_le_bytes());
1199 digest.update(self.immediate_sequences.to_le_bytes());
1200 digest.update(self.immediate_tokens.to_le_bytes());
1201 digest.update(self.immediate_pages.to_le_bytes());
1202 digest.update(self.topology_fingerprint.as_bytes());
1203 format!("{:x}", digest.finalize())
1204 }
1205}
1206
1207#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1211pub struct DeviceReusableExecutionCapture {
1212 program_id: DeviceReusableExecutionProgramId,
1213 node_count: u32,
1214 eager_boundary_node_indices: Box<[u32]>,
1215 per_wave_binding_node_indices: Box<[u32]>,
1216}
1217
1218impl DeviceReusableExecutionCapture {
1219 pub fn new(
1220 program_id: DeviceReusableExecutionProgramId,
1221 node_count: u32,
1222 eager_boundary_node_indices: Vec<u32>,
1223 mut per_wave_binding_node_indices: Vec<u32>,
1224 ) -> Result<Self, VNextError> {
1225 if node_count == 0
1226 || eager_boundary_node_indices
1227 .windows(2)
1228 .any(|pair| pair[0] >= pair[1])
1229 || eager_boundary_node_indices
1230 .iter()
1231 .any(|node_index| *node_index >= node_count)
1232 {
1233 return Err(VNextError::InvalidExecutionPlan {
1234 reason: "reusable execution capture topology is empty or non-canonical".to_owned(),
1235 });
1236 }
1237 per_wave_binding_node_indices.sort_unstable();
1238 per_wave_binding_node_indices.dedup();
1239 if per_wave_binding_node_indices
1240 .iter()
1241 .any(|node_index| *node_index >= node_count)
1242 {
1243 return Err(VNextError::InvalidExecutionPlan {
1244 reason: "reusable execution binding node is outside the captured topology"
1245 .to_owned(),
1246 });
1247 }
1248 Ok(Self {
1249 program_id,
1250 node_count,
1251 eager_boundary_node_indices: eager_boundary_node_indices.into_boxed_slice(),
1252 per_wave_binding_node_indices: per_wave_binding_node_indices.into_boxed_slice(),
1253 })
1254 }
1255
1256 pub fn program_id(&self) -> &DeviceReusableExecutionProgramId {
1257 &self.program_id
1258 }
1259
1260 pub const fn node_count(&self) -> u32 {
1261 self.node_count
1262 }
1263
1264 pub fn eager_boundary_node_indices(&self) -> &[u32] {
1265 &self.eager_boundary_node_indices
1266 }
1267
1268 pub fn per_wave_binding_node_indices(&self) -> &[u32] {
1269 &self.per_wave_binding_node_indices
1270 }
1271}
1272
1273#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1277#[serde(rename_all = "snake_case")]
1278pub enum DeviceReusableExecutionProgramGapReason {
1279 MissingComputeCommand,
1280 ProviderReplayKeyMissing,
1281 ReusableAddressScopeMissing,
1282 ReusableAddressScopeConflict,
1283 CaptureRejected,
1284 CachedCaptureRejected,
1285 WarmupRequired,
1286 QuiescenceDeferred,
1287 CapacityDeferred,
1288 Evicted,
1289 OutsidePreparation,
1290}
1291
1292impl DeviceReusableExecutionProgramGapReason {
1293 pub const fn as_str(self) -> &'static str {
1294 match self {
1295 Self::MissingComputeCommand => "missing_compute_command",
1296 Self::ProviderReplayKeyMissing => "provider_replay_key_missing",
1297 Self::ReusableAddressScopeMissing => "reusable_address_scope_missing",
1298 Self::ReusableAddressScopeConflict => "reusable_address_scope_conflict",
1299 Self::CaptureRejected => "capture_rejected",
1300 Self::CachedCaptureRejected => "cached_capture_rejected",
1301 Self::WarmupRequired => "warmup_required",
1302 Self::QuiescenceDeferred => "quiescence_deferred",
1303 Self::CapacityDeferred => "capacity_deferred",
1304 Self::Evicted => "evicted",
1305 Self::OutsidePreparation => "outside_preparation",
1306 }
1307 }
1308}
1309
1310#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1311#[serde(rename_all = "snake_case")]
1312pub enum DeviceReusableExecutionProgramState {
1313 Partial,
1314 DeterminismReady,
1315}
1316
1317#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1319pub struct DeviceReusableExecutionProgramGap {
1320 node_index: u32,
1321 reason: DeviceReusableExecutionProgramGapReason,
1322}
1323
1324impl DeviceReusableExecutionProgramGap {
1325 pub const fn new(node_index: u32, reason: DeviceReusableExecutionProgramGapReason) -> Self {
1326 Self { node_index, reason }
1327 }
1328
1329 pub const fn node_index(self) -> u32 {
1330 self.node_index
1331 }
1332
1333 pub const fn reason(self) -> DeviceReusableExecutionProgramGapReason {
1334 self.reason
1335 }
1336}
1337
1338#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1340pub struct DeviceReusableExecutionSegment {
1341 ordinal: u32,
1342 start_node_index: u32,
1343 end_node_index: u32,
1344 logical_command_count: u32,
1345}
1346
1347impl DeviceReusableExecutionSegment {
1348 pub fn new(
1349 ordinal: u32,
1350 start_node_index: u32,
1351 end_node_index: u32,
1352 logical_command_count: u32,
1353 ) -> Result<Self, VNextError> {
1354 if end_node_index <= start_node_index || logical_command_count == 0 {
1355 return Err(VNextError::InvalidExecutionPlan {
1356 reason: "reusable execution segment is empty".to_owned(),
1357 });
1358 }
1359 Ok(Self {
1360 ordinal,
1361 start_node_index,
1362 end_node_index,
1363 logical_command_count,
1364 })
1365 }
1366
1367 pub const fn ordinal(&self) -> u32 {
1368 self.ordinal
1369 }
1370
1371 pub const fn start_node_index(&self) -> u32 {
1372 self.start_node_index
1373 }
1374
1375 pub const fn end_node_index(&self) -> u32 {
1376 self.end_node_index
1377 }
1378
1379 pub const fn logical_command_count(&self) -> u32 {
1380 self.logical_command_count
1381 }
1382
1383 pub const fn contains_node(&self, node_index: u32) -> bool {
1384 node_index >= self.start_node_index && node_index < self.end_node_index
1385 }
1386}
1387
1388#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1393pub struct DeviceReusableExecutionProgram {
1394 program_id: DeviceReusableExecutionProgramId,
1395 node_count: u32,
1396 eager_boundary_node_indices: Box<[u32]>,
1397 segments: Box<[DeviceReusableExecutionSegment]>,
1398 per_wave_binding_node_indices: Box<[u32]>,
1399 gaps: Box<[DeviceReusableExecutionProgramGap]>,
1400}
1401
1402impl DeviceReusableExecutionProgram {
1403 pub fn new(
1404 capture: &DeviceReusableExecutionCapture,
1405 segments: Vec<DeviceReusableExecutionSegment>,
1406 mut per_wave_binding_node_indices: Vec<u32>,
1407 gaps: Vec<DeviceReusableExecutionProgramGap>,
1408 ) -> Result<Self, VNextError> {
1409 if (segments.is_empty() && gaps.is_empty())
1410 || segments
1411 .iter()
1412 .enumerate()
1413 .any(|(ordinal, segment)| segment.ordinal() as usize != ordinal)
1414 || segments
1415 .windows(2)
1416 .any(|pair| pair[0].end_node_index() > pair[1].start_node_index())
1417 {
1418 return Err(VNextError::InvalidExecutionPlan {
1419 reason: "reusable execution program segments are empty, unordered, or overlap"
1420 .to_owned(),
1421 });
1422 }
1423 let node_count = capture.node_count() as usize;
1424 let mut coverage = vec![0_u8; node_count];
1425 for node_index in capture.eager_boundary_node_indices() {
1426 coverage[*node_index as usize] = 1;
1427 }
1428 for segment in &segments {
1429 let start = segment.start_node_index() as usize;
1430 let end = segment.end_node_index() as usize;
1431 let Some(range) = coverage.get_mut(start..end) else {
1432 return Err(VNextError::InvalidExecutionPlan {
1433 reason: "reusable execution segment is outside the captured topology"
1434 .to_owned(),
1435 });
1436 };
1437 if range.iter().any(|marker| *marker != 0) {
1438 return Err(VNextError::InvalidExecutionPlan {
1439 reason: "reusable execution segment overlaps an eager boundary or another classification"
1440 .to_owned(),
1441 });
1442 }
1443 range.fill(2);
1444 }
1445 if gaps
1446 .windows(2)
1447 .any(|pair| pair[0].node_index() >= pair[1].node_index())
1448 {
1449 return Err(VNextError::InvalidExecutionPlan {
1450 reason: "reusable execution program gaps are unordered or duplicated".to_owned(),
1451 });
1452 }
1453 for gap in &gaps {
1454 let Some(marker) = coverage.get_mut(gap.node_index() as usize) else {
1455 return Err(VNextError::InvalidExecutionPlan {
1456 reason: "reusable execution program gap is outside the captured topology"
1457 .to_owned(),
1458 });
1459 };
1460 if *marker != 0 {
1461 return Err(VNextError::InvalidExecutionPlan {
1462 reason: "reusable execution program gap overlaps a resident segment or eager boundary"
1463 .to_owned(),
1464 });
1465 }
1466 *marker = 3;
1467 }
1468 if coverage.iter().any(|marker| *marker == 0) {
1469 return Err(VNextError::InvalidExecutionPlan {
1470 reason: "reusable execution program does not classify every captured topology node"
1471 .to_owned(),
1472 });
1473 }
1474 per_wave_binding_node_indices.sort_unstable();
1475 per_wave_binding_node_indices.dedup();
1476 if per_wave_binding_node_indices.iter().any(|node_index| {
1477 !segments
1478 .iter()
1479 .any(|segment| segment.contains_node(*node_index))
1480 }) {
1481 return Err(VNextError::InvalidExecutionPlan {
1482 reason: "reusable execution binding node is outside every resident segment"
1483 .to_owned(),
1484 });
1485 }
1486 Ok(Self {
1487 program_id: capture.program_id().clone(),
1488 node_count: capture.node_count(),
1489 eager_boundary_node_indices: capture
1490 .eager_boundary_node_indices()
1491 .to_vec()
1492 .into_boxed_slice(),
1493 segments: segments.into_boxed_slice(),
1494 per_wave_binding_node_indices: per_wave_binding_node_indices.into_boxed_slice(),
1495 gaps: gaps.into_boxed_slice(),
1496 })
1497 }
1498
1499 pub fn program_id(&self) -> &DeviceReusableExecutionProgramId {
1500 &self.program_id
1501 }
1502
1503 pub const fn node_count(&self) -> u32 {
1504 self.node_count
1505 }
1506
1507 pub fn eager_boundary_node_indices(&self) -> &[u32] {
1508 &self.eager_boundary_node_indices
1509 }
1510
1511 pub fn segments(&self) -> &[DeviceReusableExecutionSegment] {
1512 &self.segments
1513 }
1514
1515 pub fn per_wave_binding_node_indices(&self) -> &[u32] {
1516 &self.per_wave_binding_node_indices
1517 }
1518
1519 pub fn gaps(&self) -> &[DeviceReusableExecutionProgramGap] {
1520 &self.gaps
1521 }
1522
1523 pub const fn state(&self) -> DeviceReusableExecutionProgramState {
1524 if self.gaps.is_empty() {
1525 DeviceReusableExecutionProgramState::DeterminismReady
1526 } else {
1527 DeviceReusableExecutionProgramState::Partial
1528 }
1529 }
1530
1531 pub fn has_resident_segments(&self) -> bool {
1532 !self.segments.is_empty()
1533 }
1534
1535 pub fn is_determinism_ready(&self) -> bool {
1536 self.state() == DeviceReusableExecutionProgramState::DeterminismReady
1537 }
1538}
1539
1540#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1542pub struct DeviceReusableExecutionInvocation {
1543 program_id: DeviceReusableExecutionProgramId,
1544 segment: DeviceReusableExecutionSegment,
1545 participant_count: u32,
1546 token_count: u64,
1547}
1548
1549impl DeviceReusableExecutionInvocation {
1550 pub fn new(
1551 program_id: DeviceReusableExecutionProgramId,
1552 segment: DeviceReusableExecutionSegment,
1553 participant_count: u32,
1554 token_count: u64,
1555 ) -> Result<Self, VNextError> {
1556 if participant_count == 0
1557 || token_count == 0
1558 || program_id.immediate_sequences() != participant_count
1559 || program_id.immediate_tokens() != token_count
1560 {
1561 return Err(VNextError::InvalidExecutionPlan {
1562 reason: "reusable execution invocation differs from its program work shape"
1563 .to_owned(),
1564 });
1565 }
1566 Ok(Self {
1567 program_id,
1568 segment,
1569 participant_count,
1570 token_count,
1571 })
1572 }
1573
1574 pub fn program_id(&self) -> &DeviceReusableExecutionProgramId {
1575 &self.program_id
1576 }
1577
1578 pub const fn segment(&self) -> &DeviceReusableExecutionSegment {
1579 &self.segment
1580 }
1581
1582 pub const fn participant_count(&self) -> u32 {
1583 self.participant_count
1584 }
1585
1586 pub const fn token_count(&self) -> u64 {
1587 self.token_count
1588 }
1589}
1590
1591#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1592#[serde(rename_all = "snake_case")]
1593pub enum DeviceReusableExecutionPreparationState {
1594 Unsupported,
1595 Preparing,
1596 Ready,
1597}
1598
1599#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1606pub struct DeviceReusableExecutionPreparation {
1607 state: DeviceReusableExecutionPreparationState,
1608 maximum_executables: u64,
1609 resident_executables: u64,
1610 rejected_executables: u64,
1611 captured_executables: u64,
1612 uploaded_executables: u64,
1613 capacity_deferred_executables: u64,
1614}
1615
1616impl DeviceReusableExecutionPreparation {
1617 pub const fn unsupported() -> Self {
1618 Self {
1619 state: DeviceReusableExecutionPreparationState::Unsupported,
1620 maximum_executables: 0,
1621 resident_executables: 0,
1622 rejected_executables: 0,
1623 captured_executables: 0,
1624 uploaded_executables: 0,
1625 capacity_deferred_executables: 0,
1626 }
1627 }
1628
1629 pub fn preparing(plan: DeviceReusableExecutionPlan) -> Self {
1630 Self {
1631 state: DeviceReusableExecutionPreparationState::Preparing,
1632 maximum_executables: u64::try_from(plan.maximum_executables()).unwrap_or(u64::MAX),
1633 ..Self::unsupported()
1634 }
1635 }
1636
1637 pub fn preparing_with_progress(
1638 plan: DeviceReusableExecutionPlan,
1639 resident_executables: usize,
1640 rejected_executables: usize,
1641 captured_executables: u64,
1642 uploaded_executables: u64,
1643 capacity_deferred_executables: u64,
1644 ) -> Result<Self, super::VNextError> {
1645 Self::with_progress(
1646 DeviceReusableExecutionPreparationState::Preparing,
1647 plan,
1648 resident_executables,
1649 rejected_executables,
1650 captured_executables,
1651 uploaded_executables,
1652 capacity_deferred_executables,
1653 )
1654 }
1655
1656 pub fn ready(
1657 plan: DeviceReusableExecutionPlan,
1658 resident_executables: usize,
1659 rejected_executables: usize,
1660 captured_executables: u64,
1661 uploaded_executables: u64,
1662 capacity_deferred_executables: u64,
1663 ) -> Result<Self, super::VNextError> {
1664 Self::with_progress(
1665 DeviceReusableExecutionPreparationState::Ready,
1666 plan,
1667 resident_executables,
1668 rejected_executables,
1669 captured_executables,
1670 uploaded_executables,
1671 capacity_deferred_executables,
1672 )
1673 }
1674
1675 fn with_progress(
1676 state: DeviceReusableExecutionPreparationState,
1677 plan: DeviceReusableExecutionPlan,
1678 resident_executables: usize,
1679 rejected_executables: usize,
1680 captured_executables: u64,
1681 uploaded_executables: u64,
1682 capacity_deferred_executables: u64,
1683 ) -> Result<Self, super::VNextError> {
1684 if resident_executables > plan.maximum_executables()
1685 || uploaded_executables < u64::try_from(resident_executables).unwrap_or(u64::MAX)
1686 || captured_executables < uploaded_executables
1687 {
1688 return Err(super::VNextError::InvalidExecutionPlan {
1689 reason: "reusable execution preparation receipt is internally inconsistent"
1690 .to_owned(),
1691 });
1692 }
1693 Ok(Self {
1694 state,
1695 maximum_executables: u64::try_from(plan.maximum_executables()).unwrap_or(u64::MAX),
1696 resident_executables: u64::try_from(resident_executables).unwrap_or(u64::MAX),
1697 rejected_executables: u64::try_from(rejected_executables).unwrap_or(u64::MAX),
1698 captured_executables,
1699 uploaded_executables,
1700 capacity_deferred_executables,
1701 })
1702 }
1703
1704 pub const fn state(self) -> DeviceReusableExecutionPreparationState {
1705 self.state
1706 }
1707
1708 pub const fn maximum_executables(self) -> u64 {
1709 self.maximum_executables
1710 }
1711
1712 pub const fn resident_executables(self) -> u64 {
1713 self.resident_executables
1714 }
1715
1716 pub const fn rejected_executables(self) -> u64 {
1717 self.rejected_executables
1718 }
1719
1720 pub const fn captured_executables(self) -> u64 {
1721 self.captured_executables
1722 }
1723
1724 pub const fn uploaded_executables(self) -> u64 {
1725 self.uploaded_executables
1726 }
1727
1728 pub const fn capacity_deferred_executables(self) -> u64 {
1729 self.capacity_deferred_executables
1730 }
1731}
1732
1733impl DeviceReusableExecutionTrim {
1734 pub fn new(released_executables: usize, released_rejections: usize) -> Self {
1735 Self {
1736 released_executables: u64::try_from(released_executables).unwrap_or(u64::MAX),
1737 released_rejections: u64::try_from(released_rejections).unwrap_or(u64::MAX),
1738 }
1739 }
1740
1741 pub const fn released_executables(self) -> u64 {
1742 self.released_executables
1743 }
1744
1745 pub const fn released_rejections(self) -> u64 {
1746 self.released_rejections
1747 }
1748}
1749
1750pub trait DeviceSubmissionTimingSink: Send + Sync {
1757 const ENABLED: bool;
1758
1759 fn record_device_submission(&self, stage: DeviceSubmissionStage, elapsed: Duration);
1760
1761 fn record_reusable_execution(&self, _observation: DeviceReusableExecutionObservation) {}
1762}
1763
1764pub struct DisabledDeviceSubmissionTimingSink;
1765
1766impl DeviceSubmissionTimingSink for DisabledDeviceSubmissionTimingSink {
1767 const ENABLED: bool = false;
1768
1769 fn record_device_submission(&self, _stage: DeviceSubmissionStage, _elapsed: Duration) {
1770 unreachable!("disabled device submission timing cannot record")
1771 }
1772}
1773
1774#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1775#[serde(rename_all = "snake_case")]
1776pub enum DeviceTimingUnavailableReason {
1777 BackendUnsupported,
1778 BackendMeasurementFailed,
1779 DurationOverflow,
1780}
1781
1782#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1783#[serde(rename_all = "snake_case", tag = "status", content = "detail")]
1784pub enum DeviceTimingMeasurement<T> {
1785 NotRequested,
1786 Measured(T),
1787 Unavailable(DeviceTimingUnavailableReason),
1788}
1789
1790impl<T> DeviceTimingMeasurement<T> {
1791 pub const fn measured(&self) -> Option<&T> {
1792 match self {
1793 Self::Measured(measured) => Some(measured),
1794 Self::NotRequested | Self::Unavailable(_) => None,
1795 }
1796 }
1797}
1798
1799#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1800#[serde(rename_all = "snake_case")]
1801pub enum DeviceTimingClock {
1802 DeviceEventElapsed,
1803}
1804
1805#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1806pub struct DeviceExecutionTiming {
1807 elapsed_ns: u64,
1808 clock: DeviceTimingClock,
1809}
1810
1811impl DeviceExecutionTiming {
1812 pub const fn device_event_elapsed(elapsed_ns: u64) -> Self {
1813 Self {
1814 elapsed_ns,
1815 clock: DeviceTimingClock::DeviceEventElapsed,
1816 }
1817 }
1818
1819 pub const fn elapsed_ns(self) -> u64 {
1820 self.elapsed_ns
1821 }
1822
1823 pub const fn clock(self) -> DeviceTimingClock {
1824 self.clock
1825 }
1826}
1827
1828#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1832#[serde(rename_all = "snake_case")]
1833pub enum DeviceExecutionIntervalKind {
1834 Compute,
1835 Transfer,
1836}
1837
1838impl DeviceExecutionIntervalKind {
1839 pub const fn as_str(self) -> &'static str {
1840 match self {
1841 Self::Compute => "compute",
1842 Self::Transfer => "transfer",
1843 }
1844 }
1845}
1846
1847#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1848pub struct DeviceExecutionInterval {
1849 kind: DeviceExecutionIntervalKind,
1850 start_offset_ns: u64,
1851 end_offset_ns: u64,
1852 subwork_id: Option<&'static str>,
1853}
1854
1855impl DeviceExecutionInterval {
1856 pub fn new(
1857 kind: DeviceExecutionIntervalKind,
1858 start_offset_ns: u64,
1859 end_offset_ns: u64,
1860 ) -> Option<Self> {
1861 (end_offset_ns > start_offset_ns).then_some(Self {
1862 kind,
1863 start_offset_ns,
1864 end_offset_ns,
1865 subwork_id: None,
1866 })
1867 }
1868
1869 pub fn new_labeled(
1870 kind: DeviceExecutionIntervalKind,
1871 start_offset_ns: u64,
1872 end_offset_ns: u64,
1873 subwork_id: &'static str,
1874 ) -> Option<Self> {
1875 (!subwork_id.is_empty() && end_offset_ns > start_offset_ns).then_some(Self {
1876 kind,
1877 start_offset_ns,
1878 end_offset_ns,
1879 subwork_id: Some(subwork_id),
1880 })
1881 }
1882
1883 pub const fn kind(self) -> DeviceExecutionIntervalKind {
1884 self.kind
1885 }
1886
1887 pub const fn start_offset_ns(self) -> u64 {
1888 self.start_offset_ns
1889 }
1890
1891 pub const fn end_offset_ns(self) -> u64 {
1892 self.end_offset_ns
1893 }
1894
1895 pub const fn subwork_id(self) -> Option<&'static str> {
1896 self.subwork_id
1897 }
1898
1899 pub const fn elapsed_ns(self) -> u64 {
1900 self.end_offset_ns - self.start_offset_ns
1901 }
1902}
1903
1904#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1908pub struct DeviceCommandExecutionTiming {
1909 command_index: u32,
1910 intervals: Box<[DeviceExecutionInterval]>,
1911 elapsed_ns: u64,
1912}
1913
1914impl DeviceCommandExecutionTiming {
1915 pub fn new(command_index: u32, intervals: Vec<DeviceExecutionInterval>) -> Option<Self> {
1916 if intervals.is_empty()
1917 || intervals
1918 .windows(2)
1919 .any(|pair| pair[0].end_offset_ns() > pair[1].start_offset_ns())
1920 {
1921 return None;
1922 }
1923 let elapsed_ns = intervals.iter().try_fold(0_u64, |total, interval| {
1924 total.checked_add(interval.elapsed_ns())
1925 })?;
1926 Some(Self {
1927 command_index,
1928 intervals: intervals.into_boxed_slice(),
1929 elapsed_ns,
1930 })
1931 }
1932
1933 pub const fn command_index(&self) -> u32 {
1934 self.command_index
1935 }
1936
1937 pub fn intervals(&self) -> &[DeviceExecutionInterval] {
1938 &self.intervals
1939 }
1940
1941 pub fn elapsed_ns(&self) -> u64 {
1942 self.elapsed_ns
1943 }
1944}
1945
1946#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1947#[serde(rename_all = "snake_case")]
1948pub enum DeviceExecutionSpanKind {
1949 EagerCommand,
1950 ReusableExecutable,
1951}
1952
1953impl DeviceExecutionSpanKind {
1954 pub const fn as_str(self) -> &'static str {
1955 match self {
1956 Self::EagerCommand => "eager_command",
1957 Self::ReusableExecutable => "reusable_executable",
1958 }
1959 }
1960}
1961
1962#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1963#[serde(rename_all = "snake_case", tag = "status", content = "detail")]
1964pub enum DeviceExecutionSpanMeasurement {
1965 Measured {
1966 intervals: Box<[DeviceExecutionInterval]>,
1967 elapsed_ns: u64,
1968 },
1969 Unavailable(DeviceTimingUnavailableReason),
1970}
1971
1972impl DeviceExecutionSpanMeasurement {
1973 pub fn measured(intervals: Vec<DeviceExecutionInterval>) -> Option<Self> {
1974 if intervals.is_empty()
1975 || intervals
1976 .windows(2)
1977 .any(|pair| pair[0].end_offset_ns() > pair[1].start_offset_ns())
1978 {
1979 return None;
1980 }
1981 let elapsed_ns = intervals.iter().try_fold(0_u64, |total, interval| {
1982 total.checked_add(interval.elapsed_ns())
1983 })?;
1984 Some(Self::Measured {
1985 intervals: intervals.into_boxed_slice(),
1986 elapsed_ns,
1987 })
1988 }
1989
1990 pub const fn unavailable(reason: DeviceTimingUnavailableReason) -> Self {
1991 Self::Unavailable(reason)
1992 }
1993
1994 pub fn intervals(&self) -> Option<&[DeviceExecutionInterval]> {
1995 match self {
1996 Self::Measured { intervals, .. } => Some(intervals),
1997 Self::Unavailable(_) => None,
1998 }
1999 }
2000
2001 pub const fn elapsed_ns(&self) -> Option<u64> {
2002 match self {
2003 Self::Measured { elapsed_ns, .. } => Some(*elapsed_ns),
2004 Self::Unavailable(_) => None,
2005 }
2006 }
2007
2008 pub const fn unavailable_reason(&self) -> Option<DeviceTimingUnavailableReason> {
2009 match self {
2010 Self::Measured { .. } => None,
2011 Self::Unavailable(reason) => Some(*reason),
2012 }
2013 }
2014}
2015
2016#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
2022pub struct DeviceSubmissionExecutionSpan {
2023 start_command_index: u32,
2024 end_command_index: u32,
2025 kind: DeviceExecutionSpanKind,
2026 measurement: DeviceExecutionSpanMeasurement,
2027 #[serde(skip_serializing_if = "Option::is_none")]
2028 reusable_executable_fingerprint: Option<Box<str>>,
2029}
2030
2031impl DeviceSubmissionExecutionSpan {
2032 pub fn measured(
2033 start_command_index: u32,
2034 end_command_index: u32,
2035 kind: DeviceExecutionSpanKind,
2036 intervals: Vec<DeviceExecutionInterval>,
2037 ) -> Option<Self> {
2038 let measurement = DeviceExecutionSpanMeasurement::measured(intervals)?;
2039 Self::new(start_command_index, end_command_index, kind, measurement)
2040 }
2041
2042 pub fn unavailable(
2043 start_command_index: u32,
2044 end_command_index: u32,
2045 kind: DeviceExecutionSpanKind,
2046 reason: DeviceTimingUnavailableReason,
2047 ) -> Option<Self> {
2048 Self::new(
2049 start_command_index,
2050 end_command_index,
2051 kind,
2052 DeviceExecutionSpanMeasurement::unavailable(reason),
2053 )
2054 }
2055
2056 fn new(
2057 start_command_index: u32,
2058 end_command_index: u32,
2059 kind: DeviceExecutionSpanKind,
2060 measurement: DeviceExecutionSpanMeasurement,
2061 ) -> Option<Self> {
2062 if end_command_index <= start_command_index
2063 || (kind == DeviceExecutionSpanKind::EagerCommand
2064 && end_command_index != start_command_index.checked_add(1)?)
2065 {
2066 return None;
2067 }
2068 Some(Self {
2069 start_command_index,
2070 end_command_index,
2071 kind,
2072 measurement,
2073 reusable_executable_fingerprint: None,
2074 })
2075 }
2076
2077 pub fn with_reusable_executable_fingerprint(mut self, fingerprint: String) -> Option<Self> {
2078 if self.kind != DeviceExecutionSpanKind::ReusableExecutable
2079 || fingerprint.len() != 64
2080 || !fingerprint
2081 .bytes()
2082 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
2083 {
2084 return None;
2085 }
2086 self.reusable_executable_fingerprint = Some(fingerprint.into_boxed_str());
2087 Some(self)
2088 }
2089
2090 fn from_command(command: DeviceCommandExecutionTiming) -> Option<Self> {
2091 let end_command_index = command.command_index.checked_add(1)?;
2092 Self::measured(
2093 command.command_index,
2094 end_command_index,
2095 DeviceExecutionSpanKind::EagerCommand,
2096 command.intervals.into_vec(),
2097 )
2098 }
2099
2100 pub const fn start_command_index(&self) -> u32 {
2101 self.start_command_index
2102 }
2103
2104 pub const fn end_command_index(&self) -> u32 {
2105 self.end_command_index
2106 }
2107
2108 pub const fn command_count(&self) -> u32 {
2109 self.end_command_index - self.start_command_index
2110 }
2111
2112 pub const fn kind(&self) -> DeviceExecutionSpanKind {
2113 self.kind
2114 }
2115
2116 pub const fn measurement(&self) -> &DeviceExecutionSpanMeasurement {
2117 &self.measurement
2118 }
2119
2120 pub fn reusable_executable_fingerprint(&self) -> Option<&str> {
2121 self.reusable_executable_fingerprint.as_deref()
2122 }
2123
2124 pub const fn contains_command(&self, command_index: u32) -> bool {
2125 command_index >= self.start_command_index && command_index < self.end_command_index
2126 }
2127}
2128
2129#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
2133pub struct DeviceSubmissionExecutionTiming {
2134 command_count: u32,
2135 spans: Box<[DeviceSubmissionExecutionSpan]>,
2136}
2137
2138impl DeviceSubmissionExecutionTiming {
2139 pub fn new(commands: Vec<DeviceCommandExecutionTiming>) -> Option<Self> {
2140 let command_count = commands.last()?.command_index().checked_add(1)?;
2141 let spans = commands
2142 .into_iter()
2143 .map(DeviceSubmissionExecutionSpan::from_command)
2144 .collect::<Option<Vec<_>>>()?;
2145 Self::from_spans(command_count, spans)
2146 }
2147
2148 pub fn from_spans(
2149 command_count: u32,
2150 spans: Vec<DeviceSubmissionExecutionSpan>,
2151 ) -> Option<Self> {
2152 if command_count == 0 || spans.is_empty() {
2153 return None;
2154 }
2155 let mut expected_start = 0_u32;
2156 for span in &spans {
2157 if span.start_command_index() != expected_start
2158 || span.end_command_index() > command_count
2159 {
2160 return None;
2161 }
2162 expected_start = span.end_command_index();
2163 }
2164 if expected_start != command_count {
2165 return None;
2166 }
2167 Some(Self {
2168 command_count,
2169 spans: spans.into_boxed_slice(),
2170 })
2171 }
2172
2173 pub const fn command_count(&self) -> u32 {
2174 self.command_count
2175 }
2176
2177 pub fn spans(&self) -> &[DeviceSubmissionExecutionSpan] {
2178 &self.spans
2179 }
2180
2181 pub fn span_for_command(&self, command_index: u32) -> Option<&DeviceSubmissionExecutionSpan> {
2182 let index = self
2183 .spans
2184 .partition_point(|span| span.end_command_index() <= command_index);
2185 self.spans
2186 .get(index)
2187 .filter(|span| span.contains_command(command_index))
2188 }
2189}
2190
2191#[derive(Debug, Serialize)]
2194#[must_use = "a device terminal receipt owns exact fence timing evidence"]
2195pub struct DeviceTerminalReceipt<E> {
2196 terminal: DeviceTerminal<E>,
2197 execution_timing: DeviceTimingMeasurement<DeviceExecutionTiming>,
2198 submission_timing: DeviceTimingMeasurement<DeviceSubmissionExecutionTiming>,
2199}
2200
2201impl<E> DeviceTerminalReceipt<E> {
2202 pub fn unprofiled(terminal: DeviceTerminal<E>) -> Self {
2203 Self {
2204 terminal,
2205 execution_timing: DeviceTimingMeasurement::NotRequested,
2206 submission_timing: DeviceTimingMeasurement::NotRequested,
2207 }
2208 }
2209
2210 pub fn profiled(
2211 terminal: DeviceTerminal<E>,
2212 execution_timing: DeviceTimingMeasurement<DeviceExecutionTiming>,
2213 ) -> Self {
2214 Self {
2215 terminal,
2216 execution_timing,
2217 submission_timing: DeviceTimingMeasurement::NotRequested,
2218 }
2219 }
2220
2221 pub fn profiled_with_submission_timing(
2222 terminal: DeviceTerminal<E>,
2223 execution_timing: DeviceTimingMeasurement<DeviceExecutionTiming>,
2224 submission_timing: DeviceTimingMeasurement<DeviceSubmissionExecutionTiming>,
2225 ) -> Self {
2226 Self {
2227 terminal,
2228 execution_timing,
2229 submission_timing,
2230 }
2231 }
2232
2233 pub const fn terminal(&self) -> &DeviceTerminal<E> {
2234 &self.terminal
2235 }
2236
2237 pub const fn execution_timing(&self) -> &DeviceTimingMeasurement<DeviceExecutionTiming> {
2238 &self.execution_timing
2239 }
2240
2241 pub const fn submission_timing(
2242 &self,
2243 ) -> &DeviceTimingMeasurement<DeviceSubmissionExecutionTiming> {
2244 &self.submission_timing
2245 }
2246
2247 pub fn into_parts(
2248 self,
2249 ) -> (
2250 DeviceTerminal<E>,
2251 DeviceTimingMeasurement<DeviceExecutionTiming>,
2252 DeviceTimingMeasurement<DeviceSubmissionExecutionTiming>,
2253 ) {
2254 (self.terminal, self.execution_timing, self.submission_timing)
2255 }
2256}
2257
2258#[derive(Debug, Serialize)]
2264#[must_use = "a definitely-not-submitted failure owns the only safe retry classification"]
2265pub struct DefinitelyNotSubmitted<E> {
2266 error: E,
2267}
2268
2269impl<E> DefinitelyNotSubmitted<E> {
2270 pub fn new(error: E) -> Self {
2271 Self { error }
2272 }
2273
2274 pub fn error(&self) -> &E {
2275 &self.error
2276 }
2277
2278 pub fn into_error(self) -> E {
2279 self.error
2280 }
2281}
2282
2283#[derive(Debug, Serialize)]
2286#[serde(rename_all = "snake_case", tag = "status", content = "error")]
2287#[must_use = "a device terminal determines whether in-flight resources are release-safe"]
2288pub enum DeviceTerminal<E> {
2289 Succeeded,
2290 FailedButQuiescent(E),
2291}
2292
2293impl<E> DeviceTerminal<E> {
2294 pub const fn is_succeeded(&self) -> bool {
2295 matches!(self, Self::Succeeded)
2296 }
2297}
2298
2299#[derive(Debug, Serialize)]
2302#[serde(rename_all = "snake_case", tag = "status", content = "detail")]
2303#[must_use = "a fence query must preserve pending or indeterminate ownership"]
2304pub enum FenceQuery<E> {
2305 Pending,
2306 Terminal(DeviceTerminalReceipt<E>),
2307 Indeterminate(E),
2308}
2309
2310impl<E> FenceQuery<E> {
2311 pub const fn is_pending(&self) -> bool {
2312 matches!(self, Self::Pending)
2313 }
2314}
2315
2316#[derive(Debug, Serialize)]
2319#[must_use = "an indeterminate fence retains recovery and quarantine ownership"]
2320pub struct FenceIndeterminate<E> {
2321 error: E,
2322}
2323
2324impl<E> FenceIndeterminate<E> {
2325 pub fn new(error: E) -> Self {
2326 Self { error }
2327 }
2328
2329 pub fn error(&self) -> &E {
2330 &self.error
2331 }
2332
2333 pub fn into_error(self) -> E {
2334 self.error
2335 }
2336}
2337
2338#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
2342pub struct DeviceErrorReport {
2343 failure: FailureEnvelope,
2344}
2345
2346impl DeviceErrorReport {
2347 pub fn new(
2348 code: impl Into<String>,
2349 message: impl Into<String>,
2350 retryable: bool,
2351 ) -> Result<Self, VNextError> {
2352 Ok(Self {
2353 failure: FailureEnvelope::new(FailureDomain::Device, code, message, retryable)?,
2354 })
2355 }
2356
2357 pub fn code(&self) -> &str {
2358 self.failure.code()
2359 }
2360
2361 pub fn message(&self) -> &str {
2362 self.failure.message()
2363 }
2364
2365 pub const fn retryable(&self) -> bool {
2366 self.failure.retryable()
2367 }
2368
2369 fn into_failure(self) -> FailureEnvelope {
2370 self.failure
2371 }
2372}
2373
2374#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
2375pub struct HostTransferLayout {
2376 element_type: ElementType,
2377 element_count: u64,
2378}
2379
2380impl HostTransferLayout {
2381 pub fn new(element_type: ElementType, element_count: u64) -> Result<Self, super::VNextError> {
2382 if element_count == 0
2383 || element_count
2384 .checked_mul(element_type.size_bytes())
2385 .is_none()
2386 {
2387 return Err(super::VNextError::InvalidExecutionPlan {
2388 reason: "host transfer layout is empty or overflows u64".to_owned(),
2389 });
2390 }
2391 Ok(Self {
2392 element_type,
2393 element_count,
2394 })
2395 }
2396
2397 pub fn byte_len(self) -> Result<u64, super::VNextError> {
2398 self.element_count
2399 .checked_mul(self.element_type.size_bytes())
2400 .ok_or_else(|| super::VNextError::InvalidExecutionPlan {
2401 reason: "host transfer byte count overflows u64".to_owned(),
2402 })
2403 }
2404
2405 pub fn validate_bytes(self, bytes: usize) -> Result<(), super::VNextError> {
2406 if self.byte_len()? != bytes as u64 {
2407 return Err(super::VNextError::InvalidExecutionPlan {
2408 reason: "host transfer byte count does not match its element layout".to_owned(),
2409 });
2410 }
2411 Ok(())
2412 }
2413
2414 pub fn element_type(self) -> ElementType {
2415 self.element_type
2416 }
2417
2418 pub fn element_count(self) -> u64 {
2419 self.element_count
2420 }
2421}
2422
2423#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
2432#[serde(rename_all = "snake_case")]
2433pub enum DeviceCommandPhase {
2434 Initialization,
2435 DynamicBinding,
2436 Compute,
2437 ResultBinding,
2438}
2439
2440#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
2446#[serde(transparent)]
2447pub struct DeviceNativeOperationId(&'static str);
2448
2449impl DeviceNativeOperationId {
2450 pub const MAX_BYTES: usize = 256;
2451
2452 pub const fn new(value: &'static str) -> Option<Self> {
2453 let bytes = value.as_bytes();
2454 if bytes.is_empty() || bytes.len() > Self::MAX_BYTES {
2455 return None;
2456 }
2457 let mut index = 0;
2458 while index < bytes.len() {
2459 let byte = bytes[index];
2460 if !matches!(
2461 byte,
2462 b'a'..=b'z'
2463 | b'A'..=b'Z'
2464 | b'0'..=b'9'
2465 | b'.'
2466 | b'_'
2467 | b':'
2468 | b'/'
2469 | b'-'
2470 ) {
2471 return None;
2472 }
2473 index += 1;
2474 }
2475 Some(Self(value))
2476 }
2477
2478 pub const fn as_str(self) -> &'static str {
2479 self.0
2480 }
2481
2482 const fn built_in(value: &'static str) -> Self {
2483 match Self::new(value) {
2484 Some(identity) => identity,
2485 None => panic!("built-in native operation identity must be portable"),
2486 }
2487 }
2488}
2489
2490pub const DEVICE_COPY_NATIVE_OPERATION_ID: DeviceNativeOperationId =
2491 DeviceNativeOperationId::built_in("device.copy");
2492pub const HOST_UPLOAD_NATIVE_OPERATION_ID: DeviceNativeOperationId =
2493 DeviceNativeOperationId::built_in("host.upload");
2494pub const DEVICE_ZERO_NATIVE_OPERATION_ID: DeviceNativeOperationId =
2495 DeviceNativeOperationId::built_in("device.zero");
2496
2497#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
2504pub struct DeviceNativeWorkAttribution {
2505 command_index: u32,
2506 node_index: Option<u32>,
2507 command_phase: DeviceCommandPhase,
2508 native_op_id: DeviceNativeOperationId,
2509 execution_path: DeviceExecutionPath,
2510 batching_form: DeviceBatchingForm,
2511 participant_start: u32,
2512 participant_count: u32,
2513 token_count: u64,
2514 compute_dispatch_count: u64,
2515 transfer_command_count: u64,
2516 reusable_graph_node_count: Option<u64>,
2517}
2518
2519impl DeviceNativeWorkAttribution {
2520 #[allow(clippy::too_many_arguments)]
2521 pub fn new(
2522 command_index: u32,
2523 node_index: Option<u32>,
2524 command_phase: DeviceCommandPhase,
2525 native_op_id: DeviceNativeOperationId,
2526 execution_path: DeviceExecutionPath,
2527 batching_form: DeviceBatchingForm,
2528 participant_count: u32,
2529 token_count: u64,
2530 compute_dispatch_count: u64,
2531 transfer_command_count: u64,
2532 reusable_graph_node_count: Option<u64>,
2533 ) -> Option<Self> {
2534 Self::with_participant_range(
2535 command_index,
2536 node_index,
2537 command_phase,
2538 native_op_id,
2539 execution_path,
2540 batching_form,
2541 0,
2542 participant_count,
2543 token_count,
2544 compute_dispatch_count,
2545 transfer_command_count,
2546 reusable_graph_node_count,
2547 )
2548 }
2549
2550 #[allow(clippy::too_many_arguments)]
2551 pub fn with_participant_range(
2552 command_index: u32,
2553 node_index: Option<u32>,
2554 command_phase: DeviceCommandPhase,
2555 native_op_id: DeviceNativeOperationId,
2556 execution_path: DeviceExecutionPath,
2557 batching_form: DeviceBatchingForm,
2558 participant_start: u32,
2559 participant_count: u32,
2560 token_count: u64,
2561 compute_dispatch_count: u64,
2562 transfer_command_count: u64,
2563 reusable_graph_node_count: Option<u64>,
2564 ) -> Option<Self> {
2565 if (compute_dispatch_count == 0 && transfer_command_count == 0)
2566 || (node_index.is_some() && participant_count == 0)
2567 || participant_start.checked_add(participant_count).is_none()
2568 || (node_index.is_none() && participant_start != 0)
2569 || (reusable_graph_node_count.is_some()
2570 && execution_path != DeviceExecutionPath::Replayed)
2571 {
2572 return None;
2573 }
2574 Some(Self {
2575 command_index,
2576 node_index,
2577 command_phase,
2578 native_op_id,
2579 execution_path,
2580 batching_form,
2581 participant_start,
2582 participant_count,
2583 token_count,
2584 compute_dispatch_count,
2585 transfer_command_count,
2586 reusable_graph_node_count,
2587 })
2588 }
2589
2590 pub const fn command_index(&self) -> u32 {
2591 self.command_index
2592 }
2593
2594 pub const fn node_index(&self) -> Option<u32> {
2595 self.node_index
2596 }
2597
2598 pub const fn command_phase(&self) -> DeviceCommandPhase {
2599 self.command_phase
2600 }
2601
2602 pub const fn native_op_id(&self) -> &'static str {
2603 self.native_op_id.as_str()
2604 }
2605
2606 pub const fn execution_path(&self) -> DeviceExecutionPath {
2607 self.execution_path
2608 }
2609
2610 pub const fn batching_form(&self) -> DeviceBatchingForm {
2611 self.batching_form
2612 }
2613
2614 pub const fn participant_start(&self) -> u32 {
2615 self.participant_start
2616 }
2617
2618 pub const fn participant_count(&self) -> u32 {
2619 self.participant_count
2620 }
2621
2622 pub const fn participant_end(&self) -> u32 {
2623 self.participant_start + self.participant_count
2624 }
2625
2626 pub const fn token_count(&self) -> u64 {
2627 self.token_count
2628 }
2629
2630 pub const fn compute_dispatch_count(&self) -> u64 {
2631 self.compute_dispatch_count
2632 }
2633
2634 pub const fn transfer_command_count(&self) -> u64 {
2635 self.transfer_command_count
2636 }
2637
2638 pub const fn reusable_graph_node_count(&self) -> Option<u64> {
2643 self.reusable_graph_node_count
2644 }
2645}
2646
2647#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
2653pub struct DeviceReplayedLogicalCommandAttribution {
2654 logical_command_ordinal: u32,
2655 node_index: u32,
2656 native_op_id: DeviceNativeOperationId,
2657 batching_form: DeviceBatchingForm,
2658 participant_count: u32,
2659 token_count: u64,
2660 compute_dispatch_count: u64,
2661 transfer_command_count: u64,
2662 reusable_graph_node_count: u64,
2663}
2664
2665impl DeviceReplayedLogicalCommandAttribution {
2666 #[allow(clippy::too_many_arguments)]
2667 pub fn new(
2668 logical_command_ordinal: u32,
2669 node_index: u32,
2670 native_op_id: DeviceNativeOperationId,
2671 batching_form: DeviceBatchingForm,
2672 participant_count: u32,
2673 token_count: u64,
2674 compute_dispatch_count: u64,
2675 transfer_command_count: u64,
2676 reusable_graph_node_count: u64,
2677 ) -> Option<Self> {
2678 if participant_count == 0
2679 || (compute_dispatch_count == 0 && transfer_command_count == 0)
2680 || reusable_graph_node_count == 0
2681 {
2682 return None;
2683 }
2684 Some(Self {
2685 logical_command_ordinal,
2686 node_index,
2687 native_op_id,
2688 batching_form,
2689 participant_count,
2690 token_count,
2691 compute_dispatch_count,
2692 transfer_command_count,
2693 reusable_graph_node_count,
2694 })
2695 }
2696
2697 pub const fn logical_command_ordinal(&self) -> u32 {
2698 self.logical_command_ordinal
2699 }
2700
2701 pub const fn node_index(&self) -> u32 {
2702 self.node_index
2703 }
2704
2705 pub const fn native_op_id(&self) -> &'static str {
2706 self.native_op_id.as_str()
2707 }
2708
2709 pub const fn batching_form(&self) -> DeviceBatchingForm {
2710 self.batching_form
2711 }
2712
2713 pub const fn participant_count(&self) -> u32 {
2714 self.participant_count
2715 }
2716
2717 pub const fn token_count(&self) -> u64 {
2718 self.token_count
2719 }
2720
2721 pub const fn compute_dispatch_count(&self) -> u64 {
2722 self.compute_dispatch_count
2723 }
2724
2725 pub const fn transfer_command_count(&self) -> u64 {
2726 self.transfer_command_count
2727 }
2728
2729 pub const fn reusable_graph_node_count(&self) -> u64 {
2730 self.reusable_graph_node_count
2731 }
2732}
2733
2734#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
2740pub struct DeviceReplayedSegmentAttribution {
2741 physical_command_index: u32,
2742 program_id: DeviceReusableExecutionProgramId,
2743 segment: DeviceReusableExecutionSegment,
2744 reusable_executable_fingerprint: String,
2745 logical_commands: Box<[DeviceReplayedLogicalCommandAttribution]>,
2746}
2747
2748impl DeviceReplayedSegmentAttribution {
2749 pub fn new(
2750 physical_command_index: u32,
2751 program_id: DeviceReusableExecutionProgramId,
2752 segment: DeviceReusableExecutionSegment,
2753 reusable_executable_fingerprint: String,
2754 logical_commands: Vec<DeviceReplayedLogicalCommandAttribution>,
2755 ) -> Option<Self> {
2756 let canonical_sha256 = reusable_executable_fingerprint.len() == 64
2757 && reusable_executable_fingerprint
2758 .bytes()
2759 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte));
2760 if !canonical_sha256
2761 || segment
2762 .start_node_index()
2763 .checked_add(segment.logical_command_count())
2764 != Some(segment.end_node_index())
2765 || logical_commands.len() != segment.logical_command_count() as usize
2766 || logical_commands
2767 .iter()
2768 .enumerate()
2769 .any(|(ordinal, command)| {
2770 u32::try_from(ordinal).ok() != Some(command.logical_command_ordinal())
2771 || segment
2772 .start_node_index()
2773 .checked_add(command.logical_command_ordinal())
2774 != Some(command.node_index())
2775 })
2776 {
2777 return None;
2778 }
2779 Some(Self {
2780 physical_command_index,
2781 program_id,
2782 segment,
2783 reusable_executable_fingerprint,
2784 logical_commands: logical_commands.into_boxed_slice(),
2785 })
2786 }
2787
2788 pub const fn physical_command_index(&self) -> u32 {
2789 self.physical_command_index
2790 }
2791
2792 pub fn program_id(&self) -> &DeviceReusableExecutionProgramId {
2793 &self.program_id
2794 }
2795
2796 pub const fn segment(&self) -> &DeviceReusableExecutionSegment {
2797 &self.segment
2798 }
2799
2800 pub fn reusable_executable_fingerprint(&self) -> &str {
2801 &self.reusable_executable_fingerprint
2802 }
2803
2804 pub fn logical_commands(&self) -> &[DeviceReplayedLogicalCommandAttribution] {
2805 &self.logical_commands
2806 }
2807}
2808
2809#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
2810pub struct DeviceSubmissionAttribution {
2811 commands: Box<[DeviceNativeWorkAttribution]>,
2812 replayed_segments: Box<[DeviceReplayedSegmentAttribution]>,
2813}
2814
2815impl DeviceSubmissionAttribution {
2816 pub fn new(commands: Vec<DeviceNativeWorkAttribution>) -> Option<Self> {
2817 Self::with_replayed_segments(commands, Vec::new())
2818 }
2819
2820 pub fn with_replayed_segments(
2821 commands: Vec<DeviceNativeWorkAttribution>,
2822 replayed_segments: Vec<DeviceReplayedSegmentAttribution>,
2823 ) -> Option<Self> {
2824 if commands.is_empty()
2825 || commands
2826 .windows(2)
2827 .any(|pair| pair[0].command_index() >= pair[1].command_index())
2828 || replayed_segments.windows(2).any(|pair| {
2829 pair[0].physical_command_index() >= pair[1].physical_command_index()
2830 || pair[0].segment().end_node_index() > pair[1].segment().start_node_index()
2831 })
2832 {
2833 return None;
2834 }
2835 for segment in &replayed_segments {
2836 let physical_index = commands
2837 .binary_search_by_key(&segment.physical_command_index(), |command| {
2838 command.command_index()
2839 })
2840 .ok()?;
2841 let physical = commands.get(physical_index)?;
2842 let logical_graph_node_count = segment
2843 .logical_commands()
2844 .iter()
2845 .try_fold(0_u64, |total, logical| {
2846 total.checked_add(logical.reusable_graph_node_count())
2847 })?;
2848 if physical.command_index() != segment.physical_command_index()
2849 || physical.command_phase() != DeviceCommandPhase::Compute
2850 || physical.execution_path() != DeviceExecutionPath::Replayed
2851 || physical.reusable_graph_node_count() != Some(logical_graph_node_count)
2852 || physical.node_index() != Some(segment.segment().start_node_index())
2853 || physical.participant_start() != 0
2854 || physical.participant_count() != segment.program_id().immediate_sequences()
2855 || physical.token_count() != segment.program_id().immediate_tokens()
2856 || segment
2857 .logical_commands()
2858 .iter()
2859 .any(|logical| logical.participant_count() != physical.participant_count())
2860 {
2861 return None;
2862 }
2863 }
2864 Some(Self {
2865 commands: commands.into_boxed_slice(),
2866 replayed_segments: replayed_segments.into_boxed_slice(),
2867 })
2868 }
2869
2870 pub fn commands(&self) -> &[DeviceNativeWorkAttribution] {
2871 &self.commands
2872 }
2873
2874 pub fn replayed_segments(&self) -> &[DeviceReplayedSegmentAttribution] {
2875 &self.replayed_segments
2876 }
2877}
2878
2879#[must_use = "encoded device operations must be appended to a submission batch"]
2886pub struct EncodedDeviceOperation<C> {
2887 program_bindings: Vec<C>,
2888 dynamic_bindings: Vec<C>,
2889 compute: C,
2890 result_bindings: Vec<C>,
2891}
2892
2893impl<C> EncodedDeviceOperation<C> {
2894 pub fn compute(command: C) -> Self {
2895 Self {
2896 program_bindings: Vec::new(),
2897 dynamic_bindings: Vec::new(),
2898 compute: command,
2899 result_bindings: Vec::new(),
2900 }
2901 }
2902
2903 pub fn with_program_binding(mut self, command: C) -> Self {
2909 self.program_bindings.push(command);
2910 self
2911 }
2912
2913 pub fn with_dynamic_binding(mut self, command: C) -> Self {
2914 self.dynamic_bindings.push(command);
2915 self
2916 }
2917
2918 pub fn with_result_binding(mut self, command: C) -> Self {
2919 self.result_bindings.push(command);
2920 self
2921 }
2922
2923 pub fn dynamic_binding_count(&self) -> usize {
2924 self.dynamic_bindings.len()
2925 }
2926
2927 pub fn program_binding_count(&self) -> usize {
2928 self.program_bindings.len()
2929 }
2930
2931 pub fn result_binding_count(&self) -> usize {
2932 self.result_bindings.len()
2933 }
2934
2935 pub(crate) fn into_parts(self) -> (Vec<C>, Vec<C>, C, Vec<C>) {
2936 (
2937 self.program_bindings,
2938 self.dynamic_bindings,
2939 self.compute,
2940 self.result_bindings,
2941 )
2942 }
2943}
2944
2945#[must_use = "reusable execution bindings must accompany their segment launch"]
2949pub struct EncodedReusableExecutionBindings<C> {
2950 program_bindings: Vec<C>,
2951 dynamic_bindings: Vec<C>,
2952 result_bindings: Vec<C>,
2953}
2954
2955impl<C> EncodedReusableExecutionBindings<C> {
2956 pub fn empty() -> Self {
2957 Self {
2958 program_bindings: Vec::new(),
2959 dynamic_bindings: Vec::new(),
2960 result_bindings: Vec::new(),
2961 }
2962 }
2963
2964 pub fn with_program_binding(mut self, command: C) -> Self {
2965 self.program_bindings.push(command);
2966 self
2967 }
2968
2969 pub fn with_dynamic_binding(mut self, command: C) -> Self {
2970 self.dynamic_bindings.push(command);
2971 self
2972 }
2973
2974 pub fn with_result_binding(mut self, command: C) -> Self {
2975 self.result_bindings.push(command);
2976 self
2977 }
2978
2979 pub fn from_operation(operation: EncodedDeviceOperation<C>) -> Self {
2980 let (program_bindings, dynamic_bindings, _compute, result_bindings) =
2981 operation.into_parts();
2982 Self {
2983 program_bindings,
2984 dynamic_bindings,
2985 result_bindings,
2986 }
2987 }
2988
2989 pub fn program_binding_count(&self) -> usize {
2990 self.program_bindings.len()
2991 }
2992
2993 pub fn dynamic_binding_count(&self) -> usize {
2994 self.dynamic_bindings.len()
2995 }
2996
2997 pub fn result_binding_count(&self) -> usize {
2998 self.result_bindings.len()
2999 }
3000
3001 pub(crate) fn into_parts(self) -> (Vec<C>, Vec<C>, Vec<C>) {
3002 (
3003 self.program_bindings,
3004 self.dynamic_bindings,
3005 self.result_bindings,
3006 )
3007 }
3008}
3009
3010pub struct DeviceCommandEntry<C> {
3013 phase: DeviceCommandPhase,
3014 node_index: Option<u32>,
3015 logical_work: Option<DeviceCommandLogicalWork>,
3016 command: C,
3017}
3018
3019impl<C> DeviceCommandEntry<C> {
3020 pub const fn phase(&self) -> DeviceCommandPhase {
3021 self.phase
3022 }
3023
3024 pub const fn node_index(&self) -> Option<u32> {
3025 self.node_index
3026 }
3027
3028 pub const fn logical_work(&self) -> Option<DeviceCommandLogicalWork> {
3029 self.logical_work
3030 }
3031
3032 pub const fn command(&self) -> &C {
3033 &self.command
3034 }
3035
3036 pub fn into_parts(
3037 self,
3038 ) -> (
3039 DeviceCommandPhase,
3040 Option<u32>,
3041 Option<DeviceCommandLogicalWork>,
3042 C,
3043 ) {
3044 (self.phase, self.node_index, self.logical_work, self.command)
3045 }
3046}
3047
3048#[must_use = "encoded device command batches must be submitted"]
3055pub struct DeviceCommandBatch<C> {
3056 commands: Vec<DeviceCommandEntry<C>>,
3057 timing_mode: DeviceTimingMode,
3058 compute_path_requirement: DeviceComputePathRequirement,
3059 declared_eager_compute_node_indices: Vec<u32>,
3060 attribution_requirement: DeviceSubmissionAttributionRequirement,
3061 reusable_execution_capture: Option<DeviceReusableExecutionCapture>,
3062}
3063
3064impl<C> DeviceCommandBatch<C> {
3065 pub(crate) fn singleton(command: C) -> Self {
3066 Self {
3067 commands: vec![DeviceCommandEntry {
3068 phase: DeviceCommandPhase::Compute,
3069 node_index: None,
3070 logical_work: None,
3071 command,
3072 }],
3073 timing_mode: DeviceTimingMode::Off,
3074 compute_path_requirement: DeviceComputePathRequirement::Adaptive,
3075 declared_eager_compute_node_indices: Vec::new(),
3076 attribution_requirement: DeviceSubmissionAttributionRequirement::None,
3077 reusable_execution_capture: None,
3078 }
3079 }
3080
3081 pub(crate) fn with_capacity(capacity: usize) -> Self {
3082 Self {
3083 commands: Vec::with_capacity(capacity),
3084 timing_mode: DeviceTimingMode::Off,
3085 compute_path_requirement: DeviceComputePathRequirement::Adaptive,
3086 declared_eager_compute_node_indices: Vec::new(),
3087 attribution_requirement: DeviceSubmissionAttributionRequirement::None,
3088 reusable_execution_capture: None,
3089 }
3090 }
3091
3092 pub(crate) fn with_capacity_and_timing(capacity: usize, timing_mode: DeviceTimingMode) -> Self {
3093 Self {
3094 commands: Vec::with_capacity(capacity),
3095 timing_mode,
3096 compute_path_requirement: DeviceComputePathRequirement::Adaptive,
3097 declared_eager_compute_node_indices: Vec::new(),
3098 attribution_requirement: DeviceSubmissionAttributionRequirement::None,
3099 reusable_execution_capture: None,
3100 }
3101 }
3102
3103 pub(crate) fn with_capacity_timing_and_compute_path(
3104 capacity: usize,
3105 timing_mode: DeviceTimingMode,
3106 compute_path_requirement: DeviceComputePathRequirement,
3107 ) -> Self {
3108 Self {
3109 commands: Vec::with_capacity(capacity),
3110 timing_mode,
3111 compute_path_requirement,
3112 declared_eager_compute_node_indices: Vec::new(),
3113 attribution_requirement: DeviceSubmissionAttributionRequirement::None,
3114 reusable_execution_capture: None,
3115 }
3116 }
3117
3118 pub(crate) fn set_declared_eager_compute_node_indices(
3119 &mut self,
3120 node_indices: Vec<u32>,
3121 ) -> Result<(), VNextError> {
3122 if self.compute_path_requirement
3123 != DeviceComputePathRequirement::ReplayedWithDeclaredEagerBoundaries
3124 || node_indices.is_empty()
3125 || node_indices.windows(2).any(|pair| pair[0] >= pair[1])
3126 || !self.declared_eager_compute_node_indices.is_empty()
3127 {
3128 return Err(VNextError::InvalidExecutionPlan {
3129 reason: "declared eager compute boundaries are absent, unordered, duplicated, or attached to the wrong path requirement"
3130 .to_owned(),
3131 });
3132 }
3133 self.declared_eager_compute_node_indices = node_indices;
3134 Ok(())
3135 }
3136
3137 pub(crate) fn require_logical_execution_path_attribution(&mut self) {
3138 self.attribution_requirement = DeviceSubmissionAttributionRequirement::LogicalExecutionPath;
3139 }
3140
3141 pub(crate) fn set_reusable_execution_capture(
3142 &mut self,
3143 capture: DeviceReusableExecutionCapture,
3144 ) -> Result<(), VNextError> {
3145 if self.reusable_execution_capture.is_some() {
3146 return Err(VNextError::InvalidExecutionPlan {
3147 reason: "device command batch already owns reusable execution capture metadata"
3148 .to_owned(),
3149 });
3150 }
3151 self.reusable_execution_capture = Some(capture);
3152 Ok(())
3153 }
3154
3155 pub fn reusable_execution_capture(&self) -> Option<&DeviceReusableExecutionCapture> {
3156 self.reusable_execution_capture.as_ref()
3157 }
3158
3159 pub(crate) fn push_initialization(&mut self, command: C) {
3160 self.commands.push(DeviceCommandEntry {
3161 phase: DeviceCommandPhase::Initialization,
3162 node_index: None,
3163 logical_work: None,
3164 command,
3165 });
3166 }
3167
3168 pub(crate) fn push_node_initialization(
3169 &mut self,
3170 node_index: u32,
3171 logical_work: DeviceCommandLogicalWork,
3172 command: C,
3173 ) {
3174 self.commands.push(DeviceCommandEntry {
3175 phase: DeviceCommandPhase::Initialization,
3176 node_index: Some(node_index),
3177 logical_work: Some(logical_work),
3178 command,
3179 });
3180 }
3181
3182 pub(crate) fn push_dynamic_binding(&mut self, command: C) {
3183 self.commands.push(DeviceCommandEntry {
3184 phase: DeviceCommandPhase::DynamicBinding,
3185 node_index: None,
3186 logical_work: None,
3187 command,
3188 });
3189 }
3190
3191 pub(crate) fn push_compute(&mut self, command: C) {
3192 self.commands.push(DeviceCommandEntry {
3193 phase: DeviceCommandPhase::Compute,
3194 node_index: None,
3195 logical_work: None,
3196 command,
3197 });
3198 }
3199
3200 pub(crate) fn push_result_binding(&mut self, command: C) {
3201 self.commands.push(DeviceCommandEntry {
3202 phase: DeviceCommandPhase::ResultBinding,
3203 node_index: None,
3204 logical_work: None,
3205 command,
3206 });
3207 }
3208
3209 pub(crate) fn push_operation(&mut self, node_index: u32, operation: EncodedDeviceOperation<C>) {
3210 let (program_bindings, dynamic_bindings, compute, result_bindings) = operation.into_parts();
3211 for command in program_bindings {
3212 self.commands.push(DeviceCommandEntry {
3213 phase: DeviceCommandPhase::DynamicBinding,
3214 node_index: Some(node_index),
3215 logical_work: None,
3216 command,
3217 });
3218 }
3219 self.push_operation_parts(node_index, dynamic_bindings, compute, result_bindings);
3220 }
3221
3222 pub(crate) fn push_operation_parts(
3223 &mut self,
3224 node_index: u32,
3225 dynamic_bindings: Vec<C>,
3226 compute: C,
3227 result_bindings: Vec<C>,
3228 ) {
3229 for command in dynamic_bindings {
3230 self.commands.push(DeviceCommandEntry {
3231 phase: DeviceCommandPhase::DynamicBinding,
3232 node_index: Some(node_index),
3233 logical_work: None,
3234 command,
3235 });
3236 }
3237 self.commands.push(DeviceCommandEntry {
3238 phase: DeviceCommandPhase::Compute,
3239 node_index: Some(node_index),
3240 logical_work: None,
3241 command: compute,
3242 });
3243 for command in result_bindings {
3244 self.commands.push(DeviceCommandEntry {
3245 phase: DeviceCommandPhase::ResultBinding,
3246 node_index: Some(node_index),
3247 logical_work: None,
3248 command,
3249 });
3250 }
3251 }
3252
3253 pub(crate) fn push(&mut self, command: C) {
3254 self.push_compute(command);
3255 }
3256
3257 pub fn len(&self) -> usize {
3258 self.commands.len()
3259 }
3260
3261 pub fn is_empty(&self) -> bool {
3262 self.commands.is_empty()
3263 }
3264
3265 pub const fn timing_mode(&self) -> DeviceTimingMode {
3266 self.timing_mode
3267 }
3268
3269 pub const fn compute_path_requirement(&self) -> DeviceComputePathRequirement {
3270 self.compute_path_requirement
3271 }
3272
3273 pub fn declared_eager_compute_node_indices(&self) -> &[u32] {
3274 &self.declared_eager_compute_node_indices
3275 }
3276
3277 pub const fn attribution_requirement(&self) -> DeviceSubmissionAttributionRequirement {
3278 self.attribution_requirement
3279 }
3280
3281 pub fn into_commands(self) -> Vec<C> {
3282 self.commands
3283 .into_iter()
3284 .map(|entry| entry.command)
3285 .collect()
3286 }
3287
3288 pub fn into_entries(self) -> Vec<DeviceCommandEntry<C>> {
3289 self.commands
3290 }
3291}
3292
3293pub trait StaticWeightImportSession<B, E> {
3301 fn import_component(
3302 &mut self,
3303 payload: &WeightComponentPayload<'_>,
3304 destination: &B,
3305 destination_offset_bytes: u64,
3306 ) -> Result<(), E>;
3307
3308 fn seal(self: Box<Self>) -> Result<(), E>;
3309}
3310
3311pub struct StaticWeightTransformDestination<'request, B> {
3313 component: &'request WeightComponentSpec,
3314 buffer: &'request B,
3315 destination_offset_bytes: u64,
3316}
3317
3318impl<'request, B> StaticWeightTransformDestination<'request, B> {
3319 pub(crate) const fn new(
3320 component: &'request WeightComponentSpec,
3321 buffer: &'request B,
3322 destination_offset_bytes: u64,
3323 ) -> Self {
3324 Self {
3325 component,
3326 buffer,
3327 destination_offset_bytes,
3328 }
3329 }
3330
3331 pub fn component(&self) -> &WeightComponentSpec {
3332 self.component
3333 }
3334
3335 pub const fn buffer(&self) -> &B {
3336 self.buffer
3337 }
3338
3339 pub const fn destination_offset_bytes(&self) -> u64 {
3340 self.destination_offset_bytes
3341 }
3342}
3343
3344pub struct StaticWeightTransformRequest<'request, 'source, B> {
3350 plan: &'request StaticWeightTransformPlan,
3351 sources: &'request [WeightComponentSegments<'source>],
3352 destinations: &'request [StaticWeightTransformDestination<'request, B>],
3353 scratch: &'request B,
3354}
3355
3356impl<'request, 'source, B> StaticWeightTransformRequest<'request, 'source, B> {
3357 pub(crate) const fn new(
3358 plan: &'request StaticWeightTransformPlan,
3359 sources: &'request [WeightComponentSegments<'source>],
3360 destinations: &'request [StaticWeightTransformDestination<'request, B>],
3361 scratch: &'request B,
3362 ) -> Self {
3363 Self {
3364 plan,
3365 sources,
3366 destinations,
3367 scratch,
3368 }
3369 }
3370
3371 pub const fn plan(&self) -> &StaticWeightTransformPlan {
3372 self.plan
3373 }
3374
3375 pub fn sources(&self) -> &[WeightComponentSegments<'source>] {
3376 self.sources
3377 }
3378
3379 pub fn destinations(&self) -> &[StaticWeightTransformDestination<'request, B>] {
3380 self.destinations
3381 }
3382
3383 pub const fn scratch(&self) -> &B {
3384 self.scratch
3385 }
3386}
3387
3388pub trait DeviceRuntime: Send + Sync + 'static {
3394 type Buffer: Send + Sync + 'static;
3395 type Stream: Send + 'static;
3396 type Command: Send + 'static;
3397 type Fence: Send + 'static;
3398 type Error: Error + Send + Sync + 'static;
3399
3400 fn descriptor(&self) -> &DeviceDescriptor;
3401
3402 fn attention_execution_policy(&self) -> AttentionExecutionPolicy;
3405
3406 fn allocate(&self, permit: DeviceAllocationPermit<'_>) -> Result<Self::Buffer, Self::Error>;
3411
3412 fn buffer_descriptor(&self, buffer: &Self::Buffer) -> BufferDescriptor;
3413
3414 fn begin_static_weight_import(
3418 &self,
3419 ) -> Option<
3420 Result<Box<dyn StaticWeightImportSession<Self::Buffer, Self::Error> + '_>, Self::Error>,
3421 > {
3422 None
3423 }
3424
3425 fn encode_static_weight_transform(
3432 &self,
3433 _request: StaticWeightTransformRequest<'_, '_, Self::Buffer>,
3434 ) -> Option<Result<Self::Command, Self::Error>> {
3435 None
3436 }
3437
3438 fn create_stream(&self) -> Result<Self::Stream, Self::Error>;
3439
3440 fn stream_state(&self, stream: &Self::Stream) -> StreamState;
3441
3442 fn configure_reusable_executables(
3445 &self,
3446 _stream: &mut Self::Stream,
3447 _plan: DeviceReusableExecutionPlan,
3448 ) -> Result<DeviceReusableExecutionPreparation, Self::Error> {
3449 Ok(DeviceReusableExecutionPreparation::unsupported())
3450 }
3451
3452 fn seal_reusable_executables(
3456 &self,
3457 _stream: &mut Self::Stream,
3458 ) -> Result<DeviceReusableExecutionPreparation, Self::Error> {
3459 Ok(DeviceReusableExecutionPreparation::unsupported())
3460 }
3461
3462 fn reusable_executable_preparation(
3466 &self,
3467 _stream: &Self::Stream,
3468 ) -> Result<DeviceReusableExecutionPreparation, Self::Error> {
3469 Ok(DeviceReusableExecutionPreparation::unsupported())
3470 }
3471
3472 fn reusable_execution_catalog(
3476 &self,
3477 _stream: &Self::Stream,
3478 ) -> Result<Vec<DeviceReusableExecutionProgram>, Self::Error> {
3479 Ok(Vec::new())
3480 }
3481
3482 fn encode_reusable_execution(
3488 &self,
3489 _invocation: DeviceReusableExecutionInvocation,
3490 ) -> Result<Option<Self::Command>, Self::Error> {
3491 Ok(None)
3492 }
3493
3494 fn trim_reusable_executables(
3497 &self,
3498 _stream: &mut Self::Stream,
3499 ) -> Result<DeviceReusableExecutionTrim, Self::Error> {
3500 Ok(DeviceReusableExecutionTrim::default())
3501 }
3502
3503 fn encode_copy(
3504 &self,
3505 source: &Self::Buffer,
3506 destination: &Self::Buffer,
3507 region: CopyRegion,
3508 ) -> Result<Self::Command, Self::Error>;
3509
3510 fn encode_upload(
3511 &self,
3512 source: &[u8],
3513 source_layout: HostTransferLayout,
3514 destination: &Self::Buffer,
3515 destination_offset_bytes: u64,
3516 ) -> Result<Self::Command, Self::Error>;
3517
3518 fn encode_zero(
3519 &self,
3520 destination: &Self::Buffer,
3521 destination_offset_bytes: u64,
3522 length_bytes: u64,
3523 ) -> Result<Self::Command, Self::Error>;
3524
3525 fn coalesce_program_bindings(
3531 &self,
3532 commands: Vec<Self::Command>,
3533 ) -> Result<Vec<Self::Command>, Self::Error> {
3534 Ok(commands)
3535 }
3536
3537 fn submit(
3547 &self,
3548 stream: &mut Self::Stream,
3549 commands: DeviceCommandBatch<Self::Command>,
3550 ) -> Result<Self::Fence, DefinitelyNotSubmitted<Self::Error>>;
3551
3552 fn submit_with_timing<S>(
3556 &self,
3557 stream: &mut Self::Stream,
3558 commands: DeviceCommandBatch<Self::Command>,
3559 timing_sink: &S,
3560 ) -> Result<Self::Fence, DefinitelyNotSubmitted<Self::Error>>
3561 where
3562 Self: Sized,
3563 S: DeviceSubmissionTimingSink,
3564 {
3565 let _ = timing_sink;
3566 self.submit(stream, commands)
3567 }
3568
3569 fn submission_attribution(&self, _fence: &Self::Fence) -> Option<DeviceSubmissionAttribution> {
3574 None
3575 }
3576
3577 fn query_fence(&self, fence: &Self::Fence) -> FenceQuery<Self::Error>;
3580
3581 fn wait_fence(
3584 &self,
3585 fence: &Self::Fence,
3586 ) -> Result<DeviceTerminalReceipt<Self::Error>, FenceIndeterminate<Self::Error>>;
3587
3588 fn synchronize(&self, stream: &mut Self::Stream) -> Result<(), Self::Error>;
3589
3590 fn readback(
3591 &self,
3592 stream: &mut Self::Stream,
3593 source: &Self::Buffer,
3594 region: CopyRegion,
3595 output_layout: HostTransferLayout,
3596 ) -> Result<Vec<u8>, Self::Error>;
3597
3598 fn describe_error(&self, error: &Self::Error) -> Result<DeviceErrorReport, VNextError>;
3599}
3600
3601pub fn classify_device_error<R: DeviceRuntime + ?Sized>(
3604 runtime: &R,
3605 identity: ExecutionIdentityEnvelope,
3606 error: &R::Error,
3607) -> Result<IdentifiedFailure, VNextError> {
3608 runtime.descriptor().validate()?;
3609 if identity.parts().device_id.as_ref() != Some(&runtime.descriptor().id)
3610 || identity
3611 .parts()
3612 .runtime_implementation_fingerprint
3613 .as_deref()
3614 != Some(
3615 runtime
3616 .descriptor()
3617 .runtime_implementation_fingerprint
3618 .as_str(),
3619 )
3620 {
3621 return Err(VNextError::InvalidExecutionPlan {
3622 reason: "device error identity differs from the concrete runtime device implementation"
3623 .to_owned(),
3624 });
3625 }
3626 IdentifiedFailure::new(identity, runtime.describe_error(error)?.into_failure())
3627}
3628
3629#[cfg(test)]
3630mod execution_timing_tests {
3631 use super::*;
3632 use crate::vnext::{
3633 ReusableExecutionBucketSpec, ReusableExecutionCapacity, ReusableExecutionClassId,
3634 };
3635
3636 #[test]
3637 fn native_operation_identity_is_portable_and_bounded() {
3638 let identity = DeviceNativeOperationId::new("cuda.op_1:variant/path-name").unwrap();
3639 assert_eq!(identity.as_str(), "cuda.op_1:variant/path-name");
3640 assert!(DeviceNativeOperationId::new("").is_none());
3641 assert!(DeviceNativeOperationId::new("device zero").is_none());
3642 assert!(DeviceNativeOperationId::new("native.操作").is_none());
3643 let oversized = Box::leak(
3644 "x".repeat(DeviceNativeOperationId::MAX_BYTES + 1)
3645 .into_boxed_str(),
3646 );
3647 assert!(DeviceNativeOperationId::new(oversized).is_none());
3648 assert_eq!(DEVICE_COPY_NATIVE_OPERATION_ID.as_str(), "device.copy");
3649 assert_eq!(HOST_UPLOAD_NATIVE_OPERATION_ID.as_str(), "host.upload");
3650 assert_eq!(DEVICE_ZERO_NATIVE_OPERATION_ID.as_str(), "device.zero");
3651 }
3652
3653 #[test]
3654 fn timing_capabilities_are_independent_from_compute_path_requirement() {
3655 assert!(DeviceTimingMode::Replay.completion_enabled());
3656 assert!(DeviceTimingMode::Replay.physical_span_attribution_enabled());
3657 assert!(!DeviceTimingMode::Replay.kernel_attribution_enabled());
3658 assert!(DeviceTimingMode::Kernel.physical_span_attribution_enabled());
3659 assert!(DeviceTimingMode::Kernel.kernel_attribution_enabled());
3660 assert!(!DeviceTimingMode::Kernel.direct_reusable_execution_allowed());
3661 assert!(DeviceTimingMode::Verification.completion_enabled());
3662 assert!(DeviceTimingMode::Verification.physical_span_attribution_enabled());
3663 assert!(DeviceTimingMode::Verification.kernel_attribution_enabled());
3664 assert!(!DeviceTimingMode::Verification.direct_reusable_execution_allowed());
3665 assert!(!DeviceTimingMode::Completion.physical_span_attribution_enabled());
3666 assert!(!DeviceTimingMode::Off.completion_enabled());
3667
3668 let mut batch = DeviceCommandBatch::<()>::with_capacity_timing_and_compute_path(
3669 1,
3670 DeviceTimingMode::Replay,
3671 DeviceComputePathRequirement::EagerOnly,
3672 );
3673 assert_eq!(
3674 batch.compute_path_requirement(),
3675 DeviceComputePathRequirement::EagerOnly
3676 );
3677 assert_eq!(
3678 batch.attribution_requirement(),
3679 DeviceSubmissionAttributionRequirement::None
3680 );
3681 batch.require_logical_execution_path_attribution();
3682 assert_eq!(
3683 batch.attribution_requirement(),
3684 DeviceSubmissionAttributionRequirement::LogicalExecutionPath
3685 );
3686 assert_eq!(batch.timing_mode(), DeviceTimingMode::Replay);
3687 }
3688
3689 #[test]
3690 fn mixed_replay_boundaries_are_explicit_canonical_batch_metadata() {
3691 let mut batch = DeviceCommandBatch::<()>::with_capacity_timing_and_compute_path(
3692 2,
3693 DeviceTimingMode::Replay,
3694 DeviceComputePathRequirement::ReplayedWithDeclaredEagerBoundaries,
3695 );
3696 batch
3697 .set_declared_eager_compute_node_indices(vec![0, 3])
3698 .unwrap();
3699 assert_eq!(batch.declared_eager_compute_node_indices(), &[0, 3]);
3700 assert_eq!(
3701 serde_json::to_value(batch.compute_path_requirement()).unwrap(),
3702 "replayed_with_declared_eager_boundaries"
3703 );
3704
3705 let mut duplicate = DeviceCommandBatch::<()>::with_capacity_timing_and_compute_path(
3706 2,
3707 DeviceTimingMode::Replay,
3708 DeviceComputePathRequirement::ReplayedWithDeclaredEagerBoundaries,
3709 );
3710 assert!(duplicate
3711 .set_declared_eager_compute_node_indices(vec![1, 1])
3712 .is_err());
3713 let mut wrong_mode = DeviceCommandBatch::<()>::with_capacity(1);
3714 assert!(wrong_mode
3715 .set_declared_eager_compute_node_indices(vec![0])
3716 .is_err());
3717 }
3718
3719 #[test]
3720 fn command_timing_requires_positive_ordered_nonoverlapping_intervals() {
3721 assert!(
3722 DeviceExecutionInterval::new(DeviceExecutionIntervalKind::Compute, 10, 10).is_none()
3723 );
3724 assert!(
3725 DeviceExecutionInterval::new(DeviceExecutionIntervalKind::Compute, 11, 10).is_none()
3726 );
3727
3728 let first =
3729 DeviceExecutionInterval::new(DeviceExecutionIntervalKind::Compute, 10, 20).unwrap();
3730 let adjacent =
3731 DeviceExecutionInterval::new(DeviceExecutionIntervalKind::Transfer, 20, 30).unwrap();
3732 let overlapping =
3733 DeviceExecutionInterval::new(DeviceExecutionIntervalKind::Transfer, 19, 30).unwrap();
3734 assert!(DeviceCommandExecutionTiming::new(0, vec![first, adjacent]).is_some());
3735 assert!(DeviceCommandExecutionTiming::new(0, vec![first, overlapping]).is_none());
3736 let labeled = DeviceExecutionInterval::new_labeled(
3737 DeviceExecutionIntervalKind::Compute,
3738 30,
3739 40,
3740 "projection.qkv",
3741 )
3742 .unwrap();
3743 assert_eq!(labeled.subwork_id(), Some("projection.qkv"));
3744 assert!(DeviceExecutionInterval::new_labeled(
3745 DeviceExecutionIntervalKind::Compute,
3746 30,
3747 40,
3748 "",
3749 )
3750 .is_none());
3751 }
3752
3753 #[test]
3754 fn submission_timing_requires_complete_nonoverlapping_command_coverage() {
3755 let command = |command_index| {
3756 DeviceCommandExecutionTiming::new(
3757 command_index,
3758 vec![
3759 DeviceExecutionInterval::new(DeviceExecutionIntervalKind::Compute, 0, 1)
3760 .unwrap(),
3761 ],
3762 )
3763 .unwrap()
3764 };
3765 let commands = DeviceSubmissionExecutionTiming::new(vec![command(0), command(1)]).unwrap();
3766 assert_eq!(commands.command_count(), 2);
3767 assert_eq!(commands.spans().len(), 2);
3768 assert!(commands
3769 .spans()
3770 .iter()
3771 .all(|span| span.kind() == DeviceExecutionSpanKind::EagerCommand));
3772 assert!(DeviceSubmissionExecutionTiming::new(vec![command(0), command(2)]).is_none());
3773 assert!(DeviceSubmissionExecutionTiming::new(vec![command(1), command(1)]).is_none());
3774 assert!(DeviceSubmissionExecutionTiming::new(vec![command(2), command(1)]).is_none());
3775 }
3776
3777 #[test]
3778 fn submission_timing_preserves_measured_and_unavailable_physical_spans() {
3779 let eager = DeviceSubmissionExecutionSpan::measured(
3780 0,
3781 1,
3782 DeviceExecutionSpanKind::EagerCommand,
3783 vec![
3784 DeviceExecutionInterval::new(DeviceExecutionIntervalKind::Transfer, 0, 10).unwrap(),
3785 ],
3786 )
3787 .unwrap();
3788 let replay = DeviceSubmissionExecutionSpan::measured(
3789 1,
3790 4,
3791 DeviceExecutionSpanKind::ReusableExecutable,
3792 vec![DeviceExecutionInterval::new_labeled(
3793 DeviceExecutionIntervalKind::Compute,
3794 10,
3795 40,
3796 "cuda reusable executable",
3797 )
3798 .unwrap()],
3799 )
3800 .unwrap()
3801 .with_reusable_executable_fingerprint("a".repeat(64))
3802 .unwrap();
3803 let unavailable = DeviceSubmissionExecutionSpan::unavailable(
3804 4,
3805 5,
3806 DeviceExecutionSpanKind::EagerCommand,
3807 DeviceTimingUnavailableReason::BackendMeasurementFailed,
3808 )
3809 .unwrap();
3810
3811 let timing =
3812 DeviceSubmissionExecutionTiming::from_spans(5, vec![eager, replay, unavailable])
3813 .unwrap();
3814 assert_eq!(timing.command_count(), 5);
3815 assert_eq!(
3816 timing.span_for_command(2).unwrap().kind(),
3817 DeviceExecutionSpanKind::ReusableExecutable
3818 );
3819 assert_eq!(
3820 timing
3821 .span_for_command(2)
3822 .unwrap()
3823 .reusable_executable_fingerprint(),
3824 Some("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
3825 );
3826 assert_eq!(
3827 serde_json::to_value(timing.span_for_command(2).unwrap())
3828 .unwrap()
3829 .get("reusable_executable_fingerprint"),
3830 Some(&serde_json::json!(
3831 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
3832 ))
3833 );
3834 assert_eq!(
3835 timing
3836 .span_for_command(4)
3837 .unwrap()
3838 .measurement()
3839 .unavailable_reason(),
3840 Some(DeviceTimingUnavailableReason::BackendMeasurementFailed)
3841 );
3842 assert!(timing.span_for_command(5).is_none());
3843 }
3844
3845 #[test]
3846 fn physical_spans_reject_gaps_overlaps_and_invalid_eager_ranges() {
3847 let interval = || {
3848 vec![DeviceExecutionInterval::new(DeviceExecutionIntervalKind::Compute, 0, 1).unwrap()]
3849 };
3850 assert!(DeviceSubmissionExecutionSpan::measured(
3851 0,
3852 2,
3853 DeviceExecutionSpanKind::EagerCommand,
3854 interval(),
3855 )
3856 .is_none());
3857 assert!(DeviceSubmissionExecutionSpan::measured(
3858 0,
3859 1,
3860 DeviceExecutionSpanKind::EagerCommand,
3861 interval(),
3862 )
3863 .unwrap()
3864 .with_reusable_executable_fingerprint("a".repeat(64))
3865 .is_none());
3866 assert!(DeviceSubmissionExecutionSpan::measured(
3867 0,
3868 2,
3869 DeviceExecutionSpanKind::ReusableExecutable,
3870 interval(),
3871 )
3872 .unwrap()
3873 .with_reusable_executable_fingerprint("A".repeat(64))
3874 .is_none());
3875 let first = DeviceSubmissionExecutionSpan::measured(
3876 0,
3877 1,
3878 DeviceExecutionSpanKind::EagerCommand,
3879 interval(),
3880 )
3881 .unwrap();
3882 assert!(serde_json::to_value(&first)
3883 .unwrap()
3884 .get("reusable_executable_fingerprint")
3885 .is_none());
3886 let gap = DeviceSubmissionExecutionSpan::measured(
3887 2,
3888 3,
3889 DeviceExecutionSpanKind::EagerCommand,
3890 interval(),
3891 )
3892 .unwrap();
3893 assert!(DeviceSubmissionExecutionTiming::from_spans(3, vec![first.clone(), gap]).is_none());
3894 let overlap = DeviceSubmissionExecutionSpan::measured(
3895 0,
3896 2,
3897 DeviceExecutionSpanKind::ReusableExecutable,
3898 interval(),
3899 )
3900 .unwrap();
3901 assert!(DeviceSubmissionExecutionTiming::from_spans(2, vec![first, overlap]).is_none());
3902 }
3903
3904 #[test]
3905 fn native_graph_node_observation_belongs_only_to_replayed_work() {
3906 let row = |execution_path, graph_nodes| {
3907 DeviceNativeWorkAttribution::new(
3908 0,
3909 Some(0),
3910 DeviceCommandPhase::Compute,
3911 DeviceNativeOperationId::new("test.compute").unwrap(),
3912 execution_path,
3913 DeviceBatchingForm::Scalar,
3914 1,
3915 1,
3916 1,
3917 0,
3918 graph_nodes,
3919 )
3920 };
3921
3922 assert!(row(DeviceExecutionPath::Eager, Some(2)).is_none());
3923 let replayed = row(DeviceExecutionPath::Replayed, Some(2)).unwrap();
3924 assert_eq!(replayed.reusable_graph_node_count(), Some(2));
3925 assert_eq!(
3926 serde_json::to_value(replayed).unwrap()["reusable_graph_node_count"],
3927 serde_json::json!(2)
3928 );
3929 }
3930
3931 #[test]
3932 fn native_work_attribution_preserves_bounded_participant_range() {
3933 let row = DeviceNativeWorkAttribution::with_participant_range(
3934 3,
3935 Some(7),
3936 DeviceCommandPhase::Initialization,
3937 DeviceNativeOperationId::new("test.restore").unwrap(),
3938 DeviceExecutionPath::Eager,
3939 DeviceBatchingForm::Scalar,
3940 2,
3941 1,
3942 4,
3943 0,
3944 1,
3945 None,
3946 )
3947 .unwrap();
3948 assert_eq!(row.participant_start(), 2);
3949 assert_eq!(row.participant_count(), 1);
3950 assert_eq!(row.participant_end(), 3);
3951 assert!(DeviceNativeWorkAttribution::with_participant_range(
3952 3,
3953 Some(7),
3954 DeviceCommandPhase::Initialization,
3955 DeviceNativeOperationId::new("test.restore").unwrap(),
3956 DeviceExecutionPath::Eager,
3957 DeviceBatchingForm::Scalar,
3958 u32::MAX,
3959 2,
3960 4,
3961 0,
3962 1,
3963 None,
3964 )
3965 .is_none());
3966 }
3967
3968 fn replay_test_program_id() -> DeviceReusableExecutionProgramId {
3969 let plan_hash: PlanHash =
3970 serde_json::from_value(serde_json::json!("a".repeat(64))).unwrap();
3971 let bucket = ReusableExecutionBucketSpec::new(
3972 ReusableExecutionClassId::new("device-attribution-test").unwrap(),
3973 ReusableExecutionCapacity::new(2, 3, 1).unwrap(),
3974 )
3975 .unwrap();
3976 DeviceReusableExecutionProgramId::new(
3977 plan_hash,
3978 "b".repeat(64),
3979 ExecutionLaneId::mint().unwrap(),
3980 bucket.bucket_id().clone(),
3981 "c".repeat(64),
3982 "d".repeat(64),
3983 7,
3984 2,
3985 3,
3986 1,
3987 )
3988 .unwrap()
3989 }
3990
3991 fn replay_test_physical(
3992 execution_path: DeviceExecutionPath,
3993 node_index: u32,
3994 graph_node_count: Option<u64>,
3995 ) -> DeviceNativeWorkAttribution {
3996 DeviceNativeWorkAttribution::new(
3997 0,
3998 Some(node_index),
3999 DeviceCommandPhase::Compute,
4000 DeviceNativeOperationId::new("vnext_reusable_execution").unwrap(),
4001 execution_path,
4002 DeviceBatchingForm::ParticipantLoop,
4003 2,
4004 3,
4005 1,
4006 0,
4007 graph_node_count,
4008 )
4009 .unwrap()
4010 }
4011
4012 fn replay_test_logical(
4013 ordinal: u32,
4014 node_index: u32,
4015 graph_node_count: u64,
4016 ) -> DeviceReplayedLogicalCommandAttribution {
4017 replay_test_logical_with_tokens(ordinal, node_index, 3, graph_node_count)
4018 }
4019
4020 fn replay_test_logical_with_tokens(
4021 ordinal: u32,
4022 node_index: u32,
4023 token_count: u64,
4024 graph_node_count: u64,
4025 ) -> DeviceReplayedLogicalCommandAttribution {
4026 DeviceReplayedLogicalCommandAttribution::new(
4027 ordinal,
4028 node_index,
4029 DeviceNativeOperationId::new("test.logical.compute").unwrap(),
4030 DeviceBatchingForm::ParticipantLoop,
4031 2,
4032 token_count,
4033 1,
4034 0,
4035 graph_node_count,
4036 )
4037 .unwrap()
4038 }
4039
4040 #[test]
4041 fn replayed_segment_separates_one_physical_launch_from_logical_plan_nodes() {
4042 let attribution = DeviceSubmissionAttribution::with_replayed_segments(
4043 vec![replay_test_physical(
4044 DeviceExecutionPath::Replayed,
4045 4,
4046 Some(5),
4047 )],
4048 vec![DeviceReplayedSegmentAttribution::new(
4049 0,
4050 replay_test_program_id(),
4051 DeviceReusableExecutionSegment::new(0, 4, 6, 2).unwrap(),
4052 "e".repeat(64),
4053 vec![
4054 replay_test_logical_with_tokens(0, 4, 3, 2),
4055 replay_test_logical_with_tokens(1, 5, 1, 3),
4056 ],
4057 )
4058 .unwrap()],
4059 )
4060 .unwrap();
4061
4062 assert_eq!(attribution.commands().len(), 1);
4063 assert_eq!(attribution.replayed_segments().len(), 1);
4064 assert_eq!(
4065 attribution.replayed_segments()[0].logical_commands().len(),
4066 2
4067 );
4068 }
4069
4070 #[test]
4071 fn replayed_segment_rejects_incomplete_or_mismatched_logical_attribution() {
4072 assert!(DeviceReplayedLogicalCommandAttribution::new(
4073 0,
4074 4,
4075 DeviceNativeOperationId::new("test.logical.compute").unwrap(),
4076 DeviceBatchingForm::ParticipantLoop,
4077 2,
4078 3,
4079 1,
4080 0,
4081 0,
4082 )
4083 .is_none());
4084
4085 let segment = DeviceReusableExecutionSegment::new(0, 4, 6, 2).unwrap();
4086 assert!(DeviceReplayedSegmentAttribution::new(
4087 0,
4088 replay_test_program_id(),
4089 segment.clone(),
4090 "e".repeat(64),
4091 vec![replay_test_logical(0, 4, 2), replay_test_logical(1, 6, 3)],
4092 )
4093 .is_none());
4094
4095 let replayed = || {
4096 DeviceReplayedSegmentAttribution::new(
4097 0,
4098 replay_test_program_id(),
4099 segment.clone(),
4100 "e".repeat(64),
4101 vec![replay_test_logical(0, 4, 2), replay_test_logical(1, 5, 3)],
4102 )
4103 .unwrap()
4104 };
4105 assert!(DeviceSubmissionAttribution::with_replayed_segments(
4106 vec![replay_test_physical(
4107 DeviceExecutionPath::Replayed,
4108 4,
4109 Some(4),
4110 )],
4111 vec![replayed()],
4112 )
4113 .is_none());
4114 assert!(DeviceSubmissionAttribution::with_replayed_segments(
4115 vec![replay_test_physical(DeviceExecutionPath::Eager, 4, None)],
4116 vec![replayed()],
4117 )
4118 .is_none());
4119 assert!(DeviceSubmissionAttribution::with_replayed_segments(
4120 vec![replay_test_physical(
4121 DeviceExecutionPath::Replayed,
4122 5,
4123 Some(5),
4124 )],
4125 vec![replayed()],
4126 )
4127 .is_none());
4128 }
4129}
4130
4131#[cfg(test)]
4132mod deferred_cleanup_tests {
4133 use super::*;
4134 use std::collections::VecDeque;
4135 use std::sync::atomic::{AtomicBool, AtomicUsize};
4136 use std::sync::{Arc, Barrier};
4137
4138 struct ScriptedCleanup {
4139 outcomes: VecDeque<DeferredDeviceCleanupDisposition>,
4140 attempts: Arc<AtomicUsize>,
4141 dropped: Arc<AtomicBool>,
4142 }
4143
4144 impl DeferredDeviceCleanupTask for ScriptedCleanup {
4145 fn try_cleanup(&mut self) -> DeferredDeviceCleanupDisposition {
4146 self.attempts.fetch_add(1, Ordering::AcqRel);
4147 self.outcomes
4148 .pop_front()
4149 .unwrap_or(DeferredDeviceCleanupDisposition::Completed)
4150 }
4151 }
4152
4153 impl Drop for ScriptedCleanup {
4154 fn drop(&mut self) {
4155 self.dropped.store(true, Ordering::Release);
4156 }
4157 }
4158
4159 struct PanicOnceCleanup {
4160 first: bool,
4161 attempts: Arc<AtomicUsize>,
4162 }
4163
4164 impl DeferredDeviceCleanupTask for PanicOnceCleanup {
4165 fn try_cleanup(&mut self) -> DeferredDeviceCleanupDisposition {
4166 self.attempts.fetch_add(1, Ordering::AcqRel);
4167 if self.first {
4168 self.first = false;
4169 panic!("injected deferred cleanup panic");
4170 }
4171 DeferredDeviceCleanupDisposition::Completed
4172 }
4173 }
4174
4175 struct BlockingCleanup {
4176 entered: Arc<Barrier>,
4177 release: Arc<Barrier>,
4178 }
4179
4180 impl DeferredDeviceCleanupTask for BlockingCleanup {
4181 fn try_cleanup(&mut self) -> DeferredDeviceCleanupDisposition {
4182 self.entered.wait();
4183 self.release.wait();
4184 DeferredDeviceCleanupDisposition::Completed
4185 }
4186 }
4187
4188 fn scripted(
4189 outcomes: impl IntoIterator<Item = DeferredDeviceCleanupDisposition>,
4190 ) -> (ScriptedCleanup, Arc<AtomicUsize>, Arc<AtomicBool>) {
4191 let attempts = Arc::new(AtomicUsize::new(0));
4192 let dropped = Arc::new(AtomicBool::new(false));
4193 (
4194 ScriptedCleanup {
4195 outcomes: outcomes.into_iter().collect(),
4196 attempts: Arc::clone(&attempts),
4197 dropped: Arc::clone(&dropped),
4198 },
4199 attempts,
4200 dropped,
4201 )
4202 }
4203
4204 #[test]
4205 fn encoded_operation_preserves_program_dynamic_compute_and_result_boundaries() {
4206 let operation = EncodedDeviceOperation::compute("compute")
4207 .with_program_binding("program-bind")
4208 .with_dynamic_binding("bind-a")
4209 .with_dynamic_binding("bind-b")
4210 .with_result_binding("writeback");
4211 let mut batch = DeviceCommandBatch::with_capacity(5);
4212 batch.push_operation(0, operation);
4213
4214 let entries = batch.into_entries();
4215 assert_eq!(
4216 entries
4217 .iter()
4218 .map(DeviceCommandEntry::phase)
4219 .collect::<Vec<_>>(),
4220 vec![
4221 DeviceCommandPhase::DynamicBinding,
4222 DeviceCommandPhase::DynamicBinding,
4223 DeviceCommandPhase::DynamicBinding,
4224 DeviceCommandPhase::Compute,
4225 DeviceCommandPhase::ResultBinding,
4226 ]
4227 );
4228 assert_eq!(
4229 entries
4230 .into_iter()
4231 .map(DeviceCommandEntry::into_parts)
4232 .map(|(_, _, _, command)| command)
4233 .collect::<Vec<_>>(),
4234 vec!["program-bind", "bind-a", "bind-b", "compute", "writeback",]
4235 );
4236 }
4237
4238 #[test]
4239 fn node_initialization_carries_core_owned_logical_work() {
4240 let logical_work =
4241 DeviceCommandLogicalWork::new(DeviceBatchingForm::Packed, 4, 17).unwrap();
4242 let mut batch = DeviceCommandBatch::with_capacity(1);
4243 batch.push_node_initialization(7, logical_work, "workspace-zero");
4244
4245 let mut entries = batch.into_entries();
4246 assert_eq!(entries.len(), 1);
4247 let entry = entries.pop().unwrap();
4248 assert_eq!(entry.phase(), DeviceCommandPhase::Initialization);
4249 assert_eq!(entry.node_index(), Some(7));
4250 assert_eq!(entry.logical_work(), Some(logical_work));
4251 assert_eq!(entry.command(), &"workspace-zero");
4252 assert_eq!(logical_work.participant_start(), 0);
4253 assert_eq!(logical_work.participant_end(), 4);
4254 let scoped =
4255 DeviceCommandLogicalWork::for_participant_range(DeviceBatchingForm::Scalar, 3, 1, 5)
4256 .unwrap();
4257 assert_eq!(scoped.participant_start(), 3);
4258 assert_eq!(scoped.participant_count(), 1);
4259 assert_eq!(scoped.participant_end(), 4);
4260 assert!(DeviceCommandLogicalWork::new(DeviceBatchingForm::Packed, 0, 17).is_err());
4261 assert!(DeviceCommandLogicalWork::for_participant_range(
4262 DeviceBatchingForm::Packed,
4263 u32::MAX,
4264 2,
4265 17,
4266 )
4267 .is_err());
4268 }
4269
4270 #[test]
4271 fn reusable_execution_observation_preserves_each_fallback_and_replay_counter() {
4272 let mut observation = DeviceReusableExecutionObservation::default();
4273 observation.observe_candidate_segment();
4274 observation.observe_captured_segment();
4275 observation.observe_uploaded_segment();
4276 observation.observe_cache_hit_segment();
4277 observation.observe_cached_rejected_segment();
4278 observation.observe_capture_rejection();
4279 observation.observe_quiescence_deferred_segment();
4280 observation.observe_capacity_deferred_segment();
4281 observation.observe_outside_preparation_segment();
4282 observation.observe_evicted_segment();
4283 observation.observe_replayed_segment(3);
4284 observation.observe_eager_command();
4285
4286 assert_eq!(observation.candidate_segments(), 1);
4287 assert_eq!(observation.captured_segments(), 1);
4288 assert_eq!(observation.uploaded_segments(), 1);
4289 assert_eq!(observation.cache_hit_segments(), 1);
4290 assert_eq!(observation.cached_rejected_segments(), 1);
4291 assert_eq!(observation.capture_rejected_segments(), 1);
4292 assert_eq!(observation.quiescence_deferred_segments(), 1);
4293 assert_eq!(observation.capacity_deferred_segments(), 1);
4294 assert_eq!(observation.outside_preparation_segments(), 1);
4295 assert_eq!(observation.evicted_segments(), 1);
4296 assert_eq!(observation.replayed_segments(), 1);
4297 assert_eq!(observation.replayed_commands(), 3);
4298 assert_eq!(observation.eager_commands(), 1);
4299
4300 let value = serde_json::to_value(observation).expect("observation serializes");
4301 for field in [
4302 "candidate_segments",
4303 "captured_segments",
4304 "uploaded_segments",
4305 "cache_hit_segments",
4306 "cached_rejected_segments",
4307 "capture_rejected_segments",
4308 "quiescence_deferred_segments",
4309 "capacity_deferred_segments",
4310 "outside_preparation_segments",
4311 "evicted_segments",
4312 "replayed_segments",
4313 "eager_commands",
4314 ] {
4315 assert_eq!(value[field], 1, "counter {field} must remain typed");
4316 }
4317 assert_eq!(value["replayed_commands"], 3);
4318 }
4319
4320 #[test]
4321 fn retryable_and_quarantined_cleanup_owners_remain_reachable() {
4322 for first in [
4323 DeferredDeviceCleanupDisposition::Retryable,
4324 DeferredDeviceCleanupDisposition::Quarantined,
4325 ] {
4326 let domain = new_deferred_device_cleanup_domain();
4327 let (task, attempts, dropped) =
4328 scripted([first, DeferredDeviceCleanupDisposition::Completed]);
4329 defer_device_cleanup(domain, task);
4330
4331 let first_receipt = maintain_deferred_device_cleanups(domain, 1);
4332 assert_eq!(first_receipt.attempted(), 1);
4333 assert_eq!(first_receipt.completed(), 0);
4334 assert_eq!(first_receipt.status_after().pending(), 1);
4335 assert!(!dropped.load(Ordering::Acquire));
4336
4337 let second_receipt = maintain_deferred_device_cleanups(domain, 1);
4338 assert_eq!(second_receipt.completed(), 1);
4339 assert_eq!(second_receipt.status_after().pending(), 0);
4340 assert_eq!(attempts.load(Ordering::Acquire), 2);
4341 assert!(dropped.load(Ordering::Acquire));
4342 assert!(retire_deferred_device_cleanup_domain(domain));
4343 }
4344 }
4345
4346 #[test]
4347 fn panicking_cleanup_owner_is_retried_in_place() {
4348 let domain = new_deferred_device_cleanup_domain();
4349 let attempts = Arc::new(AtomicUsize::new(0));
4350 defer_device_cleanup(
4351 domain,
4352 PanicOnceCleanup {
4353 first: true,
4354 attempts: Arc::clone(&attempts),
4355 },
4356 );
4357
4358 let first = maintain_deferred_device_cleanups(domain, 1);
4359 assert_eq!(first.panicked(), 1);
4360 assert_eq!(first.status_after().panicked(), 1);
4361 assert_eq!(first.status_after().pending(), 1);
4362 let second = maintain_deferred_device_cleanups(domain, 1);
4363 assert_eq!(second.completed(), 1);
4364 assert_eq!(second.status_after().pending(), 0);
4365 assert_eq!(attempts.load(Ordering::Acquire), 2);
4366 assert!(retire_deferred_device_cleanup_domain(domain));
4367 }
4368
4369 #[test]
4370 fn saturation_keeps_every_owner_and_bounds_each_maintenance_pass() {
4371 let domain = new_deferred_device_cleanup_domain();
4372 let task_count = MAX_DEFERRED_DEVICE_CLEANUP_TASKS + 1;
4373 for _ in 0..task_count {
4374 let (task, _, _) = scripted([DeferredDeviceCleanupDisposition::Completed]);
4375 defer_device_cleanup(domain, task);
4376 }
4377 let saturated = deferred_device_cleanup_status(domain);
4378 assert_eq!(saturated.pending(), task_count);
4379 assert!(saturated.is_saturated());
4380
4381 let first = maintain_deferred_device_cleanups(
4382 domain,
4383 MAX_DEFERRED_DEVICE_CLEANUP_MAINTENANCE_TASKS,
4384 );
4385 assert_eq!(first.attempted(), MAX_DEFERRED_DEVICE_CLEANUP_TASKS);
4386 assert_eq!(first.completed(), MAX_DEFERRED_DEVICE_CLEANUP_TASKS);
4387 assert_eq!(first.status_after().pending(), 1);
4388 let second = maintain_deferred_device_cleanups(domain, 1);
4389 assert_eq!(second.completed(), 1);
4390 assert_eq!(second.status_after().pending(), 0);
4391 assert!(retire_deferred_device_cleanup_domain(domain));
4392 }
4393
4394 #[test]
4395 fn blocked_cleanup_does_not_withhold_sibling_task_or_registry() {
4396 let domain = new_deferred_device_cleanup_domain();
4397 let entered = Arc::new(Barrier::new(2));
4398 let release = Arc::new(Barrier::new(2));
4399 defer_device_cleanup(
4400 domain,
4401 BlockingCleanup {
4402 entered: Arc::clone(&entered),
4403 release: Arc::clone(&release),
4404 },
4405 );
4406 let (ready, _, _) = scripted([DeferredDeviceCleanupDisposition::Completed]);
4407 defer_device_cleanup(domain, ready);
4408
4409 let (blocked_receipt, ready_receipt) = std::thread::scope(|scope| {
4410 let worker = std::thread::Builder::new()
4411 .name("vnext-cleanup-domain-isolation".to_owned())
4412 .spawn_scoped(scope, move || maintain_deferred_device_cleanups(domain, 2))
4413 .expect("the single bounded cleanup isolation worker starts");
4414 entered.wait();
4415 let ready_receipt = maintain_deferred_device_cleanups(domain, 1);
4416 release.wait();
4417 let blocked_receipt = worker
4418 .join()
4419 .expect("the bounded cleanup isolation worker does not panic");
4420 (blocked_receipt, ready_receipt)
4421 });
4422
4423 assert_eq!(ready_receipt.completed(), 1);
4424 assert_eq!(blocked_receipt.completed(), 1);
4425 assert_eq!(deferred_device_cleanup_status(domain).pending(), 0);
4426 assert!(retire_deferred_device_cleanup_domain(domain));
4427 }
4428}