pub struct Stream { /* private fields */ }Expand description
An ordered command stream. Mutating operations require exclusive access. Prepared kernels and allocations can be reused without a compiler/cache lookup.
Implementations§
Source§impl Stream
impl Stream
Sourcepub fn access_graph(&self) -> Result<AccessGraph<'_>>
pub fn access_graph(&self) -> Result<AccessGraph<'_>>
Begin an access-aware graph recording on this stream.
Source§impl Stream
impl Stream
Sourcepub fn with_memory_budget(self, budget: MemoryBudget) -> Self
pub fn with_memory_budget(self, budget: MemoryBudget) -> Self
Charge subsequent allocations (including native upload/readback staging) against a shared residency ceiling. Existing allocations are unchanged. Charges remain with buffers, queued uses and recorded graphs, and leak with quarantined native storage after an uncertain failure. The ceiling covers requested extents, not native allocator rounding or code objects.
Sourcepub fn memory_budget(&self) -> Option<&MemoryBudget>
pub fn memory_budget(&self) -> Option<&MemoryBudget>
Allocation ceiling for this stream, including transfer staging.
Sourcepub fn record_event(&mut self) -> Result<Event>
pub fn record_event(&mut self) -> Result<Event>
Submit preceding work and record a single immutable completion event. Recording does not wait on the host and does not reclaim upload staging.
Sourcepub fn wait_event(&mut self, event: &Event) -> Result<()>
pub fn wait_event(&mut self, event: &Event) -> Result<()>
Queue a device-side dependency before subsequent work on this stream. The event must come from the same device. This call does not wait on the host.
Sourcepub fn open() -> Result<Self>
pub fn open() -> Result<Self>
Open an ordered stream on device zero.
Examples found in repository?
4fn main() -> hrx::Result<()> {
5 let runtime = Runtime::new()?;
6 let buffer = runtime.allocate(4096, MemoryPlacement::GpuLocal)?;
7 let stream = Stream::open()?;
8 let raw = stream.allocate(4096)?;
9 for count in [128, 512, 1024, 2048] {
10 let begin = Instant::now();
11 let mut graph = runtime.graph();
12 for _ in 0..count {
13 graph.fill(buffer.view(), 0)?;
14 }
15 let graph = graph.prepare()?;
16 let inferred = begin.elapsed();
17 let begin = Instant::now();
18 let mut native = stream.graph()?;
19 let mut last = None;
20 for _ in 0..count {
21 last = Some(native.fill(last.as_slice(), raw.binding(), 0)?);
22 }
23 let _native = native.finish()?;
24 println!(
25 "nodes={count} inferred_us={} explicit_us={}",
26 inferred.as_micros(),
27 begin.elapsed().as_micros()
28 );
29 graph.submit()?.wait()?;
30 }
31 Ok(())
32}More examples
21fn gpu_kernel(
22 runtime: &Runtime,
23 source: &str,
24 symbol: &str,
25 config: &[(&str, String)],
26 elements: usize,
27 bindings: Vec<BindingContract>,
28) -> Result<GpuKernel> {
29 let compiler = hrx::loom::Compiler::for_target(None, runtime.gpu()?.target())?;
30 let module = compiler.module(source);
31 let mut spec = hrx::loom::Specialization::new(symbol);
32 for (key, value) in config {
33 spec.set_config(*key, value.clone());
34 }
35 let artifact = module.compile(&spec)?;
36 let stream = hrx::gpu::Stream::open()?;
37 let raw = unsafe { stream.load_artifact(&artifact) }?;
38 let mut constants = hrx::gpu::Constants::new();
39 match raw.info().constant_byte_length {
40 8 => constants.push(elements as u32)?,
41 12 => constants.push(elements as u64)?,
42 _ => return Err(hrx::Error::Message("unexpected scalar ABI".into())),
43 }
44 constants.push(1f32)?;
45 unsafe {
46 runtime.load_gpu_kernel(
47 artifact.path(),
48 symbol,
49 [elements.div_ceil(256) as u32, 1, 1],
50 [256, 1, 1],
51 KernelContract {
52 bindings,
53 constants: constants.as_bytes().to_vec(),
54 },
55 )
56 }
57}16fn main() -> Result<()> {
17 let mib: usize = match std::env::args().collect::<Vec<_>>().as_slice() {
18 [_] => 16,
19 [_, flag, value] if flag == "--mib" => value.parse().expect("--mib expects an integer"),
20 _ => {
21 eprintln!("usage: import_probe [--mib N]");
22 std::process::exit(2);
23 }
24 };
25 let bytes = mib * 1024 * 1024;
26 let layout = Layout::from_size_align(bytes, PAGE).expect("page-aligned layout");
27
28 // Owned for the whole scope below; the imported buffer never frees it.
29 let host = unsafe { alloc_zeroed(layout) };
30 assert!(!host.is_null(), "host allocation failed");
31 let result = (|| -> Result<()> {
32 let mut stream = Stream::open()?;
33 let buffer = match unsafe { stream.import_host(host.cast(), bytes) } {
34 Ok(buffer) => buffer,
35 Err(error) => {
36 println!("import: UNSUPPORTED ({error})");
37 return Ok(());
38 }
39 };
40 println!("import: ok ({mib} MiB at {host:p})");
41
42 // Prove the host pages start clear, so a match afterwards can only be the GPU.
43 let before = unsafe { std::slice::from_raw_parts(host, bytes) };
44 assert!(before.iter().all(|&b| b == 0), "host memory was not zeroed");
45
46 stream.fill(buffer.binding(), PATTERN)?;
47 stream.synchronize()?;
48
49 // Read the ORIGINAL host pointer -- no download, no staging buffer.
50 let after = unsafe { std::slice::from_raw_parts(host, bytes) };
51 let wrong = after.iter().filter(|&&b| b != PATTERN).count();
52 if wrong == 0 {
53 println!("gpu fill visible on host pointer: YES (zero-copy confirmed)");
54 } else {
55 println!("gpu fill visible on host pointer: NO ({wrong}/{bytes} bytes unwritten)");
56 }
57
58 // A device-side read of the same buffer must also see host writes.
59 let mirror = stream.allocate(bytes)?;
60 unsafe { std::slice::from_raw_parts_mut(host, bytes) }.fill(0x3c);
61 stream.copy(mirror.binding(), buffer.binding())?;
62 let mut check = [0u8; 64];
63 stream.read_blocking(mirror.binding().slice(0, check.len())?, &mut check)?;
64 let coherent = check.iter().all(|&b| b == 0x3c);
65 println!(
66 "host write visible to device: {}",
67 if coherent { "YES" } else { "NO" }
68 );
69 Ok(())
70 })();
71 unsafe { dealloc(host, layout) };
72 result
73}88fn main() -> Result<()> {
89 let options = match parse() {
90 Ok(options) => options,
91 Err(message) => {
92 eprintln!("gpu_load: {message}");
93 std::process::exit(2);
94 }
95 };
96 let size = options.mib * 1024 * 1024;
97 let mut stream = Stream::open()?;
98 let source = stream.allocate(size)?;
99 let destination = stream.allocate(size)?;
100 // Touch both allocations so the steady-state loop never pays first-use faults.
101 stream.fill(source.binding(), 0x3c)?;
102 stream.fill(destination.binding(), 0)?;
103 stream.synchronize()?;
104
105 let run_batch = |stream: &mut Stream| -> Result<()> {
106 for _ in 0..options.batch {
107 match options.mode {
108 Mode::Copy => stream.copy(destination.binding(), source.binding())?,
109 Mode::Fill => stream.fill(destination.binding(), 0x5a)?,
110 }
111 }
112 stream.synchronize()
113 };
114
115 // Warm up outside the measured window: the first batch pays queue and paging costs.
116 run_batch(&mut stream)?;
117
118 let window = Duration::from_secs_f64(options.seconds);
119 let start = Instant::now();
120 let mut iterations = 0usize;
121 // Per-batch rates, so the harness can see whether contention was steady or bursty.
122 let mut batch_rates = Vec::new();
123 while start.elapsed() < window {
124 let batch_start = Instant::now();
125 run_batch(&mut stream)?;
126 let seconds = batch_start.elapsed().as_secs_f64();
127 iterations += options.batch;
128 let bytes = options.mode.traffic(size) * options.batch;
129 batch_rates.push(bytes as f64 / seconds / 1e9);
130 }
131 let elapsed = start.elapsed().as_secs_f64();
132 let total = options.mode.traffic(size) as f64 * iterations as f64;
133 let mean = total / elapsed / 1e9;
134 batch_rates.sort_by(f64::total_cmp);
135 let median = batch_rates[batch_rates.len() / 2];
136 let low = batch_rates[batch_rates.len() / 20];
137
138 println!(
139 concat!(
140 r#"{{"device":"gpu","mode":"{}","buffer_mib":{},"batch":{},"#,
141 r#""seconds":{:.4},"iterations":{},"gb_s_mean":{:.3},"#,
142 r#""gb_s_median":{:.3},"gb_s_p5":{:.3}}}"#
143 ),
144 options.mode.name(),
145 options.mib,
146 options.batch,
147 elapsed,
148 iterations,
149 mean,
150 median,
151 low
152 );
153 eprintln!(
154 "gpu_load: {} {:.1} GB/s mean over {:.1}s ({} iterations)",
155 options.mode.name(),
156 mean,
157 elapsed,
158 iterations
159 );
160 Ok(())
161}81fn main() -> Result<()> {
82 let args: Vec<String> = std::env::args().skip(1).collect();
83 let (xclbin, mib) = match args.as_slice() {
84 [xclbin] => (xclbin.clone(), 4usize),
85 [xclbin, flag, value] if flag == "--mib" => (
86 xclbin.clone(),
87 value.parse().expect("--mib expects an integer"),
88 ),
89 _ => {
90 eprintln!("usage: unified_probe <xclbin> [--mib N]");
91 std::process::exit(2);
92 }
93 };
94 let bytes = mib * 1024 * 1024;
95 let layout = Layout::from_size_align(bytes, PAGE).expect("page-aligned layout");
96 let host = unsafe { alloc_zeroed(layout) };
97 assert!(!host.is_null(), "host allocation failed");
98
99 let result = (|| -> Result<()> {
100 println!("host allocation: {mib} MiB at {host:p}");
101
102 let context = match unsafe { dvxrt::Context::new(0, &xclbin) } {
103 Ok(context) => context,
104 Err(error) => {
105 println!("npu context: UNAVAILABLE ({error})");
106 return Ok(());
107 }
108 };
109 let group = context.group_id(ARG_A).expect("group id for A");
110
111 // 1. Can amdxdna pin pages we allocated? Reported either way -- if it can, either
112 // runtime may own the allocation; if it cannot, only the NPU's allocator may.
113 match unsafe { context.import_bo(host.cast(), bytes, dvxrt::BoKind::HostOnly, group) } {
114 Ok(_) => println!("npu userptr import (host-owned pages): YES"),
115 Err(error) => println!("npu userptr import (host-owned pages): NO ({error})"),
116 }
117
118 // 2. The direction that matters: let the NPU's allocator own the pages -- it is the
119 // more constrained of the two -- and import its host mapping into the GPU runtime.
120 let bo = context
121 .alloc_bo(bytes, dvxrt::BoKind::HostOnly, group)
122 .expect("allocate an NPU BO");
123 let shared = bo.map().expect("map the NPU BO");
124 println!("npu allocation mapped at {shared:p}");
125 assert_eq!(shared as usize % PAGE, 0, "NPU mapping is not page-aligned");
126
127 let mut stream = Stream::open()?;
128 let imported = unsafe { stream.import_host(shared.cast(), bytes) };
129 match &imported {
130 Ok(_) => println!("gpu import of the NPU mapping: YES"),
131 Err(error) => println!("gpu import of the NPU mapping: NO ({error})"),
132 }
133
134 // 3. If the shared mapping took, check that a GPU write lands in the NPU's BO.
135 if let Ok(buffer) = &imported {
136 stream.fill(buffer.binding(), GPU_PATTERN)?;
137 stream.synchronize()?;
138 bo.sync(false, bytes).expect("invalidate for device reads");
139 let mut seen = vec![0u8; 4096];
140 bo.read(&mut seen).expect("read through the NPU BO");
141 let wrong = seen.iter().filter(|&&b| b != GPU_PATTERN).count();
142 println!(
143 "gpu write -> npu read: {}",
144 if wrong == 0 {
145 "YES".into()
146 } else {
147 format!("NO ({wrong} bytes differ)")
148 }
149 );
150 }
151
152 // 3b. The dma-buf route: the GPU allocates, HSA exports the allocation as a
153 // dma-buf, and XRT imports that descriptor. Neither runtime copies, and unlike
154 // the host-pointer routes above neither driver has to pin the other's pages.
155 // The GPU-side import above HSA-locked these pages, so they are exportable.
156 let locked = unsafe { stream.import_host(host.cast(), bytes) };
157 let export = match &locked {
158 Ok(buffer) => {
159 stream.fill(buffer.binding(), GPU_PATTERN)?;
160 stream.synchronize()?;
161 export_dmabuf(host.cast(), bytes)
162 }
163 Err(error) => Err(format!("host pages are not GPU-resident: {error}")),
164 };
165 match export {
166 Ok((fd, size)) => {
167 println!("hsa dma-buf export of a GPU buffer: YES (fd {fd})");
168 match unsafe { context.import_dmabuf(fd, size) } {
169 Ok(imported) => {
170 println!("npu import of the GPU dma-buf: YES");
171 let mut seen = vec![0u8; 4096];
172 match imported.read(&mut seen) {
173 Ok(()) => {
174 let wrong = seen.iter().filter(|&&b| b != GPU_PATTERN).count();
175 println!(
176 " gpu write -> npu read over dma-buf: {}",
177 if wrong == 0 {
178 "YES".into()
179 } else {
180 format!("NO ({wrong} bytes differ)")
181 }
182 );
183 }
184 Err(error) => {
185 println!(" read through the imported BO failed: {error}")
186 }
187 }
188 }
189 Err(error) => println!("npu import of the GPU dma-buf: NO ({error})"),
190 }
191 }
192 Err(error) => println!("hsa dma-buf export of a GPU buffer: NO ({error})"),
193 }
194
195 // 4. And the reverse: the NPU's BO writes, the GPU reads.
196 if let Ok(buffer) = &imported {
197 bo.write(&vec![NPU_PATTERN; bytes])
198 .expect("write through the NPU BO");
199 let mirror = stream.allocate(bytes)?;
200 stream.copy(mirror.binding(), buffer.binding())?;
201 let mut check = [0u8; 64];
202 stream.read_blocking(mirror.binding().slice(0, check.len())?, &mut check)?;
203 let back = check.iter().all(|&b| b == NPU_PATTERN);
204 println!("npu write -> gpu read: {}", if back { "YES" } else { "NO" });
205 }
206 Ok(())
207 })();
208 unsafe { dealloc(host, layout) };
209 result
210}30fn main() -> Result<()> {
31 let mut stream = Stream::open()?;
32 let mut metrics = BTreeMap::new();
33 let samples = std::env::var("HRX_BENCH_SAMPLES")
34 .ok()
35 .and_then(|value| value.parse::<usize>().ok())
36 .filter(|&value| value > 0)
37 .unwrap_or(9);
38 let (ns, _) = measure(&mut stream, samples, 50_000, |stream| {
39 for _ in 0..50_000 {
40 let buffer = stream.scratch(1024 * 1024)?;
41 black_box(&buffer);
42 stream.recycle(buffer)?;
43 }
44 Ok(())
45 })?;
46 metrics.insert("scratch_reuse_ns", ns);
47 let (ns, _) = measure(&mut stream, samples, 2_000, |stream| {
48 for _ in 0..2_000 {
49 black_box(stream.allocate(1024 * 1024)?);
50 }
51 Ok(())
52 })?;
53 metrics.insert("allocate_drop_ns", ns);
54
55 let bytes = 256 * 1024 * 1024;
56 let chunk = vec![0x3cu8; 4 * 1024 * 1024];
57 let weights = stream.allocate(bytes)?;
58 let (_, ns) = measure(&mut stream, samples, 1, |stream| {
59 for offset in (0..bytes).step_by(chunk.len()) {
60 stream.upload(weights.binding().slice(offset, chunk.len())?, &chunk)?;
61 }
62 Ok(())
63 })?;
64 metrics.insert(
65 "queued_upload_gib_s",
66 bytes as f64 / (1024f64.powi(3) * ns * 1e-9),
67 );
68 let mut check = [0u8; 16];
69 stream.read_blocking(
70 weights.try_slice(bytes - check.len(), check.len())?,
71 &mut check,
72 )?;
73 assert_eq!(check, [0x3c; 16]);
74 drop(weights);
75
76 let compiler = hrx::loom::Compiler::for_stream(None, &stream)?;
77 let module = compiler.module(include_str!("../tests/kernels/euler.loom"));
78 let mut spec = hrx::loom::Specialization::new("krea2_euler");
79 spec.set_config("krea2.euler.grid_x", "1");
80 spec.set_config("krea2.euler.grid_y", "1");
81 let artifact = module.compile(&spec)?;
82 // This repository owns the source; use its exact dimensions and scalar layout.
83 let kernel = unsafe { stream.load_artifact(&artifact)? };
84 let sample = stream.allocate(512)?;
85 let velocity = stream.allocate(512)?;
86 let ones: Vec<u8> = (0..256).flat_map(|_| 0x3f80u16.to_le_bytes()).collect();
87 stream.upload_blocking(sample.binding(), &ones)?;
88 stream.upload_blocking(velocity.binding(), &ones)?;
89 let mut constants = Constants::new();
90 match kernel.info().constant_byte_length {
91 8 => constants.push(256u32)?,
92 12 => constants.push(256u64)?,
93 n => return Err(hrx::Error::Message(format!("unexpected constant size {n}"))),
94 }
95 constants.push(0f32)?;
96 let bindings = [sample.binding(), velocity.binding()];
97 let (enqueue, complete) = measure(&mut stream, samples, 2048, |stream| {
98 for _ in 0..2048 {
99 // The source accesses exactly the 256 bf16 elements in each buffer.
100 unsafe {
101 stream.dispatch(&kernel, [1; 3], [256, 1, 1], &constants, &bindings)?;
102 }
103 }
104 Ok(())
105 })?;
106 metrics.insert("dispatch_enqueue_ns", enqueue);
107 metrics.insert("dispatch_complete_ns", complete);
108
109 // Use disjoint allocations in both graphs. Even an idempotent store is a
110 // write: concurrent Euler dispatches cannot share their in/out buffer.
111 let mut graph_buffers = Vec::new();
112 for _ in 0..32 {
113 let sample = stream.allocate(512)?;
114 let velocity = stream.allocate(512)?;
115 stream.upload(sample.binding(), &ones)?;
116 stream.upload(velocity.binding(), &ones)?;
117 graph_buffers.push((sample, velocity));
118 }
119 stream.synchronize()?;
120 let mut builder = stream.graph()?;
121 let mut previous = None;
122 for (sample, velocity) in &graph_buffers {
123 let after = previous.as_slice();
124 previous = Some(unsafe {
125 builder.dispatch(
126 after,
127 &kernel,
128 [1; 3],
129 [256, 1, 1],
130 &constants,
131 &[sample.binding(), velocity.binding()],
132 )?
133 });
134 }
135 let chained = builder.finish()?;
136
137 // The same disjoint workloads, this time allowing concurrent scheduling.
138 let mut builder = stream.graph()?;
139 for (sample, velocity) in &graph_buffers {
140 unsafe {
141 builder.dispatch(
142 &[],
143 &kernel,
144 [1; 3],
145 [256, 1, 1],
146 &constants,
147 &[sample.binding(), velocity.binding()],
148 )?;
149 }
150 }
151 let independent = builder.finish()?;
152 let mut graphs = [chained, independent];
153 let mut enqueue = [Vec::new(), Vec::new()];
154 let mut complete = [Vec::new(), Vec::new()];
155 for round in 0..samples + 3 {
156 for arm in if round % 2 == 0 { [0, 1] } else { [1, 0] } {
157 stream.synchronize()?;
158 let start = Instant::now();
159 for _ in 0..256 {
160 stream.launch(&mut graphs[arm])?;
161 }
162 let submitted = start.elapsed();
163 stream.synchronize()?;
164 let elapsed = start.elapsed();
165 if round >= 3 {
166 enqueue[arm].push(submitted.as_secs_f64() * 1e9 / 256.0);
167 complete[arm].push(elapsed.as_secs_f64() * 1e9 / (256.0 * 32.0));
168 }
169 }
170 }
171 for values in enqueue.iter_mut().chain(complete.iter_mut()) {
172 values.sort_by(f64::total_cmp);
173 }
174 let ns = complete[0][samples / 2];
175 let ns_free = complete[1][samples / 2];
176 metrics.insert("graph_enqueue_ns_per_replay", enqueue[0][samples / 2]);
177 metrics.insert("graph_complete_ns_per_kernel", ns);
178 metrics.insert("graph_independent_ns_per_kernel", ns_free);
179 metrics.insert("graph_scheduling_difference_ns_per_kernel", ns - ns_free);
180 for (sample, _) in &graph_buffers {
181 let mut output = vec![0; 512];
182 stream.read_blocking(sample.binding(), &mut output)?;
183 assert_eq!(output, ones);
184 }
185 let mut output = vec![0; 512];
186 stream.read_blocking(sample.binding(), &mut output)?;
187 assert_eq!(output, ones);
188 println!("{}", serde_json::to_string(&metrics)?);
189 Ok(())
190}Sourcepub fn device_id(&self) -> usize
pub fn device_id(&self) -> usize
Identifies the device this stream runs on, for callers that keep per-device state. Kernels are device-scoped, so a cache of loaded executables is only valid for the device that loaded them.
Sourcepub fn id(&self) -> usize
pub fn id(&self) -> usize
Identifies this stream, for callers that keep per-stream state.
Buffers are device-scoped, so nothing here rejects one used on a sibling stream — correct, because events can order that. What events cannot fix is reuse: a pool handing a buffer out again relies on the queue that used it last running in order, which holds within a stream and not across them. A caller pooling allocations needs to say which stream a block came from, and this is how. Unique among live streams; a value may repeat once its stream is dropped.
Sourcepub fn allocate(&self, bytes: usize) -> Result<Buffer>
pub fn allocate(&self, bytes: usize) -> Result<Buffer>
Allocate storage owned by this stream; zero bytes is rounded to one.
Examples found in repository?
4fn main() -> hrx::Result<()> {
5 let runtime = Runtime::new()?;
6 let buffer = runtime.allocate(4096, MemoryPlacement::GpuLocal)?;
7 let stream = Stream::open()?;
8 let raw = stream.allocate(4096)?;
9 for count in [128, 512, 1024, 2048] {
10 let begin = Instant::now();
11 let mut graph = runtime.graph();
12 for _ in 0..count {
13 graph.fill(buffer.view(), 0)?;
14 }
15 let graph = graph.prepare()?;
16 let inferred = begin.elapsed();
17 let begin = Instant::now();
18 let mut native = stream.graph()?;
19 let mut last = None;
20 for _ in 0..count {
21 last = Some(native.fill(last.as_slice(), raw.binding(), 0)?);
22 }
23 let _native = native.finish()?;
24 println!(
25 "nodes={count} inferred_us={} explicit_us={}",
26 inferred.as_micros(),
27 begin.elapsed().as_micros()
28 );
29 graph.submit()?.wait()?;
30 }
31 Ok(())
32}More examples
16fn main() -> Result<()> {
17 let mib: usize = match std::env::args().collect::<Vec<_>>().as_slice() {
18 [_] => 16,
19 [_, flag, value] if flag == "--mib" => value.parse().expect("--mib expects an integer"),
20 _ => {
21 eprintln!("usage: import_probe [--mib N]");
22 std::process::exit(2);
23 }
24 };
25 let bytes = mib * 1024 * 1024;
26 let layout = Layout::from_size_align(bytes, PAGE).expect("page-aligned layout");
27
28 // Owned for the whole scope below; the imported buffer never frees it.
29 let host = unsafe { alloc_zeroed(layout) };
30 assert!(!host.is_null(), "host allocation failed");
31 let result = (|| -> Result<()> {
32 let mut stream = Stream::open()?;
33 let buffer = match unsafe { stream.import_host(host.cast(), bytes) } {
34 Ok(buffer) => buffer,
35 Err(error) => {
36 println!("import: UNSUPPORTED ({error})");
37 return Ok(());
38 }
39 };
40 println!("import: ok ({mib} MiB at {host:p})");
41
42 // Prove the host pages start clear, so a match afterwards can only be the GPU.
43 let before = unsafe { std::slice::from_raw_parts(host, bytes) };
44 assert!(before.iter().all(|&b| b == 0), "host memory was not zeroed");
45
46 stream.fill(buffer.binding(), PATTERN)?;
47 stream.synchronize()?;
48
49 // Read the ORIGINAL host pointer -- no download, no staging buffer.
50 let after = unsafe { std::slice::from_raw_parts(host, bytes) };
51 let wrong = after.iter().filter(|&&b| b != PATTERN).count();
52 if wrong == 0 {
53 println!("gpu fill visible on host pointer: YES (zero-copy confirmed)");
54 } else {
55 println!("gpu fill visible on host pointer: NO ({wrong}/{bytes} bytes unwritten)");
56 }
57
58 // A device-side read of the same buffer must also see host writes.
59 let mirror = stream.allocate(bytes)?;
60 unsafe { std::slice::from_raw_parts_mut(host, bytes) }.fill(0x3c);
61 stream.copy(mirror.binding(), buffer.binding())?;
62 let mut check = [0u8; 64];
63 stream.read_blocking(mirror.binding().slice(0, check.len())?, &mut check)?;
64 let coherent = check.iter().all(|&b| b == 0x3c);
65 println!(
66 "host write visible to device: {}",
67 if coherent { "YES" } else { "NO" }
68 );
69 Ok(())
70 })();
71 unsafe { dealloc(host, layout) };
72 result
73}88fn main() -> Result<()> {
89 let options = match parse() {
90 Ok(options) => options,
91 Err(message) => {
92 eprintln!("gpu_load: {message}");
93 std::process::exit(2);
94 }
95 };
96 let size = options.mib * 1024 * 1024;
97 let mut stream = Stream::open()?;
98 let source = stream.allocate(size)?;
99 let destination = stream.allocate(size)?;
100 // Touch both allocations so the steady-state loop never pays first-use faults.
101 stream.fill(source.binding(), 0x3c)?;
102 stream.fill(destination.binding(), 0)?;
103 stream.synchronize()?;
104
105 let run_batch = |stream: &mut Stream| -> Result<()> {
106 for _ in 0..options.batch {
107 match options.mode {
108 Mode::Copy => stream.copy(destination.binding(), source.binding())?,
109 Mode::Fill => stream.fill(destination.binding(), 0x5a)?,
110 }
111 }
112 stream.synchronize()
113 };
114
115 // Warm up outside the measured window: the first batch pays queue and paging costs.
116 run_batch(&mut stream)?;
117
118 let window = Duration::from_secs_f64(options.seconds);
119 let start = Instant::now();
120 let mut iterations = 0usize;
121 // Per-batch rates, so the harness can see whether contention was steady or bursty.
122 let mut batch_rates = Vec::new();
123 while start.elapsed() < window {
124 let batch_start = Instant::now();
125 run_batch(&mut stream)?;
126 let seconds = batch_start.elapsed().as_secs_f64();
127 iterations += options.batch;
128 let bytes = options.mode.traffic(size) * options.batch;
129 batch_rates.push(bytes as f64 / seconds / 1e9);
130 }
131 let elapsed = start.elapsed().as_secs_f64();
132 let total = options.mode.traffic(size) as f64 * iterations as f64;
133 let mean = total / elapsed / 1e9;
134 batch_rates.sort_by(f64::total_cmp);
135 let median = batch_rates[batch_rates.len() / 2];
136 let low = batch_rates[batch_rates.len() / 20];
137
138 println!(
139 concat!(
140 r#"{{"device":"gpu","mode":"{}","buffer_mib":{},"batch":{},"#,
141 r#""seconds":{:.4},"iterations":{},"gb_s_mean":{:.3},"#,
142 r#""gb_s_median":{:.3},"gb_s_p5":{:.3}}}"#
143 ),
144 options.mode.name(),
145 options.mib,
146 options.batch,
147 elapsed,
148 iterations,
149 mean,
150 median,
151 low
152 );
153 eprintln!(
154 "gpu_load: {} {:.1} GB/s mean over {:.1}s ({} iterations)",
155 options.mode.name(),
156 mean,
157 elapsed,
158 iterations
159 );
160 Ok(())
161}81fn main() -> Result<()> {
82 let args: Vec<String> = std::env::args().skip(1).collect();
83 let (xclbin, mib) = match args.as_slice() {
84 [xclbin] => (xclbin.clone(), 4usize),
85 [xclbin, flag, value] if flag == "--mib" => (
86 xclbin.clone(),
87 value.parse().expect("--mib expects an integer"),
88 ),
89 _ => {
90 eprintln!("usage: unified_probe <xclbin> [--mib N]");
91 std::process::exit(2);
92 }
93 };
94 let bytes = mib * 1024 * 1024;
95 let layout = Layout::from_size_align(bytes, PAGE).expect("page-aligned layout");
96 let host = unsafe { alloc_zeroed(layout) };
97 assert!(!host.is_null(), "host allocation failed");
98
99 let result = (|| -> Result<()> {
100 println!("host allocation: {mib} MiB at {host:p}");
101
102 let context = match unsafe { dvxrt::Context::new(0, &xclbin) } {
103 Ok(context) => context,
104 Err(error) => {
105 println!("npu context: UNAVAILABLE ({error})");
106 return Ok(());
107 }
108 };
109 let group = context.group_id(ARG_A).expect("group id for A");
110
111 // 1. Can amdxdna pin pages we allocated? Reported either way -- if it can, either
112 // runtime may own the allocation; if it cannot, only the NPU's allocator may.
113 match unsafe { context.import_bo(host.cast(), bytes, dvxrt::BoKind::HostOnly, group) } {
114 Ok(_) => println!("npu userptr import (host-owned pages): YES"),
115 Err(error) => println!("npu userptr import (host-owned pages): NO ({error})"),
116 }
117
118 // 2. The direction that matters: let the NPU's allocator own the pages -- it is the
119 // more constrained of the two -- and import its host mapping into the GPU runtime.
120 let bo = context
121 .alloc_bo(bytes, dvxrt::BoKind::HostOnly, group)
122 .expect("allocate an NPU BO");
123 let shared = bo.map().expect("map the NPU BO");
124 println!("npu allocation mapped at {shared:p}");
125 assert_eq!(shared as usize % PAGE, 0, "NPU mapping is not page-aligned");
126
127 let mut stream = Stream::open()?;
128 let imported = unsafe { stream.import_host(shared.cast(), bytes) };
129 match &imported {
130 Ok(_) => println!("gpu import of the NPU mapping: YES"),
131 Err(error) => println!("gpu import of the NPU mapping: NO ({error})"),
132 }
133
134 // 3. If the shared mapping took, check that a GPU write lands in the NPU's BO.
135 if let Ok(buffer) = &imported {
136 stream.fill(buffer.binding(), GPU_PATTERN)?;
137 stream.synchronize()?;
138 bo.sync(false, bytes).expect("invalidate for device reads");
139 let mut seen = vec![0u8; 4096];
140 bo.read(&mut seen).expect("read through the NPU BO");
141 let wrong = seen.iter().filter(|&&b| b != GPU_PATTERN).count();
142 println!(
143 "gpu write -> npu read: {}",
144 if wrong == 0 {
145 "YES".into()
146 } else {
147 format!("NO ({wrong} bytes differ)")
148 }
149 );
150 }
151
152 // 3b. The dma-buf route: the GPU allocates, HSA exports the allocation as a
153 // dma-buf, and XRT imports that descriptor. Neither runtime copies, and unlike
154 // the host-pointer routes above neither driver has to pin the other's pages.
155 // The GPU-side import above HSA-locked these pages, so they are exportable.
156 let locked = unsafe { stream.import_host(host.cast(), bytes) };
157 let export = match &locked {
158 Ok(buffer) => {
159 stream.fill(buffer.binding(), GPU_PATTERN)?;
160 stream.synchronize()?;
161 export_dmabuf(host.cast(), bytes)
162 }
163 Err(error) => Err(format!("host pages are not GPU-resident: {error}")),
164 };
165 match export {
166 Ok((fd, size)) => {
167 println!("hsa dma-buf export of a GPU buffer: YES (fd {fd})");
168 match unsafe { context.import_dmabuf(fd, size) } {
169 Ok(imported) => {
170 println!("npu import of the GPU dma-buf: YES");
171 let mut seen = vec![0u8; 4096];
172 match imported.read(&mut seen) {
173 Ok(()) => {
174 let wrong = seen.iter().filter(|&&b| b != GPU_PATTERN).count();
175 println!(
176 " gpu write -> npu read over dma-buf: {}",
177 if wrong == 0 {
178 "YES".into()
179 } else {
180 format!("NO ({wrong} bytes differ)")
181 }
182 );
183 }
184 Err(error) => {
185 println!(" read through the imported BO failed: {error}")
186 }
187 }
188 }
189 Err(error) => println!("npu import of the GPU dma-buf: NO ({error})"),
190 }
191 }
192 Err(error) => println!("hsa dma-buf export of a GPU buffer: NO ({error})"),
193 }
194
195 // 4. And the reverse: the NPU's BO writes, the GPU reads.
196 if let Ok(buffer) = &imported {
197 bo.write(&vec![NPU_PATTERN; bytes])
198 .expect("write through the NPU BO");
199 let mirror = stream.allocate(bytes)?;
200 stream.copy(mirror.binding(), buffer.binding())?;
201 let mut check = [0u8; 64];
202 stream.read_blocking(mirror.binding().slice(0, check.len())?, &mut check)?;
203 let back = check.iter().all(|&b| b == NPU_PATTERN);
204 println!("npu write -> gpu read: {}", if back { "YES" } else { "NO" });
205 }
206 Ok(())
207 })();
208 unsafe { dealloc(host, layout) };
209 result
210}30fn main() -> Result<()> {
31 let mut stream = Stream::open()?;
32 let mut metrics = BTreeMap::new();
33 let samples = std::env::var("HRX_BENCH_SAMPLES")
34 .ok()
35 .and_then(|value| value.parse::<usize>().ok())
36 .filter(|&value| value > 0)
37 .unwrap_or(9);
38 let (ns, _) = measure(&mut stream, samples, 50_000, |stream| {
39 for _ in 0..50_000 {
40 let buffer = stream.scratch(1024 * 1024)?;
41 black_box(&buffer);
42 stream.recycle(buffer)?;
43 }
44 Ok(())
45 })?;
46 metrics.insert("scratch_reuse_ns", ns);
47 let (ns, _) = measure(&mut stream, samples, 2_000, |stream| {
48 for _ in 0..2_000 {
49 black_box(stream.allocate(1024 * 1024)?);
50 }
51 Ok(())
52 })?;
53 metrics.insert("allocate_drop_ns", ns);
54
55 let bytes = 256 * 1024 * 1024;
56 let chunk = vec![0x3cu8; 4 * 1024 * 1024];
57 let weights = stream.allocate(bytes)?;
58 let (_, ns) = measure(&mut stream, samples, 1, |stream| {
59 for offset in (0..bytes).step_by(chunk.len()) {
60 stream.upload(weights.binding().slice(offset, chunk.len())?, &chunk)?;
61 }
62 Ok(())
63 })?;
64 metrics.insert(
65 "queued_upload_gib_s",
66 bytes as f64 / (1024f64.powi(3) * ns * 1e-9),
67 );
68 let mut check = [0u8; 16];
69 stream.read_blocking(
70 weights.try_slice(bytes - check.len(), check.len())?,
71 &mut check,
72 )?;
73 assert_eq!(check, [0x3c; 16]);
74 drop(weights);
75
76 let compiler = hrx::loom::Compiler::for_stream(None, &stream)?;
77 let module = compiler.module(include_str!("../tests/kernels/euler.loom"));
78 let mut spec = hrx::loom::Specialization::new("krea2_euler");
79 spec.set_config("krea2.euler.grid_x", "1");
80 spec.set_config("krea2.euler.grid_y", "1");
81 let artifact = module.compile(&spec)?;
82 // This repository owns the source; use its exact dimensions and scalar layout.
83 let kernel = unsafe { stream.load_artifact(&artifact)? };
84 let sample = stream.allocate(512)?;
85 let velocity = stream.allocate(512)?;
86 let ones: Vec<u8> = (0..256).flat_map(|_| 0x3f80u16.to_le_bytes()).collect();
87 stream.upload_blocking(sample.binding(), &ones)?;
88 stream.upload_blocking(velocity.binding(), &ones)?;
89 let mut constants = Constants::new();
90 match kernel.info().constant_byte_length {
91 8 => constants.push(256u32)?,
92 12 => constants.push(256u64)?,
93 n => return Err(hrx::Error::Message(format!("unexpected constant size {n}"))),
94 }
95 constants.push(0f32)?;
96 let bindings = [sample.binding(), velocity.binding()];
97 let (enqueue, complete) = measure(&mut stream, samples, 2048, |stream| {
98 for _ in 0..2048 {
99 // The source accesses exactly the 256 bf16 elements in each buffer.
100 unsafe {
101 stream.dispatch(&kernel, [1; 3], [256, 1, 1], &constants, &bindings)?;
102 }
103 }
104 Ok(())
105 })?;
106 metrics.insert("dispatch_enqueue_ns", enqueue);
107 metrics.insert("dispatch_complete_ns", complete);
108
109 // Use disjoint allocations in both graphs. Even an idempotent store is a
110 // write: concurrent Euler dispatches cannot share their in/out buffer.
111 let mut graph_buffers = Vec::new();
112 for _ in 0..32 {
113 let sample = stream.allocate(512)?;
114 let velocity = stream.allocate(512)?;
115 stream.upload(sample.binding(), &ones)?;
116 stream.upload(velocity.binding(), &ones)?;
117 graph_buffers.push((sample, velocity));
118 }
119 stream.synchronize()?;
120 let mut builder = stream.graph()?;
121 let mut previous = None;
122 for (sample, velocity) in &graph_buffers {
123 let after = previous.as_slice();
124 previous = Some(unsafe {
125 builder.dispatch(
126 after,
127 &kernel,
128 [1; 3],
129 [256, 1, 1],
130 &constants,
131 &[sample.binding(), velocity.binding()],
132 )?
133 });
134 }
135 let chained = builder.finish()?;
136
137 // The same disjoint workloads, this time allowing concurrent scheduling.
138 let mut builder = stream.graph()?;
139 for (sample, velocity) in &graph_buffers {
140 unsafe {
141 builder.dispatch(
142 &[],
143 &kernel,
144 [1; 3],
145 [256, 1, 1],
146 &constants,
147 &[sample.binding(), velocity.binding()],
148 )?;
149 }
150 }
151 let independent = builder.finish()?;
152 let mut graphs = [chained, independent];
153 let mut enqueue = [Vec::new(), Vec::new()];
154 let mut complete = [Vec::new(), Vec::new()];
155 for round in 0..samples + 3 {
156 for arm in if round % 2 == 0 { [0, 1] } else { [1, 0] } {
157 stream.synchronize()?;
158 let start = Instant::now();
159 for _ in 0..256 {
160 stream.launch(&mut graphs[arm])?;
161 }
162 let submitted = start.elapsed();
163 stream.synchronize()?;
164 let elapsed = start.elapsed();
165 if round >= 3 {
166 enqueue[arm].push(submitted.as_secs_f64() * 1e9 / 256.0);
167 complete[arm].push(elapsed.as_secs_f64() * 1e9 / (256.0 * 32.0));
168 }
169 }
170 }
171 for values in enqueue.iter_mut().chain(complete.iter_mut()) {
172 values.sort_by(f64::total_cmp);
173 }
174 let ns = complete[0][samples / 2];
175 let ns_free = complete[1][samples / 2];
176 metrics.insert("graph_enqueue_ns_per_replay", enqueue[0][samples / 2]);
177 metrics.insert("graph_complete_ns_per_kernel", ns);
178 metrics.insert("graph_independent_ns_per_kernel", ns_free);
179 metrics.insert("graph_scheduling_difference_ns_per_kernel", ns - ns_free);
180 for (sample, _) in &graph_buffers {
181 let mut output = vec![0; 512];
182 stream.read_blocking(sample.binding(), &mut output)?;
183 assert_eq!(output, ones);
184 }
185 let mut output = vec![0; 512];
186 stream.read_blocking(sample.binding(), &mut output)?;
187 assert_eq!(output, ones);
188 println!("{}", serde_json::to_string(&metrics)?);
189 Ok(())
190}Sourcepub fn allocate_zeroed(&self, bytes: usize) -> Result<Buffer>
pub fn allocate_zeroed(&self, bytes: usize) -> Result<Buffer>
Allocate device-local storage and enqueue a zero fill over its requested size.
Sourcepub unsafe fn import_host(
&self,
pointer: *mut c_void,
bytes: usize,
) -> Result<Buffer>
pub unsafe fn import_host( &self, pointer: *mut c_void, bytes: usize, ) -> Result<Buffer>
Import host memory the caller owns as a device-visible buffer, without copying.
On an APU the GPU and the NPU address the same physical pages, so importing one host allocation into both runtimes is the basis of zero-copy handoff between them: the same pages can simultaneously back an XRT BO driving the NPU.
§Safety
pointer must be page-aligned, cover at least bytes, and stay allocated and
unmoved for the whole life of the returned buffer – the buffer borrows the memory
and never frees it. The host must not read or write those bytes while device work
touching them is in flight.
Examples found in repository?
16fn main() -> Result<()> {
17 let mib: usize = match std::env::args().collect::<Vec<_>>().as_slice() {
18 [_] => 16,
19 [_, flag, value] if flag == "--mib" => value.parse().expect("--mib expects an integer"),
20 _ => {
21 eprintln!("usage: import_probe [--mib N]");
22 std::process::exit(2);
23 }
24 };
25 let bytes = mib * 1024 * 1024;
26 let layout = Layout::from_size_align(bytes, PAGE).expect("page-aligned layout");
27
28 // Owned for the whole scope below; the imported buffer never frees it.
29 let host = unsafe { alloc_zeroed(layout) };
30 assert!(!host.is_null(), "host allocation failed");
31 let result = (|| -> Result<()> {
32 let mut stream = Stream::open()?;
33 let buffer = match unsafe { stream.import_host(host.cast(), bytes) } {
34 Ok(buffer) => buffer,
35 Err(error) => {
36 println!("import: UNSUPPORTED ({error})");
37 return Ok(());
38 }
39 };
40 println!("import: ok ({mib} MiB at {host:p})");
41
42 // Prove the host pages start clear, so a match afterwards can only be the GPU.
43 let before = unsafe { std::slice::from_raw_parts(host, bytes) };
44 assert!(before.iter().all(|&b| b == 0), "host memory was not zeroed");
45
46 stream.fill(buffer.binding(), PATTERN)?;
47 stream.synchronize()?;
48
49 // Read the ORIGINAL host pointer -- no download, no staging buffer.
50 let after = unsafe { std::slice::from_raw_parts(host, bytes) };
51 let wrong = after.iter().filter(|&&b| b != PATTERN).count();
52 if wrong == 0 {
53 println!("gpu fill visible on host pointer: YES (zero-copy confirmed)");
54 } else {
55 println!("gpu fill visible on host pointer: NO ({wrong}/{bytes} bytes unwritten)");
56 }
57
58 // A device-side read of the same buffer must also see host writes.
59 let mirror = stream.allocate(bytes)?;
60 unsafe { std::slice::from_raw_parts_mut(host, bytes) }.fill(0x3c);
61 stream.copy(mirror.binding(), buffer.binding())?;
62 let mut check = [0u8; 64];
63 stream.read_blocking(mirror.binding().slice(0, check.len())?, &mut check)?;
64 let coherent = check.iter().all(|&b| b == 0x3c);
65 println!(
66 "host write visible to device: {}",
67 if coherent { "YES" } else { "NO" }
68 );
69 Ok(())
70 })();
71 unsafe { dealloc(host, layout) };
72 result
73}More examples
81fn main() -> Result<()> {
82 let args: Vec<String> = std::env::args().skip(1).collect();
83 let (xclbin, mib) = match args.as_slice() {
84 [xclbin] => (xclbin.clone(), 4usize),
85 [xclbin, flag, value] if flag == "--mib" => (
86 xclbin.clone(),
87 value.parse().expect("--mib expects an integer"),
88 ),
89 _ => {
90 eprintln!("usage: unified_probe <xclbin> [--mib N]");
91 std::process::exit(2);
92 }
93 };
94 let bytes = mib * 1024 * 1024;
95 let layout = Layout::from_size_align(bytes, PAGE).expect("page-aligned layout");
96 let host = unsafe { alloc_zeroed(layout) };
97 assert!(!host.is_null(), "host allocation failed");
98
99 let result = (|| -> Result<()> {
100 println!("host allocation: {mib} MiB at {host:p}");
101
102 let context = match unsafe { dvxrt::Context::new(0, &xclbin) } {
103 Ok(context) => context,
104 Err(error) => {
105 println!("npu context: UNAVAILABLE ({error})");
106 return Ok(());
107 }
108 };
109 let group = context.group_id(ARG_A).expect("group id for A");
110
111 // 1. Can amdxdna pin pages we allocated? Reported either way -- if it can, either
112 // runtime may own the allocation; if it cannot, only the NPU's allocator may.
113 match unsafe { context.import_bo(host.cast(), bytes, dvxrt::BoKind::HostOnly, group) } {
114 Ok(_) => println!("npu userptr import (host-owned pages): YES"),
115 Err(error) => println!("npu userptr import (host-owned pages): NO ({error})"),
116 }
117
118 // 2. The direction that matters: let the NPU's allocator own the pages -- it is the
119 // more constrained of the two -- and import its host mapping into the GPU runtime.
120 let bo = context
121 .alloc_bo(bytes, dvxrt::BoKind::HostOnly, group)
122 .expect("allocate an NPU BO");
123 let shared = bo.map().expect("map the NPU BO");
124 println!("npu allocation mapped at {shared:p}");
125 assert_eq!(shared as usize % PAGE, 0, "NPU mapping is not page-aligned");
126
127 let mut stream = Stream::open()?;
128 let imported = unsafe { stream.import_host(shared.cast(), bytes) };
129 match &imported {
130 Ok(_) => println!("gpu import of the NPU mapping: YES"),
131 Err(error) => println!("gpu import of the NPU mapping: NO ({error})"),
132 }
133
134 // 3. If the shared mapping took, check that a GPU write lands in the NPU's BO.
135 if let Ok(buffer) = &imported {
136 stream.fill(buffer.binding(), GPU_PATTERN)?;
137 stream.synchronize()?;
138 bo.sync(false, bytes).expect("invalidate for device reads");
139 let mut seen = vec![0u8; 4096];
140 bo.read(&mut seen).expect("read through the NPU BO");
141 let wrong = seen.iter().filter(|&&b| b != GPU_PATTERN).count();
142 println!(
143 "gpu write -> npu read: {}",
144 if wrong == 0 {
145 "YES".into()
146 } else {
147 format!("NO ({wrong} bytes differ)")
148 }
149 );
150 }
151
152 // 3b. The dma-buf route: the GPU allocates, HSA exports the allocation as a
153 // dma-buf, and XRT imports that descriptor. Neither runtime copies, and unlike
154 // the host-pointer routes above neither driver has to pin the other's pages.
155 // The GPU-side import above HSA-locked these pages, so they are exportable.
156 let locked = unsafe { stream.import_host(host.cast(), bytes) };
157 let export = match &locked {
158 Ok(buffer) => {
159 stream.fill(buffer.binding(), GPU_PATTERN)?;
160 stream.synchronize()?;
161 export_dmabuf(host.cast(), bytes)
162 }
163 Err(error) => Err(format!("host pages are not GPU-resident: {error}")),
164 };
165 match export {
166 Ok((fd, size)) => {
167 println!("hsa dma-buf export of a GPU buffer: YES (fd {fd})");
168 match unsafe { context.import_dmabuf(fd, size) } {
169 Ok(imported) => {
170 println!("npu import of the GPU dma-buf: YES");
171 let mut seen = vec![0u8; 4096];
172 match imported.read(&mut seen) {
173 Ok(()) => {
174 let wrong = seen.iter().filter(|&&b| b != GPU_PATTERN).count();
175 println!(
176 " gpu write -> npu read over dma-buf: {}",
177 if wrong == 0 {
178 "YES".into()
179 } else {
180 format!("NO ({wrong} bytes differ)")
181 }
182 );
183 }
184 Err(error) => {
185 println!(" read through the imported BO failed: {error}")
186 }
187 }
188 }
189 Err(error) => println!("npu import of the GPU dma-buf: NO ({error})"),
190 }
191 }
192 Err(error) => println!("hsa dma-buf export of a GPU buffer: NO ({error})"),
193 }
194
195 // 4. And the reverse: the NPU's BO writes, the GPU reads.
196 if let Ok(buffer) = &imported {
197 bo.write(&vec![NPU_PATTERN; bytes])
198 .expect("write through the NPU BO");
199 let mirror = stream.allocate(bytes)?;
200 stream.copy(mirror.binding(), buffer.binding())?;
201 let mut check = [0u8; 64];
202 stream.read_blocking(mirror.binding().slice(0, check.len())?, &mut check)?;
203 let back = check.iter().all(|&b| b == NPU_PATTERN);
204 println!("npu write -> gpu read: {}", if back { "YES" } else { "NO" });
205 }
206 Ok(())
207 })();
208 unsafe { dealloc(host, layout) };
209 result
210}Sourcepub fn synchronize(&mut self) -> Result<()>
pub fn synchronize(&mut self) -> Result<()>
Submit and wait for all work, then reclaim completed upload staging.
Examples found in repository?
5fn measure(
6 stream: &mut Stream,
7 samples: usize,
8 operations: usize,
9 mut run: impl FnMut(&mut Stream) -> Result<()>,
10) -> Result<(f64, f64)> {
11 let mut enqueue = Vec::new();
12 let mut complete = Vec::new();
13 for sample in 0..samples + 3 {
14 stream.synchronize()?;
15 let start = Instant::now();
16 run(stream)?;
17 let recorded = start.elapsed();
18 stream.synchronize()?;
19 let finished = start.elapsed();
20 if sample >= 3 {
21 enqueue.push(recorded.as_secs_f64() * 1e9 / operations as f64);
22 complete.push(finished.as_secs_f64() * 1e9 / operations as f64);
23 }
24 }
25 enqueue.sort_by(f64::total_cmp);
26 complete.sort_by(f64::total_cmp);
27 Ok((enqueue[samples / 2], complete[samples / 2]))
28}
29
30fn main() -> Result<()> {
31 let mut stream = Stream::open()?;
32 let mut metrics = BTreeMap::new();
33 let samples = std::env::var("HRX_BENCH_SAMPLES")
34 .ok()
35 .and_then(|value| value.parse::<usize>().ok())
36 .filter(|&value| value > 0)
37 .unwrap_or(9);
38 let (ns, _) = measure(&mut stream, samples, 50_000, |stream| {
39 for _ in 0..50_000 {
40 let buffer = stream.scratch(1024 * 1024)?;
41 black_box(&buffer);
42 stream.recycle(buffer)?;
43 }
44 Ok(())
45 })?;
46 metrics.insert("scratch_reuse_ns", ns);
47 let (ns, _) = measure(&mut stream, samples, 2_000, |stream| {
48 for _ in 0..2_000 {
49 black_box(stream.allocate(1024 * 1024)?);
50 }
51 Ok(())
52 })?;
53 metrics.insert("allocate_drop_ns", ns);
54
55 let bytes = 256 * 1024 * 1024;
56 let chunk = vec![0x3cu8; 4 * 1024 * 1024];
57 let weights = stream.allocate(bytes)?;
58 let (_, ns) = measure(&mut stream, samples, 1, |stream| {
59 for offset in (0..bytes).step_by(chunk.len()) {
60 stream.upload(weights.binding().slice(offset, chunk.len())?, &chunk)?;
61 }
62 Ok(())
63 })?;
64 metrics.insert(
65 "queued_upload_gib_s",
66 bytes as f64 / (1024f64.powi(3) * ns * 1e-9),
67 );
68 let mut check = [0u8; 16];
69 stream.read_blocking(
70 weights.try_slice(bytes - check.len(), check.len())?,
71 &mut check,
72 )?;
73 assert_eq!(check, [0x3c; 16]);
74 drop(weights);
75
76 let compiler = hrx::loom::Compiler::for_stream(None, &stream)?;
77 let module = compiler.module(include_str!("../tests/kernels/euler.loom"));
78 let mut spec = hrx::loom::Specialization::new("krea2_euler");
79 spec.set_config("krea2.euler.grid_x", "1");
80 spec.set_config("krea2.euler.grid_y", "1");
81 let artifact = module.compile(&spec)?;
82 // This repository owns the source; use its exact dimensions and scalar layout.
83 let kernel = unsafe { stream.load_artifact(&artifact)? };
84 let sample = stream.allocate(512)?;
85 let velocity = stream.allocate(512)?;
86 let ones: Vec<u8> = (0..256).flat_map(|_| 0x3f80u16.to_le_bytes()).collect();
87 stream.upload_blocking(sample.binding(), &ones)?;
88 stream.upload_blocking(velocity.binding(), &ones)?;
89 let mut constants = Constants::new();
90 match kernel.info().constant_byte_length {
91 8 => constants.push(256u32)?,
92 12 => constants.push(256u64)?,
93 n => return Err(hrx::Error::Message(format!("unexpected constant size {n}"))),
94 }
95 constants.push(0f32)?;
96 let bindings = [sample.binding(), velocity.binding()];
97 let (enqueue, complete) = measure(&mut stream, samples, 2048, |stream| {
98 for _ in 0..2048 {
99 // The source accesses exactly the 256 bf16 elements in each buffer.
100 unsafe {
101 stream.dispatch(&kernel, [1; 3], [256, 1, 1], &constants, &bindings)?;
102 }
103 }
104 Ok(())
105 })?;
106 metrics.insert("dispatch_enqueue_ns", enqueue);
107 metrics.insert("dispatch_complete_ns", complete);
108
109 // Use disjoint allocations in both graphs. Even an idempotent store is a
110 // write: concurrent Euler dispatches cannot share their in/out buffer.
111 let mut graph_buffers = Vec::new();
112 for _ in 0..32 {
113 let sample = stream.allocate(512)?;
114 let velocity = stream.allocate(512)?;
115 stream.upload(sample.binding(), &ones)?;
116 stream.upload(velocity.binding(), &ones)?;
117 graph_buffers.push((sample, velocity));
118 }
119 stream.synchronize()?;
120 let mut builder = stream.graph()?;
121 let mut previous = None;
122 for (sample, velocity) in &graph_buffers {
123 let after = previous.as_slice();
124 previous = Some(unsafe {
125 builder.dispatch(
126 after,
127 &kernel,
128 [1; 3],
129 [256, 1, 1],
130 &constants,
131 &[sample.binding(), velocity.binding()],
132 )?
133 });
134 }
135 let chained = builder.finish()?;
136
137 // The same disjoint workloads, this time allowing concurrent scheduling.
138 let mut builder = stream.graph()?;
139 for (sample, velocity) in &graph_buffers {
140 unsafe {
141 builder.dispatch(
142 &[],
143 &kernel,
144 [1; 3],
145 [256, 1, 1],
146 &constants,
147 &[sample.binding(), velocity.binding()],
148 )?;
149 }
150 }
151 let independent = builder.finish()?;
152 let mut graphs = [chained, independent];
153 let mut enqueue = [Vec::new(), Vec::new()];
154 let mut complete = [Vec::new(), Vec::new()];
155 for round in 0..samples + 3 {
156 for arm in if round % 2 == 0 { [0, 1] } else { [1, 0] } {
157 stream.synchronize()?;
158 let start = Instant::now();
159 for _ in 0..256 {
160 stream.launch(&mut graphs[arm])?;
161 }
162 let submitted = start.elapsed();
163 stream.synchronize()?;
164 let elapsed = start.elapsed();
165 if round >= 3 {
166 enqueue[arm].push(submitted.as_secs_f64() * 1e9 / 256.0);
167 complete[arm].push(elapsed.as_secs_f64() * 1e9 / (256.0 * 32.0));
168 }
169 }
170 }
171 for values in enqueue.iter_mut().chain(complete.iter_mut()) {
172 values.sort_by(f64::total_cmp);
173 }
174 let ns = complete[0][samples / 2];
175 let ns_free = complete[1][samples / 2];
176 metrics.insert("graph_enqueue_ns_per_replay", enqueue[0][samples / 2]);
177 metrics.insert("graph_complete_ns_per_kernel", ns);
178 metrics.insert("graph_independent_ns_per_kernel", ns_free);
179 metrics.insert("graph_scheduling_difference_ns_per_kernel", ns - ns_free);
180 for (sample, _) in &graph_buffers {
181 let mut output = vec![0; 512];
182 stream.read_blocking(sample.binding(), &mut output)?;
183 assert_eq!(output, ones);
184 }
185 let mut output = vec![0; 512];
186 stream.read_blocking(sample.binding(), &mut output)?;
187 assert_eq!(output, ones);
188 println!("{}", serde_json::to_string(&metrics)?);
189 Ok(())
190}More examples
16fn main() -> Result<()> {
17 let mib: usize = match std::env::args().collect::<Vec<_>>().as_slice() {
18 [_] => 16,
19 [_, flag, value] if flag == "--mib" => value.parse().expect("--mib expects an integer"),
20 _ => {
21 eprintln!("usage: import_probe [--mib N]");
22 std::process::exit(2);
23 }
24 };
25 let bytes = mib * 1024 * 1024;
26 let layout = Layout::from_size_align(bytes, PAGE).expect("page-aligned layout");
27
28 // Owned for the whole scope below; the imported buffer never frees it.
29 let host = unsafe { alloc_zeroed(layout) };
30 assert!(!host.is_null(), "host allocation failed");
31 let result = (|| -> Result<()> {
32 let mut stream = Stream::open()?;
33 let buffer = match unsafe { stream.import_host(host.cast(), bytes) } {
34 Ok(buffer) => buffer,
35 Err(error) => {
36 println!("import: UNSUPPORTED ({error})");
37 return Ok(());
38 }
39 };
40 println!("import: ok ({mib} MiB at {host:p})");
41
42 // Prove the host pages start clear, so a match afterwards can only be the GPU.
43 let before = unsafe { std::slice::from_raw_parts(host, bytes) };
44 assert!(before.iter().all(|&b| b == 0), "host memory was not zeroed");
45
46 stream.fill(buffer.binding(), PATTERN)?;
47 stream.synchronize()?;
48
49 // Read the ORIGINAL host pointer -- no download, no staging buffer.
50 let after = unsafe { std::slice::from_raw_parts(host, bytes) };
51 let wrong = after.iter().filter(|&&b| b != PATTERN).count();
52 if wrong == 0 {
53 println!("gpu fill visible on host pointer: YES (zero-copy confirmed)");
54 } else {
55 println!("gpu fill visible on host pointer: NO ({wrong}/{bytes} bytes unwritten)");
56 }
57
58 // A device-side read of the same buffer must also see host writes.
59 let mirror = stream.allocate(bytes)?;
60 unsafe { std::slice::from_raw_parts_mut(host, bytes) }.fill(0x3c);
61 stream.copy(mirror.binding(), buffer.binding())?;
62 let mut check = [0u8; 64];
63 stream.read_blocking(mirror.binding().slice(0, check.len())?, &mut check)?;
64 let coherent = check.iter().all(|&b| b == 0x3c);
65 println!(
66 "host write visible to device: {}",
67 if coherent { "YES" } else { "NO" }
68 );
69 Ok(())
70 })();
71 unsafe { dealloc(host, layout) };
72 result
73}88fn main() -> Result<()> {
89 let options = match parse() {
90 Ok(options) => options,
91 Err(message) => {
92 eprintln!("gpu_load: {message}");
93 std::process::exit(2);
94 }
95 };
96 let size = options.mib * 1024 * 1024;
97 let mut stream = Stream::open()?;
98 let source = stream.allocate(size)?;
99 let destination = stream.allocate(size)?;
100 // Touch both allocations so the steady-state loop never pays first-use faults.
101 stream.fill(source.binding(), 0x3c)?;
102 stream.fill(destination.binding(), 0)?;
103 stream.synchronize()?;
104
105 let run_batch = |stream: &mut Stream| -> Result<()> {
106 for _ in 0..options.batch {
107 match options.mode {
108 Mode::Copy => stream.copy(destination.binding(), source.binding())?,
109 Mode::Fill => stream.fill(destination.binding(), 0x5a)?,
110 }
111 }
112 stream.synchronize()
113 };
114
115 // Warm up outside the measured window: the first batch pays queue and paging costs.
116 run_batch(&mut stream)?;
117
118 let window = Duration::from_secs_f64(options.seconds);
119 let start = Instant::now();
120 let mut iterations = 0usize;
121 // Per-batch rates, so the harness can see whether contention was steady or bursty.
122 let mut batch_rates = Vec::new();
123 while start.elapsed() < window {
124 let batch_start = Instant::now();
125 run_batch(&mut stream)?;
126 let seconds = batch_start.elapsed().as_secs_f64();
127 iterations += options.batch;
128 let bytes = options.mode.traffic(size) * options.batch;
129 batch_rates.push(bytes as f64 / seconds / 1e9);
130 }
131 let elapsed = start.elapsed().as_secs_f64();
132 let total = options.mode.traffic(size) as f64 * iterations as f64;
133 let mean = total / elapsed / 1e9;
134 batch_rates.sort_by(f64::total_cmp);
135 let median = batch_rates[batch_rates.len() / 2];
136 let low = batch_rates[batch_rates.len() / 20];
137
138 println!(
139 concat!(
140 r#"{{"device":"gpu","mode":"{}","buffer_mib":{},"batch":{},"#,
141 r#""seconds":{:.4},"iterations":{},"gb_s_mean":{:.3},"#,
142 r#""gb_s_median":{:.3},"gb_s_p5":{:.3}}}"#
143 ),
144 options.mode.name(),
145 options.mib,
146 options.batch,
147 elapsed,
148 iterations,
149 mean,
150 median,
151 low
152 );
153 eprintln!(
154 "gpu_load: {} {:.1} GB/s mean over {:.1}s ({} iterations)",
155 options.mode.name(),
156 mean,
157 elapsed,
158 iterations
159 );
160 Ok(())
161}81fn main() -> Result<()> {
82 let args: Vec<String> = std::env::args().skip(1).collect();
83 let (xclbin, mib) = match args.as_slice() {
84 [xclbin] => (xclbin.clone(), 4usize),
85 [xclbin, flag, value] if flag == "--mib" => (
86 xclbin.clone(),
87 value.parse().expect("--mib expects an integer"),
88 ),
89 _ => {
90 eprintln!("usage: unified_probe <xclbin> [--mib N]");
91 std::process::exit(2);
92 }
93 };
94 let bytes = mib * 1024 * 1024;
95 let layout = Layout::from_size_align(bytes, PAGE).expect("page-aligned layout");
96 let host = unsafe { alloc_zeroed(layout) };
97 assert!(!host.is_null(), "host allocation failed");
98
99 let result = (|| -> Result<()> {
100 println!("host allocation: {mib} MiB at {host:p}");
101
102 let context = match unsafe { dvxrt::Context::new(0, &xclbin) } {
103 Ok(context) => context,
104 Err(error) => {
105 println!("npu context: UNAVAILABLE ({error})");
106 return Ok(());
107 }
108 };
109 let group = context.group_id(ARG_A).expect("group id for A");
110
111 // 1. Can amdxdna pin pages we allocated? Reported either way -- if it can, either
112 // runtime may own the allocation; if it cannot, only the NPU's allocator may.
113 match unsafe { context.import_bo(host.cast(), bytes, dvxrt::BoKind::HostOnly, group) } {
114 Ok(_) => println!("npu userptr import (host-owned pages): YES"),
115 Err(error) => println!("npu userptr import (host-owned pages): NO ({error})"),
116 }
117
118 // 2. The direction that matters: let the NPU's allocator own the pages -- it is the
119 // more constrained of the two -- and import its host mapping into the GPU runtime.
120 let bo = context
121 .alloc_bo(bytes, dvxrt::BoKind::HostOnly, group)
122 .expect("allocate an NPU BO");
123 let shared = bo.map().expect("map the NPU BO");
124 println!("npu allocation mapped at {shared:p}");
125 assert_eq!(shared as usize % PAGE, 0, "NPU mapping is not page-aligned");
126
127 let mut stream = Stream::open()?;
128 let imported = unsafe { stream.import_host(shared.cast(), bytes) };
129 match &imported {
130 Ok(_) => println!("gpu import of the NPU mapping: YES"),
131 Err(error) => println!("gpu import of the NPU mapping: NO ({error})"),
132 }
133
134 // 3. If the shared mapping took, check that a GPU write lands in the NPU's BO.
135 if let Ok(buffer) = &imported {
136 stream.fill(buffer.binding(), GPU_PATTERN)?;
137 stream.synchronize()?;
138 bo.sync(false, bytes).expect("invalidate for device reads");
139 let mut seen = vec![0u8; 4096];
140 bo.read(&mut seen).expect("read through the NPU BO");
141 let wrong = seen.iter().filter(|&&b| b != GPU_PATTERN).count();
142 println!(
143 "gpu write -> npu read: {}",
144 if wrong == 0 {
145 "YES".into()
146 } else {
147 format!("NO ({wrong} bytes differ)")
148 }
149 );
150 }
151
152 // 3b. The dma-buf route: the GPU allocates, HSA exports the allocation as a
153 // dma-buf, and XRT imports that descriptor. Neither runtime copies, and unlike
154 // the host-pointer routes above neither driver has to pin the other's pages.
155 // The GPU-side import above HSA-locked these pages, so they are exportable.
156 let locked = unsafe { stream.import_host(host.cast(), bytes) };
157 let export = match &locked {
158 Ok(buffer) => {
159 stream.fill(buffer.binding(), GPU_PATTERN)?;
160 stream.synchronize()?;
161 export_dmabuf(host.cast(), bytes)
162 }
163 Err(error) => Err(format!("host pages are not GPU-resident: {error}")),
164 };
165 match export {
166 Ok((fd, size)) => {
167 println!("hsa dma-buf export of a GPU buffer: YES (fd {fd})");
168 match unsafe { context.import_dmabuf(fd, size) } {
169 Ok(imported) => {
170 println!("npu import of the GPU dma-buf: YES");
171 let mut seen = vec![0u8; 4096];
172 match imported.read(&mut seen) {
173 Ok(()) => {
174 let wrong = seen.iter().filter(|&&b| b != GPU_PATTERN).count();
175 println!(
176 " gpu write -> npu read over dma-buf: {}",
177 if wrong == 0 {
178 "YES".into()
179 } else {
180 format!("NO ({wrong} bytes differ)")
181 }
182 );
183 }
184 Err(error) => {
185 println!(" read through the imported BO failed: {error}")
186 }
187 }
188 }
189 Err(error) => println!("npu import of the GPU dma-buf: NO ({error})"),
190 }
191 }
192 Err(error) => println!("hsa dma-buf export of a GPU buffer: NO ({error})"),
193 }
194
195 // 4. And the reverse: the NPU's BO writes, the GPU reads.
196 if let Ok(buffer) = &imported {
197 bo.write(&vec![NPU_PATTERN; bytes])
198 .expect("write through the NPU BO");
199 let mirror = stream.allocate(bytes)?;
200 stream.copy(mirror.binding(), buffer.binding())?;
201 let mut check = [0u8; 64];
202 stream.read_blocking(mirror.binding().slice(0, check.len())?, &mut check)?;
203 let back = check.iter().all(|&b| b == NPU_PATTERN);
204 println!("npu write -> gpu read: {}", if back { "YES" } else { "NO" });
205 }
206 Ok(())
207 })();
208 unsafe { dealloc(host, layout) };
209 result
210}72fn main() -> Result<(), String> {
73 let args: Vec<String> = std::env::args().skip(1).collect();
74 let (xclbin, mib) = match args.as_slice() {
75 [xclbin] => (xclbin.clone(), 4usize),
76 [xclbin, flag, value] if flag == "--mib" => (
77 xclbin.clone(),
78 value.parse().map_err(|_| "--mib expects an integer")?,
79 ),
80 _ => return Err("usage: dmabuf_probe <xclbin> [--mib N]".into()),
81 };
82 let bytes = mib * 1024 * 1024;
83 let hsa = Hsa::open()?;
84
85 unsafe {
86 let init: libloading::Symbol<unsafe extern "C" fn() -> Status> =
87 hsa.symbol(b"hsa_init\0")?;
88 let status = init();
89 if status != HSA_STATUS_SUCCESS {
90 return Err(format!("hsa_init returned {status}"));
91 }
92
93 // --- find a GPU agent -------------------------------------------------
94 let iterate_agents: libloading::Symbol<
95 unsafe extern "C" fn(
96 unsafe extern "C" fn(u64, *mut c_void) -> Status,
97 *mut c_void,
98 ) -> Status,
99 > = hsa.symbol(b"hsa_iterate_agents\0")?;
100 let agent_info: libloading::Symbol<unsafe extern "C" fn(u64, u32, *mut c_void) -> Status> =
101 hsa.symbol(b"hsa_agent_get_info\0")?;
102
103 // The callback needs the symbol; pass it through the user-data pointer.
104 struct AgentScan<'a> {
105 info: &'a dyn Fn(u64, u32, *mut c_void) -> Status,
106 found: Found,
107 }
108 unsafe extern "C" fn on_agent(agent: u64, data: *mut c_void) -> Status {
109 let scan = unsafe { &mut *data.cast::<AgentScan>() };
110 let mut kind: u32 = 0;
111 if (scan.info)(agent, HSA_AGENT_INFO_DEVICE, (&raw mut kind).cast())
112 == HSA_STATUS_SUCCESS
113 && kind == HSA_DEVICE_TYPE_GPU
114 && scan.found.agent == 0
115 {
116 scan.found.agent = agent;
117 }
118 HSA_STATUS_SUCCESS
119 }
120 let info_fn = |agent, key, out| agent_info(agent, key, out);
121 let mut scan = AgentScan {
122 info: &info_fn,
123 found: Found::default(),
124 };
125 iterate_agents(on_agent, (&raw mut scan).cast());
126 if scan.found.agent == 0 {
127 return Err("no GPU agent found".into());
128 }
129 println!("gpu agent: 0x{:x}", scan.found.agent);
130
131 // --- find an allocatable global pool ----------------------------------
132 let iterate_pools: libloading::Symbol<
133 unsafe extern "C" fn(
134 u64,
135 unsafe extern "C" fn(u64, *mut c_void) -> Status,
136 *mut c_void,
137 ) -> Status,
138 > = hsa.symbol(b"hsa_amd_agent_iterate_memory_pools\0")?;
139 let pool_info: libloading::Symbol<unsafe extern "C" fn(u64, u32, *mut c_void) -> Status> =
140 hsa.symbol(b"hsa_amd_memory_pool_get_info\0")?;
141
142 struct PoolScan<'a> {
143 info: &'a dyn Fn(u64, u32, *mut c_void) -> Status,
144 found: Found,
145 }
146 unsafe extern "C" fn on_pool(pool: u64, data: *mut c_void) -> Status {
147 let scan = unsafe { &mut *data.cast::<PoolScan>() };
148 if scan.found.pool != 0 {
149 return HSA_STATUS_SUCCESS;
150 }
151 let mut segment: u32 = u32::MAX;
152 let mut allowed: bool = false;
153 let mut size: usize = 0;
154 (scan.info)(pool, POOL_INFO_SEGMENT, (&raw mut segment).cast());
155 (scan.info)(pool, POOL_INFO_ALLOC_ALLOWED, (&raw mut allowed).cast());
156 (scan.info)(pool, POOL_INFO_SIZE, (&raw mut size).cast());
157 if segment == SEGMENT_GLOBAL && allowed && size > 0 {
158 scan.found.pool = pool;
159 scan.found.pool_bytes = size;
160 }
161 HSA_STATUS_SUCCESS
162 }
163 let pool_fn = |pool, key, out| pool_info(pool, key, out);
164 let mut pools = PoolScan {
165 info: &pool_fn,
166 found: Found::default(),
167 };
168 iterate_pools(scan.found.agent, on_pool, (&raw mut pools).cast());
169 if pools.found.pool == 0 {
170 return Err("no allocatable global memory pool on the GPU agent".into());
171 }
172 println!(
173 "gpu memory pool: 0x{:x} ({} MiB)",
174 pools.found.pool,
175 pools.found.pool_bytes / (1024 * 1024)
176 );
177
178 // --- allocate from it and export a dma-buf ----------------------------
179 let allocate: libloading::Symbol<
180 unsafe extern "C" fn(u64, usize, u32, *mut *mut c_void) -> Status,
181 > = hsa.symbol(b"hsa_amd_memory_pool_allocate\0")?;
182 let mut pointer: *mut c_void = std::ptr::null_mut();
183 let status = allocate(pools.found.pool, bytes, 0, &raw mut pointer);
184 if status != HSA_STATUS_SUCCESS {
185 return Err(format!("hsa_amd_memory_pool_allocate returned {status}"));
186 }
187 println!("pool allocation: {mib} MiB at {pointer:p}");
188
189 // The route that would make a shared buffer a first-class hrx buffer: allocate it
190 // through the GPU runtime, take its device pointer, and export that.
191 match hrx::Stream::open() {
192 Ok(mut stream) => match stream.allocate_shared(bytes) {
193 Ok(buffer) => {
194 stream.fill(buffer.binding(), 0x11).ok();
195 stream.synchronize().ok();
196 match buffer.device_ptr() {
197 Ok(hrx_pointer) => {
198 println!("hrx shared allocation: ok, device ptr {hrx_pointer:p}");
199 let mut fd: i32 = -1;
200 let mut offset: u64 = 0;
201 let export: libloading::Symbol<
202 unsafe extern "C" fn(
203 *const c_void,
204 usize,
205 *mut i32,
206 *mut u64,
207 ) -> Status,
208 > = hsa.symbol(b"hsa_amd_portable_export_dmabuf\0")?;
209 let status = export(hrx_pointer, bytes, &raw mut fd, &raw mut offset);
210 if status != HSA_STATUS_SUCCESS {
211 println!(" dma-buf export of an hrx buffer: NO (status {status})");
212 } else {
213 println!(
214 " dma-buf export of an hrx buffer: YES (fd {fd}, offset {offset})"
215 );
216 match dvxrt::Context::new(0, &xclbin) {
217 Ok(context) => match context.import_dmabuf(fd, bytes) {
218 Ok(bo) => {
219 let mut seen = vec![0u8; 4096];
220 bo.read(&mut seen).ok();
221 let wrong = seen.iter().filter(|&&b| b != 0x11).count();
222 println!(
223 " npu reads the hrx buffer: {}",
224 if wrong == 0 {
225 "YES -- one buffer, both engines".into()
226 } else {
227 format!("NO ({wrong}/4096 differ)")
228 }
229 );
230 }
231 Err(error) => println!(" npu import: NO ({error})"),
232 },
233 Err(error) => println!(" npu context: {error}"),
234 }
235 }
236 }
237 Err(error) => println!("hrx shared allocation device ptr: NO ({error})"),
238 }
239 }
240 Err(error) => println!("hrx shared allocation: NO ({error})"),
241 },
242 Err(error) => println!("hrx stream unavailable: {error}"),
243 }
244
245 let export: libloading::Symbol<
246 unsafe extern "C" fn(*const c_void, usize, *mut i32, *mut u64) -> Status,
247 > = hsa.symbol(b"hsa_amd_portable_export_dmabuf\0")?;
248 let mut fd: i32 = -1;
249 let mut offset: u64 = 0;
250 let status = export(pointer, bytes, &raw mut fd, &raw mut offset);
251 if status != HSA_STATUS_SUCCESS {
252 println!("hsa dma-buf export of pool memory: NO (status {status})");
253 return Ok(());
254 }
255 println!("hsa dma-buf export of pool memory: YES (fd {fd}, offset {offset})");
256
257 // --- and hand that descriptor to the NPU ------------------------------
258 match dvxrt::Context::new(0, &xclbin) {
259 Ok(context) => {
260 let _group = context.group_id(ARG_A).map_err(|e| e.to_string())?;
261 match context.import_dmabuf(fd, bytes) {
262 Ok(imported) => {
263 println!("npu import of the GPU dma-buf: YES");
264 // Import alone only proves the descriptor was accepted. Write with
265 // the GPU and read through the NPU's BO to prove it is one buffer.
266 let fill: libloading::Symbol<
267 unsafe extern "C" fn(*mut c_void, u32, usize) -> Status,
268 > = hsa.symbol(b"hsa_amd_memory_fill\0")?;
269 let pattern = 0xa5a5_a5a5u32;
270 let status = fill(pointer, pattern, bytes / 4);
271 if status != HSA_STATUS_SUCCESS {
272 println!(" gpu fill of pool memory failed (status {status})");
273 return Ok(());
274 }
275 let mut seen = vec![0u8; 4096];
276 match imported.read(&mut seen) {
277 Ok(()) => {
278 let wrong = seen.iter().filter(|&&b| b != 0xa5).count();
279 if wrong == 0 {
280 println!(
281 " gpu write -> npu read over dma-buf: YES -- zero-copy confirmed"
282 );
283 } else {
284 println!(
285 " gpu write -> npu read over dma-buf: NO ({wrong}/4096 bytes differ)"
286 );
287 }
288 }
289 Err(error) => {
290 println!(" read through the imported BO failed: {error}")
291 }
292 }
293 }
294 Err(error) => println!("npu import of the GPU dma-buf: NO ({error})"),
295 }
296 }
297 Err(error) => println!("npu context: UNAVAILABLE ({error})"),
298 }
299 }
300 Ok(())
301}Sourcepub fn upload_blocking(&mut self, dst: View<'_>, bytes: &[u8]) -> Result<()>
pub fn upload_blocking(&mut self, dst: View<'_>, bytes: &[u8]) -> Result<()>
Drain pending work, then upload bytes at the start of a view and wait for completion. The input must fit within the view. Completed staging is reclaimed.
Examples found in repository?
30fn main() -> Result<()> {
31 let mut stream = Stream::open()?;
32 let mut metrics = BTreeMap::new();
33 let samples = std::env::var("HRX_BENCH_SAMPLES")
34 .ok()
35 .and_then(|value| value.parse::<usize>().ok())
36 .filter(|&value| value > 0)
37 .unwrap_or(9);
38 let (ns, _) = measure(&mut stream, samples, 50_000, |stream| {
39 for _ in 0..50_000 {
40 let buffer = stream.scratch(1024 * 1024)?;
41 black_box(&buffer);
42 stream.recycle(buffer)?;
43 }
44 Ok(())
45 })?;
46 metrics.insert("scratch_reuse_ns", ns);
47 let (ns, _) = measure(&mut stream, samples, 2_000, |stream| {
48 for _ in 0..2_000 {
49 black_box(stream.allocate(1024 * 1024)?);
50 }
51 Ok(())
52 })?;
53 metrics.insert("allocate_drop_ns", ns);
54
55 let bytes = 256 * 1024 * 1024;
56 let chunk = vec![0x3cu8; 4 * 1024 * 1024];
57 let weights = stream.allocate(bytes)?;
58 let (_, ns) = measure(&mut stream, samples, 1, |stream| {
59 for offset in (0..bytes).step_by(chunk.len()) {
60 stream.upload(weights.binding().slice(offset, chunk.len())?, &chunk)?;
61 }
62 Ok(())
63 })?;
64 metrics.insert(
65 "queued_upload_gib_s",
66 bytes as f64 / (1024f64.powi(3) * ns * 1e-9),
67 );
68 let mut check = [0u8; 16];
69 stream.read_blocking(
70 weights.try_slice(bytes - check.len(), check.len())?,
71 &mut check,
72 )?;
73 assert_eq!(check, [0x3c; 16]);
74 drop(weights);
75
76 let compiler = hrx::loom::Compiler::for_stream(None, &stream)?;
77 let module = compiler.module(include_str!("../tests/kernels/euler.loom"));
78 let mut spec = hrx::loom::Specialization::new("krea2_euler");
79 spec.set_config("krea2.euler.grid_x", "1");
80 spec.set_config("krea2.euler.grid_y", "1");
81 let artifact = module.compile(&spec)?;
82 // This repository owns the source; use its exact dimensions and scalar layout.
83 let kernel = unsafe { stream.load_artifact(&artifact)? };
84 let sample = stream.allocate(512)?;
85 let velocity = stream.allocate(512)?;
86 let ones: Vec<u8> = (0..256).flat_map(|_| 0x3f80u16.to_le_bytes()).collect();
87 stream.upload_blocking(sample.binding(), &ones)?;
88 stream.upload_blocking(velocity.binding(), &ones)?;
89 let mut constants = Constants::new();
90 match kernel.info().constant_byte_length {
91 8 => constants.push(256u32)?,
92 12 => constants.push(256u64)?,
93 n => return Err(hrx::Error::Message(format!("unexpected constant size {n}"))),
94 }
95 constants.push(0f32)?;
96 let bindings = [sample.binding(), velocity.binding()];
97 let (enqueue, complete) = measure(&mut stream, samples, 2048, |stream| {
98 for _ in 0..2048 {
99 // The source accesses exactly the 256 bf16 elements in each buffer.
100 unsafe {
101 stream.dispatch(&kernel, [1; 3], [256, 1, 1], &constants, &bindings)?;
102 }
103 }
104 Ok(())
105 })?;
106 metrics.insert("dispatch_enqueue_ns", enqueue);
107 metrics.insert("dispatch_complete_ns", complete);
108
109 // Use disjoint allocations in both graphs. Even an idempotent store is a
110 // write: concurrent Euler dispatches cannot share their in/out buffer.
111 let mut graph_buffers = Vec::new();
112 for _ in 0..32 {
113 let sample = stream.allocate(512)?;
114 let velocity = stream.allocate(512)?;
115 stream.upload(sample.binding(), &ones)?;
116 stream.upload(velocity.binding(), &ones)?;
117 graph_buffers.push((sample, velocity));
118 }
119 stream.synchronize()?;
120 let mut builder = stream.graph()?;
121 let mut previous = None;
122 for (sample, velocity) in &graph_buffers {
123 let after = previous.as_slice();
124 previous = Some(unsafe {
125 builder.dispatch(
126 after,
127 &kernel,
128 [1; 3],
129 [256, 1, 1],
130 &constants,
131 &[sample.binding(), velocity.binding()],
132 )?
133 });
134 }
135 let chained = builder.finish()?;
136
137 // The same disjoint workloads, this time allowing concurrent scheduling.
138 let mut builder = stream.graph()?;
139 for (sample, velocity) in &graph_buffers {
140 unsafe {
141 builder.dispatch(
142 &[],
143 &kernel,
144 [1; 3],
145 [256, 1, 1],
146 &constants,
147 &[sample.binding(), velocity.binding()],
148 )?;
149 }
150 }
151 let independent = builder.finish()?;
152 let mut graphs = [chained, independent];
153 let mut enqueue = [Vec::new(), Vec::new()];
154 let mut complete = [Vec::new(), Vec::new()];
155 for round in 0..samples + 3 {
156 for arm in if round % 2 == 0 { [0, 1] } else { [1, 0] } {
157 stream.synchronize()?;
158 let start = Instant::now();
159 for _ in 0..256 {
160 stream.launch(&mut graphs[arm])?;
161 }
162 let submitted = start.elapsed();
163 stream.synchronize()?;
164 let elapsed = start.elapsed();
165 if round >= 3 {
166 enqueue[arm].push(submitted.as_secs_f64() * 1e9 / 256.0);
167 complete[arm].push(elapsed.as_secs_f64() * 1e9 / (256.0 * 32.0));
168 }
169 }
170 }
171 for values in enqueue.iter_mut().chain(complete.iter_mut()) {
172 values.sort_by(f64::total_cmp);
173 }
174 let ns = complete[0][samples / 2];
175 let ns_free = complete[1][samples / 2];
176 metrics.insert("graph_enqueue_ns_per_replay", enqueue[0][samples / 2]);
177 metrics.insert("graph_complete_ns_per_kernel", ns);
178 metrics.insert("graph_independent_ns_per_kernel", ns_free);
179 metrics.insert("graph_scheduling_difference_ns_per_kernel", ns - ns_free);
180 for (sample, _) in &graph_buffers {
181 let mut output = vec![0; 512];
182 stream.read_blocking(sample.binding(), &mut output)?;
183 assert_eq!(output, ones);
184 }
185 let mut output = vec![0; 512];
186 stream.read_blocking(sample.binding(), &mut output)?;
187 assert_eq!(output, ones);
188 println!("{}", serde_json::to_string(&metrics)?);
189 Ok(())
190}Sourcepub fn upload_blocking_at(
&mut self,
dst: &Buffer,
offset: usize,
bytes: &[u8],
) -> Result<()>
pub fn upload_blocking_at( &mut self, dst: &Buffer, offset: usize, bytes: &[u8], ) -> Result<()>
Synchronously upload at a checked byte offset into an allocation.
Sourcepub fn read_blocking(&mut self, src: View<'_>, bytes: &mut [u8]) -> Result<()>
pub fn read_blocking(&mut self, src: View<'_>, bytes: &mut [u8]) -> Result<()>
Drain all pending work before a synchronous read and reclaim staging.
Examples found in repository?
16fn main() -> Result<()> {
17 let mib: usize = match std::env::args().collect::<Vec<_>>().as_slice() {
18 [_] => 16,
19 [_, flag, value] if flag == "--mib" => value.parse().expect("--mib expects an integer"),
20 _ => {
21 eprintln!("usage: import_probe [--mib N]");
22 std::process::exit(2);
23 }
24 };
25 let bytes = mib * 1024 * 1024;
26 let layout = Layout::from_size_align(bytes, PAGE).expect("page-aligned layout");
27
28 // Owned for the whole scope below; the imported buffer never frees it.
29 let host = unsafe { alloc_zeroed(layout) };
30 assert!(!host.is_null(), "host allocation failed");
31 let result = (|| -> Result<()> {
32 let mut stream = Stream::open()?;
33 let buffer = match unsafe { stream.import_host(host.cast(), bytes) } {
34 Ok(buffer) => buffer,
35 Err(error) => {
36 println!("import: UNSUPPORTED ({error})");
37 return Ok(());
38 }
39 };
40 println!("import: ok ({mib} MiB at {host:p})");
41
42 // Prove the host pages start clear, so a match afterwards can only be the GPU.
43 let before = unsafe { std::slice::from_raw_parts(host, bytes) };
44 assert!(before.iter().all(|&b| b == 0), "host memory was not zeroed");
45
46 stream.fill(buffer.binding(), PATTERN)?;
47 stream.synchronize()?;
48
49 // Read the ORIGINAL host pointer -- no download, no staging buffer.
50 let after = unsafe { std::slice::from_raw_parts(host, bytes) };
51 let wrong = after.iter().filter(|&&b| b != PATTERN).count();
52 if wrong == 0 {
53 println!("gpu fill visible on host pointer: YES (zero-copy confirmed)");
54 } else {
55 println!("gpu fill visible on host pointer: NO ({wrong}/{bytes} bytes unwritten)");
56 }
57
58 // A device-side read of the same buffer must also see host writes.
59 let mirror = stream.allocate(bytes)?;
60 unsafe { std::slice::from_raw_parts_mut(host, bytes) }.fill(0x3c);
61 stream.copy(mirror.binding(), buffer.binding())?;
62 let mut check = [0u8; 64];
63 stream.read_blocking(mirror.binding().slice(0, check.len())?, &mut check)?;
64 let coherent = check.iter().all(|&b| b == 0x3c);
65 println!(
66 "host write visible to device: {}",
67 if coherent { "YES" } else { "NO" }
68 );
69 Ok(())
70 })();
71 unsafe { dealloc(host, layout) };
72 result
73}More examples
81fn main() -> Result<()> {
82 let args: Vec<String> = std::env::args().skip(1).collect();
83 let (xclbin, mib) = match args.as_slice() {
84 [xclbin] => (xclbin.clone(), 4usize),
85 [xclbin, flag, value] if flag == "--mib" => (
86 xclbin.clone(),
87 value.parse().expect("--mib expects an integer"),
88 ),
89 _ => {
90 eprintln!("usage: unified_probe <xclbin> [--mib N]");
91 std::process::exit(2);
92 }
93 };
94 let bytes = mib * 1024 * 1024;
95 let layout = Layout::from_size_align(bytes, PAGE).expect("page-aligned layout");
96 let host = unsafe { alloc_zeroed(layout) };
97 assert!(!host.is_null(), "host allocation failed");
98
99 let result = (|| -> Result<()> {
100 println!("host allocation: {mib} MiB at {host:p}");
101
102 let context = match unsafe { dvxrt::Context::new(0, &xclbin) } {
103 Ok(context) => context,
104 Err(error) => {
105 println!("npu context: UNAVAILABLE ({error})");
106 return Ok(());
107 }
108 };
109 let group = context.group_id(ARG_A).expect("group id for A");
110
111 // 1. Can amdxdna pin pages we allocated? Reported either way -- if it can, either
112 // runtime may own the allocation; if it cannot, only the NPU's allocator may.
113 match unsafe { context.import_bo(host.cast(), bytes, dvxrt::BoKind::HostOnly, group) } {
114 Ok(_) => println!("npu userptr import (host-owned pages): YES"),
115 Err(error) => println!("npu userptr import (host-owned pages): NO ({error})"),
116 }
117
118 // 2. The direction that matters: let the NPU's allocator own the pages -- it is the
119 // more constrained of the two -- and import its host mapping into the GPU runtime.
120 let bo = context
121 .alloc_bo(bytes, dvxrt::BoKind::HostOnly, group)
122 .expect("allocate an NPU BO");
123 let shared = bo.map().expect("map the NPU BO");
124 println!("npu allocation mapped at {shared:p}");
125 assert_eq!(shared as usize % PAGE, 0, "NPU mapping is not page-aligned");
126
127 let mut stream = Stream::open()?;
128 let imported = unsafe { stream.import_host(shared.cast(), bytes) };
129 match &imported {
130 Ok(_) => println!("gpu import of the NPU mapping: YES"),
131 Err(error) => println!("gpu import of the NPU mapping: NO ({error})"),
132 }
133
134 // 3. If the shared mapping took, check that a GPU write lands in the NPU's BO.
135 if let Ok(buffer) = &imported {
136 stream.fill(buffer.binding(), GPU_PATTERN)?;
137 stream.synchronize()?;
138 bo.sync(false, bytes).expect("invalidate for device reads");
139 let mut seen = vec![0u8; 4096];
140 bo.read(&mut seen).expect("read through the NPU BO");
141 let wrong = seen.iter().filter(|&&b| b != GPU_PATTERN).count();
142 println!(
143 "gpu write -> npu read: {}",
144 if wrong == 0 {
145 "YES".into()
146 } else {
147 format!("NO ({wrong} bytes differ)")
148 }
149 );
150 }
151
152 // 3b. The dma-buf route: the GPU allocates, HSA exports the allocation as a
153 // dma-buf, and XRT imports that descriptor. Neither runtime copies, and unlike
154 // the host-pointer routes above neither driver has to pin the other's pages.
155 // The GPU-side import above HSA-locked these pages, so they are exportable.
156 let locked = unsafe { stream.import_host(host.cast(), bytes) };
157 let export = match &locked {
158 Ok(buffer) => {
159 stream.fill(buffer.binding(), GPU_PATTERN)?;
160 stream.synchronize()?;
161 export_dmabuf(host.cast(), bytes)
162 }
163 Err(error) => Err(format!("host pages are not GPU-resident: {error}")),
164 };
165 match export {
166 Ok((fd, size)) => {
167 println!("hsa dma-buf export of a GPU buffer: YES (fd {fd})");
168 match unsafe { context.import_dmabuf(fd, size) } {
169 Ok(imported) => {
170 println!("npu import of the GPU dma-buf: YES");
171 let mut seen = vec![0u8; 4096];
172 match imported.read(&mut seen) {
173 Ok(()) => {
174 let wrong = seen.iter().filter(|&&b| b != GPU_PATTERN).count();
175 println!(
176 " gpu write -> npu read over dma-buf: {}",
177 if wrong == 0 {
178 "YES".into()
179 } else {
180 format!("NO ({wrong} bytes differ)")
181 }
182 );
183 }
184 Err(error) => {
185 println!(" read through the imported BO failed: {error}")
186 }
187 }
188 }
189 Err(error) => println!("npu import of the GPU dma-buf: NO ({error})"),
190 }
191 }
192 Err(error) => println!("hsa dma-buf export of a GPU buffer: NO ({error})"),
193 }
194
195 // 4. And the reverse: the NPU's BO writes, the GPU reads.
196 if let Ok(buffer) = &imported {
197 bo.write(&vec![NPU_PATTERN; bytes])
198 .expect("write through the NPU BO");
199 let mirror = stream.allocate(bytes)?;
200 stream.copy(mirror.binding(), buffer.binding())?;
201 let mut check = [0u8; 64];
202 stream.read_blocking(mirror.binding().slice(0, check.len())?, &mut check)?;
203 let back = check.iter().all(|&b| b == NPU_PATTERN);
204 println!("npu write -> gpu read: {}", if back { "YES" } else { "NO" });
205 }
206 Ok(())
207 })();
208 unsafe { dealloc(host, layout) };
209 result
210}30fn main() -> Result<()> {
31 let mut stream = Stream::open()?;
32 let mut metrics = BTreeMap::new();
33 let samples = std::env::var("HRX_BENCH_SAMPLES")
34 .ok()
35 .and_then(|value| value.parse::<usize>().ok())
36 .filter(|&value| value > 0)
37 .unwrap_or(9);
38 let (ns, _) = measure(&mut stream, samples, 50_000, |stream| {
39 for _ in 0..50_000 {
40 let buffer = stream.scratch(1024 * 1024)?;
41 black_box(&buffer);
42 stream.recycle(buffer)?;
43 }
44 Ok(())
45 })?;
46 metrics.insert("scratch_reuse_ns", ns);
47 let (ns, _) = measure(&mut stream, samples, 2_000, |stream| {
48 for _ in 0..2_000 {
49 black_box(stream.allocate(1024 * 1024)?);
50 }
51 Ok(())
52 })?;
53 metrics.insert("allocate_drop_ns", ns);
54
55 let bytes = 256 * 1024 * 1024;
56 let chunk = vec![0x3cu8; 4 * 1024 * 1024];
57 let weights = stream.allocate(bytes)?;
58 let (_, ns) = measure(&mut stream, samples, 1, |stream| {
59 for offset in (0..bytes).step_by(chunk.len()) {
60 stream.upload(weights.binding().slice(offset, chunk.len())?, &chunk)?;
61 }
62 Ok(())
63 })?;
64 metrics.insert(
65 "queued_upload_gib_s",
66 bytes as f64 / (1024f64.powi(3) * ns * 1e-9),
67 );
68 let mut check = [0u8; 16];
69 stream.read_blocking(
70 weights.try_slice(bytes - check.len(), check.len())?,
71 &mut check,
72 )?;
73 assert_eq!(check, [0x3c; 16]);
74 drop(weights);
75
76 let compiler = hrx::loom::Compiler::for_stream(None, &stream)?;
77 let module = compiler.module(include_str!("../tests/kernels/euler.loom"));
78 let mut spec = hrx::loom::Specialization::new("krea2_euler");
79 spec.set_config("krea2.euler.grid_x", "1");
80 spec.set_config("krea2.euler.grid_y", "1");
81 let artifact = module.compile(&spec)?;
82 // This repository owns the source; use its exact dimensions and scalar layout.
83 let kernel = unsafe { stream.load_artifact(&artifact)? };
84 let sample = stream.allocate(512)?;
85 let velocity = stream.allocate(512)?;
86 let ones: Vec<u8> = (0..256).flat_map(|_| 0x3f80u16.to_le_bytes()).collect();
87 stream.upload_blocking(sample.binding(), &ones)?;
88 stream.upload_blocking(velocity.binding(), &ones)?;
89 let mut constants = Constants::new();
90 match kernel.info().constant_byte_length {
91 8 => constants.push(256u32)?,
92 12 => constants.push(256u64)?,
93 n => return Err(hrx::Error::Message(format!("unexpected constant size {n}"))),
94 }
95 constants.push(0f32)?;
96 let bindings = [sample.binding(), velocity.binding()];
97 let (enqueue, complete) = measure(&mut stream, samples, 2048, |stream| {
98 for _ in 0..2048 {
99 // The source accesses exactly the 256 bf16 elements in each buffer.
100 unsafe {
101 stream.dispatch(&kernel, [1; 3], [256, 1, 1], &constants, &bindings)?;
102 }
103 }
104 Ok(())
105 })?;
106 metrics.insert("dispatch_enqueue_ns", enqueue);
107 metrics.insert("dispatch_complete_ns", complete);
108
109 // Use disjoint allocations in both graphs. Even an idempotent store is a
110 // write: concurrent Euler dispatches cannot share their in/out buffer.
111 let mut graph_buffers = Vec::new();
112 for _ in 0..32 {
113 let sample = stream.allocate(512)?;
114 let velocity = stream.allocate(512)?;
115 stream.upload(sample.binding(), &ones)?;
116 stream.upload(velocity.binding(), &ones)?;
117 graph_buffers.push((sample, velocity));
118 }
119 stream.synchronize()?;
120 let mut builder = stream.graph()?;
121 let mut previous = None;
122 for (sample, velocity) in &graph_buffers {
123 let after = previous.as_slice();
124 previous = Some(unsafe {
125 builder.dispatch(
126 after,
127 &kernel,
128 [1; 3],
129 [256, 1, 1],
130 &constants,
131 &[sample.binding(), velocity.binding()],
132 )?
133 });
134 }
135 let chained = builder.finish()?;
136
137 // The same disjoint workloads, this time allowing concurrent scheduling.
138 let mut builder = stream.graph()?;
139 for (sample, velocity) in &graph_buffers {
140 unsafe {
141 builder.dispatch(
142 &[],
143 &kernel,
144 [1; 3],
145 [256, 1, 1],
146 &constants,
147 &[sample.binding(), velocity.binding()],
148 )?;
149 }
150 }
151 let independent = builder.finish()?;
152 let mut graphs = [chained, independent];
153 let mut enqueue = [Vec::new(), Vec::new()];
154 let mut complete = [Vec::new(), Vec::new()];
155 for round in 0..samples + 3 {
156 for arm in if round % 2 == 0 { [0, 1] } else { [1, 0] } {
157 stream.synchronize()?;
158 let start = Instant::now();
159 for _ in 0..256 {
160 stream.launch(&mut graphs[arm])?;
161 }
162 let submitted = start.elapsed();
163 stream.synchronize()?;
164 let elapsed = start.elapsed();
165 if round >= 3 {
166 enqueue[arm].push(submitted.as_secs_f64() * 1e9 / 256.0);
167 complete[arm].push(elapsed.as_secs_f64() * 1e9 / (256.0 * 32.0));
168 }
169 }
170 }
171 for values in enqueue.iter_mut().chain(complete.iter_mut()) {
172 values.sort_by(f64::total_cmp);
173 }
174 let ns = complete[0][samples / 2];
175 let ns_free = complete[1][samples / 2];
176 metrics.insert("graph_enqueue_ns_per_replay", enqueue[0][samples / 2]);
177 metrics.insert("graph_complete_ns_per_kernel", ns);
178 metrics.insert("graph_independent_ns_per_kernel", ns_free);
179 metrics.insert("graph_scheduling_difference_ns_per_kernel", ns - ns_free);
180 for (sample, _) in &graph_buffers {
181 let mut output = vec![0; 512];
182 stream.read_blocking(sample.binding(), &mut output)?;
183 assert_eq!(output, ones);
184 }
185 let mut output = vec![0; 512];
186 stream.read_blocking(sample.binding(), &mut output)?;
187 assert_eq!(output, ones);
188 println!("{}", serde_json::to_string(&metrics)?);
189 Ok(())
190}Sourcepub fn read_blocking_at(
&mut self,
src: &Buffer,
offset: usize,
bytes: &mut [u8],
) -> Result<()>
pub fn read_blocking_at( &mut self, src: &Buffer, offset: usize, bytes: &mut [u8], ) -> Result<()>
Synchronously read at a checked byte offset from an allocation.
Sourcepub fn fill(&self, dst: View<'_>, value: u8) -> Result<()>
pub fn fill(&self, dst: View<'_>, value: u8) -> Result<()>
Queue a byte-pattern fill over a nonempty view owned by this stream.
Examples found in repository?
16fn main() -> Result<()> {
17 let mib: usize = match std::env::args().collect::<Vec<_>>().as_slice() {
18 [_] => 16,
19 [_, flag, value] if flag == "--mib" => value.parse().expect("--mib expects an integer"),
20 _ => {
21 eprintln!("usage: import_probe [--mib N]");
22 std::process::exit(2);
23 }
24 };
25 let bytes = mib * 1024 * 1024;
26 let layout = Layout::from_size_align(bytes, PAGE).expect("page-aligned layout");
27
28 // Owned for the whole scope below; the imported buffer never frees it.
29 let host = unsafe { alloc_zeroed(layout) };
30 assert!(!host.is_null(), "host allocation failed");
31 let result = (|| -> Result<()> {
32 let mut stream = Stream::open()?;
33 let buffer = match unsafe { stream.import_host(host.cast(), bytes) } {
34 Ok(buffer) => buffer,
35 Err(error) => {
36 println!("import: UNSUPPORTED ({error})");
37 return Ok(());
38 }
39 };
40 println!("import: ok ({mib} MiB at {host:p})");
41
42 // Prove the host pages start clear, so a match afterwards can only be the GPU.
43 let before = unsafe { std::slice::from_raw_parts(host, bytes) };
44 assert!(before.iter().all(|&b| b == 0), "host memory was not zeroed");
45
46 stream.fill(buffer.binding(), PATTERN)?;
47 stream.synchronize()?;
48
49 // Read the ORIGINAL host pointer -- no download, no staging buffer.
50 let after = unsafe { std::slice::from_raw_parts(host, bytes) };
51 let wrong = after.iter().filter(|&&b| b != PATTERN).count();
52 if wrong == 0 {
53 println!("gpu fill visible on host pointer: YES (zero-copy confirmed)");
54 } else {
55 println!("gpu fill visible on host pointer: NO ({wrong}/{bytes} bytes unwritten)");
56 }
57
58 // A device-side read of the same buffer must also see host writes.
59 let mirror = stream.allocate(bytes)?;
60 unsafe { std::slice::from_raw_parts_mut(host, bytes) }.fill(0x3c);
61 stream.copy(mirror.binding(), buffer.binding())?;
62 let mut check = [0u8; 64];
63 stream.read_blocking(mirror.binding().slice(0, check.len())?, &mut check)?;
64 let coherent = check.iter().all(|&b| b == 0x3c);
65 println!(
66 "host write visible to device: {}",
67 if coherent { "YES" } else { "NO" }
68 );
69 Ok(())
70 })();
71 unsafe { dealloc(host, layout) };
72 result
73}More examples
88fn main() -> Result<()> {
89 let options = match parse() {
90 Ok(options) => options,
91 Err(message) => {
92 eprintln!("gpu_load: {message}");
93 std::process::exit(2);
94 }
95 };
96 let size = options.mib * 1024 * 1024;
97 let mut stream = Stream::open()?;
98 let source = stream.allocate(size)?;
99 let destination = stream.allocate(size)?;
100 // Touch both allocations so the steady-state loop never pays first-use faults.
101 stream.fill(source.binding(), 0x3c)?;
102 stream.fill(destination.binding(), 0)?;
103 stream.synchronize()?;
104
105 let run_batch = |stream: &mut Stream| -> Result<()> {
106 for _ in 0..options.batch {
107 match options.mode {
108 Mode::Copy => stream.copy(destination.binding(), source.binding())?,
109 Mode::Fill => stream.fill(destination.binding(), 0x5a)?,
110 }
111 }
112 stream.synchronize()
113 };
114
115 // Warm up outside the measured window: the first batch pays queue and paging costs.
116 run_batch(&mut stream)?;
117
118 let window = Duration::from_secs_f64(options.seconds);
119 let start = Instant::now();
120 let mut iterations = 0usize;
121 // Per-batch rates, so the harness can see whether contention was steady or bursty.
122 let mut batch_rates = Vec::new();
123 while start.elapsed() < window {
124 let batch_start = Instant::now();
125 run_batch(&mut stream)?;
126 let seconds = batch_start.elapsed().as_secs_f64();
127 iterations += options.batch;
128 let bytes = options.mode.traffic(size) * options.batch;
129 batch_rates.push(bytes as f64 / seconds / 1e9);
130 }
131 let elapsed = start.elapsed().as_secs_f64();
132 let total = options.mode.traffic(size) as f64 * iterations as f64;
133 let mean = total / elapsed / 1e9;
134 batch_rates.sort_by(f64::total_cmp);
135 let median = batch_rates[batch_rates.len() / 2];
136 let low = batch_rates[batch_rates.len() / 20];
137
138 println!(
139 concat!(
140 r#"{{"device":"gpu","mode":"{}","buffer_mib":{},"batch":{},"#,
141 r#""seconds":{:.4},"iterations":{},"gb_s_mean":{:.3},"#,
142 r#""gb_s_median":{:.3},"gb_s_p5":{:.3}}}"#
143 ),
144 options.mode.name(),
145 options.mib,
146 options.batch,
147 elapsed,
148 iterations,
149 mean,
150 median,
151 low
152 );
153 eprintln!(
154 "gpu_load: {} {:.1} GB/s mean over {:.1}s ({} iterations)",
155 options.mode.name(),
156 mean,
157 elapsed,
158 iterations
159 );
160 Ok(())
161}81fn main() -> Result<()> {
82 let args: Vec<String> = std::env::args().skip(1).collect();
83 let (xclbin, mib) = match args.as_slice() {
84 [xclbin] => (xclbin.clone(), 4usize),
85 [xclbin, flag, value] if flag == "--mib" => (
86 xclbin.clone(),
87 value.parse().expect("--mib expects an integer"),
88 ),
89 _ => {
90 eprintln!("usage: unified_probe <xclbin> [--mib N]");
91 std::process::exit(2);
92 }
93 };
94 let bytes = mib * 1024 * 1024;
95 let layout = Layout::from_size_align(bytes, PAGE).expect("page-aligned layout");
96 let host = unsafe { alloc_zeroed(layout) };
97 assert!(!host.is_null(), "host allocation failed");
98
99 let result = (|| -> Result<()> {
100 println!("host allocation: {mib} MiB at {host:p}");
101
102 let context = match unsafe { dvxrt::Context::new(0, &xclbin) } {
103 Ok(context) => context,
104 Err(error) => {
105 println!("npu context: UNAVAILABLE ({error})");
106 return Ok(());
107 }
108 };
109 let group = context.group_id(ARG_A).expect("group id for A");
110
111 // 1. Can amdxdna pin pages we allocated? Reported either way -- if it can, either
112 // runtime may own the allocation; if it cannot, only the NPU's allocator may.
113 match unsafe { context.import_bo(host.cast(), bytes, dvxrt::BoKind::HostOnly, group) } {
114 Ok(_) => println!("npu userptr import (host-owned pages): YES"),
115 Err(error) => println!("npu userptr import (host-owned pages): NO ({error})"),
116 }
117
118 // 2. The direction that matters: let the NPU's allocator own the pages -- it is the
119 // more constrained of the two -- and import its host mapping into the GPU runtime.
120 let bo = context
121 .alloc_bo(bytes, dvxrt::BoKind::HostOnly, group)
122 .expect("allocate an NPU BO");
123 let shared = bo.map().expect("map the NPU BO");
124 println!("npu allocation mapped at {shared:p}");
125 assert_eq!(shared as usize % PAGE, 0, "NPU mapping is not page-aligned");
126
127 let mut stream = Stream::open()?;
128 let imported = unsafe { stream.import_host(shared.cast(), bytes) };
129 match &imported {
130 Ok(_) => println!("gpu import of the NPU mapping: YES"),
131 Err(error) => println!("gpu import of the NPU mapping: NO ({error})"),
132 }
133
134 // 3. If the shared mapping took, check that a GPU write lands in the NPU's BO.
135 if let Ok(buffer) = &imported {
136 stream.fill(buffer.binding(), GPU_PATTERN)?;
137 stream.synchronize()?;
138 bo.sync(false, bytes).expect("invalidate for device reads");
139 let mut seen = vec![0u8; 4096];
140 bo.read(&mut seen).expect("read through the NPU BO");
141 let wrong = seen.iter().filter(|&&b| b != GPU_PATTERN).count();
142 println!(
143 "gpu write -> npu read: {}",
144 if wrong == 0 {
145 "YES".into()
146 } else {
147 format!("NO ({wrong} bytes differ)")
148 }
149 );
150 }
151
152 // 3b. The dma-buf route: the GPU allocates, HSA exports the allocation as a
153 // dma-buf, and XRT imports that descriptor. Neither runtime copies, and unlike
154 // the host-pointer routes above neither driver has to pin the other's pages.
155 // The GPU-side import above HSA-locked these pages, so they are exportable.
156 let locked = unsafe { stream.import_host(host.cast(), bytes) };
157 let export = match &locked {
158 Ok(buffer) => {
159 stream.fill(buffer.binding(), GPU_PATTERN)?;
160 stream.synchronize()?;
161 export_dmabuf(host.cast(), bytes)
162 }
163 Err(error) => Err(format!("host pages are not GPU-resident: {error}")),
164 };
165 match export {
166 Ok((fd, size)) => {
167 println!("hsa dma-buf export of a GPU buffer: YES (fd {fd})");
168 match unsafe { context.import_dmabuf(fd, size) } {
169 Ok(imported) => {
170 println!("npu import of the GPU dma-buf: YES");
171 let mut seen = vec![0u8; 4096];
172 match imported.read(&mut seen) {
173 Ok(()) => {
174 let wrong = seen.iter().filter(|&&b| b != GPU_PATTERN).count();
175 println!(
176 " gpu write -> npu read over dma-buf: {}",
177 if wrong == 0 {
178 "YES".into()
179 } else {
180 format!("NO ({wrong} bytes differ)")
181 }
182 );
183 }
184 Err(error) => {
185 println!(" read through the imported BO failed: {error}")
186 }
187 }
188 }
189 Err(error) => println!("npu import of the GPU dma-buf: NO ({error})"),
190 }
191 }
192 Err(error) => println!("hsa dma-buf export of a GPU buffer: NO ({error})"),
193 }
194
195 // 4. And the reverse: the NPU's BO writes, the GPU reads.
196 if let Ok(buffer) = &imported {
197 bo.write(&vec![NPU_PATTERN; bytes])
198 .expect("write through the NPU BO");
199 let mirror = stream.allocate(bytes)?;
200 stream.copy(mirror.binding(), buffer.binding())?;
201 let mut check = [0u8; 64];
202 stream.read_blocking(mirror.binding().slice(0, check.len())?, &mut check)?;
203 let back = check.iter().all(|&b| b == NPU_PATTERN);
204 println!("npu write -> gpu read: {}", if back { "YES" } else { "NO" });
205 }
206 Ok(())
207 })();
208 unsafe { dealloc(host, layout) };
209 result
210}72fn main() -> Result<(), String> {
73 let args: Vec<String> = std::env::args().skip(1).collect();
74 let (xclbin, mib) = match args.as_slice() {
75 [xclbin] => (xclbin.clone(), 4usize),
76 [xclbin, flag, value] if flag == "--mib" => (
77 xclbin.clone(),
78 value.parse().map_err(|_| "--mib expects an integer")?,
79 ),
80 _ => return Err("usage: dmabuf_probe <xclbin> [--mib N]".into()),
81 };
82 let bytes = mib * 1024 * 1024;
83 let hsa = Hsa::open()?;
84
85 unsafe {
86 let init: libloading::Symbol<unsafe extern "C" fn() -> Status> =
87 hsa.symbol(b"hsa_init\0")?;
88 let status = init();
89 if status != HSA_STATUS_SUCCESS {
90 return Err(format!("hsa_init returned {status}"));
91 }
92
93 // --- find a GPU agent -------------------------------------------------
94 let iterate_agents: libloading::Symbol<
95 unsafe extern "C" fn(
96 unsafe extern "C" fn(u64, *mut c_void) -> Status,
97 *mut c_void,
98 ) -> Status,
99 > = hsa.symbol(b"hsa_iterate_agents\0")?;
100 let agent_info: libloading::Symbol<unsafe extern "C" fn(u64, u32, *mut c_void) -> Status> =
101 hsa.symbol(b"hsa_agent_get_info\0")?;
102
103 // The callback needs the symbol; pass it through the user-data pointer.
104 struct AgentScan<'a> {
105 info: &'a dyn Fn(u64, u32, *mut c_void) -> Status,
106 found: Found,
107 }
108 unsafe extern "C" fn on_agent(agent: u64, data: *mut c_void) -> Status {
109 let scan = unsafe { &mut *data.cast::<AgentScan>() };
110 let mut kind: u32 = 0;
111 if (scan.info)(agent, HSA_AGENT_INFO_DEVICE, (&raw mut kind).cast())
112 == HSA_STATUS_SUCCESS
113 && kind == HSA_DEVICE_TYPE_GPU
114 && scan.found.agent == 0
115 {
116 scan.found.agent = agent;
117 }
118 HSA_STATUS_SUCCESS
119 }
120 let info_fn = |agent, key, out| agent_info(agent, key, out);
121 let mut scan = AgentScan {
122 info: &info_fn,
123 found: Found::default(),
124 };
125 iterate_agents(on_agent, (&raw mut scan).cast());
126 if scan.found.agent == 0 {
127 return Err("no GPU agent found".into());
128 }
129 println!("gpu agent: 0x{:x}", scan.found.agent);
130
131 // --- find an allocatable global pool ----------------------------------
132 let iterate_pools: libloading::Symbol<
133 unsafe extern "C" fn(
134 u64,
135 unsafe extern "C" fn(u64, *mut c_void) -> Status,
136 *mut c_void,
137 ) -> Status,
138 > = hsa.symbol(b"hsa_amd_agent_iterate_memory_pools\0")?;
139 let pool_info: libloading::Symbol<unsafe extern "C" fn(u64, u32, *mut c_void) -> Status> =
140 hsa.symbol(b"hsa_amd_memory_pool_get_info\0")?;
141
142 struct PoolScan<'a> {
143 info: &'a dyn Fn(u64, u32, *mut c_void) -> Status,
144 found: Found,
145 }
146 unsafe extern "C" fn on_pool(pool: u64, data: *mut c_void) -> Status {
147 let scan = unsafe { &mut *data.cast::<PoolScan>() };
148 if scan.found.pool != 0 {
149 return HSA_STATUS_SUCCESS;
150 }
151 let mut segment: u32 = u32::MAX;
152 let mut allowed: bool = false;
153 let mut size: usize = 0;
154 (scan.info)(pool, POOL_INFO_SEGMENT, (&raw mut segment).cast());
155 (scan.info)(pool, POOL_INFO_ALLOC_ALLOWED, (&raw mut allowed).cast());
156 (scan.info)(pool, POOL_INFO_SIZE, (&raw mut size).cast());
157 if segment == SEGMENT_GLOBAL && allowed && size > 0 {
158 scan.found.pool = pool;
159 scan.found.pool_bytes = size;
160 }
161 HSA_STATUS_SUCCESS
162 }
163 let pool_fn = |pool, key, out| pool_info(pool, key, out);
164 let mut pools = PoolScan {
165 info: &pool_fn,
166 found: Found::default(),
167 };
168 iterate_pools(scan.found.agent, on_pool, (&raw mut pools).cast());
169 if pools.found.pool == 0 {
170 return Err("no allocatable global memory pool on the GPU agent".into());
171 }
172 println!(
173 "gpu memory pool: 0x{:x} ({} MiB)",
174 pools.found.pool,
175 pools.found.pool_bytes / (1024 * 1024)
176 );
177
178 // --- allocate from it and export a dma-buf ----------------------------
179 let allocate: libloading::Symbol<
180 unsafe extern "C" fn(u64, usize, u32, *mut *mut c_void) -> Status,
181 > = hsa.symbol(b"hsa_amd_memory_pool_allocate\0")?;
182 let mut pointer: *mut c_void = std::ptr::null_mut();
183 let status = allocate(pools.found.pool, bytes, 0, &raw mut pointer);
184 if status != HSA_STATUS_SUCCESS {
185 return Err(format!("hsa_amd_memory_pool_allocate returned {status}"));
186 }
187 println!("pool allocation: {mib} MiB at {pointer:p}");
188
189 // The route that would make a shared buffer a first-class hrx buffer: allocate it
190 // through the GPU runtime, take its device pointer, and export that.
191 match hrx::Stream::open() {
192 Ok(mut stream) => match stream.allocate_shared(bytes) {
193 Ok(buffer) => {
194 stream.fill(buffer.binding(), 0x11).ok();
195 stream.synchronize().ok();
196 match buffer.device_ptr() {
197 Ok(hrx_pointer) => {
198 println!("hrx shared allocation: ok, device ptr {hrx_pointer:p}");
199 let mut fd: i32 = -1;
200 let mut offset: u64 = 0;
201 let export: libloading::Symbol<
202 unsafe extern "C" fn(
203 *const c_void,
204 usize,
205 *mut i32,
206 *mut u64,
207 ) -> Status,
208 > = hsa.symbol(b"hsa_amd_portable_export_dmabuf\0")?;
209 let status = export(hrx_pointer, bytes, &raw mut fd, &raw mut offset);
210 if status != HSA_STATUS_SUCCESS {
211 println!(" dma-buf export of an hrx buffer: NO (status {status})");
212 } else {
213 println!(
214 " dma-buf export of an hrx buffer: YES (fd {fd}, offset {offset})"
215 );
216 match dvxrt::Context::new(0, &xclbin) {
217 Ok(context) => match context.import_dmabuf(fd, bytes) {
218 Ok(bo) => {
219 let mut seen = vec![0u8; 4096];
220 bo.read(&mut seen).ok();
221 let wrong = seen.iter().filter(|&&b| b != 0x11).count();
222 println!(
223 " npu reads the hrx buffer: {}",
224 if wrong == 0 {
225 "YES -- one buffer, both engines".into()
226 } else {
227 format!("NO ({wrong}/4096 differ)")
228 }
229 );
230 }
231 Err(error) => println!(" npu import: NO ({error})"),
232 },
233 Err(error) => println!(" npu context: {error}"),
234 }
235 }
236 }
237 Err(error) => println!("hrx shared allocation device ptr: NO ({error})"),
238 }
239 }
240 Err(error) => println!("hrx shared allocation: NO ({error})"),
241 },
242 Err(error) => println!("hrx stream unavailable: {error}"),
243 }
244
245 let export: libloading::Symbol<
246 unsafe extern "C" fn(*const c_void, usize, *mut i32, *mut u64) -> Status,
247 > = hsa.symbol(b"hsa_amd_portable_export_dmabuf\0")?;
248 let mut fd: i32 = -1;
249 let mut offset: u64 = 0;
250 let status = export(pointer, bytes, &raw mut fd, &raw mut offset);
251 if status != HSA_STATUS_SUCCESS {
252 println!("hsa dma-buf export of pool memory: NO (status {status})");
253 return Ok(());
254 }
255 println!("hsa dma-buf export of pool memory: YES (fd {fd}, offset {offset})");
256
257 // --- and hand that descriptor to the NPU ------------------------------
258 match dvxrt::Context::new(0, &xclbin) {
259 Ok(context) => {
260 let _group = context.group_id(ARG_A).map_err(|e| e.to_string())?;
261 match context.import_dmabuf(fd, bytes) {
262 Ok(imported) => {
263 println!("npu import of the GPU dma-buf: YES");
264 // Import alone only proves the descriptor was accepted. Write with
265 // the GPU and read through the NPU's BO to prove it is one buffer.
266 let fill: libloading::Symbol<
267 unsafe extern "C" fn(*mut c_void, u32, usize) -> Status,
268 > = hsa.symbol(b"hsa_amd_memory_fill\0")?;
269 let pattern = 0xa5a5_a5a5u32;
270 let status = fill(pointer, pattern, bytes / 4);
271 if status != HSA_STATUS_SUCCESS {
272 println!(" gpu fill of pool memory failed (status {status})");
273 return Ok(());
274 }
275 let mut seen = vec![0u8; 4096];
276 match imported.read(&mut seen) {
277 Ok(()) => {
278 let wrong = seen.iter().filter(|&&b| b != 0xa5).count();
279 if wrong == 0 {
280 println!(
281 " gpu write -> npu read over dma-buf: YES -- zero-copy confirmed"
282 );
283 } else {
284 println!(
285 " gpu write -> npu read over dma-buf: NO ({wrong}/4096 bytes differ)"
286 );
287 }
288 }
289 Err(error) => {
290 println!(" read through the imported BO failed: {error}")
291 }
292 }
293 }
294 Err(error) => println!("npu import of the GPU dma-buf: NO ({error})"),
295 }
296 }
297 Err(error) => println!("npu context: UNAVAILABLE ({error})"),
298 }
299 }
300 Ok(())
301}Sourcepub fn copy(&self, dst: View<'_>, src: View<'_>) -> Result<()>
pub fn copy(&self, dst: View<'_>, src: View<'_>) -> Result<()>
Queue a copy between equal, nonempty views owned by this stream.
Examples found in repository?
16fn main() -> Result<()> {
17 let mib: usize = match std::env::args().collect::<Vec<_>>().as_slice() {
18 [_] => 16,
19 [_, flag, value] if flag == "--mib" => value.parse().expect("--mib expects an integer"),
20 _ => {
21 eprintln!("usage: import_probe [--mib N]");
22 std::process::exit(2);
23 }
24 };
25 let bytes = mib * 1024 * 1024;
26 let layout = Layout::from_size_align(bytes, PAGE).expect("page-aligned layout");
27
28 // Owned for the whole scope below; the imported buffer never frees it.
29 let host = unsafe { alloc_zeroed(layout) };
30 assert!(!host.is_null(), "host allocation failed");
31 let result = (|| -> Result<()> {
32 let mut stream = Stream::open()?;
33 let buffer = match unsafe { stream.import_host(host.cast(), bytes) } {
34 Ok(buffer) => buffer,
35 Err(error) => {
36 println!("import: UNSUPPORTED ({error})");
37 return Ok(());
38 }
39 };
40 println!("import: ok ({mib} MiB at {host:p})");
41
42 // Prove the host pages start clear, so a match afterwards can only be the GPU.
43 let before = unsafe { std::slice::from_raw_parts(host, bytes) };
44 assert!(before.iter().all(|&b| b == 0), "host memory was not zeroed");
45
46 stream.fill(buffer.binding(), PATTERN)?;
47 stream.synchronize()?;
48
49 // Read the ORIGINAL host pointer -- no download, no staging buffer.
50 let after = unsafe { std::slice::from_raw_parts(host, bytes) };
51 let wrong = after.iter().filter(|&&b| b != PATTERN).count();
52 if wrong == 0 {
53 println!("gpu fill visible on host pointer: YES (zero-copy confirmed)");
54 } else {
55 println!("gpu fill visible on host pointer: NO ({wrong}/{bytes} bytes unwritten)");
56 }
57
58 // A device-side read of the same buffer must also see host writes.
59 let mirror = stream.allocate(bytes)?;
60 unsafe { std::slice::from_raw_parts_mut(host, bytes) }.fill(0x3c);
61 stream.copy(mirror.binding(), buffer.binding())?;
62 let mut check = [0u8; 64];
63 stream.read_blocking(mirror.binding().slice(0, check.len())?, &mut check)?;
64 let coherent = check.iter().all(|&b| b == 0x3c);
65 println!(
66 "host write visible to device: {}",
67 if coherent { "YES" } else { "NO" }
68 );
69 Ok(())
70 })();
71 unsafe { dealloc(host, layout) };
72 result
73}More examples
88fn main() -> Result<()> {
89 let options = match parse() {
90 Ok(options) => options,
91 Err(message) => {
92 eprintln!("gpu_load: {message}");
93 std::process::exit(2);
94 }
95 };
96 let size = options.mib * 1024 * 1024;
97 let mut stream = Stream::open()?;
98 let source = stream.allocate(size)?;
99 let destination = stream.allocate(size)?;
100 // Touch both allocations so the steady-state loop never pays first-use faults.
101 stream.fill(source.binding(), 0x3c)?;
102 stream.fill(destination.binding(), 0)?;
103 stream.synchronize()?;
104
105 let run_batch = |stream: &mut Stream| -> Result<()> {
106 for _ in 0..options.batch {
107 match options.mode {
108 Mode::Copy => stream.copy(destination.binding(), source.binding())?,
109 Mode::Fill => stream.fill(destination.binding(), 0x5a)?,
110 }
111 }
112 stream.synchronize()
113 };
114
115 // Warm up outside the measured window: the first batch pays queue and paging costs.
116 run_batch(&mut stream)?;
117
118 let window = Duration::from_secs_f64(options.seconds);
119 let start = Instant::now();
120 let mut iterations = 0usize;
121 // Per-batch rates, so the harness can see whether contention was steady or bursty.
122 let mut batch_rates = Vec::new();
123 while start.elapsed() < window {
124 let batch_start = Instant::now();
125 run_batch(&mut stream)?;
126 let seconds = batch_start.elapsed().as_secs_f64();
127 iterations += options.batch;
128 let bytes = options.mode.traffic(size) * options.batch;
129 batch_rates.push(bytes as f64 / seconds / 1e9);
130 }
131 let elapsed = start.elapsed().as_secs_f64();
132 let total = options.mode.traffic(size) as f64 * iterations as f64;
133 let mean = total / elapsed / 1e9;
134 batch_rates.sort_by(f64::total_cmp);
135 let median = batch_rates[batch_rates.len() / 2];
136 let low = batch_rates[batch_rates.len() / 20];
137
138 println!(
139 concat!(
140 r#"{{"device":"gpu","mode":"{}","buffer_mib":{},"batch":{},"#,
141 r#""seconds":{:.4},"iterations":{},"gb_s_mean":{:.3},"#,
142 r#""gb_s_median":{:.3},"gb_s_p5":{:.3}}}"#
143 ),
144 options.mode.name(),
145 options.mib,
146 options.batch,
147 elapsed,
148 iterations,
149 mean,
150 median,
151 low
152 );
153 eprintln!(
154 "gpu_load: {} {:.1} GB/s mean over {:.1}s ({} iterations)",
155 options.mode.name(),
156 mean,
157 elapsed,
158 iterations
159 );
160 Ok(())
161}81fn main() -> Result<()> {
82 let args: Vec<String> = std::env::args().skip(1).collect();
83 let (xclbin, mib) = match args.as_slice() {
84 [xclbin] => (xclbin.clone(), 4usize),
85 [xclbin, flag, value] if flag == "--mib" => (
86 xclbin.clone(),
87 value.parse().expect("--mib expects an integer"),
88 ),
89 _ => {
90 eprintln!("usage: unified_probe <xclbin> [--mib N]");
91 std::process::exit(2);
92 }
93 };
94 let bytes = mib * 1024 * 1024;
95 let layout = Layout::from_size_align(bytes, PAGE).expect("page-aligned layout");
96 let host = unsafe { alloc_zeroed(layout) };
97 assert!(!host.is_null(), "host allocation failed");
98
99 let result = (|| -> Result<()> {
100 println!("host allocation: {mib} MiB at {host:p}");
101
102 let context = match unsafe { dvxrt::Context::new(0, &xclbin) } {
103 Ok(context) => context,
104 Err(error) => {
105 println!("npu context: UNAVAILABLE ({error})");
106 return Ok(());
107 }
108 };
109 let group = context.group_id(ARG_A).expect("group id for A");
110
111 // 1. Can amdxdna pin pages we allocated? Reported either way -- if it can, either
112 // runtime may own the allocation; if it cannot, only the NPU's allocator may.
113 match unsafe { context.import_bo(host.cast(), bytes, dvxrt::BoKind::HostOnly, group) } {
114 Ok(_) => println!("npu userptr import (host-owned pages): YES"),
115 Err(error) => println!("npu userptr import (host-owned pages): NO ({error})"),
116 }
117
118 // 2. The direction that matters: let the NPU's allocator own the pages -- it is the
119 // more constrained of the two -- and import its host mapping into the GPU runtime.
120 let bo = context
121 .alloc_bo(bytes, dvxrt::BoKind::HostOnly, group)
122 .expect("allocate an NPU BO");
123 let shared = bo.map().expect("map the NPU BO");
124 println!("npu allocation mapped at {shared:p}");
125 assert_eq!(shared as usize % PAGE, 0, "NPU mapping is not page-aligned");
126
127 let mut stream = Stream::open()?;
128 let imported = unsafe { stream.import_host(shared.cast(), bytes) };
129 match &imported {
130 Ok(_) => println!("gpu import of the NPU mapping: YES"),
131 Err(error) => println!("gpu import of the NPU mapping: NO ({error})"),
132 }
133
134 // 3. If the shared mapping took, check that a GPU write lands in the NPU's BO.
135 if let Ok(buffer) = &imported {
136 stream.fill(buffer.binding(), GPU_PATTERN)?;
137 stream.synchronize()?;
138 bo.sync(false, bytes).expect("invalidate for device reads");
139 let mut seen = vec![0u8; 4096];
140 bo.read(&mut seen).expect("read through the NPU BO");
141 let wrong = seen.iter().filter(|&&b| b != GPU_PATTERN).count();
142 println!(
143 "gpu write -> npu read: {}",
144 if wrong == 0 {
145 "YES".into()
146 } else {
147 format!("NO ({wrong} bytes differ)")
148 }
149 );
150 }
151
152 // 3b. The dma-buf route: the GPU allocates, HSA exports the allocation as a
153 // dma-buf, and XRT imports that descriptor. Neither runtime copies, and unlike
154 // the host-pointer routes above neither driver has to pin the other's pages.
155 // The GPU-side import above HSA-locked these pages, so they are exportable.
156 let locked = unsafe { stream.import_host(host.cast(), bytes) };
157 let export = match &locked {
158 Ok(buffer) => {
159 stream.fill(buffer.binding(), GPU_PATTERN)?;
160 stream.synchronize()?;
161 export_dmabuf(host.cast(), bytes)
162 }
163 Err(error) => Err(format!("host pages are not GPU-resident: {error}")),
164 };
165 match export {
166 Ok((fd, size)) => {
167 println!("hsa dma-buf export of a GPU buffer: YES (fd {fd})");
168 match unsafe { context.import_dmabuf(fd, size) } {
169 Ok(imported) => {
170 println!("npu import of the GPU dma-buf: YES");
171 let mut seen = vec![0u8; 4096];
172 match imported.read(&mut seen) {
173 Ok(()) => {
174 let wrong = seen.iter().filter(|&&b| b != GPU_PATTERN).count();
175 println!(
176 " gpu write -> npu read over dma-buf: {}",
177 if wrong == 0 {
178 "YES".into()
179 } else {
180 format!("NO ({wrong} bytes differ)")
181 }
182 );
183 }
184 Err(error) => {
185 println!(" read through the imported BO failed: {error}")
186 }
187 }
188 }
189 Err(error) => println!("npu import of the GPU dma-buf: NO ({error})"),
190 }
191 }
192 Err(error) => println!("hsa dma-buf export of a GPU buffer: NO ({error})"),
193 }
194
195 // 4. And the reverse: the NPU's BO writes, the GPU reads.
196 if let Ok(buffer) = &imported {
197 bo.write(&vec![NPU_PATTERN; bytes])
198 .expect("write through the NPU BO");
199 let mirror = stream.allocate(bytes)?;
200 stream.copy(mirror.binding(), buffer.binding())?;
201 let mut check = [0u8; 64];
202 stream.read_blocking(mirror.binding().slice(0, check.len())?, &mut check)?;
203 let back = check.iter().all(|&b| b == NPU_PATTERN);
204 println!("npu write -> gpu read: {}", if back { "YES" } else { "NO" });
205 }
206 Ok(())
207 })();
208 unsafe { dealloc(host, layout) };
209 result
210}Sourcepub fn upload(&mut self, dst: View<'_>, bytes: &[u8]) -> Result<()>
pub fn upload(&mut self, dst: View<'_>, bytes: &[u8]) -> Result<()>
Copy the input into runtime-owned staging, then enqueue an actual GPU buffer copy. The caller’s slice is no longer referenced when this returns. Bytes are written at the start of the view and must fit within it. Staging pressure may submit or wait for prior work to bound memory use.
Examples found in repository?
30fn main() -> Result<()> {
31 let mut stream = Stream::open()?;
32 let mut metrics = BTreeMap::new();
33 let samples = std::env::var("HRX_BENCH_SAMPLES")
34 .ok()
35 .and_then(|value| value.parse::<usize>().ok())
36 .filter(|&value| value > 0)
37 .unwrap_or(9);
38 let (ns, _) = measure(&mut stream, samples, 50_000, |stream| {
39 for _ in 0..50_000 {
40 let buffer = stream.scratch(1024 * 1024)?;
41 black_box(&buffer);
42 stream.recycle(buffer)?;
43 }
44 Ok(())
45 })?;
46 metrics.insert("scratch_reuse_ns", ns);
47 let (ns, _) = measure(&mut stream, samples, 2_000, |stream| {
48 for _ in 0..2_000 {
49 black_box(stream.allocate(1024 * 1024)?);
50 }
51 Ok(())
52 })?;
53 metrics.insert("allocate_drop_ns", ns);
54
55 let bytes = 256 * 1024 * 1024;
56 let chunk = vec![0x3cu8; 4 * 1024 * 1024];
57 let weights = stream.allocate(bytes)?;
58 let (_, ns) = measure(&mut stream, samples, 1, |stream| {
59 for offset in (0..bytes).step_by(chunk.len()) {
60 stream.upload(weights.binding().slice(offset, chunk.len())?, &chunk)?;
61 }
62 Ok(())
63 })?;
64 metrics.insert(
65 "queued_upload_gib_s",
66 bytes as f64 / (1024f64.powi(3) * ns * 1e-9),
67 );
68 let mut check = [0u8; 16];
69 stream.read_blocking(
70 weights.try_slice(bytes - check.len(), check.len())?,
71 &mut check,
72 )?;
73 assert_eq!(check, [0x3c; 16]);
74 drop(weights);
75
76 let compiler = hrx::loom::Compiler::for_stream(None, &stream)?;
77 let module = compiler.module(include_str!("../tests/kernels/euler.loom"));
78 let mut spec = hrx::loom::Specialization::new("krea2_euler");
79 spec.set_config("krea2.euler.grid_x", "1");
80 spec.set_config("krea2.euler.grid_y", "1");
81 let artifact = module.compile(&spec)?;
82 // This repository owns the source; use its exact dimensions and scalar layout.
83 let kernel = unsafe { stream.load_artifact(&artifact)? };
84 let sample = stream.allocate(512)?;
85 let velocity = stream.allocate(512)?;
86 let ones: Vec<u8> = (0..256).flat_map(|_| 0x3f80u16.to_le_bytes()).collect();
87 stream.upload_blocking(sample.binding(), &ones)?;
88 stream.upload_blocking(velocity.binding(), &ones)?;
89 let mut constants = Constants::new();
90 match kernel.info().constant_byte_length {
91 8 => constants.push(256u32)?,
92 12 => constants.push(256u64)?,
93 n => return Err(hrx::Error::Message(format!("unexpected constant size {n}"))),
94 }
95 constants.push(0f32)?;
96 let bindings = [sample.binding(), velocity.binding()];
97 let (enqueue, complete) = measure(&mut stream, samples, 2048, |stream| {
98 for _ in 0..2048 {
99 // The source accesses exactly the 256 bf16 elements in each buffer.
100 unsafe {
101 stream.dispatch(&kernel, [1; 3], [256, 1, 1], &constants, &bindings)?;
102 }
103 }
104 Ok(())
105 })?;
106 metrics.insert("dispatch_enqueue_ns", enqueue);
107 metrics.insert("dispatch_complete_ns", complete);
108
109 // Use disjoint allocations in both graphs. Even an idempotent store is a
110 // write: concurrent Euler dispatches cannot share their in/out buffer.
111 let mut graph_buffers = Vec::new();
112 for _ in 0..32 {
113 let sample = stream.allocate(512)?;
114 let velocity = stream.allocate(512)?;
115 stream.upload(sample.binding(), &ones)?;
116 stream.upload(velocity.binding(), &ones)?;
117 graph_buffers.push((sample, velocity));
118 }
119 stream.synchronize()?;
120 let mut builder = stream.graph()?;
121 let mut previous = None;
122 for (sample, velocity) in &graph_buffers {
123 let after = previous.as_slice();
124 previous = Some(unsafe {
125 builder.dispatch(
126 after,
127 &kernel,
128 [1; 3],
129 [256, 1, 1],
130 &constants,
131 &[sample.binding(), velocity.binding()],
132 )?
133 });
134 }
135 let chained = builder.finish()?;
136
137 // The same disjoint workloads, this time allowing concurrent scheduling.
138 let mut builder = stream.graph()?;
139 for (sample, velocity) in &graph_buffers {
140 unsafe {
141 builder.dispatch(
142 &[],
143 &kernel,
144 [1; 3],
145 [256, 1, 1],
146 &constants,
147 &[sample.binding(), velocity.binding()],
148 )?;
149 }
150 }
151 let independent = builder.finish()?;
152 let mut graphs = [chained, independent];
153 let mut enqueue = [Vec::new(), Vec::new()];
154 let mut complete = [Vec::new(), Vec::new()];
155 for round in 0..samples + 3 {
156 for arm in if round % 2 == 0 { [0, 1] } else { [1, 0] } {
157 stream.synchronize()?;
158 let start = Instant::now();
159 for _ in 0..256 {
160 stream.launch(&mut graphs[arm])?;
161 }
162 let submitted = start.elapsed();
163 stream.synchronize()?;
164 let elapsed = start.elapsed();
165 if round >= 3 {
166 enqueue[arm].push(submitted.as_secs_f64() * 1e9 / 256.0);
167 complete[arm].push(elapsed.as_secs_f64() * 1e9 / (256.0 * 32.0));
168 }
169 }
170 }
171 for values in enqueue.iter_mut().chain(complete.iter_mut()) {
172 values.sort_by(f64::total_cmp);
173 }
174 let ns = complete[0][samples / 2];
175 let ns_free = complete[1][samples / 2];
176 metrics.insert("graph_enqueue_ns_per_replay", enqueue[0][samples / 2]);
177 metrics.insert("graph_complete_ns_per_kernel", ns);
178 metrics.insert("graph_independent_ns_per_kernel", ns_free);
179 metrics.insert("graph_scheduling_difference_ns_per_kernel", ns - ns_free);
180 for (sample, _) in &graph_buffers {
181 let mut output = vec![0; 512];
182 stream.read_blocking(sample.binding(), &mut output)?;
183 assert_eq!(output, ones);
184 }
185 let mut output = vec![0; 512];
186 stream.read_blocking(sample.binding(), &mut output)?;
187 assert_eq!(output, ones);
188 println!("{}", serde_json::to_string(&metrics)?);
189 Ok(())
190}Sourcepub fn upload_at(
&mut self,
dst: &Buffer,
offset: usize,
bytes: &[u8],
) -> Result<()>
pub fn upload_at( &mut self, dst: &Buffer, offset: usize, bytes: &[u8], ) -> Result<()>
Enqueue an upload at a checked byte offset into an allocation.
Allocate a host-local, device-visible buffer.
Unlike Stream::allocate, the runtime hands out a device pointer for these, so
the allocation can be exported to another driver – which is what lets one buffer
serve both the GPU and the NPU. Device-local memory is faster for GPU-only work.
Examples found in repository?
72fn main() -> Result<(), String> {
73 let args: Vec<String> = std::env::args().skip(1).collect();
74 let (xclbin, mib) = match args.as_slice() {
75 [xclbin] => (xclbin.clone(), 4usize),
76 [xclbin, flag, value] if flag == "--mib" => (
77 xclbin.clone(),
78 value.parse().map_err(|_| "--mib expects an integer")?,
79 ),
80 _ => return Err("usage: dmabuf_probe <xclbin> [--mib N]".into()),
81 };
82 let bytes = mib * 1024 * 1024;
83 let hsa = Hsa::open()?;
84
85 unsafe {
86 let init: libloading::Symbol<unsafe extern "C" fn() -> Status> =
87 hsa.symbol(b"hsa_init\0")?;
88 let status = init();
89 if status != HSA_STATUS_SUCCESS {
90 return Err(format!("hsa_init returned {status}"));
91 }
92
93 // --- find a GPU agent -------------------------------------------------
94 let iterate_agents: libloading::Symbol<
95 unsafe extern "C" fn(
96 unsafe extern "C" fn(u64, *mut c_void) -> Status,
97 *mut c_void,
98 ) -> Status,
99 > = hsa.symbol(b"hsa_iterate_agents\0")?;
100 let agent_info: libloading::Symbol<unsafe extern "C" fn(u64, u32, *mut c_void) -> Status> =
101 hsa.symbol(b"hsa_agent_get_info\0")?;
102
103 // The callback needs the symbol; pass it through the user-data pointer.
104 struct AgentScan<'a> {
105 info: &'a dyn Fn(u64, u32, *mut c_void) -> Status,
106 found: Found,
107 }
108 unsafe extern "C" fn on_agent(agent: u64, data: *mut c_void) -> Status {
109 let scan = unsafe { &mut *data.cast::<AgentScan>() };
110 let mut kind: u32 = 0;
111 if (scan.info)(agent, HSA_AGENT_INFO_DEVICE, (&raw mut kind).cast())
112 == HSA_STATUS_SUCCESS
113 && kind == HSA_DEVICE_TYPE_GPU
114 && scan.found.agent == 0
115 {
116 scan.found.agent = agent;
117 }
118 HSA_STATUS_SUCCESS
119 }
120 let info_fn = |agent, key, out| agent_info(agent, key, out);
121 let mut scan = AgentScan {
122 info: &info_fn,
123 found: Found::default(),
124 };
125 iterate_agents(on_agent, (&raw mut scan).cast());
126 if scan.found.agent == 0 {
127 return Err("no GPU agent found".into());
128 }
129 println!("gpu agent: 0x{:x}", scan.found.agent);
130
131 // --- find an allocatable global pool ----------------------------------
132 let iterate_pools: libloading::Symbol<
133 unsafe extern "C" fn(
134 u64,
135 unsafe extern "C" fn(u64, *mut c_void) -> Status,
136 *mut c_void,
137 ) -> Status,
138 > = hsa.symbol(b"hsa_amd_agent_iterate_memory_pools\0")?;
139 let pool_info: libloading::Symbol<unsafe extern "C" fn(u64, u32, *mut c_void) -> Status> =
140 hsa.symbol(b"hsa_amd_memory_pool_get_info\0")?;
141
142 struct PoolScan<'a> {
143 info: &'a dyn Fn(u64, u32, *mut c_void) -> Status,
144 found: Found,
145 }
146 unsafe extern "C" fn on_pool(pool: u64, data: *mut c_void) -> Status {
147 let scan = unsafe { &mut *data.cast::<PoolScan>() };
148 if scan.found.pool != 0 {
149 return HSA_STATUS_SUCCESS;
150 }
151 let mut segment: u32 = u32::MAX;
152 let mut allowed: bool = false;
153 let mut size: usize = 0;
154 (scan.info)(pool, POOL_INFO_SEGMENT, (&raw mut segment).cast());
155 (scan.info)(pool, POOL_INFO_ALLOC_ALLOWED, (&raw mut allowed).cast());
156 (scan.info)(pool, POOL_INFO_SIZE, (&raw mut size).cast());
157 if segment == SEGMENT_GLOBAL && allowed && size > 0 {
158 scan.found.pool = pool;
159 scan.found.pool_bytes = size;
160 }
161 HSA_STATUS_SUCCESS
162 }
163 let pool_fn = |pool, key, out| pool_info(pool, key, out);
164 let mut pools = PoolScan {
165 info: &pool_fn,
166 found: Found::default(),
167 };
168 iterate_pools(scan.found.agent, on_pool, (&raw mut pools).cast());
169 if pools.found.pool == 0 {
170 return Err("no allocatable global memory pool on the GPU agent".into());
171 }
172 println!(
173 "gpu memory pool: 0x{:x} ({} MiB)",
174 pools.found.pool,
175 pools.found.pool_bytes / (1024 * 1024)
176 );
177
178 // --- allocate from it and export a dma-buf ----------------------------
179 let allocate: libloading::Symbol<
180 unsafe extern "C" fn(u64, usize, u32, *mut *mut c_void) -> Status,
181 > = hsa.symbol(b"hsa_amd_memory_pool_allocate\0")?;
182 let mut pointer: *mut c_void = std::ptr::null_mut();
183 let status = allocate(pools.found.pool, bytes, 0, &raw mut pointer);
184 if status != HSA_STATUS_SUCCESS {
185 return Err(format!("hsa_amd_memory_pool_allocate returned {status}"));
186 }
187 println!("pool allocation: {mib} MiB at {pointer:p}");
188
189 // The route that would make a shared buffer a first-class hrx buffer: allocate it
190 // through the GPU runtime, take its device pointer, and export that.
191 match hrx::Stream::open() {
192 Ok(mut stream) => match stream.allocate_shared(bytes) {
193 Ok(buffer) => {
194 stream.fill(buffer.binding(), 0x11).ok();
195 stream.synchronize().ok();
196 match buffer.device_ptr() {
197 Ok(hrx_pointer) => {
198 println!("hrx shared allocation: ok, device ptr {hrx_pointer:p}");
199 let mut fd: i32 = -1;
200 let mut offset: u64 = 0;
201 let export: libloading::Symbol<
202 unsafe extern "C" fn(
203 *const c_void,
204 usize,
205 *mut i32,
206 *mut u64,
207 ) -> Status,
208 > = hsa.symbol(b"hsa_amd_portable_export_dmabuf\0")?;
209 let status = export(hrx_pointer, bytes, &raw mut fd, &raw mut offset);
210 if status != HSA_STATUS_SUCCESS {
211 println!(" dma-buf export of an hrx buffer: NO (status {status})");
212 } else {
213 println!(
214 " dma-buf export of an hrx buffer: YES (fd {fd}, offset {offset})"
215 );
216 match dvxrt::Context::new(0, &xclbin) {
217 Ok(context) => match context.import_dmabuf(fd, bytes) {
218 Ok(bo) => {
219 let mut seen = vec![0u8; 4096];
220 bo.read(&mut seen).ok();
221 let wrong = seen.iter().filter(|&&b| b != 0x11).count();
222 println!(
223 " npu reads the hrx buffer: {}",
224 if wrong == 0 {
225 "YES -- one buffer, both engines".into()
226 } else {
227 format!("NO ({wrong}/4096 differ)")
228 }
229 );
230 }
231 Err(error) => println!(" npu import: NO ({error})"),
232 },
233 Err(error) => println!(" npu context: {error}"),
234 }
235 }
236 }
237 Err(error) => println!("hrx shared allocation device ptr: NO ({error})"),
238 }
239 }
240 Err(error) => println!("hrx shared allocation: NO ({error})"),
241 },
242 Err(error) => println!("hrx stream unavailable: {error}"),
243 }
244
245 let export: libloading::Symbol<
246 unsafe extern "C" fn(*const c_void, usize, *mut i32, *mut u64) -> Status,
247 > = hsa.symbol(b"hsa_amd_portable_export_dmabuf\0")?;
248 let mut fd: i32 = -1;
249 let mut offset: u64 = 0;
250 let status = export(pointer, bytes, &raw mut fd, &raw mut offset);
251 if status != HSA_STATUS_SUCCESS {
252 println!("hsa dma-buf export of pool memory: NO (status {status})");
253 return Ok(());
254 }
255 println!("hsa dma-buf export of pool memory: YES (fd {fd}, offset {offset})");
256
257 // --- and hand that descriptor to the NPU ------------------------------
258 match dvxrt::Context::new(0, &xclbin) {
259 Ok(context) => {
260 let _group = context.group_id(ARG_A).map_err(|e| e.to_string())?;
261 match context.import_dmabuf(fd, bytes) {
262 Ok(imported) => {
263 println!("npu import of the GPU dma-buf: YES");
264 // Import alone only proves the descriptor was accepted. Write with
265 // the GPU and read through the NPU's BO to prove it is one buffer.
266 let fill: libloading::Symbol<
267 unsafe extern "C" fn(*mut c_void, u32, usize) -> Status,
268 > = hsa.symbol(b"hsa_amd_memory_fill\0")?;
269 let pattern = 0xa5a5_a5a5u32;
270 let status = fill(pointer, pattern, bytes / 4);
271 if status != HSA_STATUS_SUCCESS {
272 println!(" gpu fill of pool memory failed (status {status})");
273 return Ok(());
274 }
275 let mut seen = vec![0u8; 4096];
276 match imported.read(&mut seen) {
277 Ok(()) => {
278 let wrong = seen.iter().filter(|&&b| b != 0xa5).count();
279 if wrong == 0 {
280 println!(
281 " gpu write -> npu read over dma-buf: YES -- zero-copy confirmed"
282 );
283 } else {
284 println!(
285 " gpu write -> npu read over dma-buf: NO ({wrong}/4096 bytes differ)"
286 );
287 }
288 }
289 Err(error) => {
290 println!(" read through the imported BO failed: {error}")
291 }
292 }
293 }
294 Err(error) => println!("npu import of the GPU dma-buf: NO ({error})"),
295 }
296 }
297 Err(error) => println!("npu context: UNAVAILABLE ({error})"),
298 }
299 }
300 Ok(())
301}Sourcepub fn submit(&mut self) -> Result<Submission<'_>>
pub fn submit(&mut self) -> Result<Submission<'_>>
Submit pending commands before querying completion; native query alone does not include the unsubmitted command buffer.
Sourcepub unsafe fn load(&self, path: &Path, symbol: &str) -> Result<Kernel>
pub unsafe fn load(&self, path: &Path, symbol: &str) -> Result<Kernel>
Load a native code object and select one export by name.
§Safety
The code object must be trusted native machine code.
Sourcepub unsafe fn load_artifact(&self, artifact: &Artifact) -> Result<Kernel>
pub unsafe fn load_artifact(&self, artifact: &Artifact) -> Result<Kernel>
Load a compiled artifact directly, without a filesystem round trip.
§Safety
The artifact must be trusted native code, as for Stream::load.
Examples found in repository?
21fn gpu_kernel(
22 runtime: &Runtime,
23 source: &str,
24 symbol: &str,
25 config: &[(&str, String)],
26 elements: usize,
27 bindings: Vec<BindingContract>,
28) -> Result<GpuKernel> {
29 let compiler = hrx::loom::Compiler::for_target(None, runtime.gpu()?.target())?;
30 let module = compiler.module(source);
31 let mut spec = hrx::loom::Specialization::new(symbol);
32 for (key, value) in config {
33 spec.set_config(*key, value.clone());
34 }
35 let artifact = module.compile(&spec)?;
36 let stream = hrx::gpu::Stream::open()?;
37 let raw = unsafe { stream.load_artifact(&artifact) }?;
38 let mut constants = hrx::gpu::Constants::new();
39 match raw.info().constant_byte_length {
40 8 => constants.push(elements as u32)?,
41 12 => constants.push(elements as u64)?,
42 _ => return Err(hrx::Error::Message("unexpected scalar ABI".into())),
43 }
44 constants.push(1f32)?;
45 unsafe {
46 runtime.load_gpu_kernel(
47 artifact.path(),
48 symbol,
49 [elements.div_ceil(256) as u32, 1, 1],
50 [256, 1, 1],
51 KernelContract {
52 bindings,
53 constants: constants.as_bytes().to_vec(),
54 },
55 )
56 }
57}More examples
30fn main() -> Result<()> {
31 let mut stream = Stream::open()?;
32 let mut metrics = BTreeMap::new();
33 let samples = std::env::var("HRX_BENCH_SAMPLES")
34 .ok()
35 .and_then(|value| value.parse::<usize>().ok())
36 .filter(|&value| value > 0)
37 .unwrap_or(9);
38 let (ns, _) = measure(&mut stream, samples, 50_000, |stream| {
39 for _ in 0..50_000 {
40 let buffer = stream.scratch(1024 * 1024)?;
41 black_box(&buffer);
42 stream.recycle(buffer)?;
43 }
44 Ok(())
45 })?;
46 metrics.insert("scratch_reuse_ns", ns);
47 let (ns, _) = measure(&mut stream, samples, 2_000, |stream| {
48 for _ in 0..2_000 {
49 black_box(stream.allocate(1024 * 1024)?);
50 }
51 Ok(())
52 })?;
53 metrics.insert("allocate_drop_ns", ns);
54
55 let bytes = 256 * 1024 * 1024;
56 let chunk = vec![0x3cu8; 4 * 1024 * 1024];
57 let weights = stream.allocate(bytes)?;
58 let (_, ns) = measure(&mut stream, samples, 1, |stream| {
59 for offset in (0..bytes).step_by(chunk.len()) {
60 stream.upload(weights.binding().slice(offset, chunk.len())?, &chunk)?;
61 }
62 Ok(())
63 })?;
64 metrics.insert(
65 "queued_upload_gib_s",
66 bytes as f64 / (1024f64.powi(3) * ns * 1e-9),
67 );
68 let mut check = [0u8; 16];
69 stream.read_blocking(
70 weights.try_slice(bytes - check.len(), check.len())?,
71 &mut check,
72 )?;
73 assert_eq!(check, [0x3c; 16]);
74 drop(weights);
75
76 let compiler = hrx::loom::Compiler::for_stream(None, &stream)?;
77 let module = compiler.module(include_str!("../tests/kernels/euler.loom"));
78 let mut spec = hrx::loom::Specialization::new("krea2_euler");
79 spec.set_config("krea2.euler.grid_x", "1");
80 spec.set_config("krea2.euler.grid_y", "1");
81 let artifact = module.compile(&spec)?;
82 // This repository owns the source; use its exact dimensions and scalar layout.
83 let kernel = unsafe { stream.load_artifact(&artifact)? };
84 let sample = stream.allocate(512)?;
85 let velocity = stream.allocate(512)?;
86 let ones: Vec<u8> = (0..256).flat_map(|_| 0x3f80u16.to_le_bytes()).collect();
87 stream.upload_blocking(sample.binding(), &ones)?;
88 stream.upload_blocking(velocity.binding(), &ones)?;
89 let mut constants = Constants::new();
90 match kernel.info().constant_byte_length {
91 8 => constants.push(256u32)?,
92 12 => constants.push(256u64)?,
93 n => return Err(hrx::Error::Message(format!("unexpected constant size {n}"))),
94 }
95 constants.push(0f32)?;
96 let bindings = [sample.binding(), velocity.binding()];
97 let (enqueue, complete) = measure(&mut stream, samples, 2048, |stream| {
98 for _ in 0..2048 {
99 // The source accesses exactly the 256 bf16 elements in each buffer.
100 unsafe {
101 stream.dispatch(&kernel, [1; 3], [256, 1, 1], &constants, &bindings)?;
102 }
103 }
104 Ok(())
105 })?;
106 metrics.insert("dispatch_enqueue_ns", enqueue);
107 metrics.insert("dispatch_complete_ns", complete);
108
109 // Use disjoint allocations in both graphs. Even an idempotent store is a
110 // write: concurrent Euler dispatches cannot share their in/out buffer.
111 let mut graph_buffers = Vec::new();
112 for _ in 0..32 {
113 let sample = stream.allocate(512)?;
114 let velocity = stream.allocate(512)?;
115 stream.upload(sample.binding(), &ones)?;
116 stream.upload(velocity.binding(), &ones)?;
117 graph_buffers.push((sample, velocity));
118 }
119 stream.synchronize()?;
120 let mut builder = stream.graph()?;
121 let mut previous = None;
122 for (sample, velocity) in &graph_buffers {
123 let after = previous.as_slice();
124 previous = Some(unsafe {
125 builder.dispatch(
126 after,
127 &kernel,
128 [1; 3],
129 [256, 1, 1],
130 &constants,
131 &[sample.binding(), velocity.binding()],
132 )?
133 });
134 }
135 let chained = builder.finish()?;
136
137 // The same disjoint workloads, this time allowing concurrent scheduling.
138 let mut builder = stream.graph()?;
139 for (sample, velocity) in &graph_buffers {
140 unsafe {
141 builder.dispatch(
142 &[],
143 &kernel,
144 [1; 3],
145 [256, 1, 1],
146 &constants,
147 &[sample.binding(), velocity.binding()],
148 )?;
149 }
150 }
151 let independent = builder.finish()?;
152 let mut graphs = [chained, independent];
153 let mut enqueue = [Vec::new(), Vec::new()];
154 let mut complete = [Vec::new(), Vec::new()];
155 for round in 0..samples + 3 {
156 for arm in if round % 2 == 0 { [0, 1] } else { [1, 0] } {
157 stream.synchronize()?;
158 let start = Instant::now();
159 for _ in 0..256 {
160 stream.launch(&mut graphs[arm])?;
161 }
162 let submitted = start.elapsed();
163 stream.synchronize()?;
164 let elapsed = start.elapsed();
165 if round >= 3 {
166 enqueue[arm].push(submitted.as_secs_f64() * 1e9 / 256.0);
167 complete[arm].push(elapsed.as_secs_f64() * 1e9 / (256.0 * 32.0));
168 }
169 }
170 }
171 for values in enqueue.iter_mut().chain(complete.iter_mut()) {
172 values.sort_by(f64::total_cmp);
173 }
174 let ns = complete[0][samples / 2];
175 let ns_free = complete[1][samples / 2];
176 metrics.insert("graph_enqueue_ns_per_replay", enqueue[0][samples / 2]);
177 metrics.insert("graph_complete_ns_per_kernel", ns);
178 metrics.insert("graph_independent_ns_per_kernel", ns_free);
179 metrics.insert("graph_scheduling_difference_ns_per_kernel", ns - ns_free);
180 for (sample, _) in &graph_buffers {
181 let mut output = vec![0; 512];
182 stream.read_blocking(sample.binding(), &mut output)?;
183 assert_eq!(output, ones);
184 }
185 let mut output = vec![0; 512];
186 stream.read_blocking(sample.binding(), &mut output)?;
187 assert_eq!(output, ones);
188 println!("{}", serde_json::to_string(&metrics)?);
189 Ok(())
190}Sourcepub unsafe fn dispatch(
&self,
kernel: &Kernel,
grid: [u32; 3],
block: [u32; 3],
constants: &Constants,
bindings: &[View<'_>],
) -> Result<()>
pub unsafe fn dispatch( &self, kernel: &Kernel, grid: [u32; 3], block: [u32; 3], constants: &Constants, bindings: &[View<'_>], ) -> Result<()>
Queue a kernel invocation with explicitly packed constants and borrowed bindings.
§Safety
Kernel, dimensions, constants and binding spans must agree. GPU addressing is not sandboxed by a binding’s length.
Examples found in repository?
30fn main() -> Result<()> {
31 let mut stream = Stream::open()?;
32 let mut metrics = BTreeMap::new();
33 let samples = std::env::var("HRX_BENCH_SAMPLES")
34 .ok()
35 .and_then(|value| value.parse::<usize>().ok())
36 .filter(|&value| value > 0)
37 .unwrap_or(9);
38 let (ns, _) = measure(&mut stream, samples, 50_000, |stream| {
39 for _ in 0..50_000 {
40 let buffer = stream.scratch(1024 * 1024)?;
41 black_box(&buffer);
42 stream.recycle(buffer)?;
43 }
44 Ok(())
45 })?;
46 metrics.insert("scratch_reuse_ns", ns);
47 let (ns, _) = measure(&mut stream, samples, 2_000, |stream| {
48 for _ in 0..2_000 {
49 black_box(stream.allocate(1024 * 1024)?);
50 }
51 Ok(())
52 })?;
53 metrics.insert("allocate_drop_ns", ns);
54
55 let bytes = 256 * 1024 * 1024;
56 let chunk = vec![0x3cu8; 4 * 1024 * 1024];
57 let weights = stream.allocate(bytes)?;
58 let (_, ns) = measure(&mut stream, samples, 1, |stream| {
59 for offset in (0..bytes).step_by(chunk.len()) {
60 stream.upload(weights.binding().slice(offset, chunk.len())?, &chunk)?;
61 }
62 Ok(())
63 })?;
64 metrics.insert(
65 "queued_upload_gib_s",
66 bytes as f64 / (1024f64.powi(3) * ns * 1e-9),
67 );
68 let mut check = [0u8; 16];
69 stream.read_blocking(
70 weights.try_slice(bytes - check.len(), check.len())?,
71 &mut check,
72 )?;
73 assert_eq!(check, [0x3c; 16]);
74 drop(weights);
75
76 let compiler = hrx::loom::Compiler::for_stream(None, &stream)?;
77 let module = compiler.module(include_str!("../tests/kernels/euler.loom"));
78 let mut spec = hrx::loom::Specialization::new("krea2_euler");
79 spec.set_config("krea2.euler.grid_x", "1");
80 spec.set_config("krea2.euler.grid_y", "1");
81 let artifact = module.compile(&spec)?;
82 // This repository owns the source; use its exact dimensions and scalar layout.
83 let kernel = unsafe { stream.load_artifact(&artifact)? };
84 let sample = stream.allocate(512)?;
85 let velocity = stream.allocate(512)?;
86 let ones: Vec<u8> = (0..256).flat_map(|_| 0x3f80u16.to_le_bytes()).collect();
87 stream.upload_blocking(sample.binding(), &ones)?;
88 stream.upload_blocking(velocity.binding(), &ones)?;
89 let mut constants = Constants::new();
90 match kernel.info().constant_byte_length {
91 8 => constants.push(256u32)?,
92 12 => constants.push(256u64)?,
93 n => return Err(hrx::Error::Message(format!("unexpected constant size {n}"))),
94 }
95 constants.push(0f32)?;
96 let bindings = [sample.binding(), velocity.binding()];
97 let (enqueue, complete) = measure(&mut stream, samples, 2048, |stream| {
98 for _ in 0..2048 {
99 // The source accesses exactly the 256 bf16 elements in each buffer.
100 unsafe {
101 stream.dispatch(&kernel, [1; 3], [256, 1, 1], &constants, &bindings)?;
102 }
103 }
104 Ok(())
105 })?;
106 metrics.insert("dispatch_enqueue_ns", enqueue);
107 metrics.insert("dispatch_complete_ns", complete);
108
109 // Use disjoint allocations in both graphs. Even an idempotent store is a
110 // write: concurrent Euler dispatches cannot share their in/out buffer.
111 let mut graph_buffers = Vec::new();
112 for _ in 0..32 {
113 let sample = stream.allocate(512)?;
114 let velocity = stream.allocate(512)?;
115 stream.upload(sample.binding(), &ones)?;
116 stream.upload(velocity.binding(), &ones)?;
117 graph_buffers.push((sample, velocity));
118 }
119 stream.synchronize()?;
120 let mut builder = stream.graph()?;
121 let mut previous = None;
122 for (sample, velocity) in &graph_buffers {
123 let after = previous.as_slice();
124 previous = Some(unsafe {
125 builder.dispatch(
126 after,
127 &kernel,
128 [1; 3],
129 [256, 1, 1],
130 &constants,
131 &[sample.binding(), velocity.binding()],
132 )?
133 });
134 }
135 let chained = builder.finish()?;
136
137 // The same disjoint workloads, this time allowing concurrent scheduling.
138 let mut builder = stream.graph()?;
139 for (sample, velocity) in &graph_buffers {
140 unsafe {
141 builder.dispatch(
142 &[],
143 &kernel,
144 [1; 3],
145 [256, 1, 1],
146 &constants,
147 &[sample.binding(), velocity.binding()],
148 )?;
149 }
150 }
151 let independent = builder.finish()?;
152 let mut graphs = [chained, independent];
153 let mut enqueue = [Vec::new(), Vec::new()];
154 let mut complete = [Vec::new(), Vec::new()];
155 for round in 0..samples + 3 {
156 for arm in if round % 2 == 0 { [0, 1] } else { [1, 0] } {
157 stream.synchronize()?;
158 let start = Instant::now();
159 for _ in 0..256 {
160 stream.launch(&mut graphs[arm])?;
161 }
162 let submitted = start.elapsed();
163 stream.synchronize()?;
164 let elapsed = start.elapsed();
165 if round >= 3 {
166 enqueue[arm].push(submitted.as_secs_f64() * 1e9 / 256.0);
167 complete[arm].push(elapsed.as_secs_f64() * 1e9 / (256.0 * 32.0));
168 }
169 }
170 }
171 for values in enqueue.iter_mut().chain(complete.iter_mut()) {
172 values.sort_by(f64::total_cmp);
173 }
174 let ns = complete[0][samples / 2];
175 let ns_free = complete[1][samples / 2];
176 metrics.insert("graph_enqueue_ns_per_replay", enqueue[0][samples / 2]);
177 metrics.insert("graph_complete_ns_per_kernel", ns);
178 metrics.insert("graph_independent_ns_per_kernel", ns_free);
179 metrics.insert("graph_scheduling_difference_ns_per_kernel", ns - ns_free);
180 for (sample, _) in &graph_buffers {
181 let mut output = vec![0; 512];
182 stream.read_blocking(sample.binding(), &mut output)?;
183 assert_eq!(output, ones);
184 }
185 let mut output = vec![0; 512];
186 stream.read_blocking(sample.binding(), &mut output)?;
187 assert_eq!(output, ones);
188 println!("{}", serde_json::to_string(&metrics)?);
189 Ok(())
190}Sourcepub fn scratch(&mut self, bytes: usize) -> Result<Buffer>
pub fn scratch(&mut self, bytes: usize) -> Result<Buffer>
Reuse a cached allocation or allocate new scratch storage. Reuse follows
the stream’s command order and requires no host wait.
Return it explicitly with Stream::recycle after recording its last use.
Dropping a buffer releases it rather than returning it to this pool.
Examples found in repository?
30fn main() -> Result<()> {
31 let mut stream = Stream::open()?;
32 let mut metrics = BTreeMap::new();
33 let samples = std::env::var("HRX_BENCH_SAMPLES")
34 .ok()
35 .and_then(|value| value.parse::<usize>().ok())
36 .filter(|&value| value > 0)
37 .unwrap_or(9);
38 let (ns, _) = measure(&mut stream, samples, 50_000, |stream| {
39 for _ in 0..50_000 {
40 let buffer = stream.scratch(1024 * 1024)?;
41 black_box(&buffer);
42 stream.recycle(buffer)?;
43 }
44 Ok(())
45 })?;
46 metrics.insert("scratch_reuse_ns", ns);
47 let (ns, _) = measure(&mut stream, samples, 2_000, |stream| {
48 for _ in 0..2_000 {
49 black_box(stream.allocate(1024 * 1024)?);
50 }
51 Ok(())
52 })?;
53 metrics.insert("allocate_drop_ns", ns);
54
55 let bytes = 256 * 1024 * 1024;
56 let chunk = vec![0x3cu8; 4 * 1024 * 1024];
57 let weights = stream.allocate(bytes)?;
58 let (_, ns) = measure(&mut stream, samples, 1, |stream| {
59 for offset in (0..bytes).step_by(chunk.len()) {
60 stream.upload(weights.binding().slice(offset, chunk.len())?, &chunk)?;
61 }
62 Ok(())
63 })?;
64 metrics.insert(
65 "queued_upload_gib_s",
66 bytes as f64 / (1024f64.powi(3) * ns * 1e-9),
67 );
68 let mut check = [0u8; 16];
69 stream.read_blocking(
70 weights.try_slice(bytes - check.len(), check.len())?,
71 &mut check,
72 )?;
73 assert_eq!(check, [0x3c; 16]);
74 drop(weights);
75
76 let compiler = hrx::loom::Compiler::for_stream(None, &stream)?;
77 let module = compiler.module(include_str!("../tests/kernels/euler.loom"));
78 let mut spec = hrx::loom::Specialization::new("krea2_euler");
79 spec.set_config("krea2.euler.grid_x", "1");
80 spec.set_config("krea2.euler.grid_y", "1");
81 let artifact = module.compile(&spec)?;
82 // This repository owns the source; use its exact dimensions and scalar layout.
83 let kernel = unsafe { stream.load_artifact(&artifact)? };
84 let sample = stream.allocate(512)?;
85 let velocity = stream.allocate(512)?;
86 let ones: Vec<u8> = (0..256).flat_map(|_| 0x3f80u16.to_le_bytes()).collect();
87 stream.upload_blocking(sample.binding(), &ones)?;
88 stream.upload_blocking(velocity.binding(), &ones)?;
89 let mut constants = Constants::new();
90 match kernel.info().constant_byte_length {
91 8 => constants.push(256u32)?,
92 12 => constants.push(256u64)?,
93 n => return Err(hrx::Error::Message(format!("unexpected constant size {n}"))),
94 }
95 constants.push(0f32)?;
96 let bindings = [sample.binding(), velocity.binding()];
97 let (enqueue, complete) = measure(&mut stream, samples, 2048, |stream| {
98 for _ in 0..2048 {
99 // The source accesses exactly the 256 bf16 elements in each buffer.
100 unsafe {
101 stream.dispatch(&kernel, [1; 3], [256, 1, 1], &constants, &bindings)?;
102 }
103 }
104 Ok(())
105 })?;
106 metrics.insert("dispatch_enqueue_ns", enqueue);
107 metrics.insert("dispatch_complete_ns", complete);
108
109 // Use disjoint allocations in both graphs. Even an idempotent store is a
110 // write: concurrent Euler dispatches cannot share their in/out buffer.
111 let mut graph_buffers = Vec::new();
112 for _ in 0..32 {
113 let sample = stream.allocate(512)?;
114 let velocity = stream.allocate(512)?;
115 stream.upload(sample.binding(), &ones)?;
116 stream.upload(velocity.binding(), &ones)?;
117 graph_buffers.push((sample, velocity));
118 }
119 stream.synchronize()?;
120 let mut builder = stream.graph()?;
121 let mut previous = None;
122 for (sample, velocity) in &graph_buffers {
123 let after = previous.as_slice();
124 previous = Some(unsafe {
125 builder.dispatch(
126 after,
127 &kernel,
128 [1; 3],
129 [256, 1, 1],
130 &constants,
131 &[sample.binding(), velocity.binding()],
132 )?
133 });
134 }
135 let chained = builder.finish()?;
136
137 // The same disjoint workloads, this time allowing concurrent scheduling.
138 let mut builder = stream.graph()?;
139 for (sample, velocity) in &graph_buffers {
140 unsafe {
141 builder.dispatch(
142 &[],
143 &kernel,
144 [1; 3],
145 [256, 1, 1],
146 &constants,
147 &[sample.binding(), velocity.binding()],
148 )?;
149 }
150 }
151 let independent = builder.finish()?;
152 let mut graphs = [chained, independent];
153 let mut enqueue = [Vec::new(), Vec::new()];
154 let mut complete = [Vec::new(), Vec::new()];
155 for round in 0..samples + 3 {
156 for arm in if round % 2 == 0 { [0, 1] } else { [1, 0] } {
157 stream.synchronize()?;
158 let start = Instant::now();
159 for _ in 0..256 {
160 stream.launch(&mut graphs[arm])?;
161 }
162 let submitted = start.elapsed();
163 stream.synchronize()?;
164 let elapsed = start.elapsed();
165 if round >= 3 {
166 enqueue[arm].push(submitted.as_secs_f64() * 1e9 / 256.0);
167 complete[arm].push(elapsed.as_secs_f64() * 1e9 / (256.0 * 32.0));
168 }
169 }
170 }
171 for values in enqueue.iter_mut().chain(complete.iter_mut()) {
172 values.sort_by(f64::total_cmp);
173 }
174 let ns = complete[0][samples / 2];
175 let ns_free = complete[1][samples / 2];
176 metrics.insert("graph_enqueue_ns_per_replay", enqueue[0][samples / 2]);
177 metrics.insert("graph_complete_ns_per_kernel", ns);
178 metrics.insert("graph_independent_ns_per_kernel", ns_free);
179 metrics.insert("graph_scheduling_difference_ns_per_kernel", ns - ns_free);
180 for (sample, _) in &graph_buffers {
181 let mut output = vec![0; 512];
182 stream.read_blocking(sample.binding(), &mut output)?;
183 assert_eq!(output, ones);
184 }
185 let mut output = vec![0; 512];
186 stream.read_blocking(sample.binding(), &mut output)?;
187 assert_eq!(output, ones);
188 println!("{}", serde_json::to_string(&metrics)?);
189 Ok(())
190}Sourcepub fn recycle(&mut self, buffer: Buffer) -> Result<()>
pub fn recycle(&mut self, buffer: Buffer) -> Result<()>
Return an allocation to this stream’s bounded scratch pool after recording its final use. A later scratch request may reuse its storage. Returns an error for a buffer another stream allocated: a scratch pool is one stream’s private free list, even though the buffer itself is usable from any stream on the device. Recycling needs mutable stream access; there is no automatic return on drop.
Examples found in repository?
30fn main() -> Result<()> {
31 let mut stream = Stream::open()?;
32 let mut metrics = BTreeMap::new();
33 let samples = std::env::var("HRX_BENCH_SAMPLES")
34 .ok()
35 .and_then(|value| value.parse::<usize>().ok())
36 .filter(|&value| value > 0)
37 .unwrap_or(9);
38 let (ns, _) = measure(&mut stream, samples, 50_000, |stream| {
39 for _ in 0..50_000 {
40 let buffer = stream.scratch(1024 * 1024)?;
41 black_box(&buffer);
42 stream.recycle(buffer)?;
43 }
44 Ok(())
45 })?;
46 metrics.insert("scratch_reuse_ns", ns);
47 let (ns, _) = measure(&mut stream, samples, 2_000, |stream| {
48 for _ in 0..2_000 {
49 black_box(stream.allocate(1024 * 1024)?);
50 }
51 Ok(())
52 })?;
53 metrics.insert("allocate_drop_ns", ns);
54
55 let bytes = 256 * 1024 * 1024;
56 let chunk = vec![0x3cu8; 4 * 1024 * 1024];
57 let weights = stream.allocate(bytes)?;
58 let (_, ns) = measure(&mut stream, samples, 1, |stream| {
59 for offset in (0..bytes).step_by(chunk.len()) {
60 stream.upload(weights.binding().slice(offset, chunk.len())?, &chunk)?;
61 }
62 Ok(())
63 })?;
64 metrics.insert(
65 "queued_upload_gib_s",
66 bytes as f64 / (1024f64.powi(3) * ns * 1e-9),
67 );
68 let mut check = [0u8; 16];
69 stream.read_blocking(
70 weights.try_slice(bytes - check.len(), check.len())?,
71 &mut check,
72 )?;
73 assert_eq!(check, [0x3c; 16]);
74 drop(weights);
75
76 let compiler = hrx::loom::Compiler::for_stream(None, &stream)?;
77 let module = compiler.module(include_str!("../tests/kernels/euler.loom"));
78 let mut spec = hrx::loom::Specialization::new("krea2_euler");
79 spec.set_config("krea2.euler.grid_x", "1");
80 spec.set_config("krea2.euler.grid_y", "1");
81 let artifact = module.compile(&spec)?;
82 // This repository owns the source; use its exact dimensions and scalar layout.
83 let kernel = unsafe { stream.load_artifact(&artifact)? };
84 let sample = stream.allocate(512)?;
85 let velocity = stream.allocate(512)?;
86 let ones: Vec<u8> = (0..256).flat_map(|_| 0x3f80u16.to_le_bytes()).collect();
87 stream.upload_blocking(sample.binding(), &ones)?;
88 stream.upload_blocking(velocity.binding(), &ones)?;
89 let mut constants = Constants::new();
90 match kernel.info().constant_byte_length {
91 8 => constants.push(256u32)?,
92 12 => constants.push(256u64)?,
93 n => return Err(hrx::Error::Message(format!("unexpected constant size {n}"))),
94 }
95 constants.push(0f32)?;
96 let bindings = [sample.binding(), velocity.binding()];
97 let (enqueue, complete) = measure(&mut stream, samples, 2048, |stream| {
98 for _ in 0..2048 {
99 // The source accesses exactly the 256 bf16 elements in each buffer.
100 unsafe {
101 stream.dispatch(&kernel, [1; 3], [256, 1, 1], &constants, &bindings)?;
102 }
103 }
104 Ok(())
105 })?;
106 metrics.insert("dispatch_enqueue_ns", enqueue);
107 metrics.insert("dispatch_complete_ns", complete);
108
109 // Use disjoint allocations in both graphs. Even an idempotent store is a
110 // write: concurrent Euler dispatches cannot share their in/out buffer.
111 let mut graph_buffers = Vec::new();
112 for _ in 0..32 {
113 let sample = stream.allocate(512)?;
114 let velocity = stream.allocate(512)?;
115 stream.upload(sample.binding(), &ones)?;
116 stream.upload(velocity.binding(), &ones)?;
117 graph_buffers.push((sample, velocity));
118 }
119 stream.synchronize()?;
120 let mut builder = stream.graph()?;
121 let mut previous = None;
122 for (sample, velocity) in &graph_buffers {
123 let after = previous.as_slice();
124 previous = Some(unsafe {
125 builder.dispatch(
126 after,
127 &kernel,
128 [1; 3],
129 [256, 1, 1],
130 &constants,
131 &[sample.binding(), velocity.binding()],
132 )?
133 });
134 }
135 let chained = builder.finish()?;
136
137 // The same disjoint workloads, this time allowing concurrent scheduling.
138 let mut builder = stream.graph()?;
139 for (sample, velocity) in &graph_buffers {
140 unsafe {
141 builder.dispatch(
142 &[],
143 &kernel,
144 [1; 3],
145 [256, 1, 1],
146 &constants,
147 &[sample.binding(), velocity.binding()],
148 )?;
149 }
150 }
151 let independent = builder.finish()?;
152 let mut graphs = [chained, independent];
153 let mut enqueue = [Vec::new(), Vec::new()];
154 let mut complete = [Vec::new(), Vec::new()];
155 for round in 0..samples + 3 {
156 for arm in if round % 2 == 0 { [0, 1] } else { [1, 0] } {
157 stream.synchronize()?;
158 let start = Instant::now();
159 for _ in 0..256 {
160 stream.launch(&mut graphs[arm])?;
161 }
162 let submitted = start.elapsed();
163 stream.synchronize()?;
164 let elapsed = start.elapsed();
165 if round >= 3 {
166 enqueue[arm].push(submitted.as_secs_f64() * 1e9 / 256.0);
167 complete[arm].push(elapsed.as_secs_f64() * 1e9 / (256.0 * 32.0));
168 }
169 }
170 }
171 for values in enqueue.iter_mut().chain(complete.iter_mut()) {
172 values.sort_by(f64::total_cmp);
173 }
174 let ns = complete[0][samples / 2];
175 let ns_free = complete[1][samples / 2];
176 metrics.insert("graph_enqueue_ns_per_replay", enqueue[0][samples / 2]);
177 metrics.insert("graph_complete_ns_per_kernel", ns);
178 metrics.insert("graph_independent_ns_per_kernel", ns_free);
179 metrics.insert("graph_scheduling_difference_ns_per_kernel", ns - ns_free);
180 for (sample, _) in &graph_buffers {
181 let mut output = vec![0; 512];
182 stream.read_blocking(sample.binding(), &mut output)?;
183 assert_eq!(output, ones);
184 }
185 let mut output = vec![0; 512];
186 stream.read_blocking(sample.binding(), &mut output)?;
187 assert_eq!(output, ones);
188 println!("{}", serde_json::to_string(&metrics)?);
189 Ok(())
190}Sourcepub fn set_scratch_limit(&mut self, bytes: usize)
pub fn set_scratch_limit(&mut self, bytes: usize)
Set the scratch byte budget, evicting the largest size classes as needed.
Source§impl Stream
impl Stream
Sourcepub fn graph(&self) -> Result<Graph<'_>>
pub fn graph(&self) -> Result<Graph<'_>>
Begin recording while borrowing this stream and the recorded resources.
finish instantiates an owned executable and ends those borrows.
fn escape() -> hrx::Result<hrx::Graph<'static>> {
let stream = hrx::Stream::open()?;
stream.graph()
}Examples found in repository?
4fn main() -> hrx::Result<()> {
5 let runtime = Runtime::new()?;
6 let buffer = runtime.allocate(4096, MemoryPlacement::GpuLocal)?;
7 let stream = Stream::open()?;
8 let raw = stream.allocate(4096)?;
9 for count in [128, 512, 1024, 2048] {
10 let begin = Instant::now();
11 let mut graph = runtime.graph();
12 for _ in 0..count {
13 graph.fill(buffer.view(), 0)?;
14 }
15 let graph = graph.prepare()?;
16 let inferred = begin.elapsed();
17 let begin = Instant::now();
18 let mut native = stream.graph()?;
19 let mut last = None;
20 for _ in 0..count {
21 last = Some(native.fill(last.as_slice(), raw.binding(), 0)?);
22 }
23 let _native = native.finish()?;
24 println!(
25 "nodes={count} inferred_us={} explicit_us={}",
26 inferred.as_micros(),
27 begin.elapsed().as_micros()
28 );
29 graph.submit()?.wait()?;
30 }
31 Ok(())
32}More examples
30fn main() -> Result<()> {
31 let mut stream = Stream::open()?;
32 let mut metrics = BTreeMap::new();
33 let samples = std::env::var("HRX_BENCH_SAMPLES")
34 .ok()
35 .and_then(|value| value.parse::<usize>().ok())
36 .filter(|&value| value > 0)
37 .unwrap_or(9);
38 let (ns, _) = measure(&mut stream, samples, 50_000, |stream| {
39 for _ in 0..50_000 {
40 let buffer = stream.scratch(1024 * 1024)?;
41 black_box(&buffer);
42 stream.recycle(buffer)?;
43 }
44 Ok(())
45 })?;
46 metrics.insert("scratch_reuse_ns", ns);
47 let (ns, _) = measure(&mut stream, samples, 2_000, |stream| {
48 for _ in 0..2_000 {
49 black_box(stream.allocate(1024 * 1024)?);
50 }
51 Ok(())
52 })?;
53 metrics.insert("allocate_drop_ns", ns);
54
55 let bytes = 256 * 1024 * 1024;
56 let chunk = vec![0x3cu8; 4 * 1024 * 1024];
57 let weights = stream.allocate(bytes)?;
58 let (_, ns) = measure(&mut stream, samples, 1, |stream| {
59 for offset in (0..bytes).step_by(chunk.len()) {
60 stream.upload(weights.binding().slice(offset, chunk.len())?, &chunk)?;
61 }
62 Ok(())
63 })?;
64 metrics.insert(
65 "queued_upload_gib_s",
66 bytes as f64 / (1024f64.powi(3) * ns * 1e-9),
67 );
68 let mut check = [0u8; 16];
69 stream.read_blocking(
70 weights.try_slice(bytes - check.len(), check.len())?,
71 &mut check,
72 )?;
73 assert_eq!(check, [0x3c; 16]);
74 drop(weights);
75
76 let compiler = hrx::loom::Compiler::for_stream(None, &stream)?;
77 let module = compiler.module(include_str!("../tests/kernels/euler.loom"));
78 let mut spec = hrx::loom::Specialization::new("krea2_euler");
79 spec.set_config("krea2.euler.grid_x", "1");
80 spec.set_config("krea2.euler.grid_y", "1");
81 let artifact = module.compile(&spec)?;
82 // This repository owns the source; use its exact dimensions and scalar layout.
83 let kernel = unsafe { stream.load_artifact(&artifact)? };
84 let sample = stream.allocate(512)?;
85 let velocity = stream.allocate(512)?;
86 let ones: Vec<u8> = (0..256).flat_map(|_| 0x3f80u16.to_le_bytes()).collect();
87 stream.upload_blocking(sample.binding(), &ones)?;
88 stream.upload_blocking(velocity.binding(), &ones)?;
89 let mut constants = Constants::new();
90 match kernel.info().constant_byte_length {
91 8 => constants.push(256u32)?,
92 12 => constants.push(256u64)?,
93 n => return Err(hrx::Error::Message(format!("unexpected constant size {n}"))),
94 }
95 constants.push(0f32)?;
96 let bindings = [sample.binding(), velocity.binding()];
97 let (enqueue, complete) = measure(&mut stream, samples, 2048, |stream| {
98 for _ in 0..2048 {
99 // The source accesses exactly the 256 bf16 elements in each buffer.
100 unsafe {
101 stream.dispatch(&kernel, [1; 3], [256, 1, 1], &constants, &bindings)?;
102 }
103 }
104 Ok(())
105 })?;
106 metrics.insert("dispatch_enqueue_ns", enqueue);
107 metrics.insert("dispatch_complete_ns", complete);
108
109 // Use disjoint allocations in both graphs. Even an idempotent store is a
110 // write: concurrent Euler dispatches cannot share their in/out buffer.
111 let mut graph_buffers = Vec::new();
112 for _ in 0..32 {
113 let sample = stream.allocate(512)?;
114 let velocity = stream.allocate(512)?;
115 stream.upload(sample.binding(), &ones)?;
116 stream.upload(velocity.binding(), &ones)?;
117 graph_buffers.push((sample, velocity));
118 }
119 stream.synchronize()?;
120 let mut builder = stream.graph()?;
121 let mut previous = None;
122 for (sample, velocity) in &graph_buffers {
123 let after = previous.as_slice();
124 previous = Some(unsafe {
125 builder.dispatch(
126 after,
127 &kernel,
128 [1; 3],
129 [256, 1, 1],
130 &constants,
131 &[sample.binding(), velocity.binding()],
132 )?
133 });
134 }
135 let chained = builder.finish()?;
136
137 // The same disjoint workloads, this time allowing concurrent scheduling.
138 let mut builder = stream.graph()?;
139 for (sample, velocity) in &graph_buffers {
140 unsafe {
141 builder.dispatch(
142 &[],
143 &kernel,
144 [1; 3],
145 [256, 1, 1],
146 &constants,
147 &[sample.binding(), velocity.binding()],
148 )?;
149 }
150 }
151 let independent = builder.finish()?;
152 let mut graphs = [chained, independent];
153 let mut enqueue = [Vec::new(), Vec::new()];
154 let mut complete = [Vec::new(), Vec::new()];
155 for round in 0..samples + 3 {
156 for arm in if round % 2 == 0 { [0, 1] } else { [1, 0] } {
157 stream.synchronize()?;
158 let start = Instant::now();
159 for _ in 0..256 {
160 stream.launch(&mut graphs[arm])?;
161 }
162 let submitted = start.elapsed();
163 stream.synchronize()?;
164 let elapsed = start.elapsed();
165 if round >= 3 {
166 enqueue[arm].push(submitted.as_secs_f64() * 1e9 / 256.0);
167 complete[arm].push(elapsed.as_secs_f64() * 1e9 / (256.0 * 32.0));
168 }
169 }
170 }
171 for values in enqueue.iter_mut().chain(complete.iter_mut()) {
172 values.sort_by(f64::total_cmp);
173 }
174 let ns = complete[0][samples / 2];
175 let ns_free = complete[1][samples / 2];
176 metrics.insert("graph_enqueue_ns_per_replay", enqueue[0][samples / 2]);
177 metrics.insert("graph_complete_ns_per_kernel", ns);
178 metrics.insert("graph_independent_ns_per_kernel", ns_free);
179 metrics.insert("graph_scheduling_difference_ns_per_kernel", ns - ns_free);
180 for (sample, _) in &graph_buffers {
181 let mut output = vec![0; 512];
182 stream.read_blocking(sample.binding(), &mut output)?;
183 assert_eq!(output, ones);
184 }
185 let mut output = vec![0; 512];
186 stream.read_blocking(sample.binding(), &mut output)?;
187 assert_eq!(output, ones);
188 println!("{}", serde_json::to_string(&metrics)?);
189 Ok(())
190}Sourcepub fn launch(&mut self, graph: &mut GraphExec) -> Result<()>
pub fn launch(&mut self, graph: &mut GraphExec) -> Result<()>
Replay an instantiated graph on its original stream.
A native submission failure may leave partial work in flight. Such a graph cannot be replayed again and its native resources are retained on drop because completion is unknown. Rejecting a foreign stream does not invalidate the graph.
Examples found in repository?
30fn main() -> Result<()> {
31 let mut stream = Stream::open()?;
32 let mut metrics = BTreeMap::new();
33 let samples = std::env::var("HRX_BENCH_SAMPLES")
34 .ok()
35 .and_then(|value| value.parse::<usize>().ok())
36 .filter(|&value| value > 0)
37 .unwrap_or(9);
38 let (ns, _) = measure(&mut stream, samples, 50_000, |stream| {
39 for _ in 0..50_000 {
40 let buffer = stream.scratch(1024 * 1024)?;
41 black_box(&buffer);
42 stream.recycle(buffer)?;
43 }
44 Ok(())
45 })?;
46 metrics.insert("scratch_reuse_ns", ns);
47 let (ns, _) = measure(&mut stream, samples, 2_000, |stream| {
48 for _ in 0..2_000 {
49 black_box(stream.allocate(1024 * 1024)?);
50 }
51 Ok(())
52 })?;
53 metrics.insert("allocate_drop_ns", ns);
54
55 let bytes = 256 * 1024 * 1024;
56 let chunk = vec![0x3cu8; 4 * 1024 * 1024];
57 let weights = stream.allocate(bytes)?;
58 let (_, ns) = measure(&mut stream, samples, 1, |stream| {
59 for offset in (0..bytes).step_by(chunk.len()) {
60 stream.upload(weights.binding().slice(offset, chunk.len())?, &chunk)?;
61 }
62 Ok(())
63 })?;
64 metrics.insert(
65 "queued_upload_gib_s",
66 bytes as f64 / (1024f64.powi(3) * ns * 1e-9),
67 );
68 let mut check = [0u8; 16];
69 stream.read_blocking(
70 weights.try_slice(bytes - check.len(), check.len())?,
71 &mut check,
72 )?;
73 assert_eq!(check, [0x3c; 16]);
74 drop(weights);
75
76 let compiler = hrx::loom::Compiler::for_stream(None, &stream)?;
77 let module = compiler.module(include_str!("../tests/kernels/euler.loom"));
78 let mut spec = hrx::loom::Specialization::new("krea2_euler");
79 spec.set_config("krea2.euler.grid_x", "1");
80 spec.set_config("krea2.euler.grid_y", "1");
81 let artifact = module.compile(&spec)?;
82 // This repository owns the source; use its exact dimensions and scalar layout.
83 let kernel = unsafe { stream.load_artifact(&artifact)? };
84 let sample = stream.allocate(512)?;
85 let velocity = stream.allocate(512)?;
86 let ones: Vec<u8> = (0..256).flat_map(|_| 0x3f80u16.to_le_bytes()).collect();
87 stream.upload_blocking(sample.binding(), &ones)?;
88 stream.upload_blocking(velocity.binding(), &ones)?;
89 let mut constants = Constants::new();
90 match kernel.info().constant_byte_length {
91 8 => constants.push(256u32)?,
92 12 => constants.push(256u64)?,
93 n => return Err(hrx::Error::Message(format!("unexpected constant size {n}"))),
94 }
95 constants.push(0f32)?;
96 let bindings = [sample.binding(), velocity.binding()];
97 let (enqueue, complete) = measure(&mut stream, samples, 2048, |stream| {
98 for _ in 0..2048 {
99 // The source accesses exactly the 256 bf16 elements in each buffer.
100 unsafe {
101 stream.dispatch(&kernel, [1; 3], [256, 1, 1], &constants, &bindings)?;
102 }
103 }
104 Ok(())
105 })?;
106 metrics.insert("dispatch_enqueue_ns", enqueue);
107 metrics.insert("dispatch_complete_ns", complete);
108
109 // Use disjoint allocations in both graphs. Even an idempotent store is a
110 // write: concurrent Euler dispatches cannot share their in/out buffer.
111 let mut graph_buffers = Vec::new();
112 for _ in 0..32 {
113 let sample = stream.allocate(512)?;
114 let velocity = stream.allocate(512)?;
115 stream.upload(sample.binding(), &ones)?;
116 stream.upload(velocity.binding(), &ones)?;
117 graph_buffers.push((sample, velocity));
118 }
119 stream.synchronize()?;
120 let mut builder = stream.graph()?;
121 let mut previous = None;
122 for (sample, velocity) in &graph_buffers {
123 let after = previous.as_slice();
124 previous = Some(unsafe {
125 builder.dispatch(
126 after,
127 &kernel,
128 [1; 3],
129 [256, 1, 1],
130 &constants,
131 &[sample.binding(), velocity.binding()],
132 )?
133 });
134 }
135 let chained = builder.finish()?;
136
137 // The same disjoint workloads, this time allowing concurrent scheduling.
138 let mut builder = stream.graph()?;
139 for (sample, velocity) in &graph_buffers {
140 unsafe {
141 builder.dispatch(
142 &[],
143 &kernel,
144 [1; 3],
145 [256, 1, 1],
146 &constants,
147 &[sample.binding(), velocity.binding()],
148 )?;
149 }
150 }
151 let independent = builder.finish()?;
152 let mut graphs = [chained, independent];
153 let mut enqueue = [Vec::new(), Vec::new()];
154 let mut complete = [Vec::new(), Vec::new()];
155 for round in 0..samples + 3 {
156 for arm in if round % 2 == 0 { [0, 1] } else { [1, 0] } {
157 stream.synchronize()?;
158 let start = Instant::now();
159 for _ in 0..256 {
160 stream.launch(&mut graphs[arm])?;
161 }
162 let submitted = start.elapsed();
163 stream.synchronize()?;
164 let elapsed = start.elapsed();
165 if round >= 3 {
166 enqueue[arm].push(submitted.as_secs_f64() * 1e9 / 256.0);
167 complete[arm].push(elapsed.as_secs_f64() * 1e9 / (256.0 * 32.0));
168 }
169 }
170 }
171 for values in enqueue.iter_mut().chain(complete.iter_mut()) {
172 values.sort_by(f64::total_cmp);
173 }
174 let ns = complete[0][samples / 2];
175 let ns_free = complete[1][samples / 2];
176 metrics.insert("graph_enqueue_ns_per_replay", enqueue[0][samples / 2]);
177 metrics.insert("graph_complete_ns_per_kernel", ns);
178 metrics.insert("graph_independent_ns_per_kernel", ns_free);
179 metrics.insert("graph_scheduling_difference_ns_per_kernel", ns - ns_free);
180 for (sample, _) in &graph_buffers {
181 let mut output = vec![0; 512];
182 stream.read_blocking(sample.binding(), &mut output)?;
183 assert_eq!(output, ones);
184 }
185 let mut output = vec![0; 512];
186 stream.read_blocking(sample.binding(), &mut output)?;
187 assert_eq!(output, ones);
188 println!("{}", serde_json::to_string(&metrics)?);
189 Ok(())
190}Trait Implementations§
Auto Trait Implementations§
impl !Freeze for Stream
impl !RefUnwindSafe for Stream
impl !Sync for Stream
impl Send for Stream
impl Unpin for Stream
impl UnsafeUnpin for Stream
impl UnwindSafe for Stream
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> DropFlavorWrapper<T> for T
impl<T> DropFlavorWrapper<T> for T
Source§impl<T, W> HasTypeWitness<W> for Twhere
W: MakeTypeWitness<Arg = T>,
T: ?Sized,
impl<T, W> HasTypeWitness<W> for Twhere
W: MakeTypeWitness<Arg = T>,
T: ?Sized,
Source§impl<T> Identity for Twhere
T: ?Sized,
impl<T> Identity for Twhere
T: ?Sized,
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more