Skip to main content

ferrum_interfaces/vnext/operation/
dispatch_contract.rs

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/// Typed host boundaries inside one prepared wave dispatch. These intervals
225/// are host wall time and must not be combined with backend device timing.
226#[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
237/// Diagnostic-only timing sink for the prepared-wave dispatch hot path.
238///
239/// The sink receives only a stage and completed host duration; it receives no
240/// command, resource, or correctness authority. `ENABLED = false` is the
241/// compile-time off path: no clock is read and `record` is never called.
242/// Enabled implementations run on the submission thread and must not block,
243/// allocate, or panic.
244pub 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/// Scratch bytes presented to every provider invocation in one submission.
422/// `ProviderContract` preserves the selected provider's declared reuse policy;
423/// explicit fill patterns are diagnostic proof inputs and are encoded before
424/// compute outside reusable executable capture.
425#[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/// Core-owned execution controls independent from timing instrumentation.
434///
435/// Determinism gates use the strict constructors. Product requests use
436/// `adaptive`, allowing the runtime to select a compatible eager or resident
437/// path without changing provider semantics.
438#[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/// One typed host input written into an exact participant's resolved plan
483/// input before any provider command executes. The request names semantic
484/// plan coordinates rather than exposing backend buffers or allocation ids.
485#[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}