metrale_bench/gate/taxon.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-26: The gate's view of the kernel taxonomy
4//! `kernels/<hardware>/<model>/<quant>/`: target enumeration, sources and
5//! configs, path-to-node helpers, and the set of targets a change affects.
6//!
7//! What a target compiles comes from `metrale_closure::layout`, the resolver
8//! the kernels build script also uses; that crate has no CUDA dependency.
9//!
10//! Owner: bench gate.
11//! Invariants:
12//! - A target that does not resolve is never reported unaffected: [`sources`]
13//! returns `None` for it, and [`affected`] includes it for any path under
14//! `kernels/`.
15//! - [`walk`] panics on a tree the resolver refuses; [`resolves`] is the
16//! non-panicking check.
17
18use std::collections::BTreeSet;
19use std::path::{Path, PathBuf};
20
21pub use metrale_closure::layout::Target;
22use metrale_closure::layout::{LayoutError, discover, walk as walk_tree};
23
24/// 2026-09-26: Every target in the tree, as the resolver enumerates it: a
25/// hardware dir holds `HARDWARE.toml`, a model dir holds `MODEL.toml` (no
26/// `common/` does), and the quants are the subdirectories of the model's
27/// resolved kernel source directory, here or in the tree it inherits.
28///
29/// Panics when the resolver refuses the tree (for example a manifest that
30/// does not parse, or a `kernel_source` redirect chain).
31pub fn walk(root: &Path) -> Vec<Target> {
32 walk_tree(root).unwrap_or_else(|e| panic!("kernels/ does not resolve: {e}"))
33}
34
35/// 2026-09-26: Whether the resolver can enumerate the tree, without the
36/// panic of [`walk`].
37pub fn resolves(root: &Path) -> bool {
38 walk_tree(root).is_ok()
39}
40
41/// 2026-09-26: Kernel module sources for `target`, after shadowing, sorted.
42/// `None` when the target does not resolve or resolves to no sources; callers
43/// treat that as affected.
44pub fn sources(root: &Path, target: &Target) -> Option<Vec<PathBuf>> {
45 let layout = discover(root, target).ok()?;
46 let sources = layout.sources();
47 // 2026-09-26: No sources is reported like a broken target: the kernels
48 // build script attests no closure hash for such a target either.
49 if sources.is_empty() {
50 return None;
51 }
52 Some(sources)
53}
54
55/// 2026-09-26: Config files that steer a target's compile without being
56/// sources, in the order `closure_attestation` in the kernels build script
57/// hashes them: `HARDWARE.toml`, every `KERNEL.toml` the resolution read
58/// (least specific first), the target's own `MODEL.toml`, then (2026-09-28) the
59/// circuit files of `metrale_closure::circuit_configs`. Files that do not
60/// exist are left out. When the target does not resolve, the list is the
61/// hardware's and the model's manifests and the circuit files only.
62/// (2026-09-30) An inherited rule chain that does not resolve is an error: a
63/// shorter list would leave an ancestor's FUSIONS.toml out of the hash.
64pub fn configs(root: &Path, target: &Target) -> Result<Vec<PathBuf>, LayoutError> {
65 let hw_dir = root.join("kernels").join(&target.hardware);
66 let mut out = vec![hw_dir.join("HARDWARE.toml")];
67 match discover(root, target) {
68 Ok(layout) => {
69 out.extend(layout.configs());
70 out.push(layout.model_dir.join("MODEL.toml"));
71 }
72 Err(_) => out.push(hw_dir.join(&target.model).join("MODEL.toml")),
73 }
74 out.extend(metrale_closure::circuit_configs(root, &target.hardware)?);
75 Ok(out.into_iter().filter(|p| p.exists()).collect())
76}
77
78/// 2026-09-26: The hardware node a repo-relative path sits under: the first
79/// component after `kernels/`.
80pub fn hardware_of(path: &str) -> Option<&str> {
81 path.strip_prefix("kernels/")?.split('/').next()
82}
83
84/// 2026-09-26: The `(hardware, model)` node a path sits under. `None` for a
85/// path outside `kernels/`, a file directly in a hardware dir, and anything
86/// under `common/`.
87pub fn model_of(path: &str) -> Option<(&str, &str)> {
88 let rest = path.strip_prefix("kernels/")?;
89 let mut parts = rest.split('/');
90 let hw = parts.next()?;
91 let model = parts.next()?;
92 if model == "common" || parts.next().is_none() {
93 return None;
94 }
95 Some((hw, model))
96}
97
98/// 2026-09-26: Targets a changed path set can affect: the union of two rules.
99///
100/// * By path. A path under a model directory affects the targets on that
101/// hardware for that model, those whose source model it is, and those that
102/// do not resolve. Any other path under `kernels/<hw>/` (`common/`,
103/// `HARDWARE.toml`) affects every target on that hardware.
104/// * By resolution. Every target, on any hardware, whose resolved inputs
105/// (`Layout::inputs`: role entries, vendored subdirectories, `KERNEL.toml`s)
106/// include the path, and every target that does not resolve. This reaches
107/// targets on a hardware that `inherits` the tree, or that name the file in
108/// `[sources] use`.
109///
110/// * (2026-09-28) By circuit. A path under `kernels/circuits/` affects every
111/// target: each one hashes those files (`metrale_closure::circuit_configs`).
112/// (2026-09-30) So does a FUSIONS.toml a target's rules are inherited from
113/// (`metrale_closure::fusions_chain`): gb10's rules reach hopper and b300
114/// through FUSIONS `inherits`, and b200 through HARDWARE.toml `inherits`. A
115/// target whose chain does not resolve is affected by any path, like one
116/// whose sources do not.
117///
118/// A path outside `kernels/` affects nothing here. `closure::excuses` refuses
119/// to excuse a path set that holds one.
120pub fn affected(root: &Path, changed: &[String]) -> BTreeSet<Target> {
121 let all = walk(root);
122 let mut out = BTreeSet::new();
123 let mut resolved: Vec<(Target, BTreeSet<PathBuf>)> = Vec::new();
124 for t in &all {
125 let inputs = discover(root, t)
126 .map(|l| l.inputs())
127 .and_then(|mut inputs| {
128 inputs.extend(metrale_closure::fusions_chain(root, &t.hardware)?);
129 Ok(inputs)
130 });
131 match inputs {
132 Ok(inputs) => resolved.push((t.clone(), inputs)),
133 // 2026-09-26: Empty inputs: affected by any path under `kernels/`.
134 Err(_) => resolved.push((t.clone(), BTreeSet::new())),
135 }
136 }
137 for path in changed {
138 if path.starts_with("kernels/circuits/") {
139 out.extend(all.iter().cloned());
140 continue;
141 }
142 let Some(hw) = hardware_of(path) else {
143 continue;
144 };
145 match model_of(path) {
146 Some((_, model)) => out.extend(
147 all.iter()
148 .filter(|t| {
149 t.hardware == hw
150 && (t.model == model
151 || discover(root, t)
152 .map(|l| l.source_model == model)
153 .unwrap_or(true))
154 })
155 .cloned(),
156 ),
157 None => out.extend(all.iter().filter(|t| t.hardware == hw).cloned()),
158 }
159 let abs = root.join(path);
160 for (t, inputs) in &resolved {
161 if inputs.is_empty() || inputs.contains(&abs) {
162 out.insert(t.clone());
163 }
164 }
165 }
166 out
167}
168
169/// 2026-09-26: Hardware nodes the changed paths sit under.
170pub fn hardware_span(changed: &[String]) -> BTreeSet<String> {
171 changed
172 .iter()
173 .filter_map(|p| hardware_of(p))
174 .map(str::to_string)
175 .collect()
176}
177
178/// 2026-09-26: `(hardware, model)` nodes the changed paths sit under.
179pub fn model_span(changed: &[String]) -> BTreeSet<(String, String)> {
180 changed
181 .iter()
182 .filter_map(|p| model_of(p))
183 .map(|(h, m)| (h.to_string(), m.to_string()))
184 .collect()
185}
186
187#[cfg(test)]
188#[path = "taxon_tests.rs"]
189mod taxon_tests;