Skip to main content

GpuDevice

Struct GpuDevice 

Source
pub struct GpuDevice { /* private fields */ }
Expand description

A selected GPU and its verified compilation target.

Implementations§

Source§

impl GpuDevice

Source

pub fn index(&self) -> i32

Physical GPU ordinal.

Source

pub fn target(&self) -> &Target

Target for compiling a kernel on this GPU.

Examples found in repository?
examples/gemm_pipeline.rs (line 31)
23fn gpu_kernel(
24    runtime: &Runtime,
25    source: &str,
26    symbol: &str,
27    config: &[(&str, String)],
28    elements: usize,
29    bindings: Vec<BindingContract>,
30) -> Result<GpuKernel> {
31    let compiler = hrx::loom::Compiler::for_target(None, runtime.gpu()?.target())?;
32    let module = compiler.module(source);
33    let mut spec = hrx::loom::Specialization::new(symbol);
34    for (key, value) in config {
35        spec.set_config(*key, value.clone());
36    }
37    let artifact = module.compile(&spec)?;
38    let stream = hrx::gpu::Stream::open()?;
39    let raw = unsafe { stream.load_artifact(&artifact) }?;
40    let mut constants = hrx::gpu::Constants::new();
41    match raw.info().constant_byte_length {
42        8 => constants.push(elements as u32)?,
43        12 => constants.push(elements as u64)?,
44        _ => return Err(hrx::Error::Message("unexpected scalar ABI".into())),
45    }
46    constants.push(1f32)?;
47    unsafe {
48        runtime.load_gpu_kernel(
49            artifact.path(),
50            symbol,
51            [elements.div_ceil(256) as u32, 1, 1],
52            [256, 1, 1],
53            KernelContract {
54                bindings,
55                constants: constants.as_bytes().to_vec(),
56            },
57        )
58    }
59}
More examples
Hide additional examples
examples/gpu_npu_parallel.rs (line 139)
104fn main() -> Result<()> {
105    let args: Vec<_> = std::env::args().skip(1).collect();
106    if args.len() > 4 || args.iter().any(|arg| arg == "--help") {
107        println!(
108            "usage: gpu_npu_parallel [gpu-elements=16777216] [npu-matrices=1024] [samples=21] [trace.json]"
109        );
110        return if args.len() <= 4 {
111            Ok(())
112        } else {
113            Err(Error::Message("too many arguments".into()))
114        };
115    }
116    let number = |index: usize, default: usize| -> Result<usize> {
117        args.get(index).map_or(Ok(default), |value| {
118            value
119                .parse()
120                .map_err(|_| Error::Message(format!("invalid integer: {value}")))
121        })
122    };
123    let elements = number(0, 1 << 24)?;
124    let matrices = number(1, 1024)?;
125    let samples = number(2, 21)?;
126    if !(256..=1 << 26).contains(&elements)
127        || !elements.is_multiple_of(256)
128        || !(1..=16384).contains(&matrices)
129        || !(3..=1000).contains(&samples)
130    {
131        return Err(Error::Message("gpu-elements must be a multiple of 256 in 256..=67108864; npu-matrices in 1..=16384; samples in 3..=1000".into()));
132    }
133    let trace_path = args.get(3).map(PathBuf::from);
134    let runtime = Runtime::new()?;
135    let gpu = runtime.gpu()?;
136    let npu = runtime.npu(0)?;
137    println!(
138        "GPU: {}; NPU: {}",
139        gpu.target().as_str(),
140        npu.target().as_str()
141    );
142    let gpu_artifact = Compiler::for_target(None, gpu.target())?
143        .import_cxx(CxxSource::new(
144            "affine.cpp",
145            format!(
146                r#"
147#include <hip/hip_runtime.h>
148__global__ [[loom::workgroup_size(256, 1, 1), loom::workgroup_count({}, 1, 1)]]
149void affine(const int* input, int* output) {{
150  unsigned i = blockIdx.x * 256u + threadIdx.x;
151  output[i] = input[i] * 3 + 7;
152}}
153"#,
154                elements / 256
155            ),
156        ))?
157        .compile(&Specialization::new("affine"))?;
158    // SAFETY: the fixed launch covers exactly elements i32 values in each
159    // nonaliasing binding; bounded input values cannot overflow the arithmetic.
160    let gpu_kernel = unsafe {
161        runtime.load_gpu_kernel(
162            gpu_artifact.path(),
163            "affine",
164            [(elements / 256) as u32, 1, 1],
165            [256, 1, 1],
166            contract(&[(elements * 4, Access::Read), (elements * 4, Access::Write)]),
167        )
168    }?;
169    let npu_artifact = Compiler::for_target(None, npu.target())?
170        .module(include_str!("kernels/batched_gemm.xdna.loom"))
171        .compile(&Specialization::new("gemm").with_config("gemm.batch", matrices.to_string()))?;
172    // SAFETY: the pipeline consumes matrices pairs of 8x8 BF16 inputs and
173    // produces the same number of 8x8 FP32 outputs on one NPU column.
174    let npu_kernel = unsafe {
175        npu.load_artifact(
176            &npu_artifact,
177            1,
178            contract(&[
179                (matrices * 128, Access::Read),
180                (matrices * 128, Access::Read),
181                (matrices * 256, Access::Write),
182            ]),
183        )
184    }?;
185    let input = runtime.allocate(elements * 4, MemoryPlacement::GpuLocal)?;
186    let output = runtime.allocate(elements * 4, MemoryPlacement::GpuLocal)?;
187    let host_input = runtime.allocate(elements * 4, MemoryPlacement::HostVisible)?;
188    let host_output = runtime.allocate(elements * 4, MemoryPlacement::HostVisible)?;
189    let mut upload = runtime.graph();
190    upload.copy(input.view(), host_input.view())?;
191    let upload = upload.prepare()?;
192    let mut download = runtime.graph();
193    download.copy(host_output.view(), output.view())?;
194    let download = download.prepare()?;
195    let a = runtime.allocate(matrices * 128, MemoryPlacement::NpuLocal(npu.clone()))?;
196    let b = runtime.allocate(matrices * 128, MemoryPlacement::NpuLocal(npu.clone()))?;
197    let c = runtime.allocate(matrices * 256, MemoryPlacement::NpuLocal(npu))?;
198
199    let mut gpu_only = runtime.graph();
200    gpu_only.gpu(&gpu_kernel, &[input.view(), output.view()])?;
201    let gpu_only = gpu_only.prepare()?;
202    let mut npu_only = runtime.graph();
203    npu_only.npu(&npu_kernel, &[a.view(), b.view(), c.view()])?;
204    let npu_only = npu_only.prepare()?;
205
206    // Disjoint buffers imply no dependency edge. One submit admits both
207    // branches to separate GPU/NPU workers; one wait joins their completions.
208    let mut parallel = runtime.graph();
209    parallel.gpu(&gpu_kernel, &[input.view(), output.view()])?;
210    parallel.npu(&npu_kernel, &[a.view(), b.view(), c.view()])?;
211    let parallel = parallel.prepare()?;
212    for pass in 0..3 {
213        initialize(&host_input, &a, &b, pass * 2)?;
214        upload.submit()?.wait()?;
215        gpu_only.submit()?.wait()?;
216        npu_only.submit()?.wait()?;
217        download.submit()?.wait()?;
218        validate(&host_output, &c, pass * 2)?;
219        // Change both inputs so validation cannot accept the sequential run's
220        // old output if either parallel branch fails to write its result.
221        initialize(&host_input, &a, &b, pass * 2 + 1)?;
222        upload.submit()?.wait()?;
223        parallel.submit()?.wait()?;
224        download.submit()?.wait()?;
225        validate(&host_output, &c, pass * 2 + 1)?;
226    }
227
228    let before = runtime.statistics();
229    let mut sequential_ms = Vec::with_capacity(samples);
230    let mut parallel_ms = Vec::with_capacity(samples);
231    for sample in 0..samples {
232        // Alternate order to reduce systematic clock/thermal ordering bias.
233        for concurrent in if sample % 2 == 0 {
234            [false, true]
235        } else {
236            [true, false]
237        } {
238            if concurrent {
239                parallel_ms.push(completed_ms(&parallel)?);
240            } else {
241                let start = Instant::now();
242                gpu_only.submit()?.wait()?;
243                npu_only.submit()?.wait()?;
244                sequential_ms.push(start.elapsed().as_secs_f64() * 1000.0);
245            }
246        }
247    }
248    let after = runtime.statistics();
249    if before.allocations != after.allocations || before.imports != after.imports {
250        return Err(Error::Message(
251            "replay unexpectedly allocated or imported storage".into(),
252        ));
253    }
254    download.submit()?.wait()?;
255    validate(&host_output, &c, 5)?;
256    let sequential = median(&mut sequential_ms);
257    let concurrent = median(&mut parallel_ms);
258    println!("{elements} GPU elements + {matrices} NPU matrices; {samples} paired samples");
259    println!(
260        "sequential median: {sequential:.3} ms; parallel median: {concurrent:.3} ms; speedup: {:.2}x",
261        sequential / concurrent
262    );
263    println!(
264        "Both CPU references match exactly, including changed-input replays. No native allocations/imports during timed replay."
265    );
266
267    // Capture separately so tracing does not affect the benchmark samples.
268    runtime.start_trace(32)?;
269    for _ in 0..4 {
270        parallel.submit()?.wait()?;
271    }
272    let trace = runtime
273        .finish_trace()
274        .ok_or_else(|| Error::Message("missing execution trace".into()))?;
275    let overlap: f64 = trace
276        .events
277        .iter()
278        .filter(|e| e.lane == 1)
279        .flat_map(|gpu| {
280            trace.events.iter().filter(|e| e.lane == 3).map(move |npu| {
281                ((gpu.start_us + gpu.duration_us).min(npu.start_us + npu.duration_us)
282                    - gpu.start_us.max(npu.start_us))
283                .max(0.0)
284            })
285        })
286        .sum();
287    println!(
288        "GPU/NPU host-observed overlap across four replays: {overlap:.1} us (not device timestamps)"
289    );
290    if let Some(path) = trace_path {
291        std::fs::write(&path, trace.to_json()?)?;
292        println!("Trace: {}", path.display());
293    }
294    Ok(())
295}

Trait Implementations§

Source§

impl Clone for GpuDevice

Source§

fn clone(&self) -> Self

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more

Auto Trait Implementations§

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> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. 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> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
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