1use serde::{Deserialize, Serialize};
2use std::{
3 fmt,
4 time::{Duration, Instant},
5};
6
7use super::super::{
8 BatchInvocationId, CompletionHandle, DefinitelyNotSubmittedRetryAuthority,
9 DefinitelyNotSubmittedWaveRetryAuthority, DeviceCommandPhase, DeviceComputePathRequirement,
10 DeviceRuntime, DeviceSubmissionAttribution, DeviceSubmissionExecutionTiming,
11 DeviceSubmissionStage, DeviceSubmissionTimingSink, DeviceTimingMeasurement, HostTransferLayout,
12 IdentifiedFailure, IndeterminateSubmissionHandle, NodeId, VNextError,
13};
14use super::foundation::invalid_operation;
15use super::{BatchOperationIdentity, OperationFailure};
16
17pub trait DispatchRetryAuthority: fmt::Debug {
18 fn prior_attempt(&self) -> BatchInvocationId;
19}
20
21impl<R: DeviceRuntime> DispatchRetryAuthority for DefinitelyNotSubmittedRetryAuthority<R> {
22 fn prior_attempt(&self) -> BatchInvocationId {
23 self.prior_attempt()
24 }
25}
26
27impl<R: DeviceRuntime> DispatchRetryAuthority for DefinitelyNotSubmittedWaveRetryAuthority<R> {
28 fn prior_attempt(&self) -> BatchInvocationId {
29 self.prior_attempt()
30 }
31}
32
33pub enum OperationDispatchError<R, Retry = DefinitelyNotSubmittedRetryAuthority<R>>
34where
35 R: DeviceRuntime,
36 Retry: DispatchRetryAuthority,
37{
38 Contract(VNextError),
39 Provider(OperationFailure),
40 Initialization(IdentifiedFailure),
41 InputUpload(IdentifiedFailure),
42 DefinitelyNotSubmitted {
43 failures: Vec<IdentifiedFailure>,
44 retry: Retry,
45 },
46 SubmissionIndeterminate {
47 recovery: IndeterminateSubmissionHandle<R>,
48 },
49 PostSubmitContract {
50 error: VNextError,
51 completion: CompletionHandle<R>,
52 },
53}
54
55pub type SubmissionWaveDispatchError<R> =
56 OperationDispatchError<R, DefinitelyNotSubmittedWaveRetryAuthority<R>>;
57
58#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
59pub struct BoundDeviceSubmissionAttribution {
60 batch_identity: BatchOperationIdentity,
61 submission_fingerprint: String,
62 device: DeviceSubmissionAttribution,
63 terminal_timing: DeviceTimingMeasurement<DeviceSubmissionExecutionTiming>,
64}
65
66impl BoundDeviceSubmissionAttribution {
67 pub(super) fn new(
68 batch_identity: BatchOperationIdentity,
69 submission_fingerprint: String,
70 device: DeviceSubmissionAttribution,
71 ) -> Result<Self, VNextError> {
72 for command in device.commands() {
73 let Some(node_index) = command.node_index() else {
74 continue;
75 };
76 let node_index_usize = usize::try_from(node_index).map_err(|_| {
77 invalid_operation(format!(
78 "device command {} node index exceeds host address space",
79 command.command_index()
80 ))
81 })?;
82 let node_participant_count = batch_identity
83 .node_participant_count(node_index_usize)
84 .and_then(|count| u32::try_from(count).ok())
85 .ok_or_else(|| {
86 invalid_operation(format!(
87 "device command {} references absent node {}",
88 command.command_index(),
89 node_index
90 ))
91 })?;
92 let participant_range_is_valid = command.participant_start() < node_participant_count
93 && command.participant_end() <= node_participant_count;
94 let command_requires_full_node =
95 command.command_phase() != DeviceCommandPhase::Initialization;
96 if !participant_range_is_valid
97 || command_requires_full_node
98 && (command.participant_start() != 0
99 || command.participant_count() != node_participant_count)
100 {
101 return Err(invalid_operation(format!(
102 "device command {} phase {:?} participant range {}..{} differs from node {} participant count {}",
103 command.command_index(),
104 command.command_phase(),
105 command.participant_start(),
106 command.participant_end(),
107 node_index,
108 node_participant_count
109 )));
110 }
111 }
112 for replayed_segment in device.replayed_segments() {
113 let program_id = replayed_segment.program_id();
114 if program_id.plan_hash() != batch_identity.plan_hash()
115 || program_id.runtime_implementation_fingerprint()
116 != batch_identity.runtime_implementation_fingerprint()
117 || program_id.lane_id() != batch_identity.lane_id()
118 || replayed_segment.logical_commands().iter().any(|command| {
119 let Ok(node_index) = usize::try_from(command.node_index()) else {
120 return true;
121 };
122 batch_identity.node_id_at(node_index).is_none()
123 || u32::try_from(
124 batch_identity
125 .node_participant_count(node_index)
126 .unwrap_or_default(),
127 )
128 .map_or(true, |count| count != command.participant_count())
129 })
130 || replayed_segment.logical_commands().first().is_none_or(|_| {
131 usize::try_from(replayed_segment.physical_command_index())
132 .ok()
133 .and_then(|index| device.commands().get(index))
134 .is_none_or(|physical| {
135 physical.participant_start() != 0
136 || program_id.immediate_sequences() != physical.participant_count()
137 || program_id.immediate_tokens() != physical.token_count()
138 })
139 })
140 {
141 return Err(invalid_operation(
142 "replayed segment attribution differs from its batch or sealed program identity",
143 ));
144 }
145 }
146 Ok(Self {
147 batch_identity,
148 submission_fingerprint,
149 device,
150 terminal_timing: DeviceTimingMeasurement::NotRequested,
151 })
152 }
153
154 pub fn bind_terminal_timing(
155 mut self,
156 terminal_timing: DeviceTimingMeasurement<DeviceSubmissionExecutionTiming>,
157 ) -> Result<Self, VNextError> {
158 if let DeviceTimingMeasurement::Measured(timing) = &terminal_timing {
159 if usize::try_from(timing.command_count()).ok() != Some(self.device.commands().len())
160 || self
161 .device
162 .commands()
163 .iter()
164 .enumerate()
165 .any(|(index, command)| {
166 u32::try_from(index).ok() != Some(command.command_index())
167 })
168 {
169 return Err(invalid_operation(
170 "terminal device timing coverage differs from submission command attribution",
171 ));
172 }
173 }
174 self.terminal_timing = terminal_timing;
175 Ok(self)
176 }
177
178 pub fn batch_identity(&self) -> &BatchOperationIdentity {
179 &self.batch_identity
180 }
181
182 pub fn submission_fingerprint(&self) -> &str {
183 &self.submission_fingerprint
184 }
185
186 pub fn device(&self) -> &DeviceSubmissionAttribution {
187 &self.device
188 }
189
190 pub const fn terminal_timing(
191 &self,
192 ) -> &DeviceTimingMeasurement<DeviceSubmissionExecutionTiming> {
193 &self.terminal_timing
194 }
195}
196
197#[must_use = "profiled submission evidence and completion must be consumed together"]
198pub struct ProfiledSubmissionHandle<R: DeviceRuntime> {
199 completion: CompletionHandle<R>,
200 attribution: Option<BoundDeviceSubmissionAttribution>,
201}
202
203impl<R: DeviceRuntime> ProfiledSubmissionHandle<R> {
204 pub(super) fn new(
205 completion: CompletionHandle<R>,
206 attribution: Option<BoundDeviceSubmissionAttribution>,
207 ) -> Self {
208 Self {
209 completion,
210 attribution,
211 }
212 }
213
214 pub fn into_parts(
215 self,
216 ) -> (
217 CompletionHandle<R>,
218 Option<BoundDeviceSubmissionAttribution>,
219 ) {
220 (self.completion, self.attribution)
221 }
222}
223
224#[derive(Debug, Clone, Copy, PartialEq, Eq)]
227pub enum SubmissionWaveDispatchStage {
228 ContractValidateAndReserve,
229 BackingAndInputEncode,
230 ProviderNodeEncode,
231 LaneReserve,
232 DeviceRuntimeSubmit,
233 CompletionArm,
234 LaneReserveSubmitAndArm,
235}
236
237pub trait SubmissionWaveDispatchTimingSink: DeviceSubmissionTimingSink {
245 fn record(&self, stage: SubmissionWaveDispatchStage, elapsed: Duration);
246}
247
248pub(super) struct DisabledSubmissionWaveDispatchTimingSink;
249
250impl DeviceSubmissionTimingSink for DisabledSubmissionWaveDispatchTimingSink {
251 const ENABLED: bool = false;
252
253 fn record_device_submission(&self, _stage: DeviceSubmissionStage, _elapsed: Duration) {
254 unreachable!("disabled device submission timing cannot record")
255 }
256}
257
258impl SubmissionWaveDispatchTimingSink for DisabledSubmissionWaveDispatchTimingSink {
259 fn record(&self, _stage: SubmissionWaveDispatchStage, _elapsed: Duration) {
260 unreachable!("disabled submission timing cannot record")
261 }
262}
263
264pub(super) struct SubmissionWaveDispatchStageTimer<'sink, S>
265where
266 S: SubmissionWaveDispatchTimingSink,
267{
268 sink: &'sink S,
269 stage: SubmissionWaveDispatchStage,
270 started: Option<Instant>,
271}
272
273impl<'sink, S> SubmissionWaveDispatchStageTimer<'sink, S>
274where
275 S: SubmissionWaveDispatchTimingSink,
276{
277 #[inline(always)]
278 pub(super) fn start(sink: &'sink S, stage: SubmissionWaveDispatchStage) -> Self {
279 Self {
280 sink,
281 stage,
282 started: S::ENABLED.then(Instant::now),
283 }
284 }
285}
286
287impl<S> Drop for SubmissionWaveDispatchStageTimer<'_, S>
288where
289 S: SubmissionWaveDispatchTimingSink,
290{
291 fn drop(&mut self) {
292 if let Some(started) = self.started.take() {
293 if !std::thread::panicking() {
294 self.sink.record(self.stage, started.elapsed());
295 }
296 }
297 }
298}
299
300#[cfg(test)]
301mod submission_wave_dispatch_timing_tests {
302 use std::time::Duration;
303
304 use super::{
305 DeviceSubmissionStage, DeviceSubmissionTimingSink, SubmissionWaveDispatchStage,
306 SubmissionWaveDispatchStageTimer, SubmissionWaveDispatchTimingSink,
307 };
308
309 struct DisabledPanicSink;
310
311 impl DeviceSubmissionTimingSink for DisabledPanicSink {
312 const ENABLED: bool = false;
313
314 fn record_device_submission(&self, _stage: DeviceSubmissionStage, _elapsed: Duration) {
315 panic!("disabled device timing sink was called");
316 }
317 }
318
319 impl SubmissionWaveDispatchTimingSink for DisabledPanicSink {
320 fn record(&self, _stage: SubmissionWaveDispatchStage, _elapsed: Duration) {
321 panic!("disabled timing sink was called");
322 }
323 }
324
325 #[test]
326 fn disabled_submission_timing_does_not_record() {
327 let timer = SubmissionWaveDispatchStageTimer::start(
328 &DisabledPanicSink,
329 SubmissionWaveDispatchStage::ProviderNodeEncode,
330 );
331 drop(timer);
332
333 assert!(!DisabledPanicSink::ENABLED);
334 }
335}
336
337impl<R, Retry> fmt::Debug for OperationDispatchError<R, Retry>
338where
339 R: DeviceRuntime,
340 Retry: DispatchRetryAuthority,
341{
342 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
343 match self {
344 Self::Contract(error) => formatter.debug_tuple("Contract").field(error).finish(),
345 Self::Provider(error) => formatter.debug_tuple("Provider").field(error).finish(),
346 Self::Initialization(error) => formatter
347 .debug_tuple("Initialization")
348 .field(error)
349 .finish(),
350 Self::InputUpload(error) => formatter.debug_tuple("InputUpload").field(error).finish(),
351 Self::DefinitelyNotSubmitted { failures, retry } => formatter
352 .debug_struct("DefinitelyNotSubmitted")
353 .field("failures", failures)
354 .field("retry", retry)
355 .finish(),
356 Self::SubmissionIndeterminate { recovery } => formatter
357 .debug_struct("SubmissionIndeterminate")
358 .field("recovery", recovery)
359 .finish(),
360 Self::PostSubmitContract { error, completion } => formatter
361 .debug_struct("PostSubmitContract")
362 .field("error", error)
363 .field("completion", completion)
364 .finish(),
365 }
366 }
367}
368
369impl<R, Retry> fmt::Display for OperationDispatchError<R, Retry>
370where
371 R: DeviceRuntime,
372 Retry: DispatchRetryAuthority,
373{
374 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
375 match self {
376 Self::Contract(error) => {
377 write!(formatter, "operation dispatch contract failed: {error}")
378 }
379 Self::Provider(error) => write!(
380 formatter,
381 "operation provider failed with {}: {}",
382 error.code(),
383 error.message()
384 ),
385 Self::Initialization(error) => write!(
386 formatter,
387 "operation backing initialization failed with {}: {}",
388 error.failure().code(),
389 error.failure().message()
390 ),
391 Self::InputUpload(error) => write!(
392 formatter,
393 "operation input upload failed with {}: {}",
394 error.failure().code(),
395 error.failure().message()
396 ),
397 Self::DefinitelyNotSubmitted { failures, retry } => write!(
398 formatter,
399 "operation attempt {} with {} participants was definitely not submitted: {}",
400 retry.prior_attempt(),
401 failures.len(),
402 failures
403 .first()
404 .map(|failure| failure.failure().message())
405 .unwrap_or("missing classified participant failure")
406 ),
407 Self::SubmissionIndeterminate { recovery } => write!(
408 formatter,
409 "operation submission may have reached the device; completion slot {} retains ownership",
410 recovery.slot_id().get()
411 ),
412 Self::PostSubmitContract { error, completion } => write!(
413 formatter,
414 "operation submission reached the device but slot {} observed a contract failure: {error}",
415 completion.slot_id().get()
416 ),
417 }
418 }
419}
420
421#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
426#[serde(rename_all = "snake_case")]
427pub enum SubmissionScratchInitialization {
428 #[default]
429 ProviderContract,
430 FillByte(u8),
431}
432
433#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
439#[serde(deny_unknown_fields)]
440pub struct SubmissionExecutionPolicy {
441 compute_path: DeviceComputePathRequirement,
442 scratch_initialization: SubmissionScratchInitialization,
443}
444
445impl SubmissionExecutionPolicy {
446 pub const fn adaptive() -> Self {
447 Self {
448 compute_path: DeviceComputePathRequirement::Adaptive,
449 scratch_initialization: SubmissionScratchInitialization::ProviderContract,
450 }
451 }
452
453 pub const fn determinism_eager(scratch_fill: u8) -> Self {
454 Self {
455 compute_path: DeviceComputePathRequirement::EagerOnly,
456 scratch_initialization: SubmissionScratchInitialization::FillByte(scratch_fill),
457 }
458 }
459
460 pub const fn determinism_replayed(scratch_fill: u8) -> Self {
461 Self {
462 compute_path: DeviceComputePathRequirement::ReplayedOnly,
463 scratch_initialization: SubmissionScratchInitialization::FillByte(scratch_fill),
464 }
465 }
466
467 pub const fn compute_path(self) -> DeviceComputePathRequirement {
468 self.compute_path
469 }
470
471 pub const fn scratch_initialization(self) -> SubmissionScratchInitialization {
472 self.scratch_initialization
473 }
474}
475
476impl Default for SubmissionExecutionPolicy {
477 fn default() -> Self {
478 Self::adaptive()
479 }
480}
481
482#[derive(Debug, Clone, PartialEq, Eq)]
486pub struct SubmissionWaveInputUpload {
487 node_id: NodeId,
488 participant_index: u32,
489 input_ordinal: u32,
490 logical_offset_bytes: u64,
491 source_layout: HostTransferLayout,
492 bytes: Vec<u8>,
493}
494
495impl SubmissionWaveInputUpload {
496 pub fn new(
497 node_id: NodeId,
498 participant_index: u32,
499 input_ordinal: u32,
500 logical_offset_bytes: u64,
501 source_layout: HostTransferLayout,
502 bytes: Vec<u8>,
503 ) -> Result<Self, VNextError> {
504 source_layout.validate_bytes(bytes.len())?;
505 let byte_len = source_layout.byte_len()?;
506 if logical_offset_bytes.checked_add(byte_len).is_none()
507 || logical_offset_bytes % source_layout.element_type().size_bytes() != 0
508 {
509 return Err(invalid_operation(
510 "submission input upload has an invalid aligned logical range",
511 ));
512 }
513 Ok(Self {
514 node_id,
515 participant_index,
516 input_ordinal,
517 logical_offset_bytes,
518 source_layout,
519 bytes,
520 })
521 }
522
523 pub fn node_id(&self) -> &NodeId {
524 &self.node_id
525 }
526
527 pub const fn participant_index(&self) -> u32 {
528 self.participant_index
529 }
530
531 pub const fn input_ordinal(&self) -> u32 {
532 self.input_ordinal
533 }
534
535 pub const fn logical_offset_bytes(&self) -> u64 {
536 self.logical_offset_bytes
537 }
538
539 pub const fn source_layout(&self) -> HostTransferLayout {
540 self.source_layout
541 }
542
543 pub fn bytes(&self) -> &[u8] {
544 &self.bytes
545 }
546}
547
548impl<R, Retry> std::error::Error for OperationDispatchError<R, Retry>
549where
550 R: DeviceRuntime,
551 Retry: DispatchRetryAuthority,
552{
553}