metrale_telemetry/kernel_audit/
report.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-26: The rendering half of the kernel audit: the embedded-kernel-set
4//! table and the unresolved-lookup report the boot gate prints.
5//!
6//! [`unresolved_report`]'s own text is plain ASCII, so it survives a non-TTY
7//! log pipe (`the_report_is_plain_ascii`); the table uses box-drawing
8//! characters.
9//!
10//! Owner: telemetry.
11//! Invariants: none beyond the types.
12
13use std::collections::BTreeMap;
14
15use super::{FailureSplit, split_failures};
16
17/// 2026-09-26: FNV-1a 64-bit content fingerprint, low 48 bits as 12 hex chars:
18/// the same function as `content_hash` in the kernels crate's `build.rs`.
19fn ptx_hash(bytes: &[u8]) -> String {
20    let mut h: u64 = 0xcbf2_9ce4_8422_2325;
21    for &b in bytes {
22        h ^= b as u64;
23        h = h.wrapping_mul(0x0000_0100_0000_01b3);
24    }
25    format!("{:012x}", h & 0xffff_ffff_ffff)
26}
27
28/// 2026-09-26: Render the embedded kernel set (`embedded`, the loaded target's
29/// `TargetPtxSet::modules`) with each module's resolution. `set_hash` is
30/// `metrale_kernels::KERNEL_SET_HASH`, `shadowed_dropped` fills the SHADOWED
31/// column, and `expected_absent` selects the failures listed as expected (via
32/// [`super::split_failures`]).
33pub fn render_kernel_table(
34    embedded: &[(&str, &[u8])],
35    set_hash: &str,
36    shadowed_dropped: &[(&str, &str)],
37    expected_absent: &[(&str, &str)],
38) -> String {
39    let rows = super::audit_rows();
40    // 2026-09-26: Per module: absent if never looked up, else whether any
41    // lookup resolved.
42    let mut mod_resolved: BTreeMap<&str, bool> = BTreeMap::new();
43    for r in &rows {
44        let e = mod_resolved.entry(r.module.as_str()).or_insert(false);
45        *e = *e || r.loaded;
46    }
47
48    let mut out = String::new();
49    out.push_str(&format!(
50        "\n\u{250c}\u{2500} Kernel load audit \u{2500} {} kernels embedded \u{b7} \
51         set-hash {} \u{2500}\n",
52        embedded.len(),
53        set_hash
54    ));
55    out.push_str(&format!(
56        "\u{2502} {:<34} {:<14} {:<20} {}\n",
57        "MODULE (operation)", "PTX-HASH", "RESOLUTION", "SHADOWED"
58    ));
59    out.push_str(&format!("\u{2502} {}\n", "\u{2500}".repeat(84)));
60    let mut sorted: Vec<&(&str, &[u8])> = embedded.iter().collect();
61    sorted.sort_by_key(|(m, _)| *m);
62    for (m, blob) in sorted {
63        let h = ptx_hash(blob);
64        let res = match mod_resolved.get(m) {
65            Some(true) => "used",
66            Some(false) => "** lookup FAILED **",
67            None => "-", // 2026-09-26: embedded, never looked up
68        };
69        // 2026-09-26: Y when this target's file of that name lacks kernels its
70        // `common/` namesake defines (`shadowed_dropped`).
71        let n_dropped = shadowed_dropped.iter().filter(|(sm, _)| sm == m).count();
72        let shadow = if n_dropped > 0 {
73            format!("Y ({n_dropped} dropped)")
74        } else {
75            "N".to_string()
76        };
77        out.push_str(&format!("\u{2502} {m:<34} {h:<14} {res:<20} {shadow}\n"));
78    }
79    out.push_str("\u{2514}\u{2500}\n");
80
81    let split = split_failures(&rows, expected_absent);
82    if !split.expected.is_empty() {
83        out.push_str(&format!(
84            "\n{} kernel(s) declared expected-absent in this target's MODEL.toml \
85             [expected_absent] (no action):\n    {}\n",
86            split.expected.len(),
87            split
88                .expected
89                .iter()
90                .map(|r| r.name())
91                .collect::<Vec<_>>()
92                .join(", ")
93        ));
94    }
95    out
96}
97
98/// 2026-09-26: The unresolved-lookup report: the numbered list of
99/// `split.required`, then the remediation paragraph once. `allowed` selects
100/// the closing paragraph: the offer to pass the allow flag, or, when it is
101/// already set, only the consequence.
102pub fn unresolved_report(
103    split: &FailureSplit,
104    shadowed_dropped: &[(&str, &str)],
105    model: &str,
106    arch: &str,
107    quant: &str,
108    allowed: bool,
109) -> String {
110    let mut out = format!(
111        "{} unresolved kernel lookup(s) for ({model}, {arch}, {quant}). Each one resolved to \
112         handle 0, so its dispatch site is on a silent fallback path:\n",
113        split.required.len()
114    );
115    for (i, r) in split.required.iter().enumerate() {
116        let n = i + 1;
117        let name = r.name();
118        let dropped = shadowed_dropped
119            .iter()
120            .any(|(sm, sf)| *sm == r.module.as_str() && *sf == r.func.as_str());
121        // 2026-09-26: A shadow-dropped kernel's fix is appended to its line.
122        let note = if dropped {
123            "  [SHADOW-DROPPED: common/ defines it, this target's kernel file shadows common/ \
124             without it - port it in as an exact piecewise copy]"
125        } else {
126            ""
127        };
128        out.push_str(&format!(
129            "  {n}. {name}  at {}:{}  ({model}, {arch}, {quant}){note}\n",
130            r.site.file(),
131            r.site.line(),
132        ));
133    }
134    out.push_str(
135        "\nEither gate the lookup on this model's config so it is never issued (see \
136         `qwen3_attention::init`), fix the build so the kernel is compiled, or declare it in \
137         this target's MODEL.toml [expected_absent] with a stated reason.\n",
138    );
139    if allowed {
140        out.push_str(
141            "\nPerformance may be seriously degraded. We recommend you open a GitHub issue \
142             and/or open a PR to solve this issue.\n",
143        );
144    } else {
145        out.push_str(
146            "\nIf you wish to allow this model to be served, you can pass\n\
147             --dangerously-allow-unresolved-kernel-lookups. But note that performance may be\n\
148             seriously degraded. We recommend you open a GitHub issue and/or open a PR to\n\
149             solve this issue.\n",
150        );
151    }
152    out
153}
154
155#[cfg(test)]
156mod tests {
157    use super::super::AuditRow;
158    use super::*;
159
160    fn row(module: &str, func: &str) -> AuditRow {
161        AuditRow {
162            module: module.to_string(),
163            func: func.to_string(),
164            loaded: false,
165            site: std::panic::Location::caller(),
166        }
167    }
168
169    /// 2026-09-26: Both modes print the numbered list and one closing
170    /// paragraph, and only the deny mode offers the flag.
171    #[test]
172    fn the_report_enumerates_then_remediates_once() {
173        let split = FailureSplit {
174            required: vec![row("gdn", "gdn_decode_multi_seq")],
175            expected: vec![],
176        };
177        let deny = unresolved_report(&split, &[], "qwen3.6-27b", "sm_121", "nvfp4", false);
178        assert!(deny.starts_with("1 unresolved kernel lookup(s)"));
179        assert!(deny.contains("  1. gdn::gdn_decode_multi_seq"));
180        assert!(deny.contains("--dangerously-allow-unresolved-kernel-lookups"));
181        assert_eq!(deny.matches("open a GitHub issue").count(), 1);
182
183        let allow = unresolved_report(&split, &[], "qwen3.6-27b", "sm_121", "nvfp4", true);
184        assert!(allow.contains("  1. gdn::gdn_decode_multi_seq"));
185        assert!(
186            !allow.contains("If you wish to allow"),
187            "the flag is already set; do not offer it again"
188        );
189        assert_eq!(allow.matches("open a GitHub issue").count(), 1);
190    }
191
192    /// 2026-09-26: The report is plain ASCII, shadow-drop note included.
193    #[test]
194    fn the_report_is_plain_ascii() {
195        let split = FailureSplit {
196            required: vec![row("gdn", "gdn_decode_multi_seq")],
197            expected: vec![],
198        };
199        let dropped = [("gdn", "gdn_decode_multi_seq")];
200        let text = unresolved_report(&split, &dropped, "m", "a", "q", false);
201        assert!(text.is_ascii(), "report must survive a non-TTY pipe");
202        assert!(text.contains("SHADOW-DROPPED"));
203    }
204
205    #[test]
206    fn expected_absent_never_lands_in_the_required_list() {
207        let rows = vec![row("mla_absorbed", "mla_batched_gemv"), row("gdn", "x")];
208        let split = split_failures(&rows, &[("mla_absorbed", "mla_batched_gemv")]);
209        assert_eq!(split.required.len(), 1);
210        assert_eq!(split.required[0].module, "gdn");
211        assert_eq!(split.expected.len(), 1);
212    }
213    /// 2026-09-26: Pins the deny-mode closing sentence word for word.
214    /// Whitespace is collapsed, because the source wraps the sentence across
215    /// lines.
216    #[test]
217    fn the_remediation_wording_is_the_one_that_was_asked_for() {
218        let split = FailureSplit {
219            required: vec![row("gdn", "gdn_decode_multi_seq")],
220            expected: vec![],
221        };
222        let deny = unresolved_report(&split, &[], "qwen3.6-27b", "sm_121", "nvfp4", false);
223        let flat = deny.split_whitespace().collect::<Vec<_>>().join(" ");
224        let required = "If you wish to allow this model to be served, you can pass \
225                        --dangerously-allow-unresolved-kernel-lookups. But note that \
226                        performance may be seriously degraded. We recommend you open a \
227                        GitHub issue and/or open a PR to solve this issue.";
228        let required = required.split_whitespace().collect::<Vec<_>>().join(" ");
229        assert!(
230            flat.contains(&required),
231            "the remediation sentence has drifted from the specified wording.\n\
232             wanted: {required}\n\
233             got:    {flat}"
234        );
235    }
236}