1mod buffer;
7mod completion;
8mod contract;
9mod dependencies;
10mod graph;
11mod handoff;
12mod native_session;
13mod scheduler;
14mod statistics;
15mod trace;
16use crate::{Error, Result};
17pub use buffer::{Buffer, BufferView, MemoryPlacement, ReadGuard, WriteGuard};
18use buffer::{Engine, HostState, Storage, Visibility};
19pub use completion::{Completion, CompletionProfile};
20pub use contract::{Access, BindingContract, KernelContract};
21pub use graph::{ExecutableGraph, GpuKernel, GpuLane, Graph, Node};
22pub use native_session::NativeSession;
23use scheduler::Core;
24pub use statistics::Statistics;
25use std::sync::{Arc, Mutex};
26pub use trace::{ExecutionTrace, TraceEvent};
27
28#[derive(Clone)]
30pub struct GpuAccess {
31 pub view: BufferView,
33 pub access: Access,
35}
36
37pub(super) struct RuntimeOwner {
38 core: Arc<Core>,
39 workers: Mutex<Vec<std::thread::JoinHandle<()>>>,
40 gpu_index: i32,
41 native_lifetime: crate::fabric::NativeLifetime,
42 copy_streams: [Mutex<Option<Arc<Mutex<graph::CopyStream>>>>; 3],
43 allocation_stream: Mutex<Option<crate::gpu::Stream>>,
44 aql_queues: [Mutex<Option<crate::fabric::AqlQueue>>; 3],
45 sdma_queues: [Mutex<Option<crate::fabric::Queue>>; 3],
46}
47impl RuntimeOwner {
48 fn device(&self) -> Result<crate::Device> {
49 crate::Device::open_with_lifetime(self.gpu_index, self.native_lifetime)
50 }
51}
52impl Drop for RuntimeOwner {
53 fn drop(&mut self) {
54 self.core
55 .state
56 .lock()
57 .unwrap_or_else(|e| e.into_inner())
58 .shutdown = true;
59 self.core.changed.notify_all();
60 for worker in self
61 .workers
62 .get_mut()
63 .unwrap_or_else(|e| e.into_inner())
64 .drain(..)
65 {
66 if worker.thread().id() != std::thread::current().id() {
67 let _ = worker.join();
68 }
69 }
70 }
71}
72#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
74pub enum CopyEngine {
75 #[default]
77 Compute,
78 Sdma,
80}
81#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
83pub enum ComputeEngine {
84 #[default]
86 Pm4,
87 Aql {
90 maximum_private_bytes: u32,
92 },
93}
94#[derive(Clone, Debug)]
96pub struct RuntimeOptions {
97 pub memory_budget: Option<crate::residency::MemoryBudget>,
101 pub gpu_index: i32,
103 pub native_lifetime: crate::fabric::NativeLifetime,
105 pub max_submissions: usize,
108 pub graph_slots: usize,
110 pub copy_engine: CopyEngine,
112 pub compute_engine: ComputeEngine,
114}
115impl Default for RuntimeOptions {
116 fn default() -> Self {
117 Self {
118 memory_budget: None,
119 gpu_index: 0,
120 native_lifetime: Default::default(),
121 max_submissions: 64,
122 graph_slots: 2,
123 copy_engine: CopyEngine::Compute,
124 compute_engine: ComputeEngine::Pm4,
125 }
126 }
127}
128#[derive(Clone)]
134pub struct Runtime {
135 pub(super) inner: Arc<RuntimeOwner>,
136 options: RuntimeOptions,
137}
138#[derive(Clone)]
140pub struct GpuDevice {
141 index: i32,
142 target: crate::Target,
143}
144impl GpuDevice {
145 pub fn index(&self) -> i32 {
147 self.index
148 }
149 pub fn target(&self) -> &crate::Target {
151 &self.target
152 }
153}
154#[cfg(feature = "npu")]
156#[derive(Clone)]
157pub struct NpuDevice {
158 native: crate::fabric::Device,
159 index: i32,
160 budget: Option<crate::residency::MemoryBudget>,
161}
162#[cfg(feature = "npu")]
163impl NpuDevice {
164 pub fn index(&self) -> i32 {
166 self.index
167 }
168 pub fn target(&self) -> &crate::Target {
170 self.native.target()
171 }
172 pub fn native(&self) -> &crate::fabric::Device {
174 &self.native
175 }
176 pub unsafe fn load_artifact(
181 &self,
182 artifact: &crate::loom::Artifact,
183 columns: u16,
184 contract: KernelContract,
185 ) -> Result<crate::npu::NpuKernel> {
186 unsafe {
187 crate::npu::NpuKernel::load(
188 self.native.clone(),
189 artifact,
190 columns,
191 contract,
192 self.budget.as_ref(),
193 )
194 }
195 }
196}
197impl Runtime {
198 pub fn start_trace(&self, event_capacity: usize) -> Result<()> {
200 self.inner.core.tracer.start(event_capacity)
201 }
202 pub fn finish_trace(&self) -> Option<ExecutionTrace> {
205 self.inner.core.tracer.finish()
206 }
207 pub fn owns(&self, view: &BufferView) -> bool {
209 Arc::ptr_eq(&self.inner, &view.buffer.storage.runtime)
210 }
211
212 pub fn same_domain(&self, other: &Self) -> bool {
214 Arc::ptr_eq(&self.inner, &other.inner)
215 }
216
217 pub fn native_lifetime(&self) -> crate::fabric::NativeLifetime {
219 self.options.native_lifetime
220 }
221
222 pub fn memory_budget(&self) -> Option<&crate::residency::MemoryBudget> {
224 self.options.memory_budget.as_ref()
225 }
226
227 pub fn new() -> Result<Self> {
229 Self::with_options(RuntimeOptions::default())
230 }
231 pub fn with_options(options: RuntimeOptions) -> Result<Self> {
233 if options.max_submissions == 0 || options.graph_slots == 0 || options.gpu_index < 0 {
234 return Err(Error::Message(
235 "runtime capacities must be nonzero and GPU index nonnegative".into(),
236 ));
237 }
238 let core = Arc::new(Core::new(options.max_submissions));
239 let owner = Arc::new(RuntimeOwner {
240 core: core.clone(),
241 workers: Mutex::new(Vec::new()),
242 gpu_index: options.gpu_index,
243 native_lifetime: options.native_lifetime,
244 copy_streams: std::array::from_fn(|_| Mutex::new(None)),
245 allocation_stream: Mutex::new(None),
246 aql_queues: std::array::from_fn(|_| Mutex::new(None)),
247 sdma_queues: std::array::from_fn(|_| Mutex::new(None)),
248 });
249 for name in ["hrx-upload", "hrx-compute", "hrx-download", "hrx-npu"] {
250 let core = core.clone();
251 let worker = std::thread::Builder::new()
252 .name(name.into())
253 .spawn(move || scheduler::worker(core))?;
254 owner
255 .workers
256 .lock()
257 .unwrap_or_else(|e| e.into_inner())
258 .push(worker);
259 }
260 Ok(Self {
261 inner: owner,
262 options,
263 })
264 }
265 pub fn gpu(&self) -> Result<GpuDevice> {
267 let device = self.inner.device()?;
268 Ok(GpuDevice {
269 index: self.inner.gpu_index,
270 target: device.target().clone(),
271 })
272 }
273 #[cfg(feature = "npu")]
275 pub fn npu(&self, index: i32) -> Result<NpuDevice> {
276 if index < 0 {
277 return Err(Error::Message("NPU index must be nonnegative".into()));
278 }
279 Ok(NpuDevice {
280 native: crate::fabric::Device::open_with_lifetime(
281 crate::fabric::Engine::Xdna,
282 index as usize,
283 self.options.native_lifetime,
284 )?,
285 index,
286 budget: self.options.memory_budget.clone(),
287 })
288 }
289 fn allocation_stream(&self) -> Result<std::sync::MutexGuard<'_, Option<crate::gpu::Stream>>> {
294 let mut stream = self
295 .inner
296 .allocation_stream
297 .lock()
298 .map_err(|_| Error::DeviceLost("allocation stream poisoned".into()))?;
299 if stream.is_none() {
300 *stream = Some(self.inner.device()?.stream()?);
301 }
302 Ok(stream)
303 }
304
305 pub fn allocate(&self, bytes: usize, placement: MemoryPlacement) -> Result<Buffer> {
307 self.allocate_inner(bytes, placement, None)
308 }
309
310 pub unsafe fn adopt_gpu_buffer(
318 &self,
319 buffer: crate::gpu::Buffer,
320 placement: MemoryPlacement,
321 ) -> Result<Buffer> {
322 let mut allocation = self.allocation_stream()?;
323 let stream = allocation.as_mut().unwrap();
324 if buffer.device_id() != stream.device_id() {
325 return Err(Error::Message("buffer belongs to another device".into()));
326 }
327 if matches!(placement, MemoryPlacement::HostVisible) || matches_npu_local(&placement) {
328 return Err(Error::Unsupported(
329 "adoption requires GpuLocal or Shared placement".into(),
330 ));
331 }
332 drop(allocation);
333 self.allocate_inner(buffer.bytes(), placement, Some(buffer))
334 }
335
336 fn allocate_inner(
337 &self,
338 bytes: usize,
339 placement: MemoryPlacement,
340 mut adopted: Option<crate::gpu::Buffer>,
341 ) -> Result<Buffer> {
342 if bytes == 0 || bytes > isize::MAX as usize {
343 return Err(Error::Message(
344 "allocation size must be in 1..=isize::MAX".into(),
345 ));
346 }
347 let mut storage = Storage {
348 _reservation: self
349 .options
350 .memory_budget
351 .as_ref()
352 .filter(|budget| {
353 !adopted
354 .as_ref()
355 .is_some_and(|buffer| buffer.charged_to(budget))
356 })
357 .map(|budget| budget.reserve(bytes))
358 .transpose()?,
359 native: None,
360 accounted: false,
361 gpu: None,
362 pointer: std::ptr::null_mut(),
363 bytes,
364 #[cfg(test)]
365 _test_memory: None,
366 shared: false,
367 host: Mutex::new(HostState::default()),
368 visibility: Mutex::new(Visibility::new()),
369 runtime: self.inner.clone(),
370 };
371 match &placement {
372 MemoryPlacement::GpuLocal => {
373 let mut allocation = self.allocation_stream()?;
374 let stream = allocation.as_mut().unwrap();
375 let buffer = if let Some(buffer) = adopted.take() {
376 buffer
377 } else {
378 stream.allocate_zeroed(bytes)?
381 }
382 .into_unpooled();
383 storage.native = Some(buffer.native.clone());
384 storage.gpu = Some(buffer);
385 storage.visibility.get_mut().unwrap().wrote(Engine::Gpu);
386 }
387 MemoryPlacement::HostVisible => {
388 let mut allocation = self.allocation_stream()?;
389 let stream = allocation.as_mut().unwrap();
390 let buffer = stream.allocate_shared(bytes)?.into_unpooled();
391 let pointer = buffer.device_ptr()?.cast::<u8>();
392 unsafe {
394 std::ptr::write_bytes(pointer, 0, bytes);
395 }
396 storage.pointer = pointer;
397 storage.native = Some(buffer.native.clone());
398 storage.gpu = Some(buffer);
399 }
400 #[cfg(feature = "npu")]
401 MemoryPlacement::Shared(device) | MemoryPlacement::NpuLocal(device) => {
402 let native = if matches!(placement, MemoryPlacement::Shared(_)) {
403 let mut allocation = self.allocation_stream()?;
404 let stream = allocation.as_mut().unwrap();
405 let gpu = if let Some(buffer) = adopted.take() {
406 buffer.share_with(device.native())?
409 } else {
410 stream.allocate_for(
411 bytes,
412 &[
413 self.inner.device()?.native().clone(),
414 device.native().clone(),
415 ],
416 )?
417 };
418 let native = gpu.native.clone();
419 storage.gpu = Some(gpu);
420 storage.shared = true;
421 native
422 } else {
423 device
424 .native()
425 .fabric()
426 .allocate(bytes, std::slice::from_ref(device.native()))?
427 };
428 storage.pointer = native.host_pointer();
429 storage.native = Some(native);
430 }
431 }
432
433 use std::sync::atomic::Ordering;
434 let counters = &self.inner.core.counters;
435 counters.allocations.fetch_add(1, Ordering::Relaxed);
436 if storage.shared {
437 counters.imports.fetch_add(1, Ordering::Relaxed);
438 }
439 let live = counters
440 .live_bytes
441 .fetch_add(bytes as u64, Ordering::Relaxed)
442 + bytes as u64;
443 counters.peak_bytes.fetch_max(live, Ordering::Relaxed);
444 storage.accounted = true;
445 Ok(Buffer {
446 storage: Arc::new(storage),
447 })
448 }
449 pub fn stream(
452 &self,
453 sanitizer: Option<crate::fabric::SanitizerRuntimeOptions>,
454 ) -> Result<crate::Stream> {
455 self.inner
456 .device()?
457 .stream_with_options(crate::StreamOptions {
458 compute_engine: self.options.compute_engine,
459 copy_engine: self.options.copy_engine,
460 memory_budget: self.options.memory_budget.clone(),
461 sanitizer,
462 })
463 }
464
465 pub fn statistics(&self) -> Statistics {
467 self.inner.core.counters.snapshot()
468 }
469
470 pub fn graph(&self) -> Graph {
472 Graph::new(self.clone())
473 }
474 pub unsafe fn load_gpu_kernel(
479 &self,
480 path: impl AsRef<std::path::Path>,
481 symbol: &str,
482 grid: [u32; 3],
483 block: [u32; 3],
484 contract: KernelContract,
485 ) -> Result<GpuKernel> {
486 let mut allocation = self.allocation_stream()?;
487 let stream = allocation.as_mut().unwrap();
488 let raw = unsafe { stream.load(path.as_ref(), symbol) }?;
489 drop(allocation);
490 unsafe { self.adopt_gpu_kernel(raw, grid, block, contract) }
491 }
492
493 pub unsafe fn load_gpu_artifact(
498 &self,
499 artifact: &crate::loom::Artifact,
500 workload_argument_bits: &[u64],
501 contract: KernelContract,
502 ) -> Result<GpuKernel> {
503 let mut allocation = self.allocation_stream()?;
504 let stream = allocation.as_mut().unwrap();
505 let (raw, launch) =
506 unsafe { stream.load_artifact_for_workload(artifact, workload_argument_bits) }?;
507 drop(allocation);
508 unsafe {
509 self.adopt_gpu_kernel(raw, launch.workgroup_count, launch.workgroup_size, contract)
510 }
511 }
512
513 pub unsafe fn load_sanitized_gpu_artifact(
524 &self,
525 artifact: &crate::loom::Artifact,
526 workload_argument_bits: &[u64],
527 contract: KernelContract,
528 report_capacity_bytes: usize,
529 ) -> Result<GpuKernel> {
530 if !matches!(self.options.compute_engine, ComputeEngine::Aql { .. }) {
531 return Err(Error::Unsupported(
532 "sanitizer reports require the AQL compute engine".into(),
533 ));
534 }
535 let mut allocation = self.allocation_stream()?;
536 let stream = allocation.as_mut().unwrap();
537 let raw = unsafe {
538 stream.load_sanitized_artifact(
539 artifact,
540 &crate::fabric::SanitizerRuntimeOptions {
541 capacity_bytes: report_capacity_bytes,
542 memory_budget: self.options.memory_budget.clone(),
543 ..Default::default()
544 },
545 )
546 }?;
547 let launch = raw.launch_config(workload_argument_bits)?;
548 drop(allocation);
549 unsafe {
550 self.adopt_gpu_kernel(raw, launch.workgroup_count, launch.workgroup_size, contract)
551 }
552 }
553
554 pub unsafe fn adopt_gpu_kernel(
559 &self,
560 raw: crate::gpu::Kernel,
561 grid: [u32; 3],
562 block: [u32; 3],
563 contract: KernelContract,
564 ) -> Result<GpuKernel> {
565 contract.validate()?;
566 crate::runtime::validate_export_launch(raw.info(), grid, block)?;
567 let mut allocation = self.allocation_stream()?;
568 let stream = allocation.as_mut().unwrap();
569 if raw.device_id() != stream.device_id() {
570 return Err(Error::Message("kernel belongs to another device".into()));
571 }
572 if raw.info().binding_count as usize != contract.bindings.len()
573 || raw.info().constant_byte_length as usize != contract.constants.len()
574 {
575 return Err(Error::Message(
576 "GPU metadata does not match contract".into(),
577 ));
578 }
579 Ok(GpuKernel {
580 raw,
581 contract,
582 grid,
583 block,
584 runtime: self.inner.clone(),
585 })
586 }
587}
588
589fn matches_npu_local(placement: &MemoryPlacement) -> bool {
590 #[cfg(feature = "npu")]
591 {
592 matches!(placement, MemoryPlacement::NpuLocal(_))
593 }
594 #[cfg(not(feature = "npu"))]
595 {
596 let _ = placement;
597 false
598 }
599}
600
601#[cfg(test)]
602mod tests;