1use launchbound_space::{Config, KernelSpec, eval_arith_expr};
10use serde::Serialize;
11use std::collections::BTreeMap;
12
13#[derive(Debug, thiserror::Error)]
14pub enum ModelError {
15 #[error("unknown compute capability {0:?} — the model has no device table for it")]
16 UnknownCc(String),
17 #[error("kernel.toml [model]: {0}")]
18 Spec(String),
19 #[error(transparent)]
20 Space(#[from] launchbound_space::SpaceError),
21}
22
23#[derive(Debug, Clone, Copy)]
26pub struct DeviceParams {
27 pub cc: &'static str,
28 pub sm_count: u32,
29 pub max_threads_per_sm: u32,
30 pub max_warps_per_sm: u32,
31 pub max_blocks_per_sm: u32,
32 pub smem_per_block_default: u64,
33 pub smem_per_sm: u64,
34}
35
36pub const DEVICES: &[DeviceParams] = &[
37 DeviceParams {
39 cc: "8.6",
40 sm_count: 80,
41 max_threads_per_sm: 1536,
42 max_warps_per_sm: 48,
43 max_blocks_per_sm: 16,
44 smem_per_block_default: 49_152,
45 smem_per_sm: 102_400,
46 },
47 DeviceParams {
49 cc: "7.5",
50 sm_count: 40,
51 max_threads_per_sm: 1024,
52 max_warps_per_sm: 32,
53 max_blocks_per_sm: 16,
54 smem_per_block_default: 49_152,
55 smem_per_sm: 65_536,
56 },
57];
58
59pub fn device(cc: &str) -> Result<DeviceParams, ModelError> {
60 DEVICES
61 .iter()
62 .find(|d| d.cc == cc)
63 .copied()
64 .ok_or_else(|| ModelError::UnknownCc(cc.to_string()))
65}
66
67#[derive(Debug, Clone, Serialize)]
70pub struct Estimate {
71 pub id: String,
72 pub config: String,
73 pub cost: f64,
74 pub occupancy: f64,
75 pub waves: f64,
76 pub smem_bytes: u64,
77 pub kind: &'static str,
79}
80
81pub fn smem_bytes(spec: &KernelSpec, config: &Config) -> Result<u64, ModelError> {
84 let path = spec.dir.join("kernel.toml");
85 let text = std::fs::read_to_string(&path)
86 .map_err(|e| ModelError::Spec(format!("{}: {e}", path.display())))?;
87 let table: toml::Value = toml::from_str(&text).map_err(|e| ModelError::Spec(e.to_string()))?;
88 let Some(expr) = table
89 .get("model")
90 .and_then(|m| m.get("smem_bytes"))
91 .and_then(|v| v.as_str())
92 else {
93 return Ok(0);
94 };
95 Ok(eval_arith_expr(expr, config, &BTreeMap::new())?)
96}
97
98fn grid_blocks(spec: &KernelSpec, config: &Config) -> Result<u64, ModelError> {
100 let path = spec.dir.join("kernel.toml");
101 let text = std::fs::read_to_string(&path)
102 .map_err(|e| ModelError::Spec(format!("{}: {e}", path.display())))?;
103 let table: toml::Value = toml::from_str(&text).map_err(|e| ModelError::Spec(e.to_string()))?;
104 let bench = table
105 .get("bench")
106 .ok_or_else(|| ModelError::Spec("no [bench] section".into()))?;
107 let elements = bench
108 .get("elements")
109 .and_then(|v| v.as_integer())
110 .unwrap_or(1) as u64;
111 let mut extra = BTreeMap::new();
112 extra.insert("elements".to_string(), elements);
113 let mut blocks = 1u64;
114 for axis in ["grid_x", "grid_y", "grid_z"] {
115 let value = match bench.get(axis) {
116 Some(toml::Value::Integer(n)) => *n as u64,
117 Some(toml::Value::String(expr)) => eval_arith_expr(expr, config, &extra)?,
118 None => 1,
119 Some(other) => return Err(ModelError::Spec(format!("{axis}: bad value {other}"))),
120 };
121 blocks = blocks.saturating_mul(value.max(1));
122 }
123 Ok(blocks)
124}
125
126pub fn estimate(
130 spec: &KernelSpec,
131 config: &Config,
132 dev: &DeviceParams,
133) -> Result<Estimate, ModelError> {
134 let threads = config.block_threads().max(1);
135 let warps_per_block = threads.div_ceil(32);
136 let smem = smem_bytes(spec, config)?;
137
138 let by_threads = (dev.max_threads_per_sm as u64) / threads;
139 let by_smem = dev.smem_per_sm.checked_div(smem).unwrap_or(u64::MAX);
140 let blocks_per_sm = by_threads.min(by_smem).min(dev.max_blocks_per_sm as u64);
141
142 if blocks_per_sm == 0 || smem > dev.smem_per_block_default {
143 return Ok(Estimate {
146 id: config.id().as_str().to_string(),
147 config: config.to_string(),
148 cost: f64::INFINITY,
149 occupancy: 0.0,
150 waves: f64::INFINITY,
151 smem_bytes: smem,
152 kind: "estimated",
153 });
154 }
155
156 let occupancy = (blocks_per_sm * warps_per_block) as f64 / dev.max_warps_per_sm as f64;
157 let occupancy = occupancy.min(1.0);
158 let grid = grid_blocks(spec, config)? as f64;
159 let waves = (grid / (blocks_per_sm * dev.sm_count as u64) as f64).max(1.0);
160 let cost = waves / occupancy.max(1e-6);
164
165 Ok(Estimate {
166 id: config.id().as_str().to_string(),
167 config: config.to_string(),
168 cost,
169 occupancy,
170 waves,
171 smem_bytes: smem,
172 kind: "estimated",
173 })
174}
175
176pub fn spearman(xs: &[f64], ys: &[f64]) -> Option<f64> {
180 if xs.len() != ys.len() || xs.len() < 3 {
181 return None;
182 }
183 let rx = ranks(xs);
184 let ry = ranks(ys);
185 let n = rx.len() as f64;
186 let mean = (n + 1.0) / 2.0;
187 let (mut num, mut dx, mut dy) = (0.0, 0.0, 0.0);
188 for (a, b) in rx.iter().zip(&ry) {
189 num += (a - mean) * (b - mean);
190 dx += (a - mean).powi(2);
191 dy += (b - mean).powi(2);
192 }
193 if dx == 0.0 || dy == 0.0 {
194 return None;
195 }
196 Some(num / (dx * dy).sqrt())
197}
198
199fn ranks(values: &[f64]) -> Vec<f64> {
200 let mut order: Vec<usize> = (0..values.len()).collect();
201 order.sort_by(|&a, &b| values[a].partial_cmp(&values[b]).expect("no NaN"));
202 let mut out = vec![0.0; values.len()];
203 let mut i = 0;
204 while i < order.len() {
205 let mut j = i;
206 while j + 1 < order.len() && values[order[j + 1]] == values[order[i]] {
207 j += 1;
208 }
209 let avg_rank = (i + j) as f64 / 2.0 + 1.0;
210 for &k in &order[i..=j] {
211 out[k] = avg_rank;
212 }
213 i = j + 1;
214 }
215 out
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221
222 #[test]
223 fn spearman_perfect_and_inverse_and_ties() {
224 assert_eq!(
225 spearman(&[1.0, 2.0, 3.0, 4.0], &[10.0, 20.0, 30.0, 40.0]),
226 Some(1.0)
227 );
228 assert_eq!(
229 spearman(&[1.0, 2.0, 3.0, 4.0], &[40.0, 30.0, 20.0, 10.0]),
230 Some(-1.0)
231 );
232 assert!(spearman(&[1.0, 2.0], &[1.0, 2.0]).is_none());
233 let r = spearman(&[1.0, 1.0, 2.0, 3.0], &[5.0, 5.0, 7.0, 9.0]).unwrap();
234 assert!(r > 0.99);
235 }
236
237 #[test]
238 fn device_table_is_closed() {
239 assert!(device("8.6").is_ok());
240 assert!(device("7.5").is_ok());
241 assert!(
242 device("9.0").is_err(),
243 "an unknown cc is an error, never a guess"
244 );
245 }
246}