#![warn(missing_docs)]
mod constraint;
mod spec;
pub use constraint::{Constraint, eval_arith_expr};
pub use spec::{Dim, DimRole, KernelSpec, SafetyExpectation, Value};
use sha2::{Digest, Sha256};
use std::collections::BTreeMap;
use std::fmt;
#[derive(Debug, thiserror::Error)]
pub enum SpaceError {
#[error("failed to read {path}: {source}")]
Io {
path: String,
source: std::io::Error,
},
#[error("failed to parse {path}: {source}")]
Parse {
path: String,
source: Box<toml::de::Error>,
},
#[error("invalid kernel spec: {0}")]
Invalid(String),
#[error("invalid constraint `{expr}`: {reason}")]
Constraint {
expr: String,
reason: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Config {
kernel: String,
values: BTreeMap<String, Value>,
}
impl Config {
pub fn kernel(&self) -> &str {
&self.kernel
}
pub fn get(&self, dim: &str) -> Option<&Value> {
self.values.get(dim)
}
pub fn values(&self) -> impl Iterator<Item = (&str, &Value)> {
self.values.iter().map(|(k, v)| (k.as_str(), v))
}
pub fn block_threads(&self) -> u64 {
["block_x", "block_y", "block_z"]
.iter()
.map(|d| match self.values.get(*d) {
Some(Value::Int(n)) => *n,
_ => 1,
})
.fold(1u64, |acc, n| acc.saturating_mul(n))
}
pub fn id(&self) -> ConfigId {
let mut hasher = Sha256::new();
hasher.update(b"launchbound.config.v1\0");
hasher.update(self.kernel.as_bytes());
hasher.update(b"\0");
for (name, value) in &self.values {
hasher.update(name.as_bytes());
hasher.update(b"=");
match value {
Value::Int(n) => hasher.update(n.to_string().as_bytes()),
Value::Str(s) => hasher.update(s.as_bytes()),
}
hasher.update(b"\n");
}
let digest = hasher.finalize();
let mut hex = String::with_capacity(16);
for byte in &digest[..8] {
hex.push_str(&format!("{byte:02x}"));
}
ConfigId(format!("c1-{hex}"))
}
pub fn spec_key(&self, spec: &KernelSpec) -> String {
let mut parts = Vec::new();
for (name, value) in &self.values {
if spec.dim(name).is_some_and(|d| d.role == DimRole::Spec) {
parts.push(format!("{name}={value}"));
}
}
parts.join(",")
}
}
impl fmt::Display for Config {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut first = true;
for (name, value) in &self.values {
if !first {
write!(f, " ")?;
}
write!(f, "{name}={value}")?;
first = false;
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ConfigId(String);
impl ConfigId {
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for ConfigId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
pub fn enumerate(spec: &KernelSpec) -> Result<Vec<Config>, SpaceError> {
let dims: Vec<&Dim> = spec.dims_sorted();
let mut out = Vec::new();
if dims.is_empty() {
return Ok(out);
}
let mut indices = vec![0usize; dims.len()];
'outer: loop {
let mut values = BTreeMap::new();
for (dim, &idx) in dims.iter().zip(&indices) {
values.insert(dim.name.clone(), dim.values[idx].clone());
}
let config = Config {
kernel: spec.name.clone(),
values,
};
if spec
.constraints
.iter()
.try_fold(true, |ok, c| c.eval(&config).map(|v| ok && v))?
{
out.push(config);
}
for pos in (0..dims.len()).rev() {
indices[pos] += 1;
if indices[pos] < dims[pos].values.len() {
continue 'outer;
}
indices[pos] = 0;
}
break;
}
Ok(out)
}
pub fn raw_size(spec: &KernelSpec) -> u64 {
spec.dims_sorted()
.iter()
.map(|d| d.values.len() as u64)
.product()
}
#[cfg(test)]
mod tests {
use super::*;
fn toy_spec() -> KernelSpec {
KernelSpec::from_toml_str(
"toy",
r#"
[kernel]
name = "toy"
entry = "toy"
domain = 1
[dims.block_x]
values = [32, 64, 128]
[dims.tile]
role = "spec"
values = [128, 256]
[constraints]
exprs = ["tile % block_x == 0"]
"#,
)
.unwrap()
}
#[test]
fn enumeration_is_deterministic_and_filtered() {
let spec = toy_spec();
let a = enumerate(&spec).unwrap();
let b = enumerate(&spec).unwrap();
assert_eq!(a, b);
assert_eq!(raw_size(&spec), 6);
assert_eq!(a.len(), 6);
}
#[test]
fn constraint_actually_filters() {
let spec = KernelSpec::from_toml_str(
"toy",
r#"
[kernel]
name = "toy"
entry = "toy"
domain = 1
[dims.block_x]
values = [32, 48]
[dims.tile]
values = [64]
[constraints]
exprs = ["tile % block_x == 0"]
"#,
)
.unwrap();
let configs = enumerate(&spec).unwrap();
assert_eq!(configs.len(), 1);
assert_eq!(configs[0].get("block_x"), Some(&Value::Int(32)));
}
#[test]
fn ids_are_stable_and_distinct() {
let spec = toy_spec();
let configs = enumerate(&spec).unwrap();
let ids: Vec<_> = configs.iter().map(|c| c.id()).collect();
let mut unique = ids.clone();
unique.sort();
unique.dedup();
assert_eq!(unique.len(), ids.len(), "duplicate config IDs");
let first = &configs[0];
assert_eq!(first.get("block_x"), Some(&Value::Int(32)));
assert_eq!(first.get("tile"), Some(&Value::Int(128)));
assert_eq!(first.id().as_str(), configs[0].id().as_str());
assert!(first.id().as_str().starts_with("c1-"));
assert_eq!(first.id().as_str().len(), 3 + 16);
}
#[test]
fn block_threads_multiplies_and_defaults() {
let spec = toy_spec();
let configs = enumerate(&spec).unwrap();
assert_eq!(configs[0].block_threads(), 32);
}
proptest::proptest! {
#[test]
fn block_threads_never_panics_and_never_wraps(
x in proptest::prelude::any::<u64>(),
y in proptest::prelude::any::<u64>(),
z in proptest::prelude::any::<u64>(),
) {
let mut values = BTreeMap::new();
values.insert("block_x".to_string(), Value::Int(x));
values.insert("block_y".to_string(), Value::Int(y));
values.insert("block_z".to_string(), Value::Int(z));
let config = Config { kernel: "proptest".to_string(), values };
let threads = config.block_threads();
if x == 0 || y == 0 || z == 0 {
proptest::prop_assert_eq!(threads, 0, "a zero axis is a zero block");
} else {
match x.checked_mul(y).and_then(|p| p.checked_mul(z)) {
Some(exact) => proptest::prop_assert_eq!(threads, exact),
None => proptest::prop_assert_eq!(threads, u64::MAX),
}
}
}
}
#[test]
fn a_block_axis_above_the_cuda_limit_is_refused_at_load() {
let err = KernelSpec::from_toml_str(
"toy",
r#"
[kernel]
name = "toy"
entry = "toy"
domain = 1
[dims.block_x]
values = [32, 2048]
"#,
)
.expect_err("block_x = 2048 must not load");
let msg = err.to_string();
assert!(
msg.contains("2048"),
"message names the offending value: {msg}"
);
assert!(msg.contains("1024"), "message names the limit: {msg}");
}
#[test]
fn the_z_axis_limit_is_sixty_four() {
let err = KernelSpec::from_toml_str(
"toy",
r#"
[kernel]
name = "toy"
entry = "toy"
domain = 1
[dims.block_z]
values = [65]
"#,
)
.expect_err("block_z = 65 must not load");
assert!(err.to_string().contains("64"), "{err}");
KernelSpec::from_toml_str(
"toy",
r#"
[kernel]
name = "toy"
entry = "toy"
domain = 1
[dims.block_z]
values = [64]
"#,
)
.expect("block_z = 64 is the limit, not past it");
}
#[test]
fn a_spec_dimension_is_not_capped_by_the_block_limit() {
KernelSpec::from_toml_str(
"toy",
r#"
[kernel]
name = "toy"
entry = "toy"
domain = 1
[dims.block_x]
values = [32]
[dims.elements]
role = "spec"
values = [1048576]
"#,
)
.expect("a spec dimension is not a block axis");
}
#[test]
fn spec_key_covers_only_spec_dims() {
let spec = toy_spec();
let configs = enumerate(&spec).unwrap();
assert_eq!(configs[0].spec_key(&spec), "tile=128");
}
}