Skip to main content

hrx/execution/
graph.rs

1use super::{
2    Access, BufferView, Completion, Engine, KernelContract, Runtime, RuntimeOwner,
3    completion::Signal,
4};
5use crate::{Error, Result};
6use std::sync::{
7    Arc, Mutex,
8    atomic::{AtomicBool, Ordering},
9};
10
11/// Independent GPU scheduling lanes. Memory hazards apply across every lane.
12#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize)]
13pub enum GpuLane {
14    /// Host-to-device transfers.
15    Upload,
16    /// Kernels, fills, and device-to-device copies.
17    #[default]
18    Compute,
19    /// Device-to-host transfers.
20    Download,
21}
22
23/// Trusted, fixed GPU launch dimensions and argument contract.
24#[derive(Clone)]
25pub struct GpuKernel {
26    pub(super) raw: crate::gpu::Kernel,
27    pub(super) contract: KernelContract,
28    pub(super) grid: [u32; 3],
29    pub(super) block: [u32; 3],
30    pub(super) runtime: Arc<RuntimeOwner>,
31}
32impl GpuKernel {
33    /// Collect value/operation sanitizer reports after submitted work completes.
34    /// Reports are shared by all graph uses of this kernel and drained once.
35    /// Returns `Busy` while native work is pending.
36    pub fn sanitizer_reports(&self) -> Result<crate::fabric::SanitizerReports> {
37        self.raw.sanitizer_reports()
38    }
39    /// Fixed invocation contract.
40    pub fn contract(&self) -> &KernelContract {
41        &self.contract
42    }
43}
44/// An operation index, useful for diagnostics. Ordering is inferred from access.
45#[derive(Clone, Copy, Debug, PartialEq, Eq)]
46pub struct Node(pub(crate) usize);
47#[derive(Clone)]
48pub(super) struct Use {
49    pub view: BufferView,
50    pub access: Access,
51}
52impl Use {
53    #[cfg(test)]
54    pub fn conflicts(&self, other: &Self) -> bool {
55        self.view.conflicts(&other.view) && (self.access.writes() || other.access.writes())
56    }
57}
58enum Description {
59    Fill(BufferView, u8),
60    Copy(BufferView, BufferView),
61    Gpu(GpuKernel, Vec<BufferView>),
62    Scoped(Arc<Mutex<ScopedGpu>>, Vec<BufferView>),
63    #[cfg(feature = "npu")]
64    Npu(crate::npu::NpuKernel, Vec<BufferView>),
65}
66type ScopedGpu = Box<dyn FnMut(&[crate::gpu::View<'_>]) -> Result<()> + Send>;
67struct Entry {
68    operation: Description,
69    uses: Vec<Use>,
70    lane: GpuLane,
71}
72/// A pipeline builder. Each operation declares placement and actual buffer use.
73pub struct Graph {
74    runtime: Runtime,
75    entries: Vec<Entry>,
76    // Private fragment temporaries only. No exposed IO or model weights enter
77    // this pool; access hazards order a later fragment's reuse after all readers.
78    fragment_scratch: Vec<(bool, BufferView)>,
79}
80impl Graph {
81    pub(crate) fn validate_runtime(&self, runtime: &Runtime) -> Result<()> {
82        if !self.runtime.same_domain(runtime) {
83            return Err(Error::Message("graph belongs to another runtime".into()));
84        }
85        Ok(())
86    }
87    /// Integrate owned native GPU execution into this graph's hazards and
88    /// producer dependencies. The callback runs on a worker, never in `poll`.
89    /// Dependencies are resolved on the host before invoking dependent regions;
90    /// graph completion is not enqueued as a wait on a native stream queue.
91    ///
92    /// # Safety
93    /// The callback must retain all private native resources it uses, access
94    /// these views only as declared, and synchronize every stream touching them
95    /// before returning. It must not let raw pointers/views escape this scope.
96    /// Private storage must not be concurrently accessible outside the callback.
97    /// On an uncertain failure return an error; captured resources and bindings
98    /// are then quarantined together, preserving native lifetimes.
99    pub unsafe fn gpu_scoped(
100        &mut self,
101        bindings: &[super::GpuAccess],
102        callback: impl FnMut(&[crate::gpu::View<'_>]) -> Result<()> + Send + 'static,
103    ) -> Result<Node> {
104        for binding in bindings {
105            binding.view.gpu()?;
106        }
107        self.push(
108            Description::Scoped(
109                Arc::new(Mutex::new(Box::new(callback))),
110                bindings.iter().map(|b| b.view.clone()).collect(),
111            ),
112            bindings
113                .iter()
114                .map(|b| Use {
115                    view: b.view.clone(),
116                    access: b.access,
117                })
118                .collect(),
119        )
120    }
121    pub(super) fn new(runtime: Runtime) -> Self {
122        Self {
123            runtime,
124            entries: Vec::new(),
125            fragment_scratch: Vec::new(),
126        }
127    }
128
129    pub(crate) fn fragment_scratch(
130        &mut self,
131        requests: &[(usize, bool)],
132    ) -> Result<Vec<BufferView>> {
133        let mut used = Vec::with_capacity(requests.len());
134        let mut result = Vec::with_capacity(requests.len());
135        for &(bytes, host_visible) in requests {
136            let best = self
137                .fragment_scratch
138                .iter()
139                .enumerate()
140                .filter(|(index, (host, view))| {
141                    !used.contains(index) && *host == host_visible && view.len() >= bytes
142                })
143                .min_by_key(|(_, (_, view))| view.len())
144                .map(|(index, _)| index);
145            let index = if let Some(index) = best {
146                index
147            } else {
148                let placement = if host_visible {
149                    super::MemoryPlacement::HostVisible
150                } else {
151                    super::MemoryPlacement::GpuLocal
152                };
153                let view = self.runtime.allocate(bytes, placement)?.view();
154                self.fragment_scratch.push((host_visible, view));
155                self.fragment_scratch.len() - 1
156            };
157            used.push(index);
158            result.push(self.fragment_scratch[index].1.slice(0..bytes)?);
159        }
160        Ok(result)
161    }
162    fn push(&mut self, operation: Description, uses: Vec<Use>) -> Result<Node> {
163        for binding in &uses {
164            if !Arc::ptr_eq(&binding.view.buffer.storage.runtime, &self.runtime.inner) {
165                return Err(Error::Message("buffer belongs to another runtime".into()));
166            }
167        }
168        let node = Node(self.entries.len());
169        self.entries.push(Entry {
170            operation,
171            uses,
172            lane: GpuLane::Compute,
173        });
174        Ok(node)
175    }
176    /// Fill a GPU-visible range with a byte pattern.
177    pub fn fill(&mut self, destination: BufferView, value: u8) -> Result<Node> {
178        destination.gpu()?;
179        self.push(
180            Description::Fill(destination.clone(), value),
181            vec![Use {
182                view: destination,
183                access: Access::Write,
184            }],
185        )
186    }
187    /// Explicit GPU copy. Lengths must match and ranges must not overlap.
188    pub fn copy(&mut self, destination: BufferView, source: BufferView) -> Result<Node> {
189        let lane = match (
190            source.buffer.storage.pointer.is_null(),
191            destination.buffer.storage.pointer.is_null(),
192        ) {
193            (false, true) => GpuLane::Upload,
194            (true, false) => GpuLane::Download,
195            _ => GpuLane::Compute,
196        };
197        self.copy_on(destination, source, lane)
198    }
199
200    /// Copy on an explicit lane, retaining the same cross-lane hazard checks.
201    pub fn copy_on(
202        &mut self,
203        destination: BufferView,
204        source: BufferView,
205        lane: GpuLane,
206    ) -> Result<Node> {
207        destination.gpu()?;
208        source.gpu()?;
209        if destination.len() != source.len() || destination.overlaps(&source) {
210            return Err(Error::Message(
211                "copy needs equal, nonoverlapping ranges".into(),
212            ));
213        }
214        let node = self.push(
215            Description::Copy(destination.clone(), source.clone()),
216            vec![
217                Use {
218                    view: destination,
219                    access: Access::Write,
220                },
221                Use {
222                    view: source,
223                    access: Access::Read,
224                },
225            ],
226        )?;
227        self.entries[node.0].lane = lane;
228        Ok(node)
229    }
230    /// Invoke a trusted GPU kernel; binding extents and alignment are checked.
231    /// The selected runtime must expose allocation-address queries (interop ABI 1),
232    /// including for GPU-local bindings. No base-pointer alignment is assumed.
233    pub fn gpu(&mut self, kernel: &GpuKernel, bindings: &[BufferView]) -> Result<Node> {
234        kernel.contract.check(bindings)?;
235        // Checked above: no writable aliasing is possible.
236        unsafe { self.gpu_aliasing(kernel, bindings) }
237    }
238
239    /// Invoke a trusted GPU kernel permitting writable argument aliases.
240    /// Extents, alignment, runtime identity and cross-operation hazards remain checked.
241    ///
242    /// # Safety
243    /// The kernel must be valid for the overlapping argument regions supplied;
244    /// no inter-thread data races or unsupported in-place accesses may occur.
245    pub unsafe fn gpu_aliasing(
246        &mut self,
247        kernel: &GpuKernel,
248        bindings: &[BufferView],
249    ) -> Result<Node> {
250        if !Arc::ptr_eq(&kernel.runtime, &self.runtime.inner) {
251            return Err(Error::Message("kernel belongs to another runtime".into()));
252        }
253        kernel.contract.check_extents(bindings)?;
254        for (binding, contract) in bindings.iter().zip(&kernel.contract.bindings) {
255            let address = binding.gpu()?.owner().allocation_address()?;
256            if !(address + binding.offset() as u64).is_multiple_of(contract.alignment as u64) {
257                return Err(Error::Message(
258                    "GPU allocation does not satisfy kernel alignment".into(),
259                ));
260            }
261        }
262        let uses = bindings
263            .iter()
264            .zip(&kernel.contract.bindings)
265            .map(|(view, c)| Use {
266                view: view.clone(),
267                access: c.access,
268            })
269            .collect();
270        self.push(Description::Gpu(kernel.clone(), bindings.to_vec()), uses)
271    }
272    /// Invoke a trusted NPU specialization with checked device and memory-bank compatibility.
273    /// Host-only and imported BOs may originate in a different program context on
274    /// the same device and memory bank. Their owning contexts remain retained.
275    #[cfg(feature = "npu")]
276    pub fn npu(&mut self, kernel: &crate::npu::NpuKernel, bindings: &[BufferView]) -> Result<Node> {
277        kernel.contract().check(bindings)?;
278        for (binding, contract) in bindings.iter().zip(&kernel.contract().bindings) {
279            let native = binding
280                .buffer
281                .storage
282                .native
283                .as_ref()
284                .ok_or_else(|| Error::Unsupported("binding has no native storage".into()))?;
285            let address = native.device_address(&kernel.inner.device)?;
286            if !(address + binding.offset() as u64).is_multiple_of(contract.alignment as u64) {
287                return Err(Error::Message(
288                    "NPU allocation does not satisfy kernel alignment".into(),
289                ));
290            }
291        }
292        let uses = bindings
293            .iter()
294            .zip(&kernel.contract().bindings)
295            .map(|(view, c)| Use {
296                view: view.clone(),
297                access: c.access,
298            })
299            .collect();
300        self.push(Description::Npu(kernel.clone(), bindings.to_vec()), uses)
301    }
302    /// Lower GPU regions and import NPU views once; reserve reusable run slots.
303    pub fn prepare(self) -> Result<ExecutableGraph> {
304        if self.entries.is_empty() {
305            return Err(Error::Message("cannot prepare an empty graph".into()));
306        }
307        let mut operations: Vec<Operation> = Vec::new();
308        let mut index = 0;
309        while index < self.entries.len() {
310            if let Description::Scoped(callback, bindings) = &self.entries[index].operation {
311                operations.push(Operation {
312                    lane: GpuLane::Compute,
313                    copy_bytes: 0,
314                    backend: Backend::Scoped(callback.clone(), bindings.clone()),
315                    uses: self.entries[index].uses.clone(),
316                    dependencies: Vec::new(),
317                });
318                index += 1;
319                continue;
320            }
321            #[cfg(feature = "npu")]
322            if let Description::Npu(kernel, bindings) = &self.entries[index].operation {
323                let arguments = bindings
324                    .iter()
325                    .map(|view| -> Result<_> {
326                        Ok(crate::fabric::XdnaBinding {
327                            buffer: view.buffer.storage.native.as_ref().ok_or_else(|| {
328                                Error::Unsupported("missing native XDNA backing".into())
329                            })?,
330                            offset: view.offset(),
331                            length: view.len(),
332                        })
333                    })
334                    .collect::<Result<Vec<_>>>()?;
335                // Native preparation validates the image's own range/usage contracts.
336                // Every invocation submits the complete establishing command.
337                let run = unsafe {
338                    kernel.inner.device.prepare_xdna(
339                        &kernel.inner.artifact,
340                        kernel.inner.columns,
341                        &arguments,
342                    )
343                }?;
344                operations.push(Operation {
345                    lane: GpuLane::Compute,
346                    copy_bytes: 0,
347                    backend: Backend::Npu(Mutex::new(run), kernel.clone()),
348                    uses: self.entries[index].uses.clone(),
349                    dependencies: Vec::new(),
350                });
351                index += 1;
352                continue;
353            }
354            if let (
355                super::ComputeEngine::Aql {
356                    maximum_private_bytes,
357                },
358                Description::Gpu(kernel, bindings),
359            ) = (
360                self.runtime.options.compute_engine,
361                &self.entries[index].operation,
362            ) {
363                let lane = self.entries[index].lane;
364                let queue =
365                    crate::cached_init(&self.runtime.inner.aql_queues[lane as usize], || {
366                        self.runtime
367                            .inner
368                            .device()?
369                            .native()
370                            .aql_queue_budgeted(maximum_private_bytes, self.runtime.memory_budget())
371                    })?;
372                let views = bindings
373                    .iter()
374                    .map(BufferView::gpu)
375                    .collect::<Result<Vec<_>>>()?;
376                let constants = crate::gpu::Constants::from_bytes(&kernel.contract.constants)?;
377                let command = unsafe {
378                    kernel
379                        .raw
380                        .prepare_aql(&queue, kernel.grid, kernel.block, &constants, &views)
381                }?;
382                operations.push(Operation {
383                    lane,
384                    copy_bytes: 0,
385                    backend: Backend::Aql(command),
386                    uses: self.entries[index].uses.clone(),
387                    dependencies: Vec::new(),
388                });
389                index += 1;
390                continue;
391            }
392            let start = index;
393            let lane = self.entries[start].lane;
394            while index < self.entries.len() {
395                if matches!(self.entries[index].operation, Description::Scoped(..)) {
396                    break;
397                }
398                if matches!(
399                    self.runtime.options.compute_engine,
400                    super::ComputeEngine::Aql { .. }
401                ) && matches!(self.entries[index].operation, Description::Gpu(..))
402                {
403                    break;
404                }
405                if self.entries[index].lane != lane {
406                    break;
407                }
408                #[cfg(feature = "npu")]
409                if matches!(self.entries[index].operation, Description::Npu(..)) {
410                    break;
411                }
412                index += 1;
413            }
414            // Dynamic tensor-to-slot copies need hazard scheduling and retained
415            // owners, but no native executable graph or per-request stream.
416            if self.entries[start..index]
417                .iter()
418                .all(|e| matches!(e.operation, Description::Copy(..)))
419            {
420                if self.runtime.options.copy_engine == super::CopyEngine::Sdma {
421                    let queue =
422                        crate::cached_init(&self.runtime.inner.sdma_queues[lane as usize], || {
423                            self.runtime.inner.device()?.native().sdma_queue()
424                        })?;
425                    let commands = self.entries[start..index]
426                        .iter()
427                        .map(|entry| {
428                            let Description::Copy(dst, src) = &entry.operation else {
429                                unreachable!()
430                            };
431                            let dst = dst.gpu()?;
432                            let src = src.gpu()?;
433                            queue.prepare_copy_reserved(
434                                &dst.owner().native,
435                                dst.offset(),
436                                &src.owner().native,
437                                src.offset(),
438                                dst.len(),
439                                self.runtime.memory_budget(),
440                            )
441                        })
442                        .collect::<Result<Vec<_>>>()?;
443                    operations.push(Operation {
444                        lane,
445                        copy_bytes: self.entries[start..index]
446                            .iter()
447                            .map(|entry| match &entry.operation {
448                                Description::Copy(dst, _) => dst.len(),
449                                _ => unreachable!(),
450                            })
451                            .sum(),
452                        backend: Backend::Sdma { commands },
453                        uses: self.entries[start..index]
454                            .iter()
455                            .flat_map(|entry| entry.uses.iter().cloned())
456                            .collect(),
457                        dependencies: Vec::new(),
458                    });
459                    continue;
460                }
461                let stream =
462                    crate::cached_init(&self.runtime.inner.copy_streams[lane as usize], || {
463                        let stream = self.runtime.inner.device()?.stream()?;
464                        self.runtime
465                            .inner
466                            .core
467                            .counters
468                            .copy_streams_created
469                            .fetch_add(1, Ordering::Relaxed);
470                        Ok(Arc::new(Mutex::new(CopyStream {
471                            stream,
472                            healthy: true,
473                        })))
474                    })?;
475                let copies: Vec<_> = self.entries[start..index]
476                    .iter()
477                    .map(|entry| {
478                        let Description::Copy(dst, src) = &entry.operation else {
479                            unreachable!()
480                        };
481                        (dst.clone(), src.clone())
482                    })
483                    .collect();
484                operations.push(Operation {
485                    lane,
486                    copy_bytes: copies.iter().map(|(dst, _)| dst.len()).sum(),
487                    backend: Backend::Copies(stream, copies),
488                    uses: self.entries[start..index]
489                        .iter()
490                        .flat_map(|e| e.uses.iter().cloned())
491                        .collect(),
492                    dependencies: Vec::new(),
493                });
494                continue;
495            }
496            let stream = self.runtime.inner.device()?.stream()?;
497            let mut graph = stream.graph()?;
498            let mut nodes = Vec::new();
499            let mut uses = Vec::new();
500            let mut frontier = super::dependencies::Frontier::default();
501            for (i, entry) in self.entries[start..index].iter().enumerate() {
502                let dependencies = frontier
503                    .dependencies(i, &entry.uses)
504                    .into_iter()
505                    .map(|j| nodes[j])
506                    .collect::<Vec<_>>();
507                let node = match &entry.operation {
508                    Description::Fill(dst, value) => {
509                        graph.fill(&dependencies, dst.gpu()?, *value)?
510                    }
511                    Description::Copy(dst, src) => {
512                        graph.copy(&dependencies, dst.gpu()?, src.gpu()?)?
513                    }
514                    Description::Gpu(kernel, bindings) => {
515                        let views = bindings
516                            .iter()
517                            .map(BufferView::gpu)
518                            .collect::<Result<Vec<_>>>()?;
519                        let constants =
520                            crate::gpu::Constants::from_bytes(&kernel.contract.constants)?;
521                        unsafe {
522                            graph.dispatch(
523                                &dependencies,
524                                &kernel.raw,
525                                kernel.grid,
526                                kernel.block,
527                                &constants,
528                                &views,
529                            )
530                        }?
531                    }
532                    Description::Scoped(..) => unreachable!(),
533                    #[cfg(feature = "npu")]
534                    Description::Npu(..) => unreachable!(),
535                };
536                nodes.push(node);
537                uses.extend(entry.uses.iter().cloned());
538            }
539            let executable = graph.finish()?;
540            self.runtime
541                .inner
542                .core
543                .counters
544                .native_graphs_prepared
545                .fetch_add(1, Ordering::Relaxed);
546            operations.push(Operation {
547                lane,
548                copy_bytes: self.entries[start..index]
549                    .iter()
550                    .filter_map(|entry| match &entry.operation {
551                        Description::Copy(dst, _) => Some(dst.len()),
552                        _ => None,
553                    })
554                    .sum(),
555                backend: Backend::Gpu(Box::new(Mutex::new((stream, executable)))),
556                uses: super::dependencies::summarize(uses),
557                dependencies: Vec::new(),
558            });
559        }
560        let mut frontier = super::dependencies::Frontier::default();
561        for (index, operation) in operations.iter_mut().enumerate() {
562            operation.uses = super::dependencies::summarize(operation.uses.drain(..));
563            operation.dependencies = frontier.dependencies(index, &operation.uses);
564        }
565        let uses =
566            super::dependencies::summarize(operations.iter().flat_map(|o| o.uses.iter().cloned()));
567        let slots = (0..self.runtime.options.graph_slots)
568            .map(|_| Slot {
569                nodes: vec![NodeState::Pending; operations.len()],
570                occupied: false,
571                failure: None,
572            })
573            .collect();
574        Ok(ExecutableGraph {
575            inner: Arc::new(Prepared {
576                quarantined: AtomicBool::new(false),
577                signals: (0..self.runtime.options.graph_slots)
578                    .map(|_| Signal::new())
579                    .collect(),
580                runtime: self.runtime.inner,
581                parallel: operations
582                    .iter()
583                    .enumerate()
584                    .skip(1)
585                    .any(|(index, op)| !op.dependencies.contains(&(index - 1))),
586                operations,
587                uses,
588                slots: Mutex::new(slots),
589            }),
590        })
591    }
592}
593pub(super) struct CopyStream {
594    stream: crate::gpu::Stream,
595    healthy: bool,
596}
597pub(super) enum Backend {
598    Aql(crate::fabric::PreparedAql),
599    Sdma {
600        commands: Vec<crate::fabric::PreparedGpu>,
601    },
602    Copies(Arc<Mutex<CopyStream>>, Vec<(BufferView, BufferView)>),
603    Gpu(Box<Mutex<(crate::gpu::Stream, crate::gpu::GraphExec)>>),
604    Scoped(Arc<Mutex<ScopedGpu>>, Vec<BufferView>),
605    #[cfg(feature = "npu")]
606    Npu(Mutex<crate::fabric::XdnaProgram>, crate::npu::NpuKernel),
607    #[cfg(test)]
608    Mock(Engine, Arc<dyn Fn() -> Result<()> + Send + Sync>),
609}
610pub(super) struct Operation {
611    pub lane: GpuLane,
612    pub copy_bytes: usize,
613    pub backend: Backend,
614    pub uses: Vec<Use>,
615    pub dependencies: Vec<usize>,
616}
617impl Operation {
618    pub fn queue(&self) -> usize {
619        match self.engine() {
620            Engine::Gpu => self.lane as usize,
621            Engine::Npu => 3,
622            Engine::Host => 4,
623        }
624    }
625    pub fn engine(&self) -> Engine {
626        match &self.backend {
627            Backend::Aql(_)
628            | Backend::Gpu(_)
629            | Backend::Scoped(..)
630            | Backend::Copies(..)
631            | Backend::Sdma { .. } => Engine::Gpu,
632            #[cfg(feature = "npu")]
633            Backend::Npu(..) => Engine::Npu,
634            #[cfg(test)]
635            Backend::Mock(engine, _) => *engine,
636        }
637    }
638    pub fn run(&self) -> Result<()> {
639        for access in &self.uses {
640            if let Some(message) = &access
641                .view
642                .buffer
643                .storage
644                .host
645                .lock()
646                .unwrap_or_else(|e| e.into_inner())
647                .poison
648            {
649                return Err(Error::DeviceLost(message.clone()));
650            }
651            access.view.buffer.storage.make_visible(self.engine())?;
652        }
653        match &self.backend {
654            Backend::Aql(command) => {
655                unsafe { command.dispatch() }?.wait()?;
656            }
657            Backend::Sdma { commands, .. } => {
658                let mut last = None;
659                let submitted: Result<()> = (|| {
660                    for command in commands {
661                        let _uses = command.retain_parts()?;
662                        for part in command.parts() {
663                            last = Some(unsafe { part.dispatch_part_wait() }?);
664                        }
665                    }
666                    Ok(())
667                })();
668                // Drain any accepted prefix even if a later submission fails.
669                if let Some(done) = last {
670                    done.wait()?;
671                }
672                submitted?;
673            }
674            Backend::Copies(stream, copies) => {
675                let mut stream = stream.lock().unwrap_or_else(|e| e.into_inner());
676                if !stream.healthy {
677                    return Err(Error::DeviceLost("copy stream is quarantined".into()));
678                }
679                let result = (|| {
680                    for (dst, src) in copies {
681                        stream.stream.copy(dst.gpu()?, src.gpu()?)?;
682                    }
683                    stream.stream.synchronize()
684                })();
685                if result.is_err() {
686                    stream.healthy = false;
687                }
688                result?;
689            }
690            Backend::Gpu(pair) => {
691                let mut pair = pair.lock().unwrap_or_else(|e| e.into_inner());
692                let (stream, graph) = &mut *pair;
693                stream.launch(graph)?;
694                stream.synchronize()?;
695            }
696            Backend::Scoped(callback, bindings) => {
697                let views = bindings
698                    .iter()
699                    .map(BufferView::gpu)
700                    .collect::<Result<Vec<_>>>()?;
701                callback.lock().unwrap_or_else(|e| e.into_inner())(&views)?;
702            }
703            #[cfg(feature = "npu")]
704            Backend::Npu(run, _kernel) => {
705                unsafe { run.lock().unwrap_or_else(|e| e.into_inner()).dispatch() }?.wait()?
706            }
707            #[cfg(test)]
708            Backend::Mock(_, action) => action()?,
709        }
710        if self.copy_bytes != 0
711            && let Some(access) = self.uses.first()
712        {
713            access
714                .view
715                .buffer
716                .storage
717                .runtime
718                .core
719                .counters
720                .lane_copy_bytes[self.lane as usize]
721                .fetch_add(self.copy_bytes as u64, std::sync::atomic::Ordering::Relaxed);
722            access
723                .view
724                .buffer
725                .storage
726                .runtime
727                .core
728                .counters
729                .copied_bytes
730                .fetch_add(self.copy_bytes as u64, std::sync::atomic::Ordering::Relaxed);
731        }
732        for access in &self.uses {
733            if access.access.writes() {
734                access
735                    .view
736                    .buffer
737                    .storage
738                    .visibility
739                    .lock()
740                    .unwrap_or_else(|e| e.into_inner())
741                    .wrote(self.engine());
742            }
743        }
744        Ok(())
745    }
746}
747#[derive(Clone, Copy, PartialEq, Eq)]
748pub(super) enum NodeState {
749    Pending,
750    Running,
751    Done,
752}
753pub(super) struct Slot {
754    pub nodes: Vec<NodeState>,
755    pub occupied: bool,
756    pub failure: Option<Arc<Error>>,
757}
758pub(super) struct Prepared {
759    pub quarantined: AtomicBool,
760    pub runtime: Arc<RuntimeOwner>,
761    pub parallel: bool,
762    pub operations: Vec<Operation>,
763    pub uses: Vec<Use>,
764    pub slots: Mutex<Vec<Slot>>,
765    pub signals: Vec<Arc<Signal>>,
766}
767/// A prepared graph with fixed bindings and reusable bounded submission slots.
768#[derive(Clone)]
769pub struct ExecutableGraph {
770    pub(super) inner: Arc<Prepared>,
771}
772impl ExecutableGraph {
773    pub(crate) fn validate_runtime(&self, runtime: &Runtime) -> Result<()> {
774        if !Arc::ptr_eq(&self.inner.runtime, &runtime.inner) {
775            return Err(Error::Message("graph belongs to another runtime".into()));
776        }
777        Ok(())
778    }
779
780    pub(crate) fn usable(&self) -> Result<()> {
781        if self.inner.quarantined.load(Ordering::Acquire) {
782            return Err(Error::DeviceLost(
783                "graph quarantined after native failure".into(),
784            ));
785        }
786        for usage in &self.inner.uses {
787            if let Some(message) = &usage
788                .view
789                .buffer
790                .storage
791                .host
792                .lock()
793                .unwrap_or_else(|e| e.into_inner())
794                .poison
795            {
796                return Err(Error::DeviceLost(message.clone()));
797            }
798        }
799        Ok(())
800    }
801    /// Enqueue without waiting for a device. Conflicting prior work is ordered.
802    /// `Busy` indicates live host mappings or exhausted pending/completion slots.
803    pub fn submit(&self) -> Result<Completion> {
804        self.submit_after(&[])
805    }
806
807    /// Submit after successful producers in this runtime. Failed or cancelled
808    /// producers prevent execution, even when no memory range overlaps.
809    pub fn submit_after(&self, dependencies: &[Completion]) -> Result<Completion> {
810        self.usable()?;
811        let core = &self.inner.runtime.core;
812        for dependency in dependencies {
813            if let Some(owner) = dependency.core.upgrade() {
814                if !Arc::ptr_eq(core, &owner) {
815                    return Err(Error::Message(
816                        "completion belongs to another runtime".into(),
817                    ));
818                }
819            } else if !dependency.is_complete() {
820                return Err(Error::Message("orphaned incomplete producer".into()));
821            }
822        }
823        let mut scheduler = core.state.lock().unwrap_or_else(|e| e.into_inner());
824        if scheduler.occupied() >= scheduler.capacity {
825            return Err(Error::Busy("runtime submission capacity exhausted".into()));
826        }
827        for access in &self.inner.uses {
828            access.view.buffer.storage.host_conflict(access.access)?;
829        }
830        let mut slots = self.inner.slots.lock().unwrap_or_else(|e| e.into_inner());
831        let (slot_index, slot) = slots
832            .iter_mut()
833            .enumerate()
834            .find(|(index, slot)| {
835                let signal = &self.inner.signals[*index];
836                !slot.occupied
837                    && signal.observers.load(Ordering::Acquire) == 0
838                    && signal.state.lock().unwrap_or_else(|e| e.into_inner()).done
839            })
840            .ok_or_else(|| {
841                Error::Busy("prepared run slots are occupied; drop completed observers or reserve more slots".into())
842            })?;
843        self.inner.signals[slot_index].reset();
844        slot.nodes.fill(NodeState::Pending);
845        slot.failure = None;
846        slot.occupied = true;
847        let completion =
848            Completion::new(self.inner.signals[slot_index].clone(), Arc::downgrade(core));
849        core.counters
850            .submissions
851            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
852        scheduler.enqueue(self.inner.clone(), slot_index, dependencies.to_vec());
853        drop(slots);
854        drop(scheduler);
855        core.changed.notify_one();
856        Ok(completion)
857    }
858    /// Number of prepared device regions (GPU regions may contain many kernels).
859    pub fn regions(&self) -> usize {
860        self.inner.operations.len()
861    }
862}