Skip to main content

Specialization

Struct Specialization 

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

One export and its exact configuration within a module.

Implementations§

Source§

impl Specialization

Source

pub fn new(symbol: impl Into<String>) -> Self

Select an export with no configuration overrides.

Examples found in repository?
examples/compile_npu.rs (line 12)
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
Hide additional examples
examples/gemm_pipeline.rs (line 33)
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}
examples/startup_bench.rs (line 50)
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}
examples/shared_roundtrip.rs (line 31)
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}
examples/shared_bench.rs (line 30)
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}
examples/mall_probe.rs (line 27)
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}
Source

pub fn with_config( self, key: impl Into<String>, value: impl Into<String>, ) -> Self

Set one fully-qualified Loom configuration value.

Examples found in repository?
examples/compile_npu.rs (line 13)
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
Hide additional examples
examples/shared_roundtrip.rs (line 32)
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}
examples/shared_bench.rs (line 31)
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}
examples/mall_probe.rs (line 28)
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}
examples/gemm_pipeline.rs (line 103)
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}
examples/gpu_npu_parallel.rs (line 170)
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(&parallel)?);
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}
Source

pub fn set_config( &mut self, key: impl Into<String>, value: impl Into<String>, ) -> &mut Self

Set one fully-qualified Loom configuration value in place.

Examples found in repository?
examples/gemm_pipeline.rs (line 35)
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/startup_bench.rs (line 51)
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}
examples/patch_bench.rs (line 76)
56fn attention(tokens: usize, repack: bool) -> Case {
57    let capacity = tokens + 32;
58    let symbol = if repack {
59        "krea2_attention_query32"
60    } else {
61        "krea2_attention_gqa_lds_f16_wmma"
62    };
63    let prefix = if repack {
64        "krea2.attention_query32"
65    } else {
66        "krea2.attention_gqa_lds_f16_wmma"
67    };
68    let mut spec = Specialization::new(symbol).with_report(ReportMode::Summary);
69    for (key, value) in [
70        ("q_stride", 512),
71        ("kv_stride", 128),
72        ("out_stride", 512),
73        ("tokens", tokens),
74        ("token_capacity", capacity),
75    ] {
76        spec.set_config(format!("{prefix}.{key}"), value.to_string());
77    }
78    spec.set_config(format!("{prefix}.scale"), "0.08838834764831845");
79    let query = |row: usize, head: usize| ((row + head * 3) % 9) as f32 * 0.0625 - 0.25;
80    let key = |row: usize| (row % 7) as f32 * 0.0625 - 0.1875;
81    let value = |row: usize, channel: usize| {
82        (row % 7) as f32 * 0.125 - 0.375 + (channel % 8) as f32 * 0.0625
83    };
84    let q = halves((0..capacity * 512).map(|i| {
85        if i / 512 < tokens {
86            query(i / 512, (i % 512) / 128)
87        } else {
88            0.0
89        }
90    }));
91    let k = halves((0..capacity * 128).map(|i| if i / 128 < tokens { key(i / 128) } else { 0.0 }));
92    let v = halves((0..capacity * 128).map(|i| {
93        let (row, channel) = if repack {
94            (i % capacity, i / capacity)
95        } else {
96            (i / 128, i % 128)
97        };
98        if row < tokens {
99            value(row, channel)
100        } else {
101            0.0
102        }
103    }));
104    // Independent f64 softmax oracle. Inputs are constant across the 128 Q/K
105    // channels, so their exact dot product is 128 times the scalar product.
106    let mut expected = Vec::with_capacity(tokens * 512);
107    for row in 0..tokens {
108        for head in 0..4 {
109            let mut numerator = 0.0;
110            let mut denominator = 0.0;
111            for token in 0..tokens {
112                let score = (128.0 * f64::from(query(row, head)) * f64::from(key(token))
113                    / 128f64.sqrt())
114                .exp();
115                numerator += score * f64::from(value(token, 0));
116                denominator += score;
117            }
118            expected.extend(
119                (0..128).map(|channel| numerator / denominator + (channel % 8) as f64 * 0.0625),
120            );
121        }
122    }
123    Case {
124        name: format!("{symbol}-{tokens}"),
125        source: if repack {
126            include_str!("../native/qualification/attention_query32.loom")
127        } else {
128            include_str!("../native/qualification/attention_gqa_lds_f16_wmma.loom")
129        },
130        spec,
131        grid: [(tokens / if repack { 32 } else { 16 }) as u32, 1, 1],
132        block: [if repack { 256 } else { 128 }, 1, 1],
133        indices: vec![tokens as u32],
134        data: vec![q, k, v, vec![0; tokens * 512 * 2]],
135        output: 3,
136        expected,
137        tolerance: 0.001,
138    }
139}
140fn gemm(large: bool, integer: bool) -> Case {
141    let (m, n, k) = if large {
142        (256usize, 1024usize, 1024usize)
143    } else {
144        (256, 512, 256)
145    };
146    let symbol = if integer {
147        "krea2_gemm_i8_256"
148    } else {
149        "h3_gemm_f16_fast_256b"
150    };
151    let prefix = if integer {
152        "krea2.gemm_i8_256"
153    } else {
154        "h3.gemm_f16_fast_256b"
155    };
156    let mut spec = Specialization::new(symbol).with_report(ReportMode::Summary);
157    for (name, value) in [
158        ("k_size", k),
159        ("n_size", n),
160        ("k_stride", k),
161        ("m_group", 1),
162    ] {
163        spec.set_config(format!("{prefix}.{name}"), value.to_string());
164    }
165    let a = |row: usize, col: usize| ((row * 3 + col * 5 + row * col) % 11) as i32 - 5;
166    let w = |row: usize, col: usize| ((row * 7 + col * 3 + row * col) % 13) as i32 - 6;
167    let bias = |col: usize| (col % 7) as f32 * 0.03125 - 0.09375;
168    // Evaluate the full dot product once per distinct row-pattern pair. Pattern
169    // repetition in the authored inputs is independent of the compiled program.
170    let mut products = [[0f32; 13]; 11];
171    for (row, values) in products.iter_mut().enumerate() {
172        for (col, product) in values.iter_mut().enumerate() {
173            let sum: i32 = (0..k).map(|i| a(row, i) * w(col, i)).sum();
174            *product = sum as f32 / 64.0;
175        }
176    }
177    let expected = (0..m * n)
178        .map(|i| {
179            let dot = products[(i / n) % 11][(i % n) % 13];
180            float(half(if integer {
181                bf16(dot)
182            } else {
183                dot + bias(i % n)
184            }))
185        })
186        .collect();
187    let data = if integer {
188        vec![
189            (0..m * k).map(|i| a(i / k, i % k) as i8 as u8).collect(),
190            (0..n * k).map(|i| w(i / k, i % k) as i8 as u8).collect(),
191            floats(vec![0.125; n]),
192            floats(vec![0.125; m]),
193            vec![0; m * n * 2],
194        ]
195    } else {
196        vec![
197            halves((0..m * k).map(|i| a(i / k, i % k) as f32 * 0.125)),
198            halves((0..n * k).map(|i| w(i / k, i % k) as f32 * 0.125)),
199            vec![0; m * n * 2],
200            floats((0..n).map(bias)),
201        ]
202    };
203    Case {
204        name: format!("{symbol}-{m}x{n}x{k}"),
205        source: if integer {
206            include_str!("../native/qualification/gemm_i8_256.loom")
207        } else {
208            include_str!("../native/qualification/gemm_f16_fast_256b.loom")
209        },
210        spec,
211        grid: [
212            (n / if integer { 128 } else { 256 }) as u32,
213            (m / if integer { 256 } else { 128 }) as u32,
214            1,
215        ],
216        block: [256, 1, 1],
217        indices: vec![m as u32],
218        data,
219        output: if integer { 4 } else { 2 },
220        expected,
221        // Permit subnormal residuals from the floating-point matrix accumulator.
222        // Integer GEMM remains exact; the absolute floor is one fp16 minimum normal.
223        tolerance: if integer { 0.0 } else { 2f64.powi(-14) },
224    }
225}
226fn conv(height: usize, cin: usize, n: usize) -> Case {
227    let symbol = "arcface_conv3x3_f16_wmma_bnprelu";
228    let mut spec = Specialization::new(symbol).with_report(ReportMode::Details);
229    for (key, value) in [
230        ("height", height),
231        ("width", height),
232        ("stride", 1),
233        ("cin_pad", cin),
234        ("cin_stride", cin),
235        ("k_size", 9 * cin),
236        ("n_size", n),
237    ] {
238        spec.set_config(
239            format!("arcface.conv3x3_f16_wmma_bnprelu.{key}"),
240            value.to_string(),
241        );
242    }
243    let m = height * height;
244    // Constant power-of-two operands: exact mathematical convolution, including
245    // zero padding at borders. Bias is zero and PReLU slope is one.
246    let expected = (0..m)
247        .flat_map(|p| {
248            let y = p / height;
249            let x = p % height;
250            let rows = if y == 0 || y + 1 == height { 2 } else { 3 };
251            let cols = if x == 0 || x + 1 == height { 2 } else { 3 };
252            std::iter::repeat_n((rows * cols * cin) as f64 / 64.0, n)
253        })
254        .collect();
255    Case {
256        name: format!("arcface_conv-{height}x{height}x{cin}x{n}"),
257        source: include_str!("../native/qualification/arcface_conv3x3_f16_wmma_bnprelu.loom"),
258        spec,
259        grid: [(n / 64) as u32, m.div_ceil(64) as u32, 1],
260        block: [256, 1, 1],
261        indices: vec![m as u32],
262        data: vec![
263            halves(std::iter::repeat_n(0.125, m * cin)),
264            halves(std::iter::repeat_n(0.125, n * 9 * cin)),
265            floats(std::iter::repeat_n(0.0, 9 * n)),
266            vec![0; m * n * 2],
267            floats(std::iter::repeat_n(1.0, n)),
268        ],
269        output: 3,
270        expected,
271        tolerance: 0.0,
272    }
273}
examples/stream_bench.rs (line 79)
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_symbol(&mut self, symbol: impl Into<String>) -> &mut Self

Select an export/root symbol in place.

Source

pub fn replace_config(&mut self, config: BTreeMap<String, String>) -> &mut Self

Replace all fully-qualified Loom configuration values.

Source

pub fn with_report(self, report: ReportMode) -> Self

Request or suppress the compiler resource report.

Examples found in repository?
examples/compile_npu.rs (line 14)
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
Hide additional examples
examples/patch_bench.rs (line 68)
56fn attention(tokens: usize, repack: bool) -> Case {
57    let capacity = tokens + 32;
58    let symbol = if repack {
59        "krea2_attention_query32"
60    } else {
61        "krea2_attention_gqa_lds_f16_wmma"
62    };
63    let prefix = if repack {
64        "krea2.attention_query32"
65    } else {
66        "krea2.attention_gqa_lds_f16_wmma"
67    };
68    let mut spec = Specialization::new(symbol).with_report(ReportMode::Summary);
69    for (key, value) in [
70        ("q_stride", 512),
71        ("kv_stride", 128),
72        ("out_stride", 512),
73        ("tokens", tokens),
74        ("token_capacity", capacity),
75    ] {
76        spec.set_config(format!("{prefix}.{key}"), value.to_string());
77    }
78    spec.set_config(format!("{prefix}.scale"), "0.08838834764831845");
79    let query = |row: usize, head: usize| ((row + head * 3) % 9) as f32 * 0.0625 - 0.25;
80    let key = |row: usize| (row % 7) as f32 * 0.0625 - 0.1875;
81    let value = |row: usize, channel: usize| {
82        (row % 7) as f32 * 0.125 - 0.375 + (channel % 8) as f32 * 0.0625
83    };
84    let q = halves((0..capacity * 512).map(|i| {
85        if i / 512 < tokens {
86            query(i / 512, (i % 512) / 128)
87        } else {
88            0.0
89        }
90    }));
91    let k = halves((0..capacity * 128).map(|i| if i / 128 < tokens { key(i / 128) } else { 0.0 }));
92    let v = halves((0..capacity * 128).map(|i| {
93        let (row, channel) = if repack {
94            (i % capacity, i / capacity)
95        } else {
96            (i / 128, i % 128)
97        };
98        if row < tokens {
99            value(row, channel)
100        } else {
101            0.0
102        }
103    }));
104    // Independent f64 softmax oracle. Inputs are constant across the 128 Q/K
105    // channels, so their exact dot product is 128 times the scalar product.
106    let mut expected = Vec::with_capacity(tokens * 512);
107    for row in 0..tokens {
108        for head in 0..4 {
109            let mut numerator = 0.0;
110            let mut denominator = 0.0;
111            for token in 0..tokens {
112                let score = (128.0 * f64::from(query(row, head)) * f64::from(key(token))
113                    / 128f64.sqrt())
114                .exp();
115                numerator += score * f64::from(value(token, 0));
116                denominator += score;
117            }
118            expected.extend(
119                (0..128).map(|channel| numerator / denominator + (channel % 8) as f64 * 0.0625),
120            );
121        }
122    }
123    Case {
124        name: format!("{symbol}-{tokens}"),
125        source: if repack {
126            include_str!("../native/qualification/attention_query32.loom")
127        } else {
128            include_str!("../native/qualification/attention_gqa_lds_f16_wmma.loom")
129        },
130        spec,
131        grid: [(tokens / if repack { 32 } else { 16 }) as u32, 1, 1],
132        block: [if repack { 256 } else { 128 }, 1, 1],
133        indices: vec![tokens as u32],
134        data: vec![q, k, v, vec![0; tokens * 512 * 2]],
135        output: 3,
136        expected,
137        tolerance: 0.001,
138    }
139}
140fn gemm(large: bool, integer: bool) -> Case {
141    let (m, n, k) = if large {
142        (256usize, 1024usize, 1024usize)
143    } else {
144        (256, 512, 256)
145    };
146    let symbol = if integer {
147        "krea2_gemm_i8_256"
148    } else {
149        "h3_gemm_f16_fast_256b"
150    };
151    let prefix = if integer {
152        "krea2.gemm_i8_256"
153    } else {
154        "h3.gemm_f16_fast_256b"
155    };
156    let mut spec = Specialization::new(symbol).with_report(ReportMode::Summary);
157    for (name, value) in [
158        ("k_size", k),
159        ("n_size", n),
160        ("k_stride", k),
161        ("m_group", 1),
162    ] {
163        spec.set_config(format!("{prefix}.{name}"), value.to_string());
164    }
165    let a = |row: usize, col: usize| ((row * 3 + col * 5 + row * col) % 11) as i32 - 5;
166    let w = |row: usize, col: usize| ((row * 7 + col * 3 + row * col) % 13) as i32 - 6;
167    let bias = |col: usize| (col % 7) as f32 * 0.03125 - 0.09375;
168    // Evaluate the full dot product once per distinct row-pattern pair. Pattern
169    // repetition in the authored inputs is independent of the compiled program.
170    let mut products = [[0f32; 13]; 11];
171    for (row, values) in products.iter_mut().enumerate() {
172        for (col, product) in values.iter_mut().enumerate() {
173            let sum: i32 = (0..k).map(|i| a(row, i) * w(col, i)).sum();
174            *product = sum as f32 / 64.0;
175        }
176    }
177    let expected = (0..m * n)
178        .map(|i| {
179            let dot = products[(i / n) % 11][(i % n) % 13];
180            float(half(if integer {
181                bf16(dot)
182            } else {
183                dot + bias(i % n)
184            }))
185        })
186        .collect();
187    let data = if integer {
188        vec![
189            (0..m * k).map(|i| a(i / k, i % k) as i8 as u8).collect(),
190            (0..n * k).map(|i| w(i / k, i % k) as i8 as u8).collect(),
191            floats(vec![0.125; n]),
192            floats(vec![0.125; m]),
193            vec![0; m * n * 2],
194        ]
195    } else {
196        vec![
197            halves((0..m * k).map(|i| a(i / k, i % k) as f32 * 0.125)),
198            halves((0..n * k).map(|i| w(i / k, i % k) as f32 * 0.125)),
199            vec![0; m * n * 2],
200            floats((0..n).map(bias)),
201        ]
202    };
203    Case {
204        name: format!("{symbol}-{m}x{n}x{k}"),
205        source: if integer {
206            include_str!("../native/qualification/gemm_i8_256.loom")
207        } else {
208            include_str!("../native/qualification/gemm_f16_fast_256b.loom")
209        },
210        spec,
211        grid: [
212            (n / if integer { 128 } else { 256 }) as u32,
213            (m / if integer { 256 } else { 128 }) as u32,
214            1,
215        ],
216        block: [256, 1, 1],
217        indices: vec![m as u32],
218        data,
219        output: if integer { 4 } else { 2 },
220        expected,
221        // Permit subnormal residuals from the floating-point matrix accumulator.
222        // Integer GEMM remains exact; the absolute floor is one fp16 minimum normal.
223        tolerance: if integer { 0.0 } else { 2f64.powi(-14) },
224    }
225}
226fn conv(height: usize, cin: usize, n: usize) -> Case {
227    let symbol = "arcface_conv3x3_f16_wmma_bnprelu";
228    let mut spec = Specialization::new(symbol).with_report(ReportMode::Details);
229    for (key, value) in [
230        ("height", height),
231        ("width", height),
232        ("stride", 1),
233        ("cin_pad", cin),
234        ("cin_stride", cin),
235        ("k_size", 9 * cin),
236        ("n_size", n),
237    ] {
238        spec.set_config(
239            format!("arcface.conv3x3_f16_wmma_bnprelu.{key}"),
240            value.to_string(),
241        );
242    }
243    let m = height * height;
244    // Constant power-of-two operands: exact mathematical convolution, including
245    // zero padding at borders. Bias is zero and PReLU slope is one.
246    let expected = (0..m)
247        .flat_map(|p| {
248            let y = p / height;
249            let x = p % height;
250            let rows = if y == 0 || y + 1 == height { 2 } else { 3 };
251            let cols = if x == 0 || x + 1 == height { 2 } else { 3 };
252            std::iter::repeat_n((rows * cols * cin) as f64 / 64.0, n)
253        })
254        .collect();
255    Case {
256        name: format!("arcface_conv-{height}x{height}x{cin}x{n}"),
257        source: include_str!("../native/qualification/arcface_conv3x3_f16_wmma_bnprelu.loom"),
258        spec,
259        grid: [(n / 64) as u32, m.div_ceil(64) as u32, 1],
260        block: [256, 1, 1],
261        indices: vec![m as u32],
262        data: vec![
263            halves(std::iter::repeat_n(0.125, m * cin)),
264            halves(std::iter::repeat_n(0.125, n * 9 * cin)),
265            floats(std::iter::repeat_n(0.0, 9 * n)),
266            vec![0; m * n * 2],
267            floats(std::iter::repeat_n(1.0, n)),
268        ],
269        output: 3,
270        expected,
271        tolerance: 0.0,
272    }
273}
Source

pub fn set_report(&mut self, report: ReportMode) -> &mut Self

Request or suppress the compiler resource report in place.

Source

pub fn symbol(&self) -> &str

Export/root symbol selected by this specialization.

Source

pub fn configuration(&self) -> &BTreeMap<String, String>

Fully-qualified compiler configuration.

Source

pub fn report_mode(&self) -> ReportMode

Compiler evidence requested by this specialization.

Trait Implementations§

Source§

impl Clone for Specialization

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

impl Debug for Specialization

Source§

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

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

impl Default for Specialization

Source§

fn default() -> Self

Returns the “default value” for a type. 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<E> ResultError for E
where E: Send + Debug + Sync,

Source§

impl<T> ResultType for T
where T: Send + Clone + Sync + Debug,

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