metrale_core/
arch.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: Decide whether PTX built for one SM architecture runs on a device, and name the target to rebuild when it does not.
4//!
5//! A kernel build compiles for the single arch in `kernels/<hw>/HARDWARE.toml`
6//! `[hardware].arch` (one `-arch=` flag per nvcc call), so a binary and a GPU
7//! can disagree. The CUDA preflight (`arch_preflight::check_arch`) calls
8//! [`ptx_arch_runs_on_device`], and `--check-kernels` reads the resulting
9//! [`ArchMismatch`] (`serve_phases/kernel_gate.rs`).
10//!
11//! Owner: core.
12//! Invariants: every function here is pure; the module uses only `std`, so it
13//! needs no GPU and no driver.
14
15/// 2026-09-25: The suffix on an `sm_XY…` architecture string. It selects the
16/// compatibility rule in [`ptx_arch_runs_on_device`].
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum SmSuffix {
19    /// 2026-09-25: No suffix: accepted on any device with a compute capability
20    /// at or above the compiled one.
21    None,
22    /// 2026-09-25: `a`, architecture-specific: accepted only on exactly the
23    /// compiled compute capability.
24    Arch,
25    /// 2026-09-25: `f`, family-specific: accepted on devices of the same major
26    /// version at or above the compiled compute capability.
27    Family,
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub struct SmArch {
32    /// 2026-09-25: Compute-capability major (the `12` of `sm_121f`).
33    pub major: u32,
34    /// 2026-09-25: Compute-capability minor (the `1` of `sm_121f`).
35    pub minor: u32,
36    pub suffix: SmSuffix,
37}
38
39/// 2026-09-25: Parse an NVIDIA `sm_XY[a|f]` architecture string.
40///
41/// Returns `None` for anything that is not an NVIDIA SM arch: `gfx1151`,
42/// `metal3.1`, fewer than two digits, or a non-digit. `None` is the "not an
43/// NVIDIA target" marker.
44///
45/// The last digit is the minor version and the rest is the major
46/// (`sm_90` = 9.0, `sm_100` = 10.0, `sm_121` = 12.1).
47pub fn parse_sm_arch(arch: &str) -> Option<SmArch> {
48    let rest = arch.strip_prefix("sm_")?;
49    let (digits, suffix) = match rest.as_bytes().last()? {
50        b'a' => (&rest[..rest.len() - 1], SmSuffix::Arch),
51        b'f' => (&rest[..rest.len() - 1], SmSuffix::Family),
52        _ => (rest, SmSuffix::None),
53    };
54    // 2026-09-25: Two digits minimum, one for the major and one for the minor;
55    // `sm_9` is refused rather than guessed at.
56    if digits.len() < 2 || !digits.bytes().all(|b| b.is_ascii_digit()) {
57        return None;
58    }
59    let (major, minor) = digits.split_at(digits.len() - 1);
60    Some(SmArch {
61        major: major.parse().ok()?,
62        minor: minor.parse().ok()?,
63        suffix,
64    })
65}
66
67/// 2026-09-25: Which `kernels/<hw>/` target ships for a device compute
68/// capability, so the mismatch message can say what to rebuild. `None` means
69/// no target ships for that capability.
70///
71/// The table is hand-written although each NVIDIA `kernels/<hw>/HARDWARE.toml`
72/// declares its `compute_capability`: metrale-core has no TOML parser
73/// dependency. `crates/kernels/tests/target_hints.rs` keeps the two in step: it
74/// asserts that every `vendor = "nvidia"` HARDWARE.toml's capability maps back
75/// to its own directory, and that every name returned here is a directory.
76pub fn target_hint(device_cc: (u32, u32)) -> Option<&'static str> {
77    match device_cc {
78        (9, 0) => Some("hopper"),
79        // 2026-09-25: b200 (`sm_100a`) and b300 (`sm_103a`) are separate targets:
80        // an `a` arch runs only on its own capability.
81        (10, 0) => Some("b200"),
82        (10, 3) => Some("b300"),
83        (12, 1) => Some("gb10"),
84        _ => None,
85    }
86}
87
88#[derive(Debug, Clone, PartialEq, Eq)]
89pub struct ArchMismatch {
90    /// 2026-09-25: The arch string the kernels were compiled for, verbatim.
91    pub compiled_arch: String,
92    /// 2026-09-25: The parsed form of `compiled_arch`; its suffix picks the
93    /// reason printed by `Display`.
94    pub compiled: SmArch,
95    /// 2026-09-25: `(major, minor)` compute capability of the device.
96    pub device_cc: (u32, u32),
97}
98
99impl std::fmt::Display for ArchMismatch {
100    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101        let (major, minor) = self.device_cc;
102        let arch = &self.compiled_arch;
103        let reason = match self.compiled.suffix {
104            SmSuffix::None => format!(
105                "portable PTX for {arch} needs compute capability {}.{} or later",
106                self.compiled.major, self.compiled.minor
107            ),
108            SmSuffix::Arch => format!(
109                "architecture-specific PTX ({arch}) runs only on compute capability {}.{}",
110                self.compiled.major, self.compiled.minor
111            ),
112            SmSuffix::Family => format!(
113                "family-specific PTX ({arch}) runs only on compute capability {}.{} or later \
114                 within the {}.x family",
115                self.compiled.major, self.compiled.minor, self.compiled.major
116            ),
117        };
118        let fix = match target_hint(self.device_cc) {
119            Some(hw) => format!(
120                "rebuild with METRALE_TARGET_HW={hw} (kernels/{hw}/HARDWARE.toml arch must match \
121                 this GPU) or use the image built for this GPU"
122            ),
123            None => format!(
124                "no shipped target matches compute capability {major}.{minor} \
125                 (kernels/<hw>/HARDWARE.toml arch must match this GPU) — \
126                 use the image built for this GPU"
127            ),
128        };
129        write!(
130            f,
131            "kernels compiled for {arch} cannot run on this GPU \
132             (compute capability {major}.{minor}): {reason}; fix: {fix}"
133        )
134    }
135}
136
137impl std::error::Error for ArchMismatch {}
138
139/// 2026-09-25: Can PTX compiled for `compiled_arch` run on a device of
140/// `device_cc`? Three rules:
141///
142/// 1. plain `sm_XY` runs on any device with CC >= X.Y;
143/// 2. `sm_XYa` runs only on CC == X.Y;
144/// 3. `sm_XYf` runs on the same major version with CC >= X.Y.
145///
146/// A `compiled_arch` that is not an NVIDIA SM arch (`gfx1151`, `metal3.1`)
147/// returns `Ok(())`, so AMD and Apple builds are never refused here. Callers
148/// that need to tell the cases apart use [`parse_sm_arch`], whose `None` is the
149/// marker.
150pub fn ptx_arch_runs_on_device(
151    compiled_arch: &str,
152    device_cc: (u32, u32),
153) -> Result<(), ArchMismatch> {
154    let Some(compiled) = parse_sm_arch(compiled_arch) else {
155        return Ok(());
156    };
157    let compiled_cc = (compiled.major, compiled.minor);
158    let runs = match compiled.suffix {
159        SmSuffix::None => device_cc >= compiled_cc,
160        SmSuffix::Arch => device_cc == compiled_cc,
161        SmSuffix::Family => device_cc.0 == compiled.major && device_cc >= compiled_cc,
162    };
163    if runs {
164        return Ok(());
165    }
166    Err(ArchMismatch {
167        compiled_arch: compiled_arch.to_string(),
168        compiled,
169        device_cc,
170    })
171}
172
173#[cfg(test)]
174#[path = "arch_tests.rs"]
175mod tests;