1use crate::{Error, Result, Target, fabric};
3use std::{
4 cell::RefCell,
5 collections::BTreeMap,
6 path::Path,
7 sync::{Arc, Mutex},
8};
9
10#[derive(Clone, Debug, Default)]
12pub struct ExportInfo {
13 pub constant_byte_length: u32,
15 pub binding_count: u32,
17 pub parameter_count: u32,
19 pub workgroup_size: [u32; 3],
21}
22#[derive(Clone, Debug, Default)]
24pub struct StreamOptions {
25 pub compute_engine: crate::execution::ComputeEngine,
27 pub copy_engine: crate::execution::CopyEngine,
29 pub memory_budget: Option<crate::residency::MemoryBudget>,
31 pub sanitizer: Option<fabric::SanitizerRuntimeOptions>,
33}
34#[derive(Clone)]
35enum Command {
36 Gpu(fabric::PreparedGpu, u8),
37 Aql(fabric::PreparedAql),
38}
39impl Command {
40 fn engine(&self) -> u8 {
41 match self {
42 Self::Gpu(_, engine) => *engine,
43 Self::Aql(_) => 2,
44 }
45 }
46 fn storage_bytes(&self) -> usize {
47 match self {
48 Self::Gpu(c, _) => c.storage_bytes(),
49 Self::Aql(c) => c.storage_bytes(),
50 }
51 }
52}
53#[derive(Clone)]
54enum Completion {
55 Gpu(fabric::Completion, u8),
56 Aql(fabric::AqlCompletion),
57}
58impl Completion {
59 fn engine(&self) -> u8 {
60 match self {
61 Self::Gpu(_, engine) => *engine,
62 Self::Aql(_) => 2,
63 }
64 }
65 fn refresh(&self) -> Result<bool> {
66 match self {
67 Self::Gpu(c, _) => c.refresh(),
68 Self::Aql(c) => c.refresh(),
69 }
70 }
71 fn wait(&self) -> Result<()> {
72 match self {
73 Self::Gpu(c, _) => c.wait(),
74 Self::Aql(c) => c.wait(),
75 }
76 }
77 fn wait_timeout(&self, timeout: std::time::Duration) -> Result<bool> {
78 match self {
79 Self::Gpu(c, _) => c.wait_timeout(timeout),
80 Self::Aql(c) => c.wait_timeout(timeout),
81 }
82 }
83}
84struct Inner {
85 device: fabric::Device,
86 queue: fabric::Queue,
87 aql: Option<fabric::AqlQueue>,
88 sdma: Option<fabric::Queue>,
89 sanitizer: Option<fabric::SanitizerRuntimeOptions>,
90 timeline: StreamTimeline,
91 free: Mutex<Vec<fabric::Buffer>>,
92 dispatch_cache: Mutex<Vec<CachedDispatch>>,
93 transfer_cache: Mutex<Vec<CachedTransfer>>,
94}
95#[derive(Default)]
96struct StreamTimeline {
97 producer: Mutex<()>,
98 last: Mutex<Option<Completion>>,
99}
100impl StreamTimeline {
101 fn submit(&self, dispatch: impl FnOnce() -> Result<Completion>) -> Result<()> {
102 let _producer = self
105 .producer
106 .lock()
107 .map_err(|_| Error::DeviceLost("stream producer poisoned".into()))?;
108 let done = dispatch()?;
109 *self
110 .last
111 .lock()
112 .map_err(|_| Error::DeviceLost("stream timeline poisoned".into()))? = Some(done);
113 Ok(())
114 }
115 fn snapshot(&self) -> Result<Option<Completion>> {
116 Ok(self
117 .last
118 .lock()
119 .map_err(|_| Error::DeviceLost("stream timeline poisoned".into()))?
120 .clone())
121 }
122}
123impl Inner {
124 fn submit(&self, command: &Command) -> Result<()> {
125 self.timeline.submit(|| {
126 if let Some(previous) = self.timeline.snapshot()?
127 && previous.engine() != command.engine()
128 {
129 previous.wait()?;
130 }
131 match command {
132 Command::Gpu(c, engine) => {
133 let _uses = c.retain_parts()?;
134 let mut last = None;
135 for part in c.parts() {
136 let done = Completion::Gpu(unsafe { part.dispatch_part_wait() }?, *engine);
137 *self
140 .timeline
141 .last
142 .lock()
143 .map_err(|_| Error::DeviceLost("stream timeline poisoned".into()))? =
144 Some(done.clone());
145 last = Some(done);
146 }
147 Ok(last.expect("prepared command has a final part"))
148 }
149 Command::Aql(c) => match unsafe { c.dispatch() } {
150 Err(Error::Busy(_)) => {
151 self.wait()?;
152 Ok(Completion::Aql(unsafe { c.dispatch() }?))
153 }
154 result => result.map(Completion::Aql),
155 },
156 }
157 })
158 }
159 fn drain_for_drop(&self) -> bool {
160 self.timeline.snapshot().ok().is_some_and(|last| {
161 last.as_ref().is_none_or(|done| {
162 done.wait_timeout(std::time::Duration::from_secs(10))
163 .unwrap_or(false)
164 })
165 })
166 }
167 fn wait(&self) -> Result<()> {
168 if let Some(done) = self.timeline.snapshot()? {
169 done.wait()?;
170 }
171 Ok(())
172 }
173}
174impl Inner {
175 fn prepare_fill(self: &Arc<Self>, dst: View<'_>, value: u8) -> Result<Command> {
176 owns(self, dst.owner)?;
177 let mut cache = self
178 .transfer_cache
179 .lock()
180 .map_err(|_| Error::DeviceLost("transfer cache poisoned".into()))?;
181 if let Some(entry) = cache.iter().find(|entry| {
182 entry.destination.same_backing(&dst.owner.native)
183 && entry.destination_offset == dst.offset
184 && entry.length == dst.length
185 && entry.source.is_none()
186 && entry.value == value
187 }) {
188 return Ok(entry.command.clone());
189 }
190 let command = Command::Gpu(
191 self.sdma.as_ref().unwrap_or(&self.queue).prepare_fill(
192 &dst.owner.native,
193 dst.offset,
194 dst.length,
195 value,
196 )?,
197 u8::from(self.sdma.is_some()),
198 );
199 if Arc::ptr_eq(&dst.owner.owner, self) {
202 if cache.len() == 128 {
203 cache.remove(0);
204 }
205 cache.push(CachedTransfer {
206 destination: dst.owner.native.clone(),
207 destination_offset: dst.offset,
208 length: dst.length,
209 source: None,
210 value,
211 command: command.clone(),
212 });
213 }
214 Ok(command)
215 }
216 fn prepare_copy(self: &Arc<Self>, dst: View<'_>, src: View<'_>) -> Result<Command> {
217 owns(self, dst.owner)?;
218 owns(self, src.owner)?;
219 if dst.len() != src.len() {
220 return Err(Error::Message("copy requires equal spans".into()));
221 }
222 let mut cache = self
223 .transfer_cache
224 .lock()
225 .map_err(|_| Error::DeviceLost("transfer cache poisoned".into()))?;
226 if let Some(entry) = cache.iter().find(|entry| {
227 entry.destination.same_backing(&dst.owner.native)
228 && entry.destination_offset == dst.offset
229 && entry.length == dst.length
230 && entry.source.as_ref().is_some_and(|(buffer, offset)| {
231 buffer.same_backing(&src.owner.native) && *offset == src.offset
232 })
233 }) {
234 return Ok(entry.command.clone());
235 }
236 let command = Command::Gpu(
237 self.sdma.as_ref().unwrap_or(&self.queue).prepare_copy(
238 &dst.owner.native,
239 dst.offset,
240 &src.owner.native,
241 src.offset,
242 src.length,
243 )?,
244 u8::from(self.sdma.is_some()),
245 );
246 if Arc::ptr_eq(&dst.owner.owner, self) && Arc::ptr_eq(&src.owner.owner, self) {
247 if cache.len() == 128 {
248 cache.remove(0);
249 }
250 cache.push(CachedTransfer {
251 destination: dst.owner.native.clone(),
252 destination_offset: dst.offset,
253 length: dst.length,
254 source: Some((src.owner.native.clone(), src.offset)),
255 value: 0,
256 command: command.clone(),
257 });
258 }
259 Ok(command)
260 }
261 unsafe fn prepare_dispatch(
262 self: &Arc<Self>,
263 kernel: &Kernel,
264 grid: [u32; 3],
265 block: [u32; 3],
266 constants: &Constants,
267 bindings: &[View<'_>],
268 arena: Option<&mut fabric::GraphArena>,
269 ) -> Result<Command> {
270 if kernel.device_id() != self.device.id() {
271 return Err(Error::Message("kernel belongs to another device".into()));
272 }
273 validate_export_launch(&kernel.info, grid, block)?;
274 if constants.len != kernel.info.constant_byte_length as usize
275 || bindings.len() != kernel.info.binding_count as usize
276 {
277 return Err(Error::Message(
278 "kernel binding or constant byte count mismatch".into(),
279 ));
280 }
281 for view in bindings {
282 owns(self, view.owner)?;
283 }
284 if let Some(queue) = &self.aql {
285 return Ok(Command::Aql(unsafe {
286 kernel.prepare_aql(queue, grid, block, constants, bindings)
287 }?));
288 }
289 let mut args = Vec::with_capacity(kernel.layout.len());
290 let mut scalar = 0;
291 let mut binding = 0;
292 for &(kind, size) in kernel.layout.iter() {
293 if kind == 1 {
294 args.push(fabric::Argument::Value(
295 &constants.bytes[scalar..scalar + size],
296 ));
297 scalar += size;
298 } else {
299 let view = bindings[binding];
300 args.push(fabric::Argument::Buffer(&view.owner.native, view.offset));
301 binding += 1;
302 }
303 }
304 unsafe {
305 match arena {
306 Some(arena) => self.queue.prepare_graph(
307 &kernel.native,
308 grid,
309 block.map(|v| v as u16),
310 &args,
311 arena,
312 ),
313 None => self
314 .queue
315 .prepare(&kernel.native, grid, block.map(|v| v as u16), &args),
316 }
317 .map(|c| Command::Gpu(c, 0))
318 }
319 }
320}
321
322#[derive(Clone)]
324pub struct Device {
325 native: fabric::Device,
326}
327impl Device {
328 pub fn open(index: i32) -> Result<Self> {
330 Self::open_with_lifetime(index, fabric::NativeLifetime::Instance)
331 }
332 pub fn open_with_lifetime(index: i32, lifetime: fabric::NativeLifetime) -> Result<Self> {
334 let index = usize::try_from(index)
335 .map_err(|_| Error::Message("GPU index must be nonnegative".into()))?;
336 Ok(Self {
337 native: fabric::Device::open_with_lifetime(fabric::Engine::Gpu, index, lifetime)?,
338 })
339 }
340 pub fn open_for(index: i32, target: &str) -> Result<Self> {
342 let device = Self::open(index)?;
343 if device.target().as_str() != target {
344 return Err(Error::Unsupported(format!(
345 "workload requires {target}, found {}",
346 device.target().as_str()
347 )));
348 }
349 Ok(device)
350 }
351 pub fn target(&self) -> &Target {
353 self.native.target()
354 }
355 pub fn set_stream_queue_count(&self, count: usize) -> Result<()> {
363 self.native.set_stream_queue_count(count)
364 }
365 pub fn stream_queue_count(&self) -> Result<usize> {
367 self.native.stream_queue_count()
368 }
369 pub fn stream(&self) -> Result<Stream> {
373 self.stream_with_options(StreamOptions::default())
374 }
375 pub fn stream_with_options(&self, mut options: StreamOptions) -> Result<Stream> {
378 if options.sanitizer.is_some()
379 && !matches!(
380 options.compute_engine,
381 crate::execution::ComputeEngine::Aql { .. }
382 )
383 {
384 return Err(Error::Unsupported("stream sanitizers require AQL".into()));
385 }
386 if let Some(sanitizer) = &mut options.sanitizer {
387 sanitizer.memory_budget = options.memory_budget.clone();
388 }
389 let aql = match options.compute_engine {
390 crate::execution::ComputeEngine::Pm4 => None,
391 crate::execution::ComputeEngine::Aql {
392 maximum_private_bytes,
393 } => Some(
394 self.native
395 .aql_queue_budgeted(maximum_private_bytes, options.memory_budget.as_ref())?,
396 ),
397 };
398 let sdma = match options.copy_engine {
399 crate::execution::CopyEngine::Compute => None,
400 crate::execution::CopyEngine::Sdma => Some(self.native.sdma_queue()?),
401 };
402 Ok(Stream {
403 inner: Arc::new(Inner {
404 device: self.native.clone(),
405 queue: self.native.stream_queue()?,
406 aql,
407 sdma,
408 sanitizer: options.sanitizer,
409 timeline: StreamTimeline::default(),
410 free: Mutex::new(Vec::with_capacity(16)),
411 dispatch_cache: Mutex::new(Vec::with_capacity(64)),
412 transfer_cache: Mutex::new(Vec::with_capacity(128)),
413 }),
414 staging: Vec::new(),
415 staging_pool: Vec::new(),
416 scratch: BTreeMap::new(),
417 scratch_bytes: 0,
418 scratch_limit: 256 * 1024 * 1024,
419 budget: options.memory_budget,
420 budget_uses: RefCell::new(BudgetUses::default()),
421 })
422 }
423 pub fn native(&self) -> &fabric::Device {
425 &self.native
426 }
427}
428pub struct Buffer {
430 pub(crate) native: fabric::Buffer,
431 bytes: usize,
432 owner: Arc<Inner>,
433 reservation: Option<Arc<crate::residency::MemoryReservation>>,
434 poolable: bool,
435}
436impl Drop for Buffer {
437 fn drop(&mut self) {
438 if let Ok(mut cache) = self.owner.dispatch_cache.lock() {
441 cache.retain(|entry| {
442 !entry
443 .bindings
444 .iter()
445 .any(|(buffer, _, _)| buffer.same_backing(&self.native))
446 });
447 }
448 if let Ok(mut cache) = self.owner.transfer_cache.lock() {
449 cache.retain(|entry| {
450 !entry.destination.same_backing(&self.native)
451 && !entry
452 .source
453 .as_ref()
454 .is_some_and(|(buffer, _)| buffer.same_backing(&self.native))
455 });
456 }
457 if self.poolable
458 && self.reservation.is_none()
459 && self.native.exclusively_owned()
460 && self.bytes <= 64 * 1024 * 1024
461 && let Ok(mut pool) = self.owner.free.lock()
462 {
463 let used: usize = pool.iter().map(fabric::Buffer::len).sum();
464 if pool.len() < 16 && self.bytes <= (64 * 1024 * 1024usize).saturating_sub(used) {
465 pool.push(self.native.clone());
466 }
467 }
468 }
469}
470#[derive(Clone, Copy, Debug)]
472pub struct View<'a> {
473 owner: &'a Buffer,
474 offset: usize,
475 length: usize,
476}
477impl<'a> View<'a> {
478 pub fn slice(self, offset: usize, length: usize) -> Result<Self> {
480 checked_span(offset, length, self.length)?;
481 Ok(Self {
482 owner: self.owner,
483 offset: self.offset + offset,
484 length,
485 })
486 }
487 pub fn owner(self) -> &'a Buffer {
489 self.owner
490 }
491 pub fn offset(self) -> usize {
493 self.offset
494 }
495 pub fn len(self) -> usize {
497 self.length
498 }
499 pub fn is_empty(self) -> bool {
501 self.length == 0
502 }
503}
504impl Buffer {
505 pub(crate) fn into_unpooled(mut self) -> Self {
508 self.poolable = false;
509 self
510 }
511 #[cfg(feature = "npu")]
512 pub(crate) fn share_with(mut self, device: &fabric::Device) -> Result<Self> {
513 self.poolable = false;
514 if self.native.device_address(device).is_err() {
515 self.native = self
516 .owner
517 .device
518 .fabric()
519 .share_owned(&self.native, &[self.owner.device.clone(), device.clone()])?;
520 }
521 Ok(self)
522 }
523 pub(crate) fn charged_to(&self, budget: &crate::residency::MemoryBudget) -> bool {
524 self.reservation
525 .as_ref()
526 .is_some_and(|reservation| budget.contains(reservation))
527 }
528 pub(crate) fn device_id(&self) -> usize {
529 self.owner.device.id()
530 }
531 pub fn bytes(&self) -> usize {
533 self.bytes
534 }
535 pub fn binding(&self) -> View<'_> {
537 View {
538 owner: self,
539 offset: 0,
540 length: self.bytes,
541 }
542 }
543 pub fn try_slice(&self, offset: usize, length: usize) -> Result<View<'_>> {
545 self.binding().slice(offset, length)
546 }
547 pub fn slice(&self, offset: usize, length: usize) -> View<'_> {
549 self.try_slice(offset, length)
550 .expect("slice exceeds allocation")
551 }
552 pub fn device_ptr(&self) -> Result<*mut std::ffi::c_void> {
557 Ok(self.native.host_pointer().cast())
558 }
559 pub unsafe fn cache_control(&self, flush: bool, offset: usize, length: usize) -> Result<()> {
566 unsafe { self.native.cache_control(flush, offset, length) }
567 }
568 pub(crate) fn allocation_address(&self) -> Result<u64> {
569 self.native.device_address(&self.owner.device)
570 }
571}
572#[derive(Clone)]
574pub struct Kernel {
575 native: fabric::Kernel,
576 info: ExportInfo,
577 layout: Arc<[(u32, usize)]>,
578 symbol: Arc<str>,
579 artifact: Option<Arc<crate::loom::Artifact>>,
580}
581impl Kernel {
582 pub fn sanitizer_reports(&self) -> Result<fabric::SanitizerReports> {
585 self.native.sanitizer_reports()
586 }
587 pub(crate) unsafe fn prepare_aql(
588 &self,
589 queue: &fabric::AqlQueue,
590 grid: [u32; 3],
591 block: [u32; 3],
592 constants: &Constants,
593 bindings: &[View<'_>],
594 ) -> Result<fabric::PreparedAql> {
595 validate_export_launch(&self.info, grid, block)?;
596 if constants.len != self.info.constant_byte_length as usize
597 || bindings.len() != self.info.binding_count as usize
598 {
599 return Err(Error::Message(
600 "kernel binding or constant byte count mismatch".into(),
601 ));
602 }
603 let mut args = Vec::with_capacity(self.layout.len());
604 let mut scalar = 0;
605 let mut binding = 0;
606 for &(kind, size) in self.layout.iter() {
607 if kind == 1 {
608 args.push(fabric::Argument::Value(
609 &constants.bytes[scalar..scalar + size],
610 ));
611 scalar += size;
612 } else {
613 let view = bindings[binding];
614 args.push(fabric::Argument::Buffer(&view.owner.native, view.offset));
615 binding += 1;
616 }
617 }
618 unsafe { queue.prepare(&self.native, grid, block.map(|v| v as u16), &args) }
619 }
620 pub(crate) fn device_id(&self) -> usize {
621 self.native.device().id()
622 }
623 pub fn info(&self) -> &ExportInfo {
625 &self.info
626 }
627 pub fn symbol(&self) -> &str {
629 &self.symbol
630 }
631 pub fn launch_program(&self) -> Result<crate::loom::LaunchProgram> {
633 self.artifact
634 .as_ref()
635 .ok_or_else(|| {
636 Error::Unsupported("raw code objects have no compiler launch companion".into())
637 })?
638 .launch_program(self.symbol())
639 }
640 pub fn launch_config(
642 &self,
643 workload_argument_bits: &[u64],
644 ) -> Result<crate::loom::LaunchConfig> {
645 let config = self.launch_program()?.evaluate(workload_argument_bits)?;
646 self.validate_launch_config(&config)?;
647 Ok(config)
648 }
649 fn validate_launch_config(&self, config: &crate::loom::LaunchConfig) -> Result<()> {
650 validate_export_launch(&self.info, config.workgroup_count, config.workgroup_size)?;
651 if config.workgroup_cluster_size != [1; 3] {
652 return Err(Error::Unsupported(
653 "clustered launches are not admitted by the PM4 runtime".into(),
654 ));
655 }
656 if config.subgroup_size != self.native.subgroup_size()
657 || config.workgroup_storage_bytes != self.native.workgroup_storage_bytes()
658 {
659 return Err(Error::Unsupported(
660 "launch program requirements differ from the loaded executable".into(),
661 ));
662 }
663 Ok(())
664 }
665 fn from_native(native: fabric::Kernel, symbol: &str) -> Result<Self> {
666 let layout = native.argument_layout()?;
667 let mut info = ExportInfo {
668 workgroup_size: native.workgroup_size(),
669 parameter_count: layout.len() as u32,
670 ..Default::default()
671 };
672 for &(kind, size) in &layout {
673 match kind {
674 1 => {
675 info.constant_byte_length =
676 info.constant_byte_length
677 .checked_add(size as u32)
678 .ok_or_else(|| Error::Message("constant layout overflow".into()))?
679 }
680 2 => info.binding_count += 1,
681 _ => {
682 return Err(Error::Unsupported(
683 "native argument kind is unsupported".into(),
684 ));
685 }
686 }
687 }
688 Ok(Self {
689 native,
690 info,
691 layout: layout.into(),
692 symbol: symbol.into(),
693 artifact: None,
694 })
695 }
696}
697#[derive(Default)]
698struct BudgetUses(std::collections::HashMap<usize, Arc<crate::residency::MemoryReservation>>);
699impl BudgetUses {
700 fn retain(&mut self, views: &[View<'_>]) {
701 for view in views {
702 if let Some(reservation) = &view.owner.reservation {
703 self.0
704 .entry(Arc::as_ptr(reservation) as usize)
705 .or_insert_with(|| reservation.clone());
706 }
707 }
708 }
709 fn clear(&mut self) {
710 self.0.clear();
711 }
712}
713const DISPATCH_CACHE_ENTRIES: usize = 1024;
714const DISPATCH_CACHE_BYTES: usize = 16 << 20;
715
716struct CachedDispatch {
717 kernel: Kernel,
718 grid: [u32; 3],
719 block: [u32; 3],
720 constants: Constants,
721 bindings: Vec<(fabric::Buffer, usize, usize)>,
722 command: Command,
723}
724impl CachedDispatch {
725 fn matches(
726 &self,
727 kernel: &Kernel,
728 grid: [u32; 3],
729 block: [u32; 3],
730 constants: &Constants,
731 bindings: &[View<'_>],
732 ) -> bool {
733 self.kernel.native.same_entry(&kernel.native)
734 && self.grid == grid
735 && self.block == block
736 && self.constants.as_bytes() == constants.as_bytes()
737 && self.bindings.len() == bindings.len()
738 && self
739 .bindings
740 .iter()
741 .zip(bindings)
742 .all(|((buffer, offset, length), view)| {
743 buffer.same_backing(&view.owner.native)
744 && *offset == view.offset
745 && *length == view.length
746 })
747 }
748}
749struct CachedTransfer {
750 destination: fabric::Buffer,
751 destination_offset: usize,
752 length: usize,
753 source: Option<(fabric::Buffer, usize)>,
754 value: u8,
755 command: Command,
756}
757pub struct Stream {
759 inner: Arc<Inner>,
760 staging: Vec<Buffer>,
761 staging_pool: Vec<Buffer>,
762 scratch: BTreeMap<usize, Vec<Buffer>>,
763 scratch_bytes: usize,
764 scratch_limit: usize,
765 budget: Option<crate::residency::MemoryBudget>,
766 budget_uses: RefCell<BudgetUses>,
767}
768impl Stream {
769 pub fn open() -> Result<Self> {
771 Device::open(0)?.stream()
772 }
773 pub fn device_id(&self) -> usize {
775 self.inner.device.id()
776 }
777 pub fn id(&self) -> usize {
779 Arc::as_ptr(&self.inner) as usize
780 }
781 pub fn target(&self) -> &Target {
783 self.inner.device.target()
784 }
785 pub fn with_memory_budget(mut self, budget: crate::residency::MemoryBudget) -> Self {
787 self.budget = Some(budget);
788 self
789 }
790 pub fn memory_budget(&self) -> Option<&crate::residency::MemoryBudget> {
792 self.budget.as_ref()
793 }
794 fn reserve(&self, bytes: usize) -> Result<Option<Arc<crate::residency::MemoryReservation>>> {
795 self.budget
796 .as_ref()
797 .map(|budget| budget.reserve(bytes.max(1)).map(Arc::new))
798 .transpose()
799 }
800 fn owns(&self, buffer: &Buffer) -> Result<()> {
801 owns(&self.inner, buffer)
802 }
803 pub fn allocate(&self, bytes: usize) -> Result<Buffer> {
806 self.allocate_initialized(bytes.max(1), None)
807 }
808 pub fn allocate_from(&self, data: &[u8]) -> Result<Buffer> {
812 if data.is_empty() {
813 return Err(Error::Message("initial data must be nonempty".into()));
814 }
815 self.allocate_initialized(data.len(), Some(data))
816 }
817 fn allocate_initialized(&self, bytes: usize, data: Option<&[u8]>) -> Result<Buffer> {
818 if self.inner.sdma.is_some() {
819 let buffer = self.allocate_shared(bytes)?;
820 if let Some(data) = data {
821 buffer.native.write(0, data)?;
822 }
823 return Ok(buffer);
824 }
825 let reservation = self.reserve(bytes)?;
826 let reused = if reservation.is_none() {
827 let mut pool = self
828 .inner
829 .free
830 .lock()
831 .map_err(|_| Error::DeviceLost("allocation pool poisoned".into()))?;
832 pool.iter()
833 .position(|buffer| buffer.len() == bytes)
834 .map(|index| pool.swap_remove(index))
835 } else {
836 None
837 };
838 let native = match reused {
839 Some(native) => {
840 if let Some(data) = data {
841 native.write(0, data)?;
842 }
843 native
844 }
845 None => {
846 let fabric = self.inner.device.fabric();
847 let devices = std::slice::from_ref(&self.inner.device);
848 match data {
849 Some(data) => fabric.allocate_from(data, devices)?,
850 None => fabric.allocate(bytes, devices)?,
851 }
852 }
853 };
854 Ok(Buffer {
855 native,
856 bytes,
857 owner: self.inner.clone(),
858 reservation,
859 poolable: true,
860 })
861 }
862 pub fn allocate_zeroed(&self, bytes: usize) -> Result<Buffer> {
864 let buffer = self.allocate(bytes)?;
865 buffer.native.zero()?;
866 Ok(buffer)
867 }
868 pub fn allocate_shared(&self, bytes: usize) -> Result<Buffer> {
870 let bytes = bytes.max(1);
871 let reservation = self.reserve(bytes)?;
872 let native = self
873 .inner
874 .device
875 .fabric()
876 .allocate_shared(bytes, std::slice::from_ref(&self.inner.device))?;
877 Ok(Buffer {
878 native,
879 bytes,
880 owner: self.inner.clone(),
881 reservation,
882 poolable: false,
883 })
884 }
885 #[cfg(feature = "npu")]
886 pub(crate) fn allocate_for(&self, bytes: usize, devices: &[fabric::Device]) -> Result<Buffer> {
887 let reservation = self.reserve(bytes)?;
888 let native = self.inner.device.fabric().allocate(bytes, devices)?;
889 native.device_address(&self.inner.device)?;
890 Ok(Buffer {
891 native,
892 bytes,
893 owner: self.inner.clone(),
894 reservation,
895 poolable: false,
896 })
897 }
898 #[cfg(target_os = "linux")]
899 pub(crate) fn native_device(&self) -> &fabric::Device {
900 &self.inner.device
901 }
902 #[cfg(target_os = "linux")]
903 pub(crate) fn storage_buffer(&self, native: fabric::Buffer) -> Result<Buffer> {
904 native.device_address(&self.inner.device)?;
905 Ok(Buffer {
906 bytes: native.len(),
907 native,
908 owner: self.inner.clone(),
909 reservation: None,
910 poolable: false,
911 })
912 }
913 pub fn synchronize(&mut self) -> Result<()> {
915 self.inner.wait()?;
916 self.reclaim_staging();
917 self.budget_uses.get_mut().clear();
918 Ok(())
919 }
920 pub fn record_event(&mut self) -> Result<Event> {
923 Ok(Event {
924 device: self.device_id(),
925 done: self.inner.timeline.snapshot()?,
926 })
927 }
928 pub fn wait_event(&mut self, event: &Event) -> Result<()> {
932 if event.device != self.device_id() {
933 return Err(Error::Message("event belongs to another device".into()));
934 }
935 if let Some(done) = &event.done {
936 match done {
937 Completion::Gpu(done, 0)
938 if self.inner.aql.is_none() && self.inner.sdma.is_none() =>
939 {
940 self.inner
941 .submit(&Command::Gpu(self.inner.queue.prepare_wait(done)?, 0))?;
942 }
943 _ => done.wait()?,
944 }
945 }
946 Ok(())
947 }
948 pub fn submit(&mut self) -> Result<Submission<'_>> {
950 Ok(Submission { stream: self })
951 }
952 pub fn upload_blocking(&mut self, dst: View<'_>, bytes: &[u8]) -> Result<()> {
954 self.owns(dst.owner)?;
955 checked_span(0, bytes.len(), dst.len())?;
956 self.synchronize()?;
957 dst.owner.native.write(dst.offset, bytes)
958 }
959 pub fn upload_blocking_at(&mut self, dst: &Buffer, offset: usize, bytes: &[u8]) -> Result<()> {
961 self.upload_blocking(dst.try_slice(offset, bytes.len())?, bytes)
962 }
963 pub fn read_blocking(&mut self, src: View<'_>, bytes: &mut [u8]) -> Result<()> {
965 self.owns(src.owner)?;
966 checked_span(0, bytes.len(), src.len())?;
967 self.synchronize()?;
968 src.owner.native.read(src.offset, bytes)
969 }
970 pub fn read_blocking_at(
972 &mut self,
973 src: &Buffer,
974 offset: usize,
975 bytes: &mut [u8],
976 ) -> Result<()> {
977 self.read_blocking(src.try_slice(offset, bytes.len())?, bytes)
978 }
979 fn prepare_fill(&self, dst: View<'_>, value: u8) -> Result<Command> {
980 self.inner.prepare_fill(dst, value)
981 }
982 fn prepare_copy(&self, dst: View<'_>, src: View<'_>) -> Result<Command> {
983 self.inner.prepare_copy(dst, src)
984 }
985 pub fn fill(&self, dst: View<'_>, value: u8) -> Result<()> {
987 self.budget_uses.borrow_mut().retain(&[dst]);
988 self.inner.submit(&self.prepare_fill(dst, value)?)
989 }
990 pub fn copy(&self, dst: View<'_>, src: View<'_>) -> Result<()> {
992 self.budget_uses.borrow_mut().retain(&[dst, src]);
993 self.inner.submit(&self.prepare_copy(dst, src)?)
994 }
995 pub fn upload(&mut self, dst: View<'_>, bytes: &[u8]) -> Result<()> {
998 self.owns(dst.owner)?;
999 checked_span(0, bytes.len(), dst.len())?;
1000 for (index, chunk) in bytes.chunks(STAGING_LIMIT).enumerate() {
1001 self.upload_chunk(dst.slice(index * STAGING_LIMIT, chunk.len())?, chunk)?;
1002 }
1003 Ok(())
1004 }
1005 fn upload_chunk(&mut self, dst: View<'_>, bytes: &[u8]) -> Result<()> {
1006 if self.staging.len() >= 8
1007 || self
1008 .staging
1009 .iter()
1010 .map(|b| b.bytes)
1011 .sum::<usize>()
1012 .saturating_add(bytes.len())
1013 > STAGING_LIMIT
1014 {
1015 self.synchronize()?;
1016 }
1017 let staging = if let Some(index) = self
1018 .staging_pool
1019 .iter()
1020 .enumerate()
1021 .filter(|(_, b)| b.bytes >= bytes.len())
1022 .min_by_key(|(_, b)| b.bytes)
1023 .map(|(index, _)| index)
1024 {
1025 self.staging_pool.swap_remove(index)
1026 } else {
1027 self.allocate(bytes.len())?
1028 };
1029 staging.native.write(0, bytes)?;
1030 self.copy(
1031 dst.slice(0, bytes.len())?,
1032 staging.try_slice(0, bytes.len())?,
1033 )?;
1034 self.staging.push(staging);
1035 Ok(())
1036 }
1037 pub fn upload_at(&mut self, dst: &Buffer, offset: usize, bytes: &[u8]) -> Result<()> {
1039 self.upload(dst.try_slice(offset, bytes.len())?, bytes)
1040 }
1041 fn reclaim_staging(&mut self) {
1042 let mut cached = self.staging_pool.iter().map(|b| b.bytes).sum::<usize>();
1043 for buffer in self.staging.drain(..) {
1044 if buffer.bytes > STAGING_LIMIT {
1045 continue;
1046 }
1047 while self.staging_pool.len() >= 8
1048 || buffer.bytes > STAGING_LIMIT.saturating_sub(cached)
1049 {
1050 let index = self
1051 .staging_pool
1052 .iter()
1053 .enumerate()
1054 .min_by_key(|(_, b)| b.bytes)
1055 .map(|(i, _)| i)
1056 .unwrap();
1057 cached -= self.staging_pool.swap_remove(index).bytes;
1058 }
1059 cached += buffer.bytes;
1060 self.staging_pool.push(buffer);
1061 }
1062 }
1063 pub unsafe fn load(&self, path: &Path, symbol: &str) -> Result<Kernel> {
1068 Kernel::from_native(
1069 unsafe { self.inner.device.load_bytes(&std::fs::read(path)?, symbol) }?,
1070 symbol,
1071 )
1072 }
1073 pub unsafe fn load_artifact(&self, artifact: &crate::loom::Artifact) -> Result<Kernel> {
1078 if let Some(options) = &self.inner.sanitizer {
1079 return unsafe { self.load_sanitized_artifact(artifact, options) };
1080 }
1081 let mut kernel = Kernel::from_native(
1082 unsafe { self.inner.device.load(artifact) }?,
1083 artifact.symbol(),
1084 )?;
1085 kernel.artifact = Some(Arc::new(artifact.clone()));
1086 Ok(kernel)
1087 }
1088 pub(crate) unsafe fn load_sanitized_artifact(
1089 &self,
1090 artifact: &crate::loom::Artifact,
1091 options: &fabric::SanitizerRuntimeOptions,
1092 ) -> Result<Kernel> {
1093 let mut kernel = Kernel::from_native(
1094 unsafe { self.inner.device.load_sanitized(artifact, options) }?,
1095 artifact.symbol(),
1096 )?;
1097 kernel.artifact = Some(Arc::new(artifact.clone()));
1098 Ok(kernel)
1099 }
1100 pub unsafe fn load_artifact_for_workload(
1105 &self,
1106 artifact: &crate::loom::Artifact,
1107 workload_argument_bits: &[u64],
1108 ) -> Result<(Kernel, crate::loom::LaunchConfig)> {
1109 let kernel = unsafe { self.load_artifact(artifact) }?;
1110 let config = kernel.launch_config(workload_argument_bits)?;
1111 Ok((kernel, config))
1112 }
1113 unsafe fn prepare_dispatch(
1114 &self,
1115 kernel: &Kernel,
1116 grid: [u32; 3],
1117 block: [u32; 3],
1118 constants: &Constants,
1119 bindings: &[View<'_>],
1120 arena: Option<&mut fabric::GraphArena>,
1121 ) -> Result<Command> {
1122 unsafe {
1123 self.inner
1124 .prepare_dispatch(kernel, grid, block, constants, bindings, arena)
1125 }
1126 }
1127 pub unsafe fn dispatch(
1132 &self,
1133 kernel: &Kernel,
1134 grid: [u32; 3],
1135 block: [u32; 3],
1136 constants: &Constants,
1137 bindings: &[View<'_>],
1138 ) -> Result<()> {
1139 self.budget_uses.borrow_mut().retain(bindings);
1140 if bindings
1143 .iter()
1144 .all(|view| Arc::ptr_eq(&view.owner.owner, &self.inner))
1145 {
1146 let mut cache = self
1147 .inner
1148 .dispatch_cache
1149 .lock()
1150 .map_err(|_| Error::DeviceLost("dispatch cache poisoned".into()))?;
1151 if let Some(entry) = cache
1152 .iter()
1153 .find(|entry| entry.matches(kernel, grid, block, constants, bindings))
1154 {
1155 return self.inner.submit(&entry.command);
1156 }
1157 let command =
1158 unsafe { self.prepare_dispatch(kernel, grid, block, constants, bindings, None) }?;
1159 self.inner.submit(&command)?;
1160 let bytes = command.storage_bytes();
1161 if bytes > DISPATCH_CACHE_BYTES {
1162 return Ok(());
1163 }
1164 let mut retained: usize = cache
1165 .iter()
1166 .map(|entry| entry.command.storage_bytes())
1167 .sum();
1168 while cache.len() >= DISPATCH_CACHE_ENTRIES || retained + bytes > DISPATCH_CACHE_BYTES {
1169 retained -= cache.remove(0).command.storage_bytes();
1170 }
1171 cache.push(CachedDispatch {
1172 kernel: kernel.clone(),
1173 grid,
1174 block,
1175 constants: constants.clone(),
1176 bindings: bindings
1177 .iter()
1178 .map(|view| (view.owner.native.clone(), view.offset, view.length))
1179 .collect(),
1180 command,
1181 });
1182 Ok(())
1183 } else {
1184 self.inner.submit(&unsafe {
1185 self.prepare_dispatch(kernel, grid, block, constants, bindings, None)
1186 }?)
1187 }
1188 }
1189 pub fn scratch(&mut self, bytes: usize) -> Result<Buffer> {
1191 let choice = self
1192 .scratch
1193 .range(bytes.max(1)..=bytes.saturating_mul(2).max(1))
1194 .next()
1195 .map(|(&size, _)| size);
1196 if let Some(size) = choice {
1197 let list = self.scratch.get_mut(&size).unwrap();
1198 let buffer = list.pop().unwrap();
1199 if list.is_empty() {
1200 self.scratch.remove(&size);
1201 }
1202 self.scratch_bytes -= buffer.bytes;
1203 Ok(buffer)
1204 } else {
1205 self.allocate(bytes)
1206 }
1207 }
1208 pub fn recycle(&mut self, buffer: Buffer) -> Result<()> {
1210 if !Arc::ptr_eq(&buffer.owner, &self.inner) {
1211 return Err(Error::Message("scratch belongs to another stream".into()));
1212 }
1213 if buffer.bytes <= self.scratch_limit.saturating_sub(self.scratch_bytes) {
1214 self.scratch_bytes += buffer.bytes;
1215 self.scratch.entry(buffer.bytes).or_default().push(buffer);
1216 }
1217 Ok(())
1218 }
1219 pub fn set_scratch_limit(&mut self, bytes: usize) {
1221 self.scratch_limit = bytes;
1222 while self.scratch_bytes > bytes {
1223 if let Some((_, list)) = self.scratch.pop_last() {
1224 self.scratch_bytes -= list.iter().map(|b| b.bytes).sum::<usize>();
1225 } else {
1226 break;
1227 }
1228 }
1229 }
1230 pub fn read(&mut self, source: View<'_>) -> Result<Readback> {
1232 let buffer = self.allocate(source.len())?;
1233 if !source.is_empty() {
1234 self.copy(buffer.try_slice(0, source.len())?, source)?;
1235 }
1236 Ok(Readback {
1237 buffer,
1238 length: source.len(),
1239 })
1240 }
1241}
1242impl Drop for Stream {
1243 fn drop(&mut self) {
1244 if !self.inner.drain_for_drop() {
1245 std::mem::forget(std::mem::take(&mut self.staging));
1246 std::mem::forget(std::mem::take(self.budget_uses.get_mut()));
1247 }
1248 }
1249}
1250#[derive(Clone)]
1252pub struct Event {
1253 device: usize,
1254 done: Option<Completion>,
1255}
1256impl Event {
1257 pub fn is_complete(&self) -> Result<bool> {
1259 self.done.as_ref().map_or(Ok(true), Completion::refresh)
1260 }
1261 pub fn synchronize(&self) -> Result<()> {
1263 self.done.as_ref().map_or(Ok(()), Completion::wait)
1264 }
1265}
1266#[must_use]
1268pub struct Submission<'a> {
1269 stream: &'a mut Stream,
1270}
1271impl Submission<'_> {
1272 pub fn is_complete(&mut self) -> Result<bool> {
1274 let done = self
1275 .stream
1276 .inner
1277 .timeline
1278 .snapshot()?
1279 .as_ref()
1280 .map_or(Ok(true), Completion::refresh)?;
1281 if done {
1282 self.stream.reclaim_staging();
1283 self.stream.budget_uses.get_mut().clear();
1284 }
1285 Ok(done)
1286 }
1287 pub fn wait(self) -> Result<()> {
1289 self.stream.synchronize()
1290 }
1291}
1292#[must_use]
1294pub struct Readback {
1295 buffer: Buffer,
1296 length: usize,
1297}
1298impl Readback {
1299 pub fn wait(self, stream: &mut Stream) -> Result<Vec<u8>> {
1301 if !Arc::ptr_eq(&self.buffer.owner, &stream.inner) {
1302 return Err(Error::Message("readback belongs to another stream".into()));
1303 }
1304 stream.synchronize()?;
1305 let mut bytes = vec![0; self.length];
1306 self.buffer.native.read(0, &mut bytes)?;
1307 Ok(bytes)
1308 }
1309}
1310fn owns(inner: &Arc<Inner>, buffer: &Buffer) -> Result<()> {
1311 if inner.device.id() == buffer.device_id() {
1312 Ok(())
1313 } else {
1314 Err(Error::Message("buffer belongs to another device".into()))
1315 }
1316}
1317pub(crate) fn checked_span(offset: usize, length: usize, capacity: usize) -> Result<()> {
1318 if offset.checked_add(length).is_none_or(|end| end > capacity) {
1319 Err(Error::Message(format!(
1320 "span {offset}+{length} exceeds {capacity} bytes"
1321 )))
1322 } else {
1323 Ok(())
1324 }
1325}
1326pub(crate) fn validate_launch(grid: [u32; 3], block: [u32; 3]) -> Result<()> {
1327 if grid.contains(&0)
1328 || block.contains(&0)
1329 || block
1330 .iter()
1331 .try_fold(1u32, |n, b| n.checked_mul(*b))
1332 .is_none_or(|n| n > 1024)
1333 {
1334 Err(Error::Message("invalid GPU launch dimensions".into()))
1335 } else {
1336 Ok(())
1337 }
1338}
1339pub(crate) fn validate_export_launch(
1340 info: &ExportInfo,
1341 grid: [u32; 3],
1342 block: [u32; 3],
1343) -> Result<()> {
1344 validate_launch(grid, block)?;
1345 if info
1346 .workgroup_size
1347 .iter()
1348 .zip(block)
1349 .any(|(&expected, actual)| expected != 0 && expected != actual)
1350 {
1351 Err(Error::Message(format!(
1352 "workgroup size {block:?} disagrees with compiled size {:?}",
1353 info.workgroup_size
1354 )))
1355 } else {
1356 Ok(())
1357 }
1358}
1359const STAGING_LIMIT: usize = 64 * 1024 * 1024;
1360
1361#[derive(Clone, Debug)]
1366pub struct Constants {
1367 bytes: [u8; 256],
1368 len: usize,
1369}
1370impl Default for Constants {
1371 fn default() -> Self {
1372 Self {
1373 bytes: [0; 256],
1374 len: 0,
1375 }
1376 }
1377}
1378impl Constants {
1379 pub fn indices(kernel: &Kernel, values: &[u32]) -> Result<Self> {
1392 let size = kernel.info.constant_byte_length as usize;
1393 let mut constants = Self::new();
1394 if values.is_empty() {
1395 if size != 0 {
1396 return Err(Error::Message("missing index constants".into()));
1397 }
1398 return Ok(constants);
1399 }
1400 if size == values.len() * 4 {
1401 for &value in values {
1402 constants.push(value)?;
1403 }
1404 } else if size == values.len() * 8 {
1405 for &value in values {
1406 constants.push(u64::from(value))?;
1407 }
1408 } else {
1409 return Err(Error::Message(
1410 "export is not a homogeneous 32/64-bit index list".into(),
1411 ));
1412 }
1413 Ok(constants)
1414 }
1415 pub fn new() -> Self {
1417 Self::default()
1418 }
1419 #[must_use = "constant packing can fail when capacity is exceeded"]
1420 pub fn push<T: Scalar>(&mut self, value: T) -> Result<()> {
1422 let bytes = value.bytes();
1423 checked_span(self.len, bytes.as_ref().len(), self.bytes.len())?;
1424 self.bytes[self.len..self.len + bytes.as_ref().len()].copy_from_slice(bytes.as_ref());
1425 self.len += bytes.as_ref().len();
1426 Ok(())
1427 }
1428 pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
1430 let mut result = Self::new();
1431 checked_span(0, bytes.len(), result.bytes.len())?;
1432 result.bytes[..bytes.len()].copy_from_slice(bytes);
1433 result.len = bytes.len();
1434 Ok(result)
1435 }
1436 pub fn as_bytes(&self) -> &[u8] {
1438 &self.bytes[..self.len]
1439 }
1440}
1441mod sealed {
1442 pub trait Sealed {}
1443}
1444pub trait Scalar: sealed::Sealed {
1446 type Bytes: AsRef<[u8]>;
1448 fn bytes(self) -> Self::Bytes;
1450}
1451macro_rules! scalars {
1452 ($($ty:ty => $n:literal),*) => {
1453 $(
1454 impl sealed::Sealed for $ty {}
1455 impl Scalar for $ty {
1456 type Bytes = [u8; $n];
1457 fn bytes(self) -> Self::Bytes { self.to_le_bytes() }
1458 }
1459 )*
1460 };
1461}
1462scalars!(u32 => 4, i32 => 4, f32 => 4, u64 => 8, i64 => 8, f64 => 8);
1463
1464#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1466pub struct Node {
1467 graph: u64,
1468 index: u32,
1469}
1470static NEXT_GRAPH_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
1471pub struct Graph<'a> {
1473 inner: Arc<Inner>,
1474 _borrow: std::marker::PhantomData<&'a Stream>,
1475 id: u64,
1476 nodes: Vec<(Recorded, Option<u32>)>,
1477 bindings: std::collections::HashMap<usize, usize>,
1478 arena: fabric::GraphArena,
1479 budget_uses: BudgetUses,
1480}
1481enum Recorded {
1482 Prepared(Command),
1483 Join,
1484}
1485pub struct GraphExec {
1487 inner: Arc<Inner>,
1488 commands: Vec<Command>,
1489 profile: Option<fabric::profile::ProfileCapture>,
1490 last: Option<Completion>,
1491 failed: bool,
1492 budget_uses: BudgetUses,
1493}
1494impl Stream {
1495 pub fn graph(&self) -> Result<Graph<'_>> {
1497 self.owned_graph()
1498 }
1499 pub fn owned_graph(&self) -> Result<Graph<'static>> {
1504 Ok(Graph {
1505 inner: self.inner.clone(),
1506 _borrow: std::marker::PhantomData,
1507 id: NEXT_GRAPH_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
1508 nodes: Vec::new(),
1509 bindings: std::collections::HashMap::new(),
1510 arena: fabric::GraphArena::default(),
1511 budget_uses: BudgetUses::default(),
1512 })
1513 }
1514 pub fn launch_profiled(&mut self, graph: &mut GraphExec) -> Result<fabric::DeviceProfile> {
1517 if graph.profile.is_none() {
1518 return Err(Error::Message(
1519 "graph was not prepared for profiling".into(),
1520 ));
1521 }
1522 self.launch(graph)?;
1523 self.synchronize()?;
1524 graph.profile.as_mut().unwrap().read()
1525 }
1526 pub fn launch(&mut self, graph: &mut GraphExec) -> Result<()> {
1528 if !Arc::ptr_eq(&graph.inner, &self.inner) {
1529 return Err(Error::Message("graph belongs to another stream".into()));
1530 }
1531 if graph.failed {
1532 return Err(Error::DeviceLost(
1533 "graph has an unretired failed launch".into(),
1534 ));
1535 }
1536 graph.failed = true;
1537 for command in &graph.commands {
1538 self.inner.submit(command)?;
1539 }
1540 graph.last = self.inner.timeline.snapshot()?;
1541 graph.failed = false;
1542 Ok(())
1543 }
1544}
1545impl<'a> Graph<'a> {
1546 pub fn binding_bytes(&self) -> usize {
1549 self.bindings.values().sum()
1550 }
1551 fn retain_bindings(&mut self, bindings: &[View<'_>]) {
1552 self.budget_uses.retain(bindings);
1553 for view in bindings {
1554 self.bindings
1555 .insert(view.owner.native.identity(), view.owner.bytes());
1556 }
1557 }
1558
1559 fn validate_dependencies(&self, after: &[Node]) -> Result<()> {
1560 for node in after {
1561 if node.graph != self.id || node.index as usize >= self.nodes.len() {
1562 return Err(Error::Message(
1563 "dependency node belongs to another graph".into(),
1564 ));
1565 }
1566 }
1567 if self.nodes.len() >= u32::MAX as usize {
1568 return Err(Error::Message("graph node capacity exceeded".into()));
1569 }
1570 Ok(())
1571 }
1572 fn record(&mut self, command: Recorded, after: &[Node]) -> Node {
1573 let index = self.nodes.len() as u32;
1574 self.nodes
1575 .push((command, after.iter().map(|node| node.index).max()));
1576 Node {
1577 graph: self.id,
1578 index,
1579 }
1580 }
1581 pub fn fill(&mut self, after: &[Node], dst: View<'_>, pattern: u8) -> Result<Node> {
1583 self.validate_dependencies(after)?;
1584 owns(&self.inner, dst.owner)?;
1585 if dst.is_empty() {
1586 return Err(Error::Message("empty graph fill".into()));
1587 }
1588 let command = self.inner.prepare_fill(dst, pattern)?;
1589 self.retain_bindings(&[dst]);
1590 Ok(self.record(Recorded::Prepared(command), after))
1591 }
1592 pub fn copy(&mut self, after: &[Node], dst: View<'_>, src: View<'_>) -> Result<Node> {
1594 self.validate_dependencies(after)?;
1595 owns(&self.inner, dst.owner)?;
1596 owns(&self.inner, src.owner)?;
1597 if dst.len() != src.len() || dst.is_empty() {
1598 return Err(Error::Message(
1599 "graph copy requires equal nonempty spans".into(),
1600 ));
1601 }
1602 let command = self.inner.prepare_copy(dst, src)?;
1603 self.retain_bindings(&[dst, src]);
1604 Ok(self.record(Recorded::Prepared(command), after))
1605 }
1606 pub unsafe fn dispatch(
1612 &mut self,
1613 after: &[Node],
1614 kernel: &Kernel,
1615 grid: [u32; 3],
1616 block: [u32; 3],
1617 constants: &Constants,
1618 bindings: &[View<'_>],
1619 ) -> Result<Node> {
1620 self.validate_dependencies(after)?;
1621 let command = unsafe {
1622 self.inner.prepare_dispatch(
1623 kernel,
1624 grid,
1625 block,
1626 constants,
1627 bindings,
1628 Some(&mut self.arena),
1629 )
1630 }?;
1631 self.retain_bindings(bindings);
1632 Ok(self.record(Recorded::Prepared(command), after))
1633 }
1634 pub fn join(&mut self, after: &[Node]) -> Result<Node> {
1636 self.validate_dependencies(after)?;
1637 Ok(self.record(Recorded::Join, after))
1638 }
1639 pub fn finish(self) -> Result<GraphExec> {
1642 self.finish_inner(None)
1643 }
1644 pub fn finish_profiled(self, labels: &[String]) -> Result<GraphExec> {
1648 self.finish_inner(Some(labels))
1649 }
1650 fn finish_inner(self, labels: Option<&[String]>) -> Result<GraphExec> {
1651 let mut ordered = Vec::new();
1652 let mut completed = 0usize;
1653 for (index, (node, dependency)) in self.nodes.into_iter().enumerate() {
1654 let command = match node {
1655 Recorded::Prepared(command) => command,
1656 Recorded::Join => continue,
1657 };
1658 let barrier = dependency.is_some_and(|node| node as usize >= completed);
1659 if barrier {
1660 completed = index;
1661 }
1662 ordered.push((command, barrier));
1663 }
1664 let (commands, profile) = if self.inner.aql.is_none() && self.inner.sdma.is_none() {
1667 let native: Vec<_> = ordered
1668 .into_iter()
1669 .map(|(command, barrier)| match command {
1670 Command::Gpu(c, 0) => (c, barrier),
1671 _ => unreachable!("PM4 stream only prepares PM4 commands"),
1672 })
1673 .collect();
1674 let (commands, profile) =
1675 unsafe { self.inner.queue.prepare_graph_batches(&native, labels) }?;
1676 (
1677 commands.into_iter().map(|c| Command::Gpu(c, 0)).collect(),
1678 profile,
1679 )
1680 } else {
1681 if labels.is_some() {
1682 return Err(Error::Unsupported(
1683 "device graph profiling requires PM4 compute and compute copies".into(),
1684 ));
1685 }
1686 (
1687 ordered.into_iter().map(|(command, _)| command).collect(),
1688 None,
1689 )
1690 };
1691 Ok(GraphExec {
1692 inner: self.inner.clone(),
1693 commands,
1694 profile,
1695 last: None,
1696 failed: false,
1697 budget_uses: self.budget_uses,
1698 })
1699 }
1700}
1701impl Drop for GraphExec {
1702 fn drop(&mut self) {
1703 if self.failed
1704 || self.last.as_ref().is_some_and(|done| {
1705 !done
1706 .wait_timeout(std::time::Duration::from_secs(10))
1707 .unwrap_or(false)
1708 })
1709 {
1710 std::mem::forget(std::mem::take(&mut self.budget_uses));
1711 }
1712 }
1713}
1714impl std::fmt::Debug for Device {
1715 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1716 f.debug_struct("Device")
1717 .field("target", self.target())
1718 .finish_non_exhaustive()
1719 }
1720}
1721impl std::fmt::Debug for Buffer {
1722 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1723 f.debug_struct("Buffer")
1724 .field("bytes", &self.bytes)
1725 .finish_non_exhaustive()
1726 }
1727}
1728impl std::fmt::Debug for Kernel {
1729 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1730 f.debug_struct("Kernel")
1731 .field("symbol", &self.symbol)
1732 .finish_non_exhaustive()
1733 }
1734}
1735impl std::fmt::Debug for Stream {
1736 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1737 f.debug_struct("Stream")
1738 .field("scratch_bytes", &self.scratch_bytes)
1739 .finish_non_exhaustive()
1740 }
1741}
1742impl std::fmt::Debug for Graph<'_> {
1743 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1744 f.debug_struct("Graph")
1745 .field("nodes", &self.nodes.len())
1746 .finish_non_exhaustive()
1747 }
1748}
1749impl std::fmt::Debug for GraphExec {
1750 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1751 f.debug_struct("GraphExec").finish_non_exhaustive()
1752 }
1753}
1754impl std::fmt::Debug for Readback {
1755 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1756 f.debug_struct("Readback").finish_non_exhaustive()
1757 }
1758}
1759impl std::fmt::Debug for Submission<'_> {
1760 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1761 f.debug_struct("Submission").finish_non_exhaustive()
1762 }
1763}
1764
1765#[cfg(test)]
1766mod tests {
1767 #[test]
1768 fn observing_a_stream_does_not_wait_for_unpublished_work() {
1769 use std::{
1770 sync::{Arc, mpsc},
1771 time::Duration,
1772 };
1773 let timeline = Arc::new(super::StreamTimeline::default());
1774 let (started, running) = mpsc::channel();
1775 let (release, gate) = mpsc::channel();
1776 let producer = timeline.clone();
1777 let worker = std::thread::spawn(move || {
1778 producer.submit(|| {
1779 started.send(()).unwrap();
1780 gate.recv().unwrap();
1781 Err(crate::Error::Busy(
1782 "simulated queue capacity failure".into(),
1783 ))
1784 })
1785 });
1786 running.recv_timeout(Duration::from_secs(1)).unwrap();
1787 let observer = timeline.clone();
1788 let (seen, observed) = mpsc::channel();
1789 let reader = std::thread::spawn(move || seen.send(observer.snapshot()).unwrap());
1790 let snapshot = observed.recv_timeout(Duration::from_secs(1));
1791 release.send(()).unwrap();
1793 assert!(matches!(worker.join().unwrap(), Err(crate::Error::Busy(_))));
1794 reader.join().unwrap();
1795 assert!(
1796 snapshot
1797 .expect("observer blocked on unpublished work")
1798 .unwrap()
1799 .is_none()
1800 );
1801 assert!(timeline.snapshot().unwrap().is_none());
1802 }
1803
1804 #[test]
1805 fn compiled_workgroup_dimensions_are_enforced() {
1806 let info = super::ExportInfo {
1807 workgroup_size: [64, 2, 1],
1808 ..Default::default()
1809 };
1810 assert!(super::validate_export_launch(&info, [1; 3], [64, 2, 1]).is_ok());
1811 assert!(super::validate_export_launch(&info, [1; 3], [128, 1, 1]).is_err());
1812 assert!(super::validate_export_launch(&info, [0, 1, 1], [64, 2, 1]).is_err());
1813 let dynamic = super::ExportInfo {
1814 workgroup_size: [0, 2, 1],
1815 ..Default::default()
1816 };
1817 assert!(super::validate_export_launch(&dynamic, [1; 3], [32, 2, 1]).is_ok());
1818 assert!(super::validate_export_launch(&dynamic, [1; 3], [32, 1, 1]).is_err());
1819 }
1820 #[test]
1822 fn a_slice_past_the_allocation_is_refused_even_when_the_sum_wraps() {
1823 let checked = |offset, length, bytes| super::checked_span(offset, length, bytes).is_ok();
1824 assert!(checked(0, 8, 8));
1825 assert!(checked(4, 4, 8));
1826 assert!(!checked(4, 5, 8));
1827 assert!(!checked(usize::MAX, 2, 8));
1829 assert!(!checked(usize::MAX - 1, 4, 8));
1830 }
1831}
1832
1833#[cfg(test)]
1834mod staging_tests {
1835 use super::*;
1836
1837 #[test]
1838 #[ignore = "requires gfx1151 and native SDMA"]
1839 fn split_transfer_failure_leaves_accepted_prefix_on_stream_timeline() -> Result<()> {
1840 let manager = crate::residency::ResidencyManager::new(512 << 20)?;
1841 {
1842 let mut stream = Device::open(0)?.stream_with_options(StreamOptions {
1843 copy_engine: crate::execution::CopyEngine::Sdma,
1844 memory_budget: Some(manager.budget()),
1845 ..Default::default()
1846 })?;
1847 let buffer = stream.allocate((256 << 20) + 17)?;
1848 stream.upload_blocking_at(&buffer, buffer.bytes() - 1, &[0])?;
1849 let command = stream.inner.prepare_fill(buffer.binding(), 0x39)?;
1850 let Command::Gpu(native, _) = &command else {
1851 unreachable!()
1852 };
1853 assert!(native.parts().count() > 1);
1854 native.exhaust_final_timeline();
1855 assert!(stream.inner.submit(&command).is_err());
1856 assert!(stream.inner.timeline.snapshot()?.is_some());
1857 stream.synchronize()?;
1858 let mut first = [0];
1859 stream.read_blocking(buffer.slice(0, 1), &mut first)?;
1860 assert_eq!(first, [0x39]);
1861 let mut last = [0];
1862 stream.read_blocking(buffer.slice(buffer.bytes() - 1, 1), &mut last)?;
1863 assert_eq!(last, [0]);
1864 }
1865 assert_eq!(manager.statistics().reserved_bytes, 0);
1866 Ok(())
1867 }
1868
1869 #[test]
1870 #[ignore = "requires gfx1151"]
1871 fn large_uploads_bound_staging_and_validate_before_publication() -> Result<()> {
1872 let manager = crate::residency::ResidencyManager::new(5 * STAGING_LIMIT)?;
1873 {
1874 let mut stream = Stream::open()?.with_memory_budget(manager.budget());
1875 let bytes = 2 * STAGING_LIMIT + 19;
1876 let buffer = stream.allocate(bytes + 16)?;
1877 stream.fill(buffer.binding(), 0x79)?;
1878 let data: Vec<_> = (0..bytes).map(|i| (i ^ (i >> 20)) as u8).collect();
1879 assert!(stream.upload(buffer.slice(17, bytes - 1), &data).is_err());
1880 let mut first = [0];
1881 stream.read_blocking(buffer.slice(0, 1), &mut first)?;
1882 assert_eq!(first, [0x79]);
1883 stream.upload(buffer.slice(3, bytes), &data)?;
1884 assert!(stream.staging.iter().map(|b| b.bytes).sum::<usize>() <= STAGING_LIMIT);
1885 let mut actual = vec![0; bytes + 16];
1886 stream.read_blocking(buffer.binding(), &mut actual)?;
1887 assert_eq!(&actual[..3], &[0x79; 3]);
1888 assert_eq!(&actual[3..3 + bytes], data);
1889 assert!(actual[3 + bytes..].iter().all(|&b| b == 0x79));
1890 assert!(stream.staging_pool.iter().map(|b| b.bytes).sum::<usize>() <= STAGING_LIMIT);
1891 }
1892 assert_eq!(manager.statistics().reserved_bytes, 0);
1893 Ok(())
1894 }
1895
1896 #[test]
1897 #[ignore = "requires gfx1151"]
1898 fn initialized_allocations_publish_all_bytes_and_preserve_pool_and_budget_rules() -> Result<()>
1899 {
1900 let device = Device::open(0)?;
1901 let mut stream = device.stream()?;
1902 assert!(stream.allocate_from(&[]).is_err());
1903 assert!(device.native.fabric().allocate_from(&[], &[]).is_err());
1904 for len in [1, 63, 65, 4097] {
1907 let mut prior = None;
1908 for round in 0..3 {
1909 let data: Vec<u8> = (0..len).map(|i| (i * 37 + round * 19) as u8).collect();
1910 let input = stream.allocate_from(&data)?;
1911 if let Some(prior) = prior {
1912 assert_eq!(input.native.identity(), prior);
1913 }
1914 prior = Some(input.native.identity());
1915 let output = stream.allocate_zeroed(len)?;
1916 stream.copy(output.binding(), input.binding())?;
1917 let mut actual = vec![0; len];
1918 stream.read_blocking(output.binding(), &mut actual)?;
1919 assert_eq!(actual, data);
1920 stream.fill(input.binding(), 0xa5)?;
1921 stream.fill(output.binding(), 0)?;
1923 stream.synchronize()?;
1924 drop(input);
1925 drop(output.into_unpooled());
1928 }
1929 }
1930 let residency = crate::residency::ResidencyManager::new(4097)?;
1931 let budget = residency.budget();
1932 let mut budgeted = device.stream()?.with_memory_budget(budget.clone());
1933 let input = budgeted.allocate_from(&vec![0x71; 4097])?;
1934 assert_eq!(budget.reserved_bytes(), 4097);
1935 assert!(budgeted.allocate_from(&[1]).is_err());
1936 assert_eq!(budget.reserved_bytes(), 4097);
1937 let mut actual = vec![0; 4097];
1938 budgeted.read_blocking(input.binding(), &mut actual)?;
1939 assert_eq!(actual, vec![0x71; 4097]);
1940 drop(input);
1941 assert_eq!(budget.reserved_bytes(), 0);
1942 Ok(())
1943 }
1944
1945 #[test]
1946 #[ignore = "requires gfx1151"]
1947 fn budgeted_commands_replay_and_evict_without_releasing_pending_charges() -> Result<()> {
1948 let residency = crate::residency::ResidencyManager::new(4096)?;
1949 let device = Device::open(0)?;
1950 let mut stream = device.stream()?.with_memory_budget(residency.budget());
1951 let mut foreign = device.stream()?;
1952 let compiler = crate::loom::Compiler::for_stream(None, &stream)?;
1953 let mut spec = crate::loom::Specialization::new("add_f32");
1954 spec.set_config("add.grid", "1");
1955 let artifact = compiler
1956 .module(include_str!("../tests/kernels/add_f32.loom"))
1957 .compile(&spec)?;
1958 let kernel = unsafe { stream.load_artifact(&artifact)? };
1960 let buffer = stream.allocate(256)?;
1961 stream.fill(buffer.binding(), 0)?;
1962 let constants = |delta: f32| -> Result<Constants> {
1963 let mut constants = Constants::new();
1964 match kernel.layout[0].1 {
1965 4 => constants.push(64u32)?,
1966 8 => constants.push(64u64)?,
1967 _ => unreachable!("index width"),
1968 }
1969 constants.push(delta)?;
1970 Ok(constants)
1971 };
1972 for _ in 0..2 {
1974 for delta in 1..=100 {
1975 unsafe {
1976 stream.dispatch(
1977 &kernel,
1978 [1; 3],
1979 [256, 1, 1],
1980 &constants(delta as f32)?,
1981 &[buffer.binding()],
1982 )?;
1983 }
1984 }
1985 stream.synchronize()?;
1986 assert_eq!(stream.inner.dispatch_cache.lock().unwrap().len(), 100);
1987 }
1988 let mut actual = [0u8; 256];
1989 stream.read_blocking(buffer.binding(), &mut actual)?;
1990 assert!(
1991 actual
1992 .as_chunks::<4>()
1993 .0
1994 .iter()
1995 .all(|bytes| f32::from_le_bytes(*bytes) == 10100.)
1996 );
1997 assert_eq!(residency.statistics().reserved_bytes, 256);
1998 for delta in 101..=1124 {
1999 unsafe {
2000 stream.dispatch(
2001 &kernel,
2002 [1; 3],
2003 [256, 1, 1],
2004 &constants(delta as f32)?,
2005 &[buffer.binding()],
2006 )?;
2007 }
2008 }
2009 stream.synchronize()?;
2010 {
2011 let cache = stream.inner.dispatch_cache.lock().unwrap();
2012 assert_eq!(cache.len(), DISPATCH_CACHE_ENTRIES);
2013 assert!(
2014 cache
2015 .iter()
2016 .map(|entry| entry.command.storage_bytes())
2017 .sum::<usize>()
2018 <= DISPATCH_CACHE_BYTES
2019 );
2020 }
2021 unsafe {
2023 foreign.dispatch(
2024 &kernel,
2025 [1; 3],
2026 [256, 1, 1],
2027 &constants(1.)?,
2028 &[buffer.binding()],
2029 )?;
2030 }
2031 foreign.synchronize()?;
2032 assert!(foreign.inner.dispatch_cache.lock().unwrap().is_empty());
2033 unsafe {
2036 stream.dispatch(
2037 &kernel,
2038 [1; 3],
2039 [256, 1, 1],
2040 &constants(1.)?,
2041 &[buffer.binding()],
2042 )?;
2043 }
2044 drop(buffer);
2045 assert!(stream.inner.dispatch_cache.lock().unwrap().is_empty());
2046 assert!(stream.inner.transfer_cache.lock().unwrap().is_empty());
2047 assert_eq!(residency.statistics().reserved_bytes, 256);
2048 stream.synchronize()?;
2049 assert_eq!(residency.statistics().reserved_bytes, 0);
2050 Ok(())
2051 }
2052
2053 #[test]
2054 #[ignore = "requires gfx1151"]
2055 fn a_scratch_pool_only_accepts_the_stream_that_allocated_the_buffer() -> Result<()> {
2056 let device = Device::open(0)?;
2057 let mut owner = device.stream()?;
2058 let mut other = device.stream()?;
2059 let private = other.scratch(4096)?;
2060 let private_raw = private.allocation_address()?;
2061 other.recycle(private)?;
2062
2063 let buffer = owner.allocate(4096)?;
2065 owner.fill(buffer.binding(), 0x5a)?;
2066 let ready = owner.record_event()?;
2067 other.wait_event(&ready)?;
2068 let mut seen = [0u8; 16];
2069 other.read_blocking(buffer.binding().slice(0, 16)?, &mut seen)?;
2070 assert_eq!(seen, [0x5a; 16], "a foreign stream can use the allocation");
2071
2072 assert!(
2074 other
2075 .recycle(buffer)
2076 .unwrap_err()
2077 .to_string()
2078 .contains("another stream")
2079 );
2080 assert!(owner.scratch.is_empty());
2081 assert_eq!(other.scratch_bytes, 4096);
2082 assert_eq!(other.scratch(4096)?.allocation_address()?, private_raw);
2083 Ok(())
2084 }
2085
2086 #[test]
2087 #[ignore = "requires gfx1151"]
2088 fn completed_staging_is_reclaimed_and_reused_on_every_completion_path() -> Result<()> {
2089 let mut stream = Stream::open()?;
2090 let buffer = stream.allocate(1024)?;
2091 stream.upload(buffer.binding(), &[7; 1024])?;
2092 let original = stream.staging[0].allocation_address()?;
2093 let mut output = [0; 1024];
2094 stream.read_blocking(buffer.binding(), &mut output)?;
2095 assert_eq!(output, [7; 1024]);
2096 assert!(stream.staging.is_empty());
2097 stream.upload(buffer.binding(), &[9; 1024])?;
2098 assert_eq!(stream.staging[0].allocation_address()?, original);
2099 stream.upload_blocking(buffer.binding(), &[11; 1024])?;
2100 assert!(stream.staging.is_empty());
2101 stream.upload(buffer.binding(), &[12; 512])?;
2102 assert_eq!(stream.staging[0].allocation_address()?, original);
2103 stream.synchronize()?;
2104 stream.upload(buffer.binding(), &[13; 1024])?;
2105 stream.inner.wait()?; assert!(stream.submit()?.is_complete()?);
2107 assert!(stream.staging.is_empty());
2108 for _ in 0..32 {
2109 stream.upload(buffer.binding(), &[1; 1024])?;
2110 assert!(stream.staging.len() <= 8);
2111 }
2112 stream.synchronize()?;
2113 assert!(stream.staging_pool.len() <= 8);
2114 assert!(stream.staging_pool.iter().map(|b| b.bytes).sum::<usize>() <= STAGING_LIMIT);
2115 Ok(())
2116 }
2117
2118 #[test]
2119 #[ignore = "requires gfx1151"]
2120 fn uploads_batch_until_pressure_and_reuse_the_smallest_capacity() -> Result<()> {
2121 let mut stream = Stream::open()?;
2122 let buffer = stream.allocate(4096)?;
2123 for size in [1024, 2048, 4096] {
2124 stream.upload(buffer.binding(), &vec![7; size])?;
2125 }
2126 assert_eq!(stream.staging.len(), 3);
2127 let medium = stream.staging[1].allocation_address()?;
2128 stream.synchronize()?;
2129 stream.upload(buffer.binding(), &[9; 1500])?;
2130 assert_eq!(stream.staging[0].allocation_address()?, medium);
2131 for _ in 0..7 {
2132 stream.upload(buffer.binding(), &[11; 1024])?;
2133 }
2134 assert_eq!(stream.staging.len(), 8);
2135 stream.upload(buffer.binding(), &[13; 1024])?;
2136 assert_eq!(stream.staging.len(), 1);
2137 stream.synchronize()?;
2138 Ok(())
2139 }
2140
2141 #[test]
2142 #[ignore = "requires gfx1151"]
2143 fn larger_uploads_replace_small_completed_staging() -> Result<()> {
2144 let mut stream = Stream::open()?;
2145 let buffer = stream.allocate(16 << 20)?;
2146 for _ in 0..8 {
2147 stream.upload(buffer.binding(), &[7; 1024])?;
2148 }
2149 stream.synchronize()?;
2150 assert_eq!(stream.staging_pool.len(), 8);
2151
2152 let source = vec![0x35; 1 << 20];
2153 stream.upload(buffer.binding(), &source)?;
2154 let large = stream.staging[0].allocation_address()?;
2155 stream.synchronize()?;
2156 let replacement = vec![0xa9; source.len()];
2157 stream.upload(buffer.binding(), &replacement)?;
2158 assert_eq!(stream.staging[0].allocation_address()?, large);
2159 let mut output = vec![0; source.len()];
2160 stream.read_blocking(buffer.try_slice(0, output.len())?, &mut output)?;
2161 assert_eq!(output, replacement);
2162 assert!(stream.staging_pool.len() <= 8);
2163 assert!(stream.staging_pool.iter().map(|b| b.bytes).sum::<usize>() <= STAGING_LIMIT);
2164
2165 let source = vec![0x63; 16 << 20];
2168 for _ in 0..9 {
2169 stream.upload(buffer.binding(), &source)?;
2170 }
2171 let mut output = vec![0; source.len()];
2172 stream.read_blocking(buffer.binding(), &mut output)?;
2173 assert_eq!(output, source);
2174 assert!(stream.staging_pool.len() <= 8);
2175 assert!(stream.staging_pool.iter().map(|b| b.bytes).sum::<usize>() <= STAGING_LIMIT);
2176 Ok(())
2177 }
2178}
2179
2180#[cfg(test)]
2181mod dag_probe {
2182 use super::*;
2183
2184 #[test]
2188 #[ignore = "requires gfx1151"]
2189 fn independent_nodes_beat_a_serial_chain() -> Result<()> {
2190 const NODES: usize = 64;
2191 let mut stream = Stream::open()?;
2192 let buffers: Vec<Buffer> = (0..NODES)
2193 .map(|_| stream.allocate(4096))
2194 .collect::<Result<_>>()?;
2195 let mut timings = Vec::new();
2196 for chained in [true, false] {
2197 let mut graph = stream.graph()?;
2198 let mut previous: Option<Node> = None;
2199 for buffer in &buffers {
2200 let after: &[Node] = match (chained, &previous) {
2201 (true, Some(node)) => std::slice::from_ref(node),
2202 _ => &[],
2203 };
2204 previous = Some(graph.fill(after, buffer.binding(), 0x3c)?);
2205 }
2206 let mut exec = graph.finish()?;
2207 for _ in 0..3 {
2208 stream.launch(&mut exec)?;
2209 stream.synchronize()?;
2210 }
2211 let mut samples = Vec::new();
2212 for _ in 0..9 {
2213 let start = std::time::Instant::now();
2214 stream.launch(&mut exec)?;
2215 stream.synchronize()?;
2216 samples.push(start.elapsed().as_secs_f64() * 1e6);
2217 }
2218 samples.sort_by(f64::total_cmp);
2219 println!(
2220 "{:<12} {NODES} nodes: {:8.1} us total, {:6.2} us/node",
2221 if chained { "serial" } else { "independent" },
2222 samples[4],
2223 samples[4] / NODES as f64
2224 );
2225 timings.push(samples[4]);
2226 let mut seen = [0u8; 4096];
2227 stream.read_blocking(buffers[NODES - 1].binding(), &mut seen)?;
2228 assert_eq!(seen, [0x3c; 4096], "every node ran");
2229 }
2230 assert!(
2234 timings[1] * 1.2 < timings[0],
2235 "omitting edges should be materially cheaper: {timings:?}"
2236 );
2237 Ok(())
2238 }
2239}
2240
2241#[cfg(test)]
2242mod engine_tests {
2243 use super::*;
2244 use crate::execution::{ComputeEngine, CopyEngine};
2245
2246 #[test]
2247 #[ignore = "requires native compiler and gfx1151"]
2248 fn engines_order_eager_graph_replay_and_foreign_events() -> Result<()> {
2249 let device = Device::open(0)?;
2250 let compiler = crate::loom::Compiler::for_target(None, device.target())?;
2251 let artifact = compiler
2252 .module(include_str!("../tests/kernels/add_f32.loom"))
2253 .compile(&crate::loom::Specialization::new("add_f32").with_config("add.grid", "1"))?;
2254 for compute_engine in [
2255 ComputeEngine::Pm4,
2256 ComputeEngine::Aql {
2257 maximum_private_bytes: 0,
2258 },
2259 ] {
2260 for copy_engine in [CopyEngine::Compute, CopyEngine::Sdma] {
2261 let mut stream = device.stream_with_options(StreamOptions {
2262 compute_engine,
2263 copy_engine,
2264 ..Default::default()
2265 })?;
2266 let kernel = unsafe { stream.load_artifact(&artifact) }?;
2267 let mut constants = Constants::new();
2268 match kernel.layout[0].1 {
2269 4 => constants.push(64u32)?,
2270 8 => constants.push(64u64)?,
2271 _ => unreachable!(),
2272 }
2273 constants.push(1f32)?;
2274 let a = stream.allocate(256)?;
2275 let b = stream.allocate(256)?;
2276 stream.fill(a.binding(), 0)?;
2277 unsafe {
2278 stream.dispatch(&kernel, [1; 3], [256, 1, 1], &constants, &[a.binding()])?;
2279 }
2280 stream.copy(b.binding(), a.binding())?;
2281 let mut graph = stream.owned_graph()?;
2282 let add = unsafe {
2283 graph.dispatch(
2284 &[],
2285 &kernel,
2286 [1; 3],
2287 [256, 1, 1],
2288 &constants,
2289 &[b.binding()],
2290 )?
2291 };
2292 graph.copy(&[add], a.binding(), b.binding())?;
2293 let mut graph = graph.finish()?;
2294 for _ in 0..3 {
2295 stream.launch(&mut graph)?;
2296 }
2297 let event = stream.record_event()?;
2298 let mut observer = device.stream()?;
2299 observer.wait_event(&event)?;
2300 let mut bytes = [0; 256];
2301 observer.read_blocking(a.binding(), &mut bytes)?;
2302 assert!(
2303 bytes
2304 .as_chunks::<4>()
2305 .0
2306 .iter()
2307 .all(|v| f32::from_le_bytes(*v) == 4.),
2308 "{compute_engine:?} / {copy_engine:?}"
2309 );
2310 stream.synchronize()?;
2311 }
2312 }
2313 Ok(())
2314 }
2315}