use launchbound_space::{Config, KernelSpec, eval_arith_expr};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::path::Path;
#[derive(Debug, thiserror::Error)]
pub enum PlanError {
#[error("kernel.toml [bench]: {0}")]
Spec(String),
#[error(transparent)]
Space(#[from] launchbound_space::SpaceError),
#[error("plan io: {0}")]
Io(String),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ArgSpec {
InF32 { len: u64 },
InU32 { len: u64, modulo: u64 },
OutF32 { len: u64 },
OutU32 { len: u64 },
LenOf { of: usize },
U32 { value: u64 },
U64 { value: u64 },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Candidate {
pub id: String,
pub config: String,
pub ptx: String,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub unsafe_candidate: bool,
pub grid: [u32; 3],
pub block: [u32; 3],
pub args: Vec<ArgSpec>,
pub warmup: u32,
pub repeats: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BenchPlan {
pub schema: String,
pub kernel: String,
pub entry: String,
pub cc: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub allow_unsafe_reason: Option<String>,
pub candidates: Vec<Candidate>,
}
#[derive(Debug, Deserialize)]
struct RawBench {
elements: u64,
#[serde(default = "default_one")]
grid_x: toml::Value,
#[serde(default = "default_one")]
grid_y: toml::Value,
#[serde(default = "default_one")]
grid_z: toml::Value,
#[serde(default = "default_warmup")]
warmup: u32,
#[serde(default = "default_repeats")]
repeats: u32,
args: Vec<RawArg>,
}
fn default_one() -> toml::Value {
toml::Value::Integer(1)
}
fn default_warmup() -> u32 {
20
}
fn default_repeats() -> u32 {
100
}
#[derive(Debug, Deserialize)]
struct RawArg {
kind: String,
#[serde(default)]
len: Option<toml::Value>,
#[serde(default)]
of: Option<usize>,
#[serde(default)]
value: Option<toml::Value>,
#[serde(default)]
modulo: Option<toml::Value>,
}
#[derive(Debug)]
pub struct BenchSpec {
raw: RawBench,
}
impl BenchSpec {
pub fn load(spec: &KernelSpec) -> Result<Self, PlanError> {
let path = spec.dir.join("kernel.toml");
let text =
std::fs::read_to_string(&path).map_err(|e| PlanError::Io(format!("{path:?}: {e}")))?;
let table: toml::Value =
toml::from_str(&text).map_err(|e| PlanError::Spec(e.to_string()))?;
let bench = table
.get("bench")
.ok_or_else(|| PlanError::Spec("kernel.toml has no [bench] section".into()))?;
let raw: RawBench = bench
.clone()
.try_into()
.map_err(|e| PlanError::Spec(format!("{e}")))?;
Ok(BenchSpec { raw })
}
pub fn candidate(
&self,
_spec: &KernelSpec,
config: &Config,
ptx_relative: &str,
) -> Result<Candidate, PlanError> {
let mut extra = BTreeMap::new();
extra.insert("elements".to_string(), self.raw.elements);
let eval = |v: &toml::Value, what: &str| -> Result<u64, PlanError> {
match v {
toml::Value::Integer(n) if *n >= 0 => Ok(*n as u64),
toml::Value::String(expr) => Ok(eval_arith_expr(expr, config, &extra)?),
other => Err(PlanError::Spec(format!(
"{what} must be a non-negative integer or expression string, got {other}"
))),
}
};
let grid = [
eval(&self.raw.grid_x, "grid_x")? as u32,
eval(&self.raw.grid_y, "grid_y")? as u32,
eval(&self.raw.grid_z, "grid_z")? as u32,
];
let block_dim = |name: &str| -> u32 {
match config.get(name) {
Some(launchbound_space::Value::Int(n)) => *n as u32,
_ => 1,
}
};
let block = [
block_dim("block_x"),
block_dim("block_y"),
block_dim("block_z"),
];
let mut args = Vec::with_capacity(self.raw.args.len());
for (i, raw) in self.raw.args.iter().enumerate() {
let need = |v: &Option<toml::Value>, field: &str| -> Result<u64, PlanError> {
let v = v.as_ref().ok_or_else(|| {
PlanError::Spec(format!("args[{i}] kind {} needs `{field}`", raw.kind))
})?;
eval(v, field)
};
let arg = match raw.kind.as_str() {
"in_f32" => ArgSpec::InF32 {
len: need(&raw.len, "len")?,
},
"in_u32" => ArgSpec::InU32 {
len: need(&raw.len, "len")?,
modulo: need(&raw.modulo, "modulo")?,
},
"out_f32" => ArgSpec::OutF32 {
len: need(&raw.len, "len")?,
},
"out_u32" => ArgSpec::OutU32 {
len: need(&raw.len, "len")?,
},
"len_of" => ArgSpec::LenOf {
of: raw
.of
.ok_or_else(|| PlanError::Spec(format!("args[{i}] len_of needs `of`")))?,
},
"u32" => ArgSpec::U32 {
value: need(&raw.value, "value")?,
},
"u64" => ArgSpec::U64 {
value: need(&raw.value, "value")?,
},
other => {
return Err(PlanError::Spec(format!(
"args[{i}]: unknown kind {other:?}"
)));
}
};
args.push(arg);
}
Ok(Candidate {
id: config.id().as_str().to_string(),
config: config.to_string(),
ptx: ptx_relative.to_string(),
unsafe_candidate: false,
grid,
block,
args,
warmup: self.raw.warmup,
repeats: self.raw.repeats,
})
}
}
impl BenchPlan {
pub fn write(&self, path: &Path) -> Result<(), PlanError> {
let json = serde_json::to_string_pretty(self).expect("plan serializes");
std::fs::write(path, json).map_err(|e| PlanError::Io(format!("{path:?}: {e}")))
}
pub fn load(path: &Path) -> Result<Self, PlanError> {
let text =
std::fs::read_to_string(path).map_err(|e| PlanError::Io(format!("{path:?}: {e}")))?;
let plan: BenchPlan =
serde_json::from_str(&text).map_err(|e| PlanError::Io(e.to_string()))?;
if plan.schema != "plan.v1" {
return Err(PlanError::Spec(format!(
"unsupported plan schema {:?}",
plan.schema
)));
}
Ok(plan)
}
pub fn param_slots(candidate: &Candidate) -> usize {
candidate.args.len()
}
}