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 copy_streams: [Mutex<Option<Arc<Mutex<graph::CopyStream>>>>; 3],
42 allocation_stream: Mutex<Option<crate::gpu::Stream>>,
43}
44impl Drop for RuntimeOwner {
45 fn drop(&mut self) {
46 self.core
47 .state
48 .lock()
49 .unwrap_or_else(|e| e.into_inner())
50 .shutdown = true;
51 self.core.changed.notify_all();
52 for worker in self
53 .workers
54 .get_mut()
55 .unwrap_or_else(|e| e.into_inner())
56 .drain(..)
57 {
58 if worker.thread().id() != std::thread::current().id() {
59 let _ = worker.join();
60 }
61 }
62 }
63}
64#[derive(Clone, Debug)]
66pub struct RuntimeOptions {
67 pub memory_budget: Option<crate::residency::MemoryBudget>,
71 pub gpu_index: i32,
73 pub max_submissions: usize,
76 pub graph_slots: usize,
78}
79impl Default for RuntimeOptions {
80 fn default() -> Self {
81 Self {
82 memory_budget: None,
83 gpu_index: 0,
84 max_submissions: 64,
85 graph_slots: 2,
86 }
87 }
88}
89#[derive(Clone)]
95pub struct Runtime {
96 pub(super) inner: Arc<RuntimeOwner>,
97 options: RuntimeOptions,
98}
99#[derive(Clone)]
101pub struct GpuDevice {
102 index: i32,
103 target: crate::Target,
104}
105impl GpuDevice {
106 pub fn index(&self) -> i32 {
108 self.index
109 }
110 pub fn target(&self) -> &crate::Target {
112 &self.target
113 }
114}
115#[cfg(feature = "npu")]
117#[derive(Clone)]
118pub struct NpuDevice {
119 native: crate::fabric::Device,
120 index: i32,
121 budget: Option<crate::residency::MemoryBudget>,
122}
123#[cfg(feature = "npu")]
124impl NpuDevice {
125 pub fn index(&self) -> i32 {
127 self.index
128 }
129 pub fn target(&self) -> &crate::Target {
131 self.native.target()
132 }
133 pub fn native(&self) -> &crate::fabric::Device {
135 &self.native
136 }
137 pub unsafe fn load_artifact(
142 &self,
143 artifact: &crate::loom::Artifact,
144 columns: u16,
145 contract: KernelContract,
146 ) -> Result<crate::npu::NpuKernel> {
147 unsafe {
148 crate::npu::NpuKernel::load(
149 self.native.clone(),
150 artifact,
151 columns,
152 contract,
153 self.budget.as_ref(),
154 )
155 }
156 }
157}
158impl Runtime {
159 pub fn start_trace(&self, event_capacity: usize) -> Result<()> {
161 self.inner.core.tracer.start(event_capacity)
162 }
163 pub fn finish_trace(&self) -> Option<ExecutionTrace> {
166 self.inner.core.tracer.finish()
167 }
168 pub fn owns(&self, view: &BufferView) -> bool {
170 Arc::ptr_eq(&self.inner, &view.buffer.storage.runtime)
171 }
172
173 pub fn same_domain(&self, other: &Self) -> bool {
175 Arc::ptr_eq(&self.inner, &other.inner)
176 }
177
178 pub fn memory_budget(&self) -> Option<&crate::residency::MemoryBudget> {
181 self.options.memory_budget.as_ref()
182 }
183
184 pub fn new() -> Result<Self> {
186 Self::with_options(RuntimeOptions::default())
187 }
188 pub fn with_options(options: RuntimeOptions) -> Result<Self> {
190 if options.max_submissions == 0 || options.graph_slots == 0 || options.gpu_index < 0 {
191 return Err(Error::Message(
192 "runtime capacities must be nonzero and GPU index nonnegative".into(),
193 ));
194 }
195 let core = Arc::new(Core::new(options.max_submissions));
196 let owner = Arc::new(RuntimeOwner {
197 core: core.clone(),
198 workers: Mutex::new(Vec::new()),
199 gpu_index: options.gpu_index,
200 copy_streams: std::array::from_fn(|_| Mutex::new(None)),
201 allocation_stream: Mutex::new(None),
202 });
203 for name in ["hrx-upload", "hrx-compute", "hrx-download", "hrx-npu"] {
204 let core = core.clone();
205 let worker = std::thread::Builder::new()
206 .name(name.into())
207 .spawn(move || scheduler::worker(core))?;
208 owner
209 .workers
210 .lock()
211 .unwrap_or_else(|e| e.into_inner())
212 .push(worker);
213 }
214 Ok(Self {
215 inner: owner,
216 options,
217 })
218 }
219 pub fn gpu(&self) -> Result<GpuDevice> {
221 let device = crate::gpu::Device::open(self.inner.gpu_index)?;
222 Ok(GpuDevice {
223 index: self.inner.gpu_index,
224 target: device.target().clone(),
225 })
226 }
227 #[cfg(feature = "npu")]
229 pub fn npu(&self, index: i32) -> Result<NpuDevice> {
230 if index < 0 {
231 return Err(Error::Message("NPU index must be nonnegative".into()));
232 }
233 Ok(NpuDevice {
234 native: crate::fabric::Device::open(crate::fabric::Engine::Xdna, index as usize)?,
235 index,
236 budget: self.options.memory_budget.clone(),
237 })
238 }
239 fn allocation_stream(&self) -> Result<std::sync::MutexGuard<'_, Option<crate::gpu::Stream>>> {
244 let mut stream = self
245 .inner
246 .allocation_stream
247 .lock()
248 .map_err(|_| Error::DeviceLost("allocation stream poisoned".into()))?;
249 if stream.is_none() {
250 *stream = Some(crate::gpu::Device::open(self.inner.gpu_index)?.stream()?);
251 }
252 Ok(stream)
253 }
254
255 pub fn allocate(&self, bytes: usize, placement: MemoryPlacement) -> Result<Buffer> {
257 self.allocate_inner(bytes, placement, None)
258 }
259
260 pub unsafe fn adopt_gpu_buffer(
268 &self,
269 buffer: crate::gpu::Buffer,
270 placement: MemoryPlacement,
271 ) -> Result<Buffer> {
272 let mut allocation = self.allocation_stream()?;
273 let stream = allocation.as_mut().unwrap();
274 if buffer.device_id() != stream.device_id() {
275 return Err(Error::Message("buffer belongs to another device".into()));
276 }
277 if matches!(placement, MemoryPlacement::HostVisible) || matches_npu_local(&placement) {
278 return Err(Error::Unsupported(
279 "adoption requires GpuLocal or Shared placement".into(),
280 ));
281 }
282 drop(allocation);
283 self.allocate_inner(buffer.bytes(), placement, Some(buffer))
284 }
285
286 fn allocate_inner(
287 &self,
288 bytes: usize,
289 placement: MemoryPlacement,
290 mut adopted: Option<crate::gpu::Buffer>,
291 ) -> Result<Buffer> {
292 if bytes == 0 || bytes > isize::MAX as usize {
293 return Err(Error::Message(
294 "allocation size must be in 1..=isize::MAX".into(),
295 ));
296 }
297 let mut storage = Storage {
298 _reservation: self
299 .options
300 .memory_budget
301 .as_ref()
302 .filter(|budget| {
303 !adopted
304 .as_ref()
305 .is_some_and(|buffer| buffer.charged_to(budget))
306 })
307 .map(|budget| budget.reserve(bytes))
308 .transpose()?,
309 native: None,
310 accounted: false,
311 gpu: None,
312 pointer: std::ptr::null_mut(),
313 bytes,
314 #[cfg(test)]
315 _test_memory: None,
316 shared: false,
317 host: Mutex::new(HostState::default()),
318 visibility: Mutex::new(Visibility::new()),
319 runtime: self.inner.clone(),
320 };
321 match &placement {
322 MemoryPlacement::GpuLocal => {
323 let mut allocation = self.allocation_stream()?;
324 let stream = allocation.as_mut().unwrap();
325 let buffer = if let Some(buffer) = adopted.take() {
326 buffer
327 } else {
328 stream.allocate_zeroed(bytes)?
331 }
332 .into_unpooled();
333 storage.native = Some(buffer.native.clone());
334 storage.gpu = Some(buffer);
335 storage.visibility.get_mut().unwrap().wrote(Engine::Gpu);
336 }
337 MemoryPlacement::HostVisible => {
338 let mut allocation = self.allocation_stream()?;
339 let stream = allocation.as_mut().unwrap();
340 let buffer = stream.allocate_shared(bytes)?.into_unpooled();
341 let pointer = buffer.device_ptr()?.cast::<u8>();
342 unsafe {
344 std::ptr::write_bytes(pointer, 0, bytes);
345 }
346 storage.pointer = pointer;
347 storage.native = Some(buffer.native.clone());
348 storage.gpu = Some(buffer);
349 }
350 #[cfg(feature = "npu")]
351 MemoryPlacement::Shared(device) | MemoryPlacement::NpuLocal(device) => {
352 let native = if matches!(placement, MemoryPlacement::Shared(_)) {
353 let mut allocation = self.allocation_stream()?;
354 let stream = allocation.as_mut().unwrap();
355 let gpu = if let Some(buffer) = adopted.take() {
356 buffer.share_with(device.native())?
359 } else {
360 stream.allocate_for(
361 bytes,
362 &[
363 crate::gpu::Device::open(self.inner.gpu_index)?
364 .native()
365 .clone(),
366 device.native().clone(),
367 ],
368 )?
369 };
370 let native = gpu.native.clone();
371 storage.gpu = Some(gpu);
372 storage.shared = true;
373 native
374 } else {
375 device
376 .native()
377 .fabric()
378 .allocate(bytes, std::slice::from_ref(device.native()))?
379 };
380 storage.pointer = native.host_pointer();
381 storage.native = Some(native);
382 }
383 }
384
385 use std::sync::atomic::Ordering;
386 let counters = &self.inner.core.counters;
387 counters.allocations.fetch_add(1, Ordering::Relaxed);
388 if storage.shared {
389 counters.imports.fetch_add(1, Ordering::Relaxed);
390 }
391 let live = counters
392 .live_bytes
393 .fetch_add(bytes as u64, Ordering::Relaxed)
394 + bytes as u64;
395 counters.peak_bytes.fetch_max(live, Ordering::Relaxed);
396 storage.accounted = true;
397 Ok(Buffer {
398 storage: Arc::new(storage),
399 })
400 }
401 pub fn statistics(&self) -> Statistics {
403 self.inner.core.counters.snapshot()
404 }
405
406 pub fn graph(&self) -> Graph {
408 Graph::new(self.clone())
409 }
410 pub unsafe fn load_gpu_kernel(
415 &self,
416 path: impl AsRef<std::path::Path>,
417 symbol: &str,
418 grid: [u32; 3],
419 block: [u32; 3],
420 contract: KernelContract,
421 ) -> Result<GpuKernel> {
422 let mut allocation = self.allocation_stream()?;
423 let stream = allocation.as_mut().unwrap();
424 let raw = unsafe { stream.load(path.as_ref(), symbol) }?;
425 drop(allocation);
426 unsafe { self.adopt_gpu_kernel(raw, grid, block, contract) }
427 }
428
429 pub unsafe fn adopt_gpu_kernel(
434 &self,
435 raw: crate::gpu::Kernel,
436 grid: [u32; 3],
437 block: [u32; 3],
438 contract: KernelContract,
439 ) -> Result<GpuKernel> {
440 contract.validate()?;
441 crate::runtime::validate_export_launch(raw.info(), grid, block)?;
442 let mut allocation = self.allocation_stream()?;
443 let stream = allocation.as_mut().unwrap();
444 if raw.device_id() != stream.device_id() {
445 return Err(Error::Message("kernel belongs to another device".into()));
446 }
447 if raw.info().binding_count as usize != contract.bindings.len()
448 || raw.info().constant_byte_length as usize != contract.constants.len()
449 {
450 return Err(Error::Message(
451 "GPU metadata does not match contract".into(),
452 ));
453 }
454 Ok(GpuKernel {
455 raw,
456 contract,
457 grid,
458 block,
459 runtime: self.inner.clone(),
460 })
461 }
462}
463
464fn matches_npu_local(placement: &MemoryPlacement) -> bool {
465 #[cfg(feature = "npu")]
466 {
467 matches!(placement, MemoryPlacement::NpuLocal(_))
468 }
469 #[cfg(not(feature = "npu"))]
470 {
471 let _ = placement;
472 false
473 }
474}
475
476#[cfg(test)]
477mod tests;