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