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#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize)]
13pub enum GpuLane {
14 Upload,
16 #[default]
18 Compute,
19 Download,
21}
22
23#[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 pub fn contract(&self) -> &KernelContract {
35 &self.contract
36 }
37}
38#[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}
66pub 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 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 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 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 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 pub fn gpu(&mut self, kernel: &GpuKernel, bindings: &[BufferView]) -> Result<Node> {
188 kernel.contract.check(bindings)?;
189 unsafe { self.gpu_aliasing(kernel, bindings) }
191 }
192
193 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 #[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 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 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 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#[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 pub fn submit(&self) -> Result<Completion> {
655 self.submit_after(&[])
656 }
657
658 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 pub fn regions(&self) -> usize {
702 self.inner.operations.len()
703 }
704}