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