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