pub struct Compiler(/* private fields */);Expand description
A pinned compiler library and reusable native state, shared by cheap clones.
Implementations§
Source§impl Compiler
impl Compiler
Sourcepub fn resolve(library: Option<&Path>) -> Result<Self>
pub fn resolve(library: Option<&Path>) -> Result<Self>
Select an explicit library, HRX_LOOM_LIBRARY, or the pinned native bundle. This loads CPU compiler code but never opens a GPU. Library mappings stay resident until process exit; contexts and workspaces remain session-owned. To upgrade a loaded library, select a new path or restart the process.
Examples found in repository?
21fn main() -> Result<()> {
22 let compiler_only = match std::env::args().nth(1).as_deref() {
23 None => false,
24 Some("--compiler-only") => true,
25 Some(_) => {
26 return Err(hrx::Error::Message(
27 "usage: startup_bench [--compiler-only]".into(),
28 ));
29 }
30 };
31 let initial = memory()?;
32 let start = Instant::now();
33 let stream = if compiler_only {
34 None
35 } else {
36 Some(Stream::open()?)
37 };
38 let open_ms = stream
39 .as_ref()
40 .map(|_| start.elapsed().as_secs_f64() * 1000.0);
41 let after_open = if compiler_only { None } else { Some(memory()?) };
42 let start = Instant::now();
43 let compiler = match &stream {
44 Some(stream) => hrx::loom::Compiler::for_stream(None, stream)?,
45 None => hrx::loom::Compiler::resolve(None)?,
46 };
47 let compiler_ms = start.elapsed().as_secs_f64() * 1000.0;
48 let after_compiler = memory()?;
49 let module = compiler.module(include_str!("../tests/kernels/euler.loom"));
50 let mut spec = Specialization::new("krea2_euler");
51 spec.set_config("krea2.euler.grid_x", "1");
52 spec.set_config("krea2.euler.grid_y", "1");
53 let start = Instant::now();
54 let artifact = module.compile(&spec)?;
55 let first_compile_ms = start.elapsed().as_secs_f64() * 1000.0;
56 let after_compile = memory()?;
57 let start = Instant::now();
58 let cached = module.compile(&spec)?;
59 let cache_hit_ms = start.elapsed().as_secs_f64() * 1000.0;
60 assert_eq!(artifact.bytes(), cached.bytes());
61 println!(
62 "{}",
63 json!({"compiler_only": compiler_only, "open_ms": open_ms, "compiler_ms": compiler_ms,
64 "first_compile_ms": first_compile_ms, "cache_hit_ms": cache_hit_ms,
65 "initial": initial, "after_open": after_open,
66 "after_compiler": after_compiler, "after_compile": after_compile,
67 "compiler_identity": artifact.compiler_identity()})
68 );
69 Ok(())
70}Sourcepub fn for_stream(library: Option<&Path>, stream: &Stream) -> Result<Self>
pub fn for_stream(library: Option<&Path>, stream: &Stream) -> Result<Self>
Resolve the shared compiler configured for a stream’s actual device target.
Examples found in repository?
21fn main() -> Result<()> {
22 let compiler_only = match std::env::args().nth(1).as_deref() {
23 None => false,
24 Some("--compiler-only") => true,
25 Some(_) => {
26 return Err(hrx::Error::Message(
27 "usage: startup_bench [--compiler-only]".into(),
28 ));
29 }
30 };
31 let initial = memory()?;
32 let start = Instant::now();
33 let stream = if compiler_only {
34 None
35 } else {
36 Some(Stream::open()?)
37 };
38 let open_ms = stream
39 .as_ref()
40 .map(|_| start.elapsed().as_secs_f64() * 1000.0);
41 let after_open = if compiler_only { None } else { Some(memory()?) };
42 let start = Instant::now();
43 let compiler = match &stream {
44 Some(stream) => hrx::loom::Compiler::for_stream(None, stream)?,
45 None => hrx::loom::Compiler::resolve(None)?,
46 };
47 let compiler_ms = start.elapsed().as_secs_f64() * 1000.0;
48 let after_compiler = memory()?;
49 let module = compiler.module(include_str!("../tests/kernels/euler.loom"));
50 let mut spec = Specialization::new("krea2_euler");
51 spec.set_config("krea2.euler.grid_x", "1");
52 spec.set_config("krea2.euler.grid_y", "1");
53 let start = Instant::now();
54 let artifact = module.compile(&spec)?;
55 let first_compile_ms = start.elapsed().as_secs_f64() * 1000.0;
56 let after_compile = memory()?;
57 let start = Instant::now();
58 let cached = module.compile(&spec)?;
59 let cache_hit_ms = start.elapsed().as_secs_f64() * 1000.0;
60 assert_eq!(artifact.bytes(), cached.bytes());
61 println!(
62 "{}",
63 json!({"compiler_only": compiler_only, "open_ms": open_ms, "compiler_ms": compiler_ms,
64 "first_compile_ms": first_compile_ms, "cache_hit_ms": cache_hit_ms,
65 "initial": initial, "after_open": after_open,
66 "after_compiler": after_compiler, "after_compile": after_compile,
67 "compiler_identity": artifact.compiler_identity()})
68 );
69 Ok(())
70}More examples
30fn main() -> Result<()> {
31 let mut stream = Stream::open()?;
32 let mut metrics = BTreeMap::new();
33 let samples = std::env::var("HRX_BENCH_SAMPLES")
34 .ok()
35 .and_then(|value| value.parse::<usize>().ok())
36 .filter(|&value| value > 0)
37 .unwrap_or(9);
38 let (ns, _) = measure(&mut stream, samples, 50_000, |stream| {
39 for _ in 0..50_000 {
40 let buffer = stream.scratch(1024 * 1024)?;
41 black_box(&buffer);
42 stream.recycle(buffer)?;
43 }
44 Ok(())
45 })?;
46 metrics.insert("scratch_reuse_ns", ns);
47 let (ns, _) = measure(&mut stream, samples, 2_000, |stream| {
48 for _ in 0..2_000 {
49 black_box(stream.allocate(1024 * 1024)?);
50 }
51 Ok(())
52 })?;
53 metrics.insert("allocate_drop_ns", ns);
54
55 let bytes = 256 * 1024 * 1024;
56 let chunk = vec![0x3cu8; 4 * 1024 * 1024];
57 let weights = stream.allocate(bytes)?;
58 let (_, ns) = measure(&mut stream, samples, 1, |stream| {
59 for offset in (0..bytes).step_by(chunk.len()) {
60 stream.upload(weights.binding().slice(offset, chunk.len())?, &chunk)?;
61 }
62 Ok(())
63 })?;
64 metrics.insert(
65 "queued_upload_gib_s",
66 bytes as f64 / (1024f64.powi(3) * ns * 1e-9),
67 );
68 let mut check = [0u8; 16];
69 stream.read_blocking(
70 weights.try_slice(bytes - check.len(), check.len())?,
71 &mut check,
72 )?;
73 assert_eq!(check, [0x3c; 16]);
74 drop(weights);
75
76 let compiler = hrx::loom::Compiler::for_stream(None, &stream)?;
77 let module = compiler.module(include_str!("../tests/kernels/euler.loom"));
78 let mut spec = hrx::loom::Specialization::new("krea2_euler");
79 spec.set_config("krea2.euler.grid_x", "1");
80 spec.set_config("krea2.euler.grid_y", "1");
81 let artifact = module.compile(&spec)?;
82 // This repository owns the source; use its exact dimensions and scalar layout.
83 let kernel = unsafe { stream.load_artifact(&artifact)? };
84 let sample = stream.allocate(512)?;
85 let velocity = stream.allocate(512)?;
86 let ones: Vec<u8> = (0..256).flat_map(|_| 0x3f80u16.to_le_bytes()).collect();
87 stream.upload_blocking(sample.binding(), &ones)?;
88 stream.upload_blocking(velocity.binding(), &ones)?;
89 let mut constants = Constants::new();
90 match kernel.info().constant_byte_length {
91 8 => constants.push(256u32)?,
92 12 => constants.push(256u64)?,
93 n => return Err(hrx::Error::Message(format!("unexpected constant size {n}"))),
94 }
95 constants.push(0f32)?;
96 let bindings = [sample.binding(), velocity.binding()];
97 let (enqueue, complete) = measure(&mut stream, samples, 2048, |stream| {
98 for _ in 0..2048 {
99 // The source accesses exactly the 256 bf16 elements in each buffer.
100 unsafe {
101 stream.dispatch(&kernel, [1; 3], [256, 1, 1], &constants, &bindings)?;
102 }
103 }
104 Ok(())
105 })?;
106 metrics.insert("dispatch_enqueue_ns", enqueue);
107 metrics.insert("dispatch_complete_ns", complete);
108
109 // Use disjoint allocations in both graphs. Even an idempotent store is a
110 // write: concurrent Euler dispatches cannot share their in/out buffer.
111 let mut graph_buffers = Vec::new();
112 for _ in 0..32 {
113 let sample = stream.allocate(512)?;
114 let velocity = stream.allocate(512)?;
115 stream.upload(sample.binding(), &ones)?;
116 stream.upload(velocity.binding(), &ones)?;
117 graph_buffers.push((sample, velocity));
118 }
119 stream.synchronize()?;
120 let mut builder = stream.graph()?;
121 let mut previous = None;
122 for (sample, velocity) in &graph_buffers {
123 let after = previous.as_slice();
124 previous = Some(unsafe {
125 builder.dispatch(
126 after,
127 &kernel,
128 [1; 3],
129 [256, 1, 1],
130 &constants,
131 &[sample.binding(), velocity.binding()],
132 )?
133 });
134 }
135 let chained = builder.finish()?;
136
137 // The same disjoint workloads, this time allowing concurrent scheduling.
138 let mut builder = stream.graph()?;
139 for (sample, velocity) in &graph_buffers {
140 unsafe {
141 builder.dispatch(
142 &[],
143 &kernel,
144 [1; 3],
145 [256, 1, 1],
146 &constants,
147 &[sample.binding(), velocity.binding()],
148 )?;
149 }
150 }
151 let independent = builder.finish()?;
152 let mut graphs = [chained, independent];
153 let mut enqueue = [Vec::new(), Vec::new()];
154 let mut complete = [Vec::new(), Vec::new()];
155 for round in 0..samples + 3 {
156 for arm in if round % 2 == 0 { [0, 1] } else { [1, 0] } {
157 stream.synchronize()?;
158 let start = Instant::now();
159 for _ in 0..256 {
160 stream.launch(&mut graphs[arm])?;
161 }
162 let submitted = start.elapsed();
163 stream.synchronize()?;
164 let elapsed = start.elapsed();
165 if round >= 3 {
166 enqueue[arm].push(submitted.as_secs_f64() * 1e9 / 256.0);
167 complete[arm].push(elapsed.as_secs_f64() * 1e9 / (256.0 * 32.0));
168 }
169 }
170 }
171 for values in enqueue.iter_mut().chain(complete.iter_mut()) {
172 values.sort_by(f64::total_cmp);
173 }
174 let ns = complete[0][samples / 2];
175 let ns_free = complete[1][samples / 2];
176 metrics.insert("graph_enqueue_ns_per_replay", enqueue[0][samples / 2]);
177 metrics.insert("graph_complete_ns_per_kernel", ns);
178 metrics.insert("graph_independent_ns_per_kernel", ns_free);
179 metrics.insert("graph_scheduling_difference_ns_per_kernel", ns - ns_free);
180 for (sample, _) in &graph_buffers {
181 let mut output = vec![0; 512];
182 stream.read_blocking(sample.binding(), &mut output)?;
183 assert_eq!(output, ones);
184 }
185 let mut output = vec![0; 512];
186 stream.read_blocking(sample.binding(), &mut output)?;
187 assert_eq!(output, ones);
188 println!("{}", serde_json::to_string(&metrics)?);
189 Ok(())
190}Sourcepub fn for_target(library: Option<&Path>, target: &Target) -> Result<Self>
pub fn for_target(library: Option<&Path>, target: &Target) -> Result<Self>
Resolve the shared compiler configured for an explicit device target.
Examples found in repository?
7fn main() -> Result<()> {
8 let packets = std::env::args().nth(1).unwrap_or_else(|| "256".into());
9 let artifact = Compiler::for_target(None, &Target::xdna())?
10 .module(include_str!("../tests/kernels/copy.xdna.loom"))
11 .compile(
12 &Specialization::new("copy")
13 .with_config("copy.packets", packets)
14 .with_report(ReportMode::Summary),
15 )?;
16 println!("{}", artifact.path().display());
17 Ok(())
18}More examples
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}
60fn main() -> Result<()> {
61 let args: Vec<_> = std::env::args().collect();
62 let args = if args.len() == 1 {
63 vec![
64 args[0].clone(),
65 String::new(),
66 "gemm".into(),
67 "8".into(),
68 "8".into(),
69 "8".into(),
70 ]
71 } else {
72 args
73 };
74 if args.len() != 6 {
75 return Err(hrx::Error::Message(
76 "usage: gemm_pipeline <gemm.loom> <symbol> M K N".into(),
77 ));
78 }
79 let parse = |i: usize| {
80 args[i]
81 .parse::<usize>()
82 .map_err(|e| hrx::Error::Message(e.to_string()))
83 };
84 let (m, k, n) = (parse(3)?, parse(4)?, parse(5)?);
85 let extent = |a: usize, b: usize, width: usize| {
86 a.checked_mul(b)
87 .filter(|&count| count > 0 && count <= (1 << 30))
88 .and_then(|count| count.checked_mul(width))
89 .ok_or_else(|| hrx::Error::Message("invalid matrix extent".into()))
90 };
91 let (a_bytes, b_bytes, c_bytes) = (extent(m, k, 2)?, extent(k, n, 2)?, extent(m, n, 4)?);
92 let runtime = Runtime::new()?;
93 let device = runtime.npu(0)?;
94 let (source, spec) = if args[1].is_empty() {
95 (
96 include_str!("../tests/kernels/gemm_bf16.xdna.loom").to_string(),
97 hrx::loom::Specialization::new("gemm"),
98 )
99 } else {
100 (
101 std::fs::read_to_string(&args[1])?,
102 hrx::loom::Specialization::new(&args[2])
103 .with_config("gemm.m", m.to_string())
104 .with_config("gemm.k", k.to_string())
105 .with_config("gemm.n", n.to_string()),
106 )
107 };
108 let artifact = hrx::loom::Compiler::for_target(None, device.target())?
109 .module(&source)
110 .compile(&spec)?;
111 let npu = unsafe {
112 device.load_artifact(
113 &artifact,
114 1,
115 KernelContract {
116 bindings: vec![
117 binding(a_bytes, Access::Read),
118 binding(b_bytes, Access::Read),
119 binding(c_bytes, Access::Write),
120 ],
121 constants: vec![],
122 },
123 )
124 }?;
125 let prep = gpu_kernel(
126 &runtime,
127 include_str!("../tests/kernels/euler.loom"),
128 "krea2_euler",
129 &[
130 ("krea2.euler.grid_x", (m * k).div_ceil(256).to_string()),
131 ("krea2.euler.grid_y", "1".into()),
132 ],
133 m * k,
134 vec![
135 binding(a_bytes, Access::ReadWrite),
136 binding(a_bytes, Access::Read),
137 ],
138 )?;
139 let epilogue = gpu_kernel(
140 &runtime,
141 include_str!("../tests/kernels/add_f32.loom"),
142 "add_f32",
143 &[("add.grid", (m * n).div_ceil(256).to_string())],
144 m * n,
145 vec![binding(c_bytes, Access::ReadWrite)],
146 )?;
147 let allocate = |bytes| runtime.allocate(bytes, MemoryPlacement::Shared(device.clone()));
148 let weights = allocate(b_bytes)?;
149 let ones = allocate(a_bytes)?;
150 for buffer in [&weights, &ones] {
151 for word in buffer.map_write()?.as_chunks_mut::<2>().0 {
152 word.copy_from_slice(&0x3f80u16.to_le_bytes());
153 }
154 }
155 let prepare = || -> Result<(ExecutableGraph, Buffer)> {
156 let a = allocate(a_bytes)?;
157 let c = allocate(c_bytes)?;
158 let mut graph = runtime.graph();
159 graph.fill(a.view(), 0)?;
160 graph.gpu(&prep, &[a.view(), ones.view()])?;
161 graph.npu(&npu, &[a.view(), weights.view(), c.view()])?;
162 graph.gpu(&epilogue, &[c.view()])?;
163 Ok((graph.prepare()?, c))
164 };
165 let (first, c0) = prepare()?;
166 let (second, c1) = prepare()?;
167 for _ in 0..5 {
168 first.submit()?.wait()?;
169 second.submit()?.wait()?;
170 }
171 let before = runtime.statistics();
172 let mut latency = Vec::with_capacity(50);
173 for _ in 0..50 {
174 let start = Instant::now();
175 first.submit()?.wait()?;
176 latency.push(start.elapsed().as_secs_f64());
177 }
178 let start = Instant::now();
179 for _ in 0..50 {
180 let a = first.submit()?;
181 let b = second.submit()?;
182 a.wait()?;
183 b.wait()?;
184 }
185 let requests_per_second = 100.0 / start.elapsed().as_secs_f64();
186 for c in [&c0, &c1] {
187 if c.map_read()?
188 .as_chunks::<4>()
189 .0
190 .iter()
191 .any(|v| f32::from_le_bytes(*v) != k as f32 + 1.0)
192 {
193 return Err(hrx::Error::Message("GEMM pipeline output mismatch".into()));
194 }
195 }
196 let after = runtime.statistics();
197 assert_eq!(before.allocations, after.allocations);
198 assert_eq!(before.imports, after.imports);
199 assert_eq!(after.copied_bytes, 0);
200 latency.sort_by(f64::total_cmp);
201 println!(
202 "M={m} K={k} N={n} p50_ms={:.3} p95_ms={:.3} pipelined_requests_s={requests_per_second:.2}",
203 percentile(&latency, 50) * 1e3,
204 percentile(&latency, 95) * 1e3
205 );
206 println!("{}", serde_json::to_string(&after)?);
207 Ok(())
208}8fn main() -> Result<()> {
9 let args: Vec<_> = std::env::args().collect();
10 if args.len() > 2 {
11 return Err(hrx::Error::Message(
12 "usage: shared_roundtrip [bytes]".into(),
13 ));
14 }
15 let bytes = args
16 .get(1)
17 .map(String::as_str)
18 .unwrap_or("1048576")
19 .parse::<usize>()
20 .map_err(|e| hrx::Error::Message(e.to_string()))?;
21 if bytes == 0 || bytes % 4096 != 0 || bytes > 64 * 1024 * 1024 {
22 return Err(hrx::Error::Message(
23 "bytes must be a multiple of 4096 in 4096..=67108864".into(),
24 ));
25 }
26 let runtime = Runtime::new()?;
27 let device = runtime.npu(0)?;
28 let artifact = hrx::loom::Compiler::for_target(None, device.target())?
29 .module(include_str!("../tests/kernels/copy.xdna.loom"))
30 .compile(
31 &hrx::loom::Specialization::new("copy")
32 .with_config("copy.packets", (bytes / 4096).to_string()),
33 )?;
34 let binding = |bytes, access| BindingContract {
35 bytes,
36 access,
37 alignment: 4,
38 layout: "u32 passthrough".into(),
39 };
40 let contract = KernelContract {
41 bindings: vec![binding(bytes, Access::Read), binding(bytes, Access::Write)],
42 constants: vec![],
43 };
44 let kernel = unsafe { device.load_artifact(&artifact, 1, contract) }?;
45 let a = runtime.allocate(bytes, MemoryPlacement::Shared(device.clone()))?;
46 let c = runtime.allocate(bytes, MemoryPlacement::Shared(device.clone()))?;
47 let result = runtime.allocate(bytes, MemoryPlacement::Shared(device.clone()))?;
48 let mut graph = runtime.graph();
49 graph.fill(a.view(), 0x5a)?;
50 graph.npu(&kernel, &[a.view(), c.view()])?;
51 graph.copy(result.view(), c.view())?;
52 let graph = graph.prepare()?;
53 for _ in 0..10 {
54 graph.submit()?.wait()?;
55 let mapped = result.map_read()?;
56 if mapped.iter().any(|&b| b != 0x5a) {
57 return Err(hrx::Error::Message("GPU/NPU roundtrip mismatch".into()));
58 }
59 }
60 println!(
61 "GPU -> NPU -> GPU: all {bytes} bytes correct, 10 executions, {} prepared regions",
62 graph.regions()
63 );
64 Ok(())
65}9fn main() -> Result<()> {
10 let args: Vec<_> = std::env::args().collect();
11 if args.len() > 2 {
12 return Err(hrx::Error::Message("usage: shared_bench [bytes]".into()));
13 }
14 let bytes = args
15 .get(1)
16 .map(String::as_str)
17 .unwrap_or("1048576")
18 .parse::<usize>()
19 .map_err(|e| hrx::Error::Message(e.to_string()))?;
20 if bytes == 0 || bytes % 4096 != 0 || bytes > 64 * 1024 * 1024 {
21 return Err(hrx::Error::Message(
22 "bytes must be a multiple of 4096 in 4096..=67108864".into(),
23 ));
24 }
25 let runtime = Runtime::new()?;
26 let device = runtime.npu(0)?;
27 let artifact = hrx::loom::Compiler::for_target(None, device.target())?
28 .module(include_str!("../tests/kernels/copy.xdna.loom"))
29 .compile(
30 &hrx::loom::Specialization::new("copy")
31 .with_config("copy.packets", (bytes / 4096).to_string()),
32 )?;
33 let binding = |bytes, access| BindingContract {
34 bytes,
35 access,
36 alignment: 4,
37 layout: "passthrough".into(),
38 };
39 let kernel = unsafe {
40 device.load_artifact(
41 &artifact,
42 1,
43 KernelContract {
44 bindings: vec![binding(bytes, Access::Read), binding(bytes, Access::Write)],
45 constants: vec![],
46 },
47 )
48 }?;
49 let a = runtime.allocate(bytes, MemoryPlacement::Shared(device.clone()))?;
50 let c = runtime.allocate(bytes, MemoryPlacement::Shared(device.clone()))?;
51 let mut graph = runtime.graph();
52 graph.fill(a.view(), 0x35)?;
53 graph.npu(&kernel, &[a.view(), c.view()])?;
54 let graph = graph.prepare()?;
55 for _ in 0..5 {
56 graph.submit()?.wait()?;
57 }
58 let mut samples = Vec::new();
59 for _ in 0..100 {
60 let start = Instant::now();
61 graph.submit()?.wait()?;
62 samples.push(start.elapsed().as_secs_f64());
63 }
64 if c.map_read()?.iter().any(|&b| b != 0x35) {
65 return Err(hrx::Error::Message("incorrect benchmark output".into()));
66 }
67 samples.sort_by(f64::total_cmp);
68 println!(
69 "bytes={bytes} p50_us={:.2} p95_us={:.2}",
70 percentile(&samples, 50) * 1e6,
71 percentile(&samples, 95) * 1e6
72 );
73 Ok(())
74}10fn main() -> Result<()> {
11 let seconds: f64 = std::env::args()
12 .nth(1)
13 .unwrap_or_else(|| "2".into())
14 .parse()
15 .map_err(|e: std::num::ParseFloatError| hrx::Error::Message(e.to_string()))?;
16 if !seconds.is_finite() || !(0.01..=3600.0).contains(&seconds) {
17 return Err(hrx::Error::Message("seconds must be in 0.01..=3600".into()));
18 }
19 let device = Device::open(Engine::Xdna, 0)?;
20 let compiler = Compiler::for_target(None, device.target())?;
21 println!("working_set_MiB GB/s dispatches/s");
22 for mib in [1, 4, 8, 16, 32, 64] {
23 let bytes = mib * 1024 * 1024;
24 let artifact = compiler
25 .module(include_str!("../tests/kernels/copy.xdna.loom"))
26 .compile(
27 &Specialization::new("copy")
28 .with_config("copy.packets", (bytes / 4096).to_string()),
29 )?;
30 let input = device
31 .fabric()
32 .allocate(bytes, std::slice::from_ref(&device))?;
33 let output = device
34 .fabric()
35 .allocate(bytes, std::slice::from_ref(&device))?;
36 input.write(0, &vec![0x5a; bytes])?;
37 let program = unsafe {
38 device.prepare_xdna(
39 &artifact,
40 1,
41 &[
42 XdnaBinding {
43 buffer: &input,
44 offset: 0,
45 length: bytes,
46 },
47 XdnaBinding {
48 buffer: &output,
49 offset: 0,
50 length: bytes,
51 },
52 ],
53 )
54 }?;
55 unsafe { program.dispatch() }?.wait()?;
56 let mut check = vec![0; bytes];
57 output.read(0, &mut check)?;
58 assert!(check.iter().all(|&b| b == 0x5a));
59 let start = Instant::now();
60 let mut count = 0;
61 while start.elapsed() < Duration::from_secs_f64(seconds) {
62 unsafe { program.dispatch() }?.wait()?;
63 count += 1;
64 }
65 let elapsed = start.elapsed().as_secs_f64();
66 println!(
67 "{} {:.3} {:.1}",
68 2 * mib,
69 2.0 * bytes as f64 * count as f64 / elapsed / 1e9,
70 count as f64 / elapsed
71 );
72 }
73 Ok(())
74}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_compiler = Compiler::for_target(None, gpu.target())?;
143 let gpu_artifact = gpu_compiler
144 .import_cxx(CxxSource::new(
145 "affine.cpp",
146 format!(
147 r#"
148#include <hip/hip_runtime.h>
149__global__ [[loom::workgroup_size(256, 1, 1), loom::workgroup_count({}, 1, 1)]]
150void affine(const int* input, int* output) {{
151 unsigned i = blockIdx.x * 256u + threadIdx.x;
152 output[i] = input[i] * 3 + 7;
153}}
154"#,
155 elements / 256
156 ),
157 ))?
158 .compile(&Specialization::new("affine"))?;
159 // SAFETY: the fixed launch covers exactly elements i32 values in each
160 // nonaliasing binding; bounded input values cannot overflow the arithmetic.
161 let gpu_kernel = unsafe {
162 runtime.load_gpu_artifact(
163 &gpu_artifact,
164 &[],
165 contract(&[(elements * 4, Access::Read), (elements * 4, Access::Write)]),
166 )
167 }?;
168 let npu_artifact = Compiler::for_target(None, npu.target())?
169 .module(include_str!("kernels/batched_gemm.xdna.loom"))
170 .compile(&Specialization::new("gemm").with_config("gemm.batch", matrices.to_string()))?;
171 // SAFETY: the pipeline consumes matrices pairs of 8x8 BF16 inputs and
172 // produces the same number of 8x8 FP32 outputs on one NPU column.
173 let npu_kernel = unsafe {
174 npu.load_artifact(
175 &npu_artifact,
176 1,
177 contract(&[
178 (matrices * 128, Access::Read),
179 (matrices * 128, Access::Read),
180 (matrices * 256, Access::Write),
181 ]),
182 )
183 }?;
184 let input = runtime.allocate(elements * 4, MemoryPlacement::GpuLocal)?;
185 let output = runtime.allocate(elements * 4, MemoryPlacement::GpuLocal)?;
186 let host_input = runtime.allocate(elements * 4, MemoryPlacement::HostVisible)?;
187 let host_output = runtime.allocate(elements * 4, MemoryPlacement::HostVisible)?;
188 let mut upload = runtime.graph();
189 upload.copy(input.view(), host_input.view())?;
190 let upload = upload.prepare()?;
191 let mut download = runtime.graph();
192 download.copy(host_output.view(), output.view())?;
193 let download = download.prepare()?;
194 let a = runtime.allocate(matrices * 128, MemoryPlacement::NpuLocal(npu.clone()))?;
195 let b = runtime.allocate(matrices * 128, MemoryPlacement::NpuLocal(npu.clone()))?;
196 let c = runtime.allocate(matrices * 256, MemoryPlacement::NpuLocal(npu))?;
197
198 let mut gpu_only = runtime.graph();
199 gpu_only.gpu(&gpu_kernel, &[input.view(), output.view()])?;
200 let gpu_only = gpu_only.prepare()?;
201 let mut npu_only = runtime.graph();
202 npu_only.npu(&npu_kernel, &[a.view(), b.view(), c.view()])?;
203 let npu_only = npu_only.prepare()?;
204
205 // Disjoint buffers imply no dependency edge. One submit admits both
206 // branches to separate GPU/NPU workers; one wait joins their completions.
207 let mut parallel = runtime.graph();
208 parallel.gpu(&gpu_kernel, &[input.view(), output.view()])?;
209 parallel.npu(&npu_kernel, &[a.view(), b.view(), c.view()])?;
210 let parallel = parallel.prepare()?;
211 for pass in 0..3 {
212 initialize(&host_input, &a, &b, pass * 2)?;
213 upload.submit()?.wait()?;
214 gpu_only.submit()?.wait()?;
215 npu_only.submit()?.wait()?;
216 download.submit()?.wait()?;
217 validate(&host_output, &c, pass * 2)?;
218 // Change both inputs so validation cannot accept the sequential run's
219 // old output if either parallel branch fails to write its result.
220 initialize(&host_input, &a, &b, pass * 2 + 1)?;
221 upload.submit()?.wait()?;
222 parallel.submit()?.wait()?;
223 download.submit()?.wait()?;
224 validate(&host_output, &c, pass * 2 + 1)?;
225 }
226
227 let before = runtime.statistics();
228 let mut sequential_ms = Vec::with_capacity(samples);
229 let mut parallel_ms = Vec::with_capacity(samples);
230 for sample in 0..samples {
231 // Alternate order to reduce systematic clock/thermal ordering bias.
232 for concurrent in if sample % 2 == 0 {
233 [false, true]
234 } else {
235 [true, false]
236 } {
237 if concurrent {
238 parallel_ms.push(completed_ms(¶llel)?);
239 } else {
240 let start = Instant::now();
241 gpu_only.submit()?.wait()?;
242 npu_only.submit()?.wait()?;
243 sequential_ms.push(start.elapsed().as_secs_f64() * 1000.0);
244 }
245 }
246 }
247 let after = runtime.statistics();
248 if before.allocations != after.allocations || before.imports != after.imports {
249 return Err(Error::Message(
250 "replay unexpectedly allocated or imported storage".into(),
251 ));
252 }
253 download.submit()?.wait()?;
254 validate(&host_output, &c, 5)?;
255 let sequential = median(&mut sequential_ms);
256 let concurrent = median(&mut parallel_ms);
257 println!("{elements} GPU elements + {matrices} NPU matrices; {samples} paired samples");
258 println!(
259 "sequential median: {sequential:.3} ms; parallel median: {concurrent:.3} ms; speedup: {:.2}x",
260 sequential / concurrent
261 );
262 println!(
263 "Both CPU references match exactly, including changed-input replays. No native allocations/imports during timed replay."
264 );
265
266 // Capture separately so tracing does not affect the benchmark samples.
267 runtime.start_trace(32)?;
268 for _ in 0..4 {
269 parallel.submit()?.wait()?;
270 }
271 let trace = runtime
272 .finish_trace()
273 .ok_or_else(|| Error::Message("missing execution trace".into()))?;
274 let overlap: f64 = trace
275 .events
276 .iter()
277 .filter(|e| e.lane == 1)
278 .flat_map(|gpu| {
279 trace.events.iter().filter(|e| e.lane == 3).map(move |npu| {
280 ((gpu.start_us + gpu.duration_us).min(npu.start_us + npu.duration_us)
281 - gpu.start_us.max(npu.start_us))
282 .max(0.0)
283 })
284 })
285 .sum();
286 println!(
287 "GPU/NPU host-observed overlap across four replays: {overlap:.1} us (not device timestamps)"
288 );
289 if let Some(path) = trace_path {
290 std::fs::write(&path, trace.to_json()?)?;
291 println!("Trace: {}", path.display());
292 }
293 Ok(())
294}The compiler for this library, target and limits, resolved once per process.
Resolving is not cheap: it canonicalizes the library path and digests the whole shared object, twice. A consumer that asks for a compiler per kernel pays that per kernel, so every consumer memoized it themselves.
The cache includes all compiler options. HRX_LOOM_LIBRARY is resolved
on each call rather than frozen at the first.
Sourcepub fn with_options(
library: Option<&Path>,
options: CompilerOptions,
) -> Result<Self>
pub fn with_options( library: Option<&Path>, options: CompilerOptions, ) -> Result<Self>
Resolve a compiler with a bounded number of exclusive workspaces.
Examples found in repository?
348fn main() -> Result<()> {
349 let args: Vec<_> = std::env::args().collect();
350 if args.len() < 3 || args.len() > 5 {
351 return Err(hrx::Error::Message(
352 "usage: patch_bench BASELINE_LIB CANDIDATE_LIB [pairs=30] [default|cu|wgp]".into(),
353 ));
354 }
355 let pairs = args
356 .get(3)
357 .map(|s| s.parse::<usize>())
358 .transpose()
359 .map_err(|e| hrx::Error::Message(e.to_string()))?
360 .unwrap_or(30);
361 if pairs < 30 {
362 return Err(hrx::Error::Message("at least 30 pairs are required".into()));
363 }
364 let mode = match args.get(4).map(String::as_str).unwrap_or("default") {
365 "default" => ProcessorMode::Default,
366 "cu" => ProcessorMode::ComputeUnit,
367 "wgp" => ProcessorMode::WorkgroupProcessor,
368 _ => {
369 return Err(hrx::Error::Message(
370 "mode must be default, cu or wgp".into(),
371 ));
372 }
373 };
374 let mut stream = Stream::open()?;
375 let mut options = CompilerOptions::for_target(stream.target());
376 options.processor_mode = mode;
377 let compilers = [
378 Compiler::with_options(Some(Path::new(&args[1])), options.clone())?,
379 Compiler::with_options(Some(Path::new(&args[2])), options)?,
380 ];
381 for case in [
382 attention(128, false),
383 attention(512, false),
384 gemm(false, false),
385 gemm(true, false),
386 gemm(false, true),
387 gemm(true, true),
388 conv(56, 64, 64),
389 conv(14, 256, 256),
390 conv(7, 512, 512),
391 ] {
392 let baseline = prepare(&mut stream, &compilers[0], &case, None)?;
393 let candidate = prepare(
394 &mut stream,
395 &compilers[1],
396 &case,
397 Some(baseline.buffers.clone()),
398 )?;
399 let mut arms = [baseline, candidate];
400 for arm in &mut arms {
401 for _ in 0..5 {
402 stream.launch(&mut arm.graph)?;
403 stream.synchronize()?;
404 }
405 check(&mut stream, arm, &case)?;
406 }
407 let errors = [
408 check(&mut stream, &arms[0], &case)?,
409 check(&mut stream, &arms[1], &case)?,
410 ];
411 println!(
412 "{}",
413 serde_json::json!({"case":case.name,"mode":format!("{mode:?}"),"baseline":arms[0].identity,"candidate":arms[1].identity,"max_absolute_error":errors,"tolerance":case.tolerance,"replays":32})
414 );
415 for pair in 0..pairs {
416 let mut ns = [0.0; 2];
417 for index in if pair % 2 == 0 { [0, 1] } else { [1, 0] } {
418 let start = Instant::now();
419 stream.launch(&mut arms[index].graph)?;
420 stream.synchronize()?;
421 ns[index] = start.elapsed().as_secs_f64() * 1e9 / 32.0;
422 }
423 println!(
424 "{}",
425 serde_json::json!({"case":case.name,"pair":pair,"baseline_ns":ns[0],"candidate_ns":ns[1]})
426 );
427 }
428 for arm in &mut arms {
429 stream.launch(&mut arm.graph)?;
430 stream.synchronize()?;
431 check(&mut stream, arm, &case)?;
432 }
433 }
434 Ok(())
435}Sourcepub fn workers(&self) -> NonZeroUsize
pub fn workers(&self) -> NonZeroUsize
Maximum simultaneous compilations, and the workspace pool’s bound.
Sourcepub fn compile_all(
&self,
requests: &[(&Module, &Specialization)],
) -> Vec<Result<Artifact>> ⓘ
pub fn compile_all( &self, requests: &[(&Module, &Specialization)], ) -> Vec<Result<Artifact>> ⓘ
Compile several specializations, up to CompilerOptions::workers at a
time, and return their outcomes in request order.
Module::compile blocks, so a single-threaded caller never reaches the
workspace pool’s bound. This is the entry point that does: it owns the
thread budget rather than leaving every consumer to rebuild the same pool.
Each request is independent; one failure does not cancel the others.
Sourcepub fn library_path(&self) -> &Path
pub fn library_path(&self) -> &Path
Selected compiler library path.
Sourcepub fn module(&self, source: &str) -> Module
pub fn module(&self, source: &str) -> Module
Retain a source module, sharing its parsed index across specializations. Parsing is deferred until the first artifact-cache miss.
Examples found in repository?
7fn main() -> Result<()> {
8 let packets = std::env::args().nth(1).unwrap_or_else(|| "256".into());
9 let artifact = Compiler::for_target(None, &Target::xdna())?
10 .module(include_str!("../tests/kernels/copy.xdna.loom"))
11 .compile(
12 &Specialization::new("copy")
13 .with_config("copy.packets", packets)
14 .with_report(ReportMode::Summary),
15 )?;
16 println!("{}", artifact.path().display());
17 Ok(())
18}More examples
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}
60fn main() -> Result<()> {
61 let args: Vec<_> = std::env::args().collect();
62 let args = if args.len() == 1 {
63 vec![
64 args[0].clone(),
65 String::new(),
66 "gemm".into(),
67 "8".into(),
68 "8".into(),
69 "8".into(),
70 ]
71 } else {
72 args
73 };
74 if args.len() != 6 {
75 return Err(hrx::Error::Message(
76 "usage: gemm_pipeline <gemm.loom> <symbol> M K N".into(),
77 ));
78 }
79 let parse = |i: usize| {
80 args[i]
81 .parse::<usize>()
82 .map_err(|e| hrx::Error::Message(e.to_string()))
83 };
84 let (m, k, n) = (parse(3)?, parse(4)?, parse(5)?);
85 let extent = |a: usize, b: usize, width: usize| {
86 a.checked_mul(b)
87 .filter(|&count| count > 0 && count <= (1 << 30))
88 .and_then(|count| count.checked_mul(width))
89 .ok_or_else(|| hrx::Error::Message("invalid matrix extent".into()))
90 };
91 let (a_bytes, b_bytes, c_bytes) = (extent(m, k, 2)?, extent(k, n, 2)?, extent(m, n, 4)?);
92 let runtime = Runtime::new()?;
93 let device = runtime.npu(0)?;
94 let (source, spec) = if args[1].is_empty() {
95 (
96 include_str!("../tests/kernels/gemm_bf16.xdna.loom").to_string(),
97 hrx::loom::Specialization::new("gemm"),
98 )
99 } else {
100 (
101 std::fs::read_to_string(&args[1])?,
102 hrx::loom::Specialization::new(&args[2])
103 .with_config("gemm.m", m.to_string())
104 .with_config("gemm.k", k.to_string())
105 .with_config("gemm.n", n.to_string()),
106 )
107 };
108 let artifact = hrx::loom::Compiler::for_target(None, device.target())?
109 .module(&source)
110 .compile(&spec)?;
111 let npu = unsafe {
112 device.load_artifact(
113 &artifact,
114 1,
115 KernelContract {
116 bindings: vec![
117 binding(a_bytes, Access::Read),
118 binding(b_bytes, Access::Read),
119 binding(c_bytes, Access::Write),
120 ],
121 constants: vec![],
122 },
123 )
124 }?;
125 let prep = gpu_kernel(
126 &runtime,
127 include_str!("../tests/kernels/euler.loom"),
128 "krea2_euler",
129 &[
130 ("krea2.euler.grid_x", (m * k).div_ceil(256).to_string()),
131 ("krea2.euler.grid_y", "1".into()),
132 ],
133 m * k,
134 vec![
135 binding(a_bytes, Access::ReadWrite),
136 binding(a_bytes, Access::Read),
137 ],
138 )?;
139 let epilogue = gpu_kernel(
140 &runtime,
141 include_str!("../tests/kernels/add_f32.loom"),
142 "add_f32",
143 &[("add.grid", (m * n).div_ceil(256).to_string())],
144 m * n,
145 vec![binding(c_bytes, Access::ReadWrite)],
146 )?;
147 let allocate = |bytes| runtime.allocate(bytes, MemoryPlacement::Shared(device.clone()));
148 let weights = allocate(b_bytes)?;
149 let ones = allocate(a_bytes)?;
150 for buffer in [&weights, &ones] {
151 for word in buffer.map_write()?.as_chunks_mut::<2>().0 {
152 word.copy_from_slice(&0x3f80u16.to_le_bytes());
153 }
154 }
155 let prepare = || -> Result<(ExecutableGraph, Buffer)> {
156 let a = allocate(a_bytes)?;
157 let c = allocate(c_bytes)?;
158 let mut graph = runtime.graph();
159 graph.fill(a.view(), 0)?;
160 graph.gpu(&prep, &[a.view(), ones.view()])?;
161 graph.npu(&npu, &[a.view(), weights.view(), c.view()])?;
162 graph.gpu(&epilogue, &[c.view()])?;
163 Ok((graph.prepare()?, c))
164 };
165 let (first, c0) = prepare()?;
166 let (second, c1) = prepare()?;
167 for _ in 0..5 {
168 first.submit()?.wait()?;
169 second.submit()?.wait()?;
170 }
171 let before = runtime.statistics();
172 let mut latency = Vec::with_capacity(50);
173 for _ in 0..50 {
174 let start = Instant::now();
175 first.submit()?.wait()?;
176 latency.push(start.elapsed().as_secs_f64());
177 }
178 let start = Instant::now();
179 for _ in 0..50 {
180 let a = first.submit()?;
181 let b = second.submit()?;
182 a.wait()?;
183 b.wait()?;
184 }
185 let requests_per_second = 100.0 / start.elapsed().as_secs_f64();
186 for c in [&c0, &c1] {
187 if c.map_read()?
188 .as_chunks::<4>()
189 .0
190 .iter()
191 .any(|v| f32::from_le_bytes(*v) != k as f32 + 1.0)
192 {
193 return Err(hrx::Error::Message("GEMM pipeline output mismatch".into()));
194 }
195 }
196 let after = runtime.statistics();
197 assert_eq!(before.allocations, after.allocations);
198 assert_eq!(before.imports, after.imports);
199 assert_eq!(after.copied_bytes, 0);
200 latency.sort_by(f64::total_cmp);
201 println!(
202 "M={m} K={k} N={n} p50_ms={:.3} p95_ms={:.3} pipelined_requests_s={requests_per_second:.2}",
203 percentile(&latency, 50) * 1e3,
204 percentile(&latency, 95) * 1e3
205 );
206 println!("{}", serde_json::to_string(&after)?);
207 Ok(())
208}279fn prepare(
280 stream: &mut Stream,
281 compiler: &Compiler,
282 case: &Case,
283 shared: Option<std::sync::Arc<Vec<Buffer>>>,
284) -> Result<Arm> {
285 let artifact = compiler.module(case.source).compile(&case.spec)?;
286 let kernel = unsafe { stream.load_artifact(&artifact) }?;
287 let constants = Constants::indices(&kernel, &case.indices)?;
288 let buffers = if let Some(buffers) = shared {
289 buffers
290 } else {
291 std::sync::Arc::new(
292 case.data
293 .iter()
294 .map(|data| {
295 let buffer = stream.allocate(data.len())?;
296 stream.upload_blocking(buffer.binding(), data)?;
297 Ok(buffer)
298 })
299 .collect::<Result<Vec<_>>>()?,
300 )
301 };
302 let bindings: Vec<_> = buffers.iter().map(Buffer::binding).collect();
303 let mut graph = stream.graph()?;
304 let mut prior = None;
305 for _ in 0..32 {
306 prior = Some(unsafe {
307 graph.dispatch(
308 prior.as_slice(),
309 &kernel,
310 case.grid,
311 case.block,
312 &constants,
313 &bindings,
314 )
315 }?);
316 }
317 let identity = serde_json::json!({"compiler":artifact.compiler_identity(), "artifact":hrx::bundle::digest(artifact.bytes()),
318 "report":artifact.report().map(|report| report.json())});
319 Ok(Arm {
320 graph: graph.finish()?,
321 buffers,
322 identity,
323 })
324}21fn main() -> Result<()> {
22 let compiler_only = match std::env::args().nth(1).as_deref() {
23 None => false,
24 Some("--compiler-only") => true,
25 Some(_) => {
26 return Err(hrx::Error::Message(
27 "usage: startup_bench [--compiler-only]".into(),
28 ));
29 }
30 };
31 let initial = memory()?;
32 let start = Instant::now();
33 let stream = if compiler_only {
34 None
35 } else {
36 Some(Stream::open()?)
37 };
38 let open_ms = stream
39 .as_ref()
40 .map(|_| start.elapsed().as_secs_f64() * 1000.0);
41 let after_open = if compiler_only { None } else { Some(memory()?) };
42 let start = Instant::now();
43 let compiler = match &stream {
44 Some(stream) => hrx::loom::Compiler::for_stream(None, stream)?,
45 None => hrx::loom::Compiler::resolve(None)?,
46 };
47 let compiler_ms = start.elapsed().as_secs_f64() * 1000.0;
48 let after_compiler = memory()?;
49 let module = compiler.module(include_str!("../tests/kernels/euler.loom"));
50 let mut spec = Specialization::new("krea2_euler");
51 spec.set_config("krea2.euler.grid_x", "1");
52 spec.set_config("krea2.euler.grid_y", "1");
53 let start = Instant::now();
54 let artifact = module.compile(&spec)?;
55 let first_compile_ms = start.elapsed().as_secs_f64() * 1000.0;
56 let after_compile = memory()?;
57 let start = Instant::now();
58 let cached = module.compile(&spec)?;
59 let cache_hit_ms = start.elapsed().as_secs_f64() * 1000.0;
60 assert_eq!(artifact.bytes(), cached.bytes());
61 println!(
62 "{}",
63 json!({"compiler_only": compiler_only, "open_ms": open_ms, "compiler_ms": compiler_ms,
64 "first_compile_ms": first_compile_ms, "cache_hit_ms": cache_hit_ms,
65 "initial": initial, "after_open": after_open,
66 "after_compiler": after_compiler, "after_compile": after_compile,
67 "compiler_identity": artifact.compiler_identity()})
68 );
69 Ok(())
70}8fn main() -> Result<()> {
9 let args: Vec<_> = std::env::args().collect();
10 if args.len() > 2 {
11 return Err(hrx::Error::Message(
12 "usage: shared_roundtrip [bytes]".into(),
13 ));
14 }
15 let bytes = args
16 .get(1)
17 .map(String::as_str)
18 .unwrap_or("1048576")
19 .parse::<usize>()
20 .map_err(|e| hrx::Error::Message(e.to_string()))?;
21 if bytes == 0 || bytes % 4096 != 0 || bytes > 64 * 1024 * 1024 {
22 return Err(hrx::Error::Message(
23 "bytes must be a multiple of 4096 in 4096..=67108864".into(),
24 ));
25 }
26 let runtime = Runtime::new()?;
27 let device = runtime.npu(0)?;
28 let artifact = hrx::loom::Compiler::for_target(None, device.target())?
29 .module(include_str!("../tests/kernels/copy.xdna.loom"))
30 .compile(
31 &hrx::loom::Specialization::new("copy")
32 .with_config("copy.packets", (bytes / 4096).to_string()),
33 )?;
34 let binding = |bytes, access| BindingContract {
35 bytes,
36 access,
37 alignment: 4,
38 layout: "u32 passthrough".into(),
39 };
40 let contract = KernelContract {
41 bindings: vec![binding(bytes, Access::Read), binding(bytes, Access::Write)],
42 constants: vec![],
43 };
44 let kernel = unsafe { device.load_artifact(&artifact, 1, contract) }?;
45 let a = runtime.allocate(bytes, MemoryPlacement::Shared(device.clone()))?;
46 let c = runtime.allocate(bytes, MemoryPlacement::Shared(device.clone()))?;
47 let result = runtime.allocate(bytes, MemoryPlacement::Shared(device.clone()))?;
48 let mut graph = runtime.graph();
49 graph.fill(a.view(), 0x5a)?;
50 graph.npu(&kernel, &[a.view(), c.view()])?;
51 graph.copy(result.view(), c.view())?;
52 let graph = graph.prepare()?;
53 for _ in 0..10 {
54 graph.submit()?.wait()?;
55 let mapped = result.map_read()?;
56 if mapped.iter().any(|&b| b != 0x5a) {
57 return Err(hrx::Error::Message("GPU/NPU roundtrip mismatch".into()));
58 }
59 }
60 println!(
61 "GPU -> NPU -> GPU: all {bytes} bytes correct, 10 executions, {} prepared regions",
62 graph.regions()
63 );
64 Ok(())
65}9fn main() -> Result<()> {
10 let args: Vec<_> = std::env::args().collect();
11 if args.len() > 2 {
12 return Err(hrx::Error::Message("usage: shared_bench [bytes]".into()));
13 }
14 let bytes = args
15 .get(1)
16 .map(String::as_str)
17 .unwrap_or("1048576")
18 .parse::<usize>()
19 .map_err(|e| hrx::Error::Message(e.to_string()))?;
20 if bytes == 0 || bytes % 4096 != 0 || bytes > 64 * 1024 * 1024 {
21 return Err(hrx::Error::Message(
22 "bytes must be a multiple of 4096 in 4096..=67108864".into(),
23 ));
24 }
25 let runtime = Runtime::new()?;
26 let device = runtime.npu(0)?;
27 let artifact = hrx::loom::Compiler::for_target(None, device.target())?
28 .module(include_str!("../tests/kernels/copy.xdna.loom"))
29 .compile(
30 &hrx::loom::Specialization::new("copy")
31 .with_config("copy.packets", (bytes / 4096).to_string()),
32 )?;
33 let binding = |bytes, access| BindingContract {
34 bytes,
35 access,
36 alignment: 4,
37 layout: "passthrough".into(),
38 };
39 let kernel = unsafe {
40 device.load_artifact(
41 &artifact,
42 1,
43 KernelContract {
44 bindings: vec![binding(bytes, Access::Read), binding(bytes, Access::Write)],
45 constants: vec![],
46 },
47 )
48 }?;
49 let a = runtime.allocate(bytes, MemoryPlacement::Shared(device.clone()))?;
50 let c = runtime.allocate(bytes, MemoryPlacement::Shared(device.clone()))?;
51 let mut graph = runtime.graph();
52 graph.fill(a.view(), 0x35)?;
53 graph.npu(&kernel, &[a.view(), c.view()])?;
54 let graph = graph.prepare()?;
55 for _ in 0..5 {
56 graph.submit()?.wait()?;
57 }
58 let mut samples = Vec::new();
59 for _ in 0..100 {
60 let start = Instant::now();
61 graph.submit()?.wait()?;
62 samples.push(start.elapsed().as_secs_f64());
63 }
64 if c.map_read()?.iter().any(|&b| b != 0x35) {
65 return Err(hrx::Error::Message("incorrect benchmark output".into()));
66 }
67 samples.sort_by(f64::total_cmp);
68 println!(
69 "bytes={bytes} p50_us={:.2} p95_us={:.2}",
70 percentile(&samples, 50) * 1e6,
71 percentile(&samples, 95) * 1e6
72 );
73 Ok(())
74}Sourcepub fn sources(&self, sources: Vec<Source>) -> Result<Module>
pub fn sources(&self, sources: Vec<Source>) -> Result<Module>
Link named Loom and C/C++ inputs in one compiler-owned source module. Source bytes, headers, macro order and import options all enter its identity.
Sourcepub fn import_cxx(&self, source: CxxSource) -> Result<Module>
pub fn import_cxx(&self, source: CxxSource) -> Result<Module>
Import a translation unit; parsing is deferred until compilation.
Examples found in repository?
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_compiler = Compiler::for_target(None, gpu.target())?;
143 let gpu_artifact = gpu_compiler
144 .import_cxx(CxxSource::new(
145 "affine.cpp",
146 format!(
147 r#"
148#include <hip/hip_runtime.h>
149__global__ [[loom::workgroup_size(256, 1, 1), loom::workgroup_count({}, 1, 1)]]
150void affine(const int* input, int* output) {{
151 unsigned i = blockIdx.x * 256u + threadIdx.x;
152 output[i] = input[i] * 3 + 7;
153}}
154"#,
155 elements / 256
156 ),
157 ))?
158 .compile(&Specialization::new("affine"))?;
159 // SAFETY: the fixed launch covers exactly elements i32 values in each
160 // nonaliasing binding; bounded input values cannot overflow the arithmetic.
161 let gpu_kernel = unsafe {
162 runtime.load_gpu_artifact(
163 &gpu_artifact,
164 &[],
165 contract(&[(elements * 4, Access::Read), (elements * 4, Access::Write)]),
166 )
167 }?;
168 let npu_artifact = Compiler::for_target(None, npu.target())?
169 .module(include_str!("kernels/batched_gemm.xdna.loom"))
170 .compile(&Specialization::new("gemm").with_config("gemm.batch", matrices.to_string()))?;
171 // SAFETY: the pipeline consumes matrices pairs of 8x8 BF16 inputs and
172 // produces the same number of 8x8 FP32 outputs on one NPU column.
173 let npu_kernel = unsafe {
174 npu.load_artifact(
175 &npu_artifact,
176 1,
177 contract(&[
178 (matrices * 128, Access::Read),
179 (matrices * 128, Access::Read),
180 (matrices * 256, Access::Write),
181 ]),
182 )
183 }?;
184 let input = runtime.allocate(elements * 4, MemoryPlacement::GpuLocal)?;
185 let output = runtime.allocate(elements * 4, MemoryPlacement::GpuLocal)?;
186 let host_input = runtime.allocate(elements * 4, MemoryPlacement::HostVisible)?;
187 let host_output = runtime.allocate(elements * 4, MemoryPlacement::HostVisible)?;
188 let mut upload = runtime.graph();
189 upload.copy(input.view(), host_input.view())?;
190 let upload = upload.prepare()?;
191 let mut download = runtime.graph();
192 download.copy(host_output.view(), output.view())?;
193 let download = download.prepare()?;
194 let a = runtime.allocate(matrices * 128, MemoryPlacement::NpuLocal(npu.clone()))?;
195 let b = runtime.allocate(matrices * 128, MemoryPlacement::NpuLocal(npu.clone()))?;
196 let c = runtime.allocate(matrices * 256, MemoryPlacement::NpuLocal(npu))?;
197
198 let mut gpu_only = runtime.graph();
199 gpu_only.gpu(&gpu_kernel, &[input.view(), output.view()])?;
200 let gpu_only = gpu_only.prepare()?;
201 let mut npu_only = runtime.graph();
202 npu_only.npu(&npu_kernel, &[a.view(), b.view(), c.view()])?;
203 let npu_only = npu_only.prepare()?;
204
205 // Disjoint buffers imply no dependency edge. One submit admits both
206 // branches to separate GPU/NPU workers; one wait joins their completions.
207 let mut parallel = runtime.graph();
208 parallel.gpu(&gpu_kernel, &[input.view(), output.view()])?;
209 parallel.npu(&npu_kernel, &[a.view(), b.view(), c.view()])?;
210 let parallel = parallel.prepare()?;
211 for pass in 0..3 {
212 initialize(&host_input, &a, &b, pass * 2)?;
213 upload.submit()?.wait()?;
214 gpu_only.submit()?.wait()?;
215 npu_only.submit()?.wait()?;
216 download.submit()?.wait()?;
217 validate(&host_output, &c, pass * 2)?;
218 // Change both inputs so validation cannot accept the sequential run's
219 // old output if either parallel branch fails to write its result.
220 initialize(&host_input, &a, &b, pass * 2 + 1)?;
221 upload.submit()?.wait()?;
222 parallel.submit()?.wait()?;
223 download.submit()?.wait()?;
224 validate(&host_output, &c, pass * 2 + 1)?;
225 }
226
227 let before = runtime.statistics();
228 let mut sequential_ms = Vec::with_capacity(samples);
229 let mut parallel_ms = Vec::with_capacity(samples);
230 for sample in 0..samples {
231 // Alternate order to reduce systematic clock/thermal ordering bias.
232 for concurrent in if sample % 2 == 0 {
233 [false, true]
234 } else {
235 [true, false]
236 } {
237 if concurrent {
238 parallel_ms.push(completed_ms(¶llel)?);
239 } else {
240 let start = Instant::now();
241 gpu_only.submit()?.wait()?;
242 npu_only.submit()?.wait()?;
243 sequential_ms.push(start.elapsed().as_secs_f64() * 1000.0);
244 }
245 }
246 }
247 let after = runtime.statistics();
248 if before.allocations != after.allocations || before.imports != after.imports {
249 return Err(Error::Message(
250 "replay unexpectedly allocated or imported storage".into(),
251 ));
252 }
253 download.submit()?.wait()?;
254 validate(&host_output, &c, 5)?;
255 let sequential = median(&mut sequential_ms);
256 let concurrent = median(&mut parallel_ms);
257 println!("{elements} GPU elements + {matrices} NPU matrices; {samples} paired samples");
258 println!(
259 "sequential median: {sequential:.3} ms; parallel median: {concurrent:.3} ms; speedup: {:.2}x",
260 sequential / concurrent
261 );
262 println!(
263 "Both CPU references match exactly, including changed-input replays. No native allocations/imports during timed replay."
264 );
265
266 // Capture separately so tracing does not affect the benchmark samples.
267 runtime.start_trace(32)?;
268 for _ in 0..4 {
269 parallel.submit()?.wait()?;
270 }
271 let trace = runtime
272 .finish_trace()
273 .ok_or_else(|| Error::Message("missing execution trace".into()))?;
274 let overlap: f64 = trace
275 .events
276 .iter()
277 .filter(|e| e.lane == 1)
278 .flat_map(|gpu| {
279 trace.events.iter().filter(|e| e.lane == 3).map(move |npu| {
280 ((gpu.start_us + gpu.duration_us).min(npu.start_us + npu.duration_us)
281 - gpu.start_us.max(npu.start_us))
282 .max(0.0)
283 })
284 })
285 .sum();
286 println!(
287 "GPU/NPU host-observed overlap across four replays: {overlap:.1} us (not device timestamps)"
288 );
289 if let Some(path) = trace_path {
290 std::fs::write(&path, trace.to_json()?)?;
291 println!("Trace: {}", path.display());
292 }
293 Ok(())
294}Trait Implementations§
Auto Trait Implementations§
impl Freeze for Compiler
impl RefUnwindSafe for Compiler
impl Send for Compiler
impl Sync for Compiler
impl Unpin for Compiler
impl UnsafeUnpin for Compiler
impl UnwindSafe for Compiler
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
Source§impl<T> DropFlavorWrapper<T> for T
impl<T> DropFlavorWrapper<T> for T
Source§impl<T, W> HasTypeWitness<W> for Twhere
W: MakeTypeWitness<Arg = T>,
T: ?Sized,
impl<T, W> HasTypeWitness<W> for Twhere
W: MakeTypeWitness<Arg = T>,
T: ?Sized,
Source§impl<T> Identity for Twhere
T: ?Sized,
impl<T> Identity for Twhere
T: ?Sized,
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more