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;