1#![warn(missing_docs)]
10
11mod constraint;
12mod spec;
13
14pub use constraint::{Constraint, eval_arith_expr};
15pub use spec::{Dim, DimRole, KernelSpec, SafetyExpectation, Value};
16
17use sha2::{Digest, Sha256};
18use std::collections::BTreeMap;
19use std::fmt;
20
21#[derive(Debug, thiserror::Error)]
23pub enum SpaceError {
24 #[error("failed to read {path}: {source}")]
26 Io {
27 path: String,
29 source: std::io::Error,
31 },
32 #[error("failed to parse {path}: {source}")]
34 Parse {
35 path: String,
37 source: Box<toml::de::Error>,
40 },
41 #[error("invalid kernel spec: {0}")]
45 Invalid(String),
46 #[error("invalid constraint `{expr}`: {reason}")]
50 Constraint {
51 expr: String,
53 reason: String,
55 },
56}
57
58#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct Config {
63 kernel: String,
64 values: BTreeMap<String, Value>,
65}
66
67impl Config {
68 pub fn kernel(&self) -> &str {
70 &self.kernel
71 }
72
73 pub fn get(&self, dim: &str) -> Option<&Value> {
76 self.values.get(dim)
77 }
78
79 pub fn values(&self) -> impl Iterator<Item = (&str, &Value)> {
85 self.values.iter().map(|(k, v)| (k.as_str(), v))
86 }
87
88 pub fn block_threads(&self) -> u64 {
104 ["block_x", "block_y", "block_z"]
105 .iter()
106 .map(|d| match self.values.get(*d) {
107 Some(Value::Int(n)) => *n,
108 _ => 1,
109 })
110 .fold(1u64, |acc, n| acc.saturating_mul(n))
111 }
112
113 pub fn id(&self) -> ConfigId {
117 let mut hasher = Sha256::new();
118 hasher.update(b"launchbound.config.v1\0");
119 hasher.update(self.kernel.as_bytes());
120 hasher.update(b"\0");
121 for (name, value) in &self.values {
122 hasher.update(name.as_bytes());
123 hasher.update(b"=");
124 match value {
125 Value::Int(n) => hasher.update(n.to_string().as_bytes()),
126 Value::Str(s) => hasher.update(s.as_bytes()),
127 }
128 hasher.update(b"\n");
129 }
130 let digest = hasher.finalize();
131 let mut hex = String::with_capacity(16);
132 for byte in &digest[..8] {
133 hex.push_str(&format!("{byte:02x}"));
134 }
135 ConfigId(format!("c1-{hex}"))
136 }
137
138 pub fn spec_key(&self, spec: &KernelSpec) -> String {
142 let mut parts = Vec::new();
143 for (name, value) in &self.values {
144 if spec.dim(name).is_some_and(|d| d.role == DimRole::Spec) {
145 parts.push(format!("{name}={value}"));
146 }
147 }
148 parts.join(",")
149 }
150}
151
152impl fmt::Display for Config {
153 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154 let mut first = true;
155 for (name, value) in &self.values {
156 if !first {
157 write!(f, " ")?;
158 }
159 write!(f, "{name}={value}")?;
160 first = false;
161 }
162 Ok(())
163 }
164}
165
166#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
168pub struct ConfigId(String);
169
170impl ConfigId {
171 pub fn as_str(&self) -> &str {
174 &self.0
175 }
176}
177
178impl fmt::Display for ConfigId {
179 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
180 f.write_str(&self.0)
181 }
182}
183
184pub fn enumerate(spec: &KernelSpec) -> Result<Vec<Config>, SpaceError> {
189 let dims: Vec<&Dim> = spec.dims_sorted();
190 let mut out = Vec::new();
191 if dims.is_empty() {
192 return Ok(out);
193 }
194 let mut indices = vec![0usize; dims.len()];
195 'outer: loop {
196 let mut values = BTreeMap::new();
197 for (dim, &idx) in dims.iter().zip(&indices) {
198 values.insert(dim.name.clone(), dim.values[idx].clone());
199 }
200 let config = Config {
201 kernel: spec.name.clone(),
202 values,
203 };
204 if spec
205 .constraints
206 .iter()
207 .try_fold(true, |ok, c| c.eval(&config).map(|v| ok && v))?
208 {
209 out.push(config);
210 }
211 for pos in (0..dims.len()).rev() {
213 indices[pos] += 1;
214 if indices[pos] < dims[pos].values.len() {
215 continue 'outer;
216 }
217 indices[pos] = 0;
218 }
219 break;
220 }
221 Ok(out)
222}
223
224pub fn raw_size(spec: &KernelSpec) -> u64 {
226 spec.dims_sorted()
227 .iter()
228 .map(|d| d.values.len() as u64)
229 .product()
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235
236 fn toy_spec() -> KernelSpec {
237 KernelSpec::from_toml_str(
238 "toy",
239 r#"
240 [kernel]
241 name = "toy"
242 entry = "toy"
243 domain = 1
244 [dims.block_x]
245 values = [32, 64, 128]
246 [dims.tile]
247 role = "spec"
248 values = [128, 256]
249 [constraints]
250 exprs = ["tile % block_x == 0"]
251 "#,
252 )
253 .unwrap()
254 }
255
256 #[test]
257 fn enumeration_is_deterministic_and_filtered() {
258 let spec = toy_spec();
259 let a = enumerate(&spec).unwrap();
260 let b = enumerate(&spec).unwrap();
261 assert_eq!(a, b);
262 assert_eq!(raw_size(&spec), 6);
266 assert_eq!(a.len(), 6);
267 }
268
269 #[test]
270 fn constraint_actually_filters() {
271 let spec = KernelSpec::from_toml_str(
272 "toy",
273 r#"
274 [kernel]
275 name = "toy"
276 entry = "toy"
277 domain = 1
278 [dims.block_x]
279 values = [32, 48]
280 [dims.tile]
281 values = [64]
282 [constraints]
283 exprs = ["tile % block_x == 0"]
284 "#,
285 )
286 .unwrap();
287 let configs = enumerate(&spec).unwrap();
288 assert_eq!(configs.len(), 1);
289 assert_eq!(configs[0].get("block_x"), Some(&Value::Int(32)));
290 }
291
292 #[test]
293 fn ids_are_stable_and_distinct() {
294 let spec = toy_spec();
295 let configs = enumerate(&spec).unwrap();
296 let ids: Vec<_> = configs.iter().map(|c| c.id()).collect();
297 let mut unique = ids.clone();
298 unique.sort();
299 unique.dedup();
300 assert_eq!(unique.len(), ids.len(), "duplicate config IDs");
301 let first = &configs[0];
304 assert_eq!(first.get("block_x"), Some(&Value::Int(32)));
305 assert_eq!(first.get("tile"), Some(&Value::Int(128)));
306 assert_eq!(first.id().as_str(), configs[0].id().as_str());
307 assert!(first.id().as_str().starts_with("c1-"));
308 assert_eq!(first.id().as_str().len(), 3 + 16);
309 }
310
311 #[test]
312 fn block_threads_multiplies_and_defaults() {
313 let spec = toy_spec();
314 let configs = enumerate(&spec).unwrap();
315 assert_eq!(configs[0].block_threads(), 32);
316 }
317
318 proptest::proptest! {
328 #[test]
329 fn block_threads_never_panics_and_never_wraps(
330 x in proptest::prelude::any::<u64>(),
331 y in proptest::prelude::any::<u64>(),
332 z in proptest::prelude::any::<u64>(),
333 ) {
334 let mut values = BTreeMap::new();
335 values.insert("block_x".to_string(), Value::Int(x));
336 values.insert("block_y".to_string(), Value::Int(y));
337 values.insert("block_z".to_string(), Value::Int(z));
338 let config = Config { kernel: "proptest".to_string(), values };
339
340 let threads = config.block_threads();
341
342 if x == 0 || y == 0 || z == 0 {
343 proptest::prop_assert_eq!(threads, 0, "a zero axis is a zero block");
344 } else {
345 match x.checked_mul(y).and_then(|p| p.checked_mul(z)) {
346 Some(exact) => proptest::prop_assert_eq!(threads, exact),
349 None => proptest::prop_assert_eq!(threads, u64::MAX),
352 }
353 }
354 }
355 }
356
357 #[test]
359 fn a_block_axis_above_the_cuda_limit_is_refused_at_load() {
360 let err = KernelSpec::from_toml_str(
361 "toy",
362 r#"
363 [kernel]
364 name = "toy"
365 entry = "toy"
366 domain = 1
367 [dims.block_x]
368 values = [32, 2048]
369 "#,
370 )
371 .expect_err("block_x = 2048 must not load");
372 let msg = err.to_string();
373 assert!(
374 msg.contains("2048"),
375 "message names the offending value: {msg}"
376 );
377 assert!(msg.contains("1024"), "message names the limit: {msg}");
378 }
379
380 #[test]
382 fn the_z_axis_limit_is_sixty_four() {
383 let err = KernelSpec::from_toml_str(
384 "toy",
385 r#"
386 [kernel]
387 name = "toy"
388 entry = "toy"
389 domain = 1
390 [dims.block_z]
391 values = [65]
392 "#,
393 )
394 .expect_err("block_z = 65 must not load");
395 assert!(err.to_string().contains("64"), "{err}");
396 KernelSpec::from_toml_str(
398 "toy",
399 r#"
400 [kernel]
401 name = "toy"
402 entry = "toy"
403 domain = 1
404 [dims.block_z]
405 values = [64]
406 "#,
407 )
408 .expect("block_z = 64 is the limit, not past it");
409 }
410
411 #[test]
413 fn a_spec_dimension_is_not_capped_by_the_block_limit() {
414 KernelSpec::from_toml_str(
415 "toy",
416 r#"
417 [kernel]
418 name = "toy"
419 entry = "toy"
420 domain = 1
421 [dims.block_x]
422 values = [32]
423 [dims.elements]
424 role = "spec"
425 values = [1048576]
426 "#,
427 )
428 .expect("a spec dimension is not a block axis");
429 }
430
431 #[test]
432 fn spec_key_covers_only_spec_dims() {
433 let spec = toy_spec();
434 let configs = enumerate(&spec).unwrap();
435 assert_eq!(configs[0].spec_key(&spec), "tile=128");
436 }
437}