1use launchbound_space::{Config, KernelSpec, eval_arith_expr};
11use serde::{Deserialize, Serialize};
12use std::collections::BTreeMap;
13use std::path::Path;
14
15#[derive(Debug, thiserror::Error)]
17pub enum PlanError {
18 #[error("kernel.toml [bench]: {0}")]
21 Spec(String),
22 #[error(transparent)]
24 Space(#[from] launchbound_space::SpaceError),
25 #[error("plan io: {0}")]
27 Io(String),
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
32#[serde(tag = "kind", rename_all = "snake_case")]
33pub enum ArgSpec {
34 InF32 {
37 len: u64,
39 },
40 InU32 {
42 len: u64,
44 modulo: u64,
47 },
48 OutF32 {
50 len: u64,
52 },
53 OutU32 {
55 len: u64,
57 },
58 LenOf {
60 of: usize,
63 },
64 U32 {
66 value: u64,
68 },
69 U64 {
71 value: u64,
73 },
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct Candidate {
85 pub id: String,
87 pub config: String,
89 pub ptx: String,
91 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
95 pub unsafe_candidate: bool,
96 pub grid: [u32; 3],
99 pub block: [u32; 3],
101 pub args: Vec<ArgSpec>,
103 pub warmup: u32,
105 pub repeats: u32,
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct BenchPlan {
112 pub schema: String,
114 pub kernel: String,
116 pub entry: String,
118 pub cc: String,
121 #[serde(default, skip_serializing_if = "Option::is_none")]
125 pub allow_unsafe_reason: Option<String>,
126 pub candidates: Vec<Candidate>,
129}
130
131#[derive(Debug, Deserialize)]
133struct RawBench {
134 elements: u64,
135 #[serde(default = "default_one")]
136 grid_x: toml::Value,
137 #[serde(default = "default_one")]
138 grid_y: toml::Value,
139 #[serde(default = "default_one")]
140 grid_z: toml::Value,
141 #[serde(default = "default_warmup")]
142 warmup: u32,
143 #[serde(default = "default_repeats")]
144 repeats: u32,
145 args: Vec<RawArg>,
146}
147
148fn default_one() -> toml::Value {
149 toml::Value::Integer(1)
150}
151fn default_warmup() -> u32 {
152 20
153}
154fn default_repeats() -> u32 {
155 100
156}
157
158#[derive(Debug, Deserialize)]
159struct RawArg {
160 kind: String,
161 #[serde(default)]
162 len: Option<toml::Value>,
163 #[serde(default)]
164 of: Option<usize>,
165 #[serde(default)]
166 value: Option<toml::Value>,
167 #[serde(default)]
168 modulo: Option<toml::Value>,
169}
170
171#[derive(Debug)]
177pub struct BenchSpec {
178 raw: RawBench,
179}
180
181impl BenchSpec {
182 pub fn load(spec: &KernelSpec) -> Result<Self, PlanError> {
184 let path = spec.dir.join("kernel.toml");
185 let text =
186 std::fs::read_to_string(&path).map_err(|e| PlanError::Io(format!("{path:?}: {e}")))?;
187 let table: toml::Value =
188 toml::from_str(&text).map_err(|e| PlanError::Spec(e.to_string()))?;
189 let bench = table
190 .get("bench")
191 .ok_or_else(|| PlanError::Spec("kernel.toml has no [bench] section".into()))?;
192 let raw: RawBench = bench
193 .clone()
194 .try_into()
195 .map_err(|e| PlanError::Spec(format!("{e}")))?;
196 Ok(BenchSpec { raw })
197 }
198
199 pub fn candidate(
201 &self,
202 _spec: &KernelSpec,
203 config: &Config,
204 ptx_relative: &str,
205 ) -> Result<Candidate, PlanError> {
206 let mut extra = BTreeMap::new();
207 extra.insert("elements".to_string(), self.raw.elements);
208 let eval = |v: &toml::Value, what: &str| -> Result<u64, PlanError> {
209 match v {
210 toml::Value::Integer(n) if *n >= 0 => Ok(*n as u64),
211 toml::Value::String(expr) => Ok(eval_arith_expr(expr, config, &extra)?),
212 other => Err(PlanError::Spec(format!(
213 "{what} must be a non-negative integer or expression string, got {other}"
214 ))),
215 }
216 };
217
218 let grid = [
219 eval(&self.raw.grid_x, "grid_x")? as u32,
220 eval(&self.raw.grid_y, "grid_y")? as u32,
221 eval(&self.raw.grid_z, "grid_z")? as u32,
222 ];
223 let block_dim = |name: &str| -> u32 {
224 match config.get(name) {
225 Some(launchbound_space::Value::Int(n)) => *n as u32,
226 _ => 1,
227 }
228 };
229 let block = [
230 block_dim("block_x"),
231 block_dim("block_y"),
232 block_dim("block_z"),
233 ];
234
235 let mut args = Vec::with_capacity(self.raw.args.len());
236 for (i, raw) in self.raw.args.iter().enumerate() {
237 let need = |v: &Option<toml::Value>, field: &str| -> Result<u64, PlanError> {
238 let v = v.as_ref().ok_or_else(|| {
239 PlanError::Spec(format!("args[{i}] kind {} needs `{field}`", raw.kind))
240 })?;
241 eval(v, field)
242 };
243 let arg = match raw.kind.as_str() {
244 "in_f32" => ArgSpec::InF32 {
245 len: need(&raw.len, "len")?,
246 },
247 "in_u32" => ArgSpec::InU32 {
248 len: need(&raw.len, "len")?,
249 modulo: need(&raw.modulo, "modulo")?,
250 },
251 "out_f32" => ArgSpec::OutF32 {
252 len: need(&raw.len, "len")?,
253 },
254 "out_u32" => ArgSpec::OutU32 {
255 len: need(&raw.len, "len")?,
256 },
257 "len_of" => ArgSpec::LenOf {
258 of: raw
259 .of
260 .ok_or_else(|| PlanError::Spec(format!("args[{i}] len_of needs `of`")))?,
261 },
262 "u32" => ArgSpec::U32 {
263 value: need(&raw.value, "value")?,
264 },
265 "u64" => ArgSpec::U64 {
266 value: need(&raw.value, "value")?,
267 },
268 other => {
269 return Err(PlanError::Spec(format!(
270 "args[{i}]: unknown kind {other:?}"
271 )));
272 }
273 };
274 args.push(arg);
275 }
276
277 Ok(Candidate {
278 id: config.id().as_str().to_string(),
279 config: config.to_string(),
280 ptx: ptx_relative.to_string(),
281 unsafe_candidate: false,
282 grid,
283 block,
284 args,
285 warmup: self.raw.warmup,
286 repeats: self.raw.repeats,
287 })
288 }
289}
290
291impl BenchPlan {
292 pub fn write(&self, path: &Path) -> Result<(), PlanError> {
294 let json = serde_json::to_string_pretty(self)
295 .map_err(|e| PlanError::Io(format!("serializing plan: {e}")))?;
296 std::fs::write(path, json).map_err(|e| PlanError::Io(format!("{path:?}: {e}")))
297 }
298
299 pub fn load(path: &Path) -> Result<Self, PlanError> {
305 let text =
306 std::fs::read_to_string(path).map_err(|e| PlanError::Io(format!("{path:?}: {e}")))?;
307 let plan: BenchPlan =
308 serde_json::from_str(&text).map_err(|e| PlanError::Io(e.to_string()))?;
309 if plan.schema != "plan.v1" {
310 return Err(PlanError::Spec(format!(
311 "unsupported plan schema {:?}",
312 plan.schema
313 )));
314 }
315 Ok(plan)
316 }
317
318 pub fn param_slots(candidate: &Candidate) -> usize {
322 candidate.args.len()
323 }
324}