Skip to main content

stream_bench/
stream_bench.rs

1//! End-to-end stream costs on a prepared GPU. Run with --release.
2use hrx::{Constants, Result, Stream};
3use std::{collections::BTreeMap, hint::black_box, time::Instant};
4
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}