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 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 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 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 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 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 pub fn gpu(&mut self, kernel: &GpuKernel, bindings: &[BufferView]) -> Result<Node> {
226 kernel.contract.check(bindings)?;
227 unsafe { self.gpu_aliasing(kernel, bindings) }
229 }
230
231 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 #[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 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 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 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#[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 pub fn submit(&self) -> Result<Completion> {
683 self.submit_after(&[])
684 }
685
686 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 pub fn regions(&self) -> usize {
739 self.inner.operations.len()
740 }
741}