Skip to main content

Stream

Struct Stream 

Source
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

Source

pub fn access_graph(&self) -> Result<AccessGraph<'_>>

Begin an access-aware graph recording on this stream.

Source§

impl Stream

Source

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.

Source

pub fn memory_budget(&self) -> Option<&MemoryBudget>

Allocation ceiling for this stream, including transfer staging.

Source

pub fn target(&self) -> &Target

Architecture reported by this stream’s device.

Source

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.

Source

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.

Source

pub fn open() -> Result<Self>

Open an ordered stream on device zero.

Examples found in repository?
examples/graph_prepare.rs (line 7)
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
Hide additional examples
examples/gemm_pipeline.rs (line 36)
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}
examples/import_probe.rs (line 32)
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}
examples/gpu_load.rs (line 97)
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}
examples/unified_probe.rs (line 127)
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}
examples/stream_bench.rs (line 31)
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}
Source

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.

Source

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.

Source

pub fn allocate(&self, bytes: usize) -> Result<Buffer>

Allocate storage owned by this stream; zero bytes is rounded to one.

Examples found in repository?
examples/graph_prepare.rs (line 8)
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
Hide additional examples
examples/import_probe.rs (line 59)
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}
examples/gpu_load.rs (line 98)
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}
examples/unified_probe.rs (line 199)
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}
examples/stream_bench.rs (line 49)
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}
Source

pub fn allocate_zeroed(&self, bytes: usize) -> Result<Buffer>

Allocate device-local storage and enqueue a zero fill over its requested size.

Source

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?
examples/import_probe.rs (line 33)
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
Hide additional examples
examples/unified_probe.rs (line 128)
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}
Source

pub fn synchronize(&mut self) -> Result<()>

Submit and wait for all work, then reclaim completed upload staging.

Examples found in repository?
examples/stream_bench.rs (line 14)
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
Hide additional examples
examples/import_probe.rs (line 47)
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}
examples/gpu_load.rs (line 103)
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}
examples/unified_probe.rs (line 137)
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}
examples/dmabuf_probe.rs (line 195)
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}
Source

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?
examples/stream_bench.rs (line 87)
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}
Source

pub fn upload_blocking_at( &mut self, dst: &Buffer, offset: usize, bytes: &[u8], ) -> Result<()>

Synchronously upload at a checked byte offset into an allocation.

Source

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?
examples/import_probe.rs (line 63)
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
Hide additional examples
examples/unified_probe.rs (line 202)
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}
examples/stream_bench.rs (lines 69-72)
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}
Source

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.

Source

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?
examples/import_probe.rs (line 46)
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
Hide additional examples
examples/gpu_load.rs (line 101)
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}
examples/unified_probe.rs (line 136)
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}
examples/dmabuf_probe.rs (line 194)
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}
Source

pub fn copy(&self, dst: View<'_>, src: View<'_>) -> Result<()>

Queue a copy between equal, nonempty views owned by this stream.

Examples found in repository?
examples/import_probe.rs (line 61)
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
Hide additional examples
examples/gpu_load.rs (line 108)
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}
examples/unified_probe.rs (line 200)
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}
Source

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?
examples/stream_bench.rs (line 60)
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}
Source

pub fn upload_at( &mut self, dst: &Buffer, offset: usize, bytes: &[u8], ) -> Result<()>

Enqueue an upload at a checked byte offset into an allocation.

Source

pub fn allocate_shared(&self, bytes: usize) -> Result<Buffer>

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?
examples/dmabuf_probe.rs (line 192)
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}
Source

pub fn submit(&mut self) -> Result<Submission<'_>>

Submit pending commands before querying completion; native query alone does not include the unsubmitted command buffer.

Source

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.

Source

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?
examples/gemm_pipeline.rs (line 37)
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
Hide additional examples
examples/stream_bench.rs (line 83)
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}
Source

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?
examples/stream_bench.rs (line 101)
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}
Source

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?
examples/stream_bench.rs (line 40)
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}
Source

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?
examples/stream_bench.rs (line 42)
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}
Source

pub fn set_scratch_limit(&mut self, bytes: usize)

Set the scratch byte budget, evicting the largest size classes as needed.

Source§

impl Stream

Source

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?
examples/graph_prepare.rs (line 18)
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
Hide additional examples
examples/stream_bench.rs (line 120)
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}
Source

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?
examples/stream_bench.rs (line 160)
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}
Source§

impl Stream

Source

pub fn read(&mut self, source: View<'_>) -> Result<Readback>

Queue a download into owned, initially unmapped host-visible storage.

Trait Implementations§

Source§

impl Debug for Stream

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Drop for Stream

Source§

fn drop(&mut self)

Executes the destructor for this type. Read more
Source§

fn pin_drop(self: Pin<&mut Self>)

🔬This is a nightly-only experimental API. (pin_ergonomics)
Execute the destructor for this type, but different to Drop::drop, it requires self to be pinned. Read more

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> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> DropFlavorWrapper<T> for T

Source§

type Flavor = MayDrop

The DropFlavor that wraps T into Self
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, W> HasTypeWitness<W> for T
where W: MakeTypeWitness<Arg = T>, T: ?Sized,

Source§

const WITNESS: W = W::MAKE

A constant of the type witness
Source§

impl<T> Identity for T
where T: ?Sized,

Source§

const TYPE_EQ: TypeEq<T, <T as Identity>::Type> = TypeEq::NEW

Proof that Self is the same type as Self::Type, provides methods for casting between Self and Self::Type.
Source§

type Type = T

The same type as Self, used to emulate type equality bounds (T == U) with associated type equality constraints (T: Identity<Type = U>).
Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ

Converts 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 more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
where F: FnOnce(&Self) -> bool,

Converts 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
Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more