metrale_circuit/hardware/
class.rs1use std::collections::{BTreeMap, BTreeSet};
20
21use super::HwError;
22use super::sources::ClassSources;
23use crate::rules::Rule;
24use crate::runtime::{RuleSet, RuntimeRoute, check_ids, overlay, parse_rule_set};
25use crate::venn::discover::{Found, KernelSources, discover};
26use crate::venn::families::{Discover, Families, How, Point, parse_families};
27use crate::venn::repo::Repo;
28
29#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct ClassInfo {
32 pub name: String,
34 pub arch: String,
36 pub inherits: Option<String>,
38 pub defines: BTreeSet<String>,
40 pub defaults: BTreeMap<String, String>,
42 pub tensor_core: Option<super::tc_policy::TcPolicy>,
44}
45
46fn toml_err(rel: &str) -> impl Fn(toml::de::Error) -> HwError + '_ {
47 move |e| HwError::Class(format!("{rel}: {e}"))
48}
49
50pub fn parse_class(name: &str, text: &str) -> Result<ClassInfo, HwError> {
52 let rel = format!("kernels/{name}/HARDWARE.toml");
53 let t: toml::Table = toml::from_str(text).map_err(toml_err(&rel))?;
54 let hw = t
55 .get("hardware")
56 .and_then(|v| v.as_table())
57 .ok_or_else(|| HwError::Class(format!("{rel}: no [hardware]")))?;
58 let arch = hw
59 .get("arch")
60 .and_then(|v| v.as_str())
61 .ok_or_else(|| HwError::Class(format!("{rel}: no [hardware] arch")))?
62 .to_string();
63 let inherits = hw
64 .get("inherits")
65 .and_then(|v| v.as_str())
66 .map(str::to_string);
67 let defines = t
68 .get("build")
69 .and_then(|b| b.get("extra_nvcc_flags"))
70 .and_then(|f| f.as_array())
71 .into_iter()
72 .flatten()
73 .filter_map(|f| f.as_str()?.strip_prefix("-D"))
74 .map(|d| d.split('=').next().unwrap_or(d).to_string())
75 .collect();
76 let defaults = t
77 .get("defaults")
78 .and_then(|d| d.as_table())
79 .into_iter()
80 .flatten()
81 .map(|(k, v)| {
82 let text = v.as_str().map_or_else(|| v.to_string(), str::to_string);
83 (k.clone(), text)
84 })
85 .collect();
86 let tensor_core =
87 super::tc_policy::parse_policy(&t).map_err(|e| HwError::Class(format!("{rel}: {e}")))?;
88 Ok(ClassInfo {
89 name: name.to_string(),
90 arch,
91 inherits,
92 defines,
93 defaults,
94 tensor_core,
95 })
96}
97
98pub fn chain(repo: &dyn Repo, class: &str) -> Result<Vec<ClassInfo>, HwError> {
100 let mut out: Vec<ClassInfo> = Vec::new();
101 let mut next = Some(class.to_string());
102 while let Some(name) = next {
103 if out.iter().any(|c| c.name == name) {
104 return Err(HwError::Class(format!(
105 "inheritance cycle through `{name}`"
106 )));
107 }
108 let text = repo
109 .read(&format!("kernels/{name}/HARDWARE.toml"))
110 .map_err(|e| HwError::Class(format!("class `{name}`: {e}")))?;
111 let info = parse_class(&name, &text)?;
112 next = info.inherits.clone();
113 out.push(info);
114 }
115 Ok(out)
116}
117
118pub fn planning_chain(repo: &dyn Repo, class: &str) -> Result<Vec<ClassInfo>, HwError> {
122 let mut out = chain(repo, class)?;
123 loop {
124 let last = out.last().map(|c| c.name.clone()).unwrap_or_default();
125 let Ok(text) = repo.read(&fusions_rel(&last)) else {
126 return Ok(out);
127 };
128 let t: toml::Table = toml::from_str(&text).map_err(toml_err(&fusions_rel(&last)))?;
129 let Some(next) = t.get("inherits").and_then(|v| v.as_str()) else {
130 return Ok(out);
131 };
132 if out.iter().any(|c| c.name == next) {
133 return Ok(out);
134 }
135 out.extend(chain(repo, next)?);
136 }
137}
138
139#[derive(Debug, Clone, PartialEq, Eq)]
141pub enum ClassRules {
142 Rules {
144 rules: Vec<Rule>,
146 files: Vec<String>,
148 runtime: Vec<RuntimeRoute>,
150 },
151 None,
153}
154
155fn fusions_rel(class: &str) -> String {
156 format!("kernels/{class}/common/FUSIONS.toml")
157}
158
159pub fn class_rules(repo: &dyn Repo, class: &str) -> Result<ClassRules, HwError> {
161 rules_of(repo, class, &mut Vec::new())
162}
163
164fn rules_of(repo: &dyn Repo, class: &str, seen: &mut Vec<String>) -> Result<ClassRules, HwError> {
165 if seen.iter().any(|c| c == class) {
166 return Err(HwError::Class(format!(
167 "FUSIONS inheritance cycle through `{class}`"
168 )));
169 }
170 seen.push(class.to_string());
171 let rel = fusions_rel(class);
172 let Ok(text) = repo.read(&rel) else {
173 let info = chain(repo, class)?;
174 return match info[0].inherits.as_deref() {
175 Some(parent) => rules_of(repo, parent, seen),
176 None => Ok(ClassRules::None),
177 };
178 };
179 let mut table: toml::Table = toml::from_str(&text).map_err(toml_err(&rel))?;
180 let inherits = table.remove("inherits");
181 let remove = table.remove("remove");
182 table
183 .entry("rule")
184 .or_insert_with(|| toml::Value::Array(Vec::new()));
185 let own_text = toml::to_string(&table).map_err(|e| HwError::Class(format!("{rel}: {e}")))?;
186 let RuleSet {
187 rules: own,
188 runtime: own_runtime,
189 } = parse_rule_set(&own_text).map_err(|e| HwError::Class(format!("{rel}: {e}")))?;
190 let Some(parent) = inherits else {
191 if remove.is_some() {
192 return Err(HwError::Class(format!(
193 "{rel}: `remove` needs `inherits`: a standalone rule set has nothing to remove"
194 )));
195 }
196 return Ok(ClassRules::Rules {
197 rules: own,
198 files: vec![rel],
199 runtime: own_runtime,
200 });
201 };
202 let parent = parent
203 .as_str()
204 .ok_or_else(|| HwError::Class(format!("{rel}: `inherits` is not a class name")))?;
205 let ClassRules::Rules {
206 rules: mut base,
207 mut files,
208 mut runtime,
209 } = rules_of(repo, parent, seen)?
210 else {
211 return Err(HwError::Class(format!(
212 "{rel}: inherits `{parent}`, which has no rules"
213 )));
214 };
215 let drop: Vec<String> = remove
216 .as_ref()
217 .and_then(|v| v.as_array())
218 .into_iter()
219 .flatten()
220 .map(|v| v.as_str().map(str::to_string))
221 .collect::<Option<_>>()
222 .ok_or_else(|| HwError::Class(format!("{rel}: `remove` is not a list of rule ids")))?;
223 for id in &drop {
224 let before = base.len();
225 base.retain(|r| &r.id != id);
226 if base.len() == before {
227 return Err(HwError::Class(format!(
228 "{rel}: removes `{id}`, which `{parent}` does not define"
229 )));
230 }
231 }
232 for r in own {
233 match base.iter_mut().find(|b| b.id == r.id) {
234 Some(slot) => *slot = r,
235 None => base.push(r),
236 }
237 }
238 overlay(&mut runtime, own_runtime);
239 check_ids(&base, &runtime).map_err(|e| HwError::Class(format!("{rel}: {e}")))?;
240 files.push(rel);
241 Ok(ClassRules::Rules {
242 rules: base,
243 files,
244 runtime,
245 })
246}
247
248pub fn class_families(
253 repo: &dyn Repo,
254 chain: &[ClassInfo],
255 sources: &ClassSources,
256) -> Result<Families, HwError> {
257 let rel = |c: &str| format!("kernels/{c}/common/KERNEL_FAMILIES.toml");
258 let mut found: Vec<(String, Families)> = Vec::new();
259 for c in chain {
260 if let Ok(text) = repo.read(&rel(&c.name)) {
261 let f = parse_families(&text).map_err(|e| HwError::Class(e.to_string()))?;
262 found.push((c.name.clone(), f));
263 }
264 }
265 let own = chain
266 .first()
267 .map(|c| c.name.clone())
268 .ok_or_else(|| HwError::Class("empty class chain".into()))?;
269 let (base_class, mut out) = found.pop().ok_or_else(|| {
270 HwError::Class(format!(
271 "no KERNEL_FAMILIES.toml along `{own}`'s planning chain: nothing to classify against"
272 ))
273 })?;
274 if base_class != own {
275 for f in &mut out.families {
276 f.evidence.clear();
277 }
278 }
279 while let Some((class, overlay)) = found.pop() {
280 for f in overlay.families {
281 let mut f = f;
282 if class != own {
283 f.evidence.clear();
284 }
285 match out.families.iter_mut().find(|g| g.id == f.id) {
286 Some(slot) => *slot = f,
287 None => out.families.push(f),
288 }
289 }
290 out.legacy.extend(overlay.legacy);
291 }
292 let manifest = std::mem::replace(&mut out.hardware, own);
293 let found = class_discovered(&out, &manifest, chain, sources);
294 for f in &mut out.families {
295 f.points = class_points(&f.points, chain, sources);
296 for d in found.iter().filter(|d| d.family == f.id) {
297 if !f.points.iter().any(|p| p.files.contains(&d.file)) {
298 f.points.push(Point {
299 values: d.values.clone(),
300 how: How::Copy,
301 files: vec![d.file.clone()],
302 compute: None,
304 pipeline: Default::default(),
305 });
306 }
307 }
308 }
309 Ok(out)
310}
311
312pub fn class_discovered(
317 fams: &Families,
318 manifest: &str,
319 chain: &[ClassInfo],
320 sources: &ClassSources,
321) -> BTreeSet<Found> {
322 let texts: BTreeMap<String, String> = sources
323 .modules
324 .values()
325 .map(|m| (m.path.clone(), m.text.clone()))
326 .collect();
327 let from = format!("kernels/{manifest}/");
328 let mut out = BTreeSet::new();
329 for tier in chain.iter().filter(|c| c.name != manifest) {
330 let to = format!("kernels/{}/", tier.name);
331 let reroot = |p: &str| p.strip_prefix(&from).map(|rest| format!("{to}{rest}"));
332 let mut moved = fams.clone();
333 for f in &mut moved.families {
334 f.discover = f
335 .discover
336 .iter()
337 .filter_map(|d| match d {
338 Discover::File { glob, values } => Some(Discover::File {
339 glob: reroot(glob)?,
340 values: values.clone(),
341 }),
342 Discover::Macro {
343 file,
344 name,
345 args,
346 map,
347 } => Some(Discover::Macro {
348 file: reroot(file).filter(|f| texts.contains_key(f))?,
349 name: name.clone(),
350 args: args.clone(),
351 map: map.clone(),
352 }),
353 })
354 .collect();
355 }
356 let src = KernelSources {
357 paths: sources.files.clone(),
358 texts: texts.clone(),
359 };
360 out.extend(discover(&moved, &src).0);
361 }
362 out
363}
364
365pub fn class_points(points: &[Point], chain: &[ClassInfo], sources: &ClassSources) -> Vec<Point> {
371 let by_name: BTreeMap<&str, &str> = sources
372 .files
373 .iter()
374 .map(|p| (file_name(p), p.as_str()))
375 .collect();
376 let compiled: BTreeSet<&str> = sources.files.iter().map(String::as_str).collect();
377 let in_chain = |p: &str| {
378 chain
379 .iter()
380 .any(|c| p.starts_with(&format!("kernels/{}/", c.name)))
381 };
382 points
383 .iter()
384 .filter_map(|p| {
385 let files: Vec<String> = p
386 .files
387 .iter()
388 .filter_map(|f| {
389 if !f.starts_with("kernels/") || compiled.contains(f.as_str()) {
390 return Some(f.clone());
391 }
392 let own = by_name.get(file_name(f)).copied()?;
393 in_chain(own).then(|| own.to_string())
394 })
395 .collect();
396 if files.is_empty() {
397 return None;
398 }
399 Some(Point {
400 values: p.values.clone(),
401 how: p.how,
402 files,
403 compute: p.compute.clone(),
404 pipeline: p.pipeline.clone(),
405 })
406 })
407 .collect()
408}
409
410fn file_name(p: &str) -> &str {
411 p.rsplit('/').next().unwrap_or(p)
412}