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