atlas_core/
compute.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! Hardware-agnostic compute target abstraction.
4//!
5//! Defines the [`ComputeTarget`] trait: the contract that any GPU compilation
6//! and runtime target must satisfy (NVIDIA/PTX, AMD/HSACO, Apple/Metal, etc.).
7//!
8//! Atlas is designed so that:
9//! - **Build time**: kernel source files are compiled by a target-specific
10//!   compiler into a target-specific binary format (PTX, SPIR-V, metallib).
11//! - **Runtime**: the binary modules are loaded via `GpuBackend::kernel()`
12//!   and executed via `GpuBackend::launch()`.
13//!
14//! This module covers the **build-time** contract. The runtime contract is
15//! defined by `GpuBackend` in spark-runtime.
16//!
17//! # Extending to new hardware
18//!
19//! 1. Create a `HARDWARE.toml` in `kernels/<hw>/` with `vendor = "<vendor>"`.
20//! 2. Implement [`ComputeTarget`] for the new vendor.
21//! 3. Write kernel source files in the vendor's language (`.cu`, `.metal`, `.cl`).
22//! 4. Implement `GpuBackend` in spark-runtime for the vendor's runtime API.
23
24use std::path::{Path, PathBuf};
25
26/// Vendor identifier parsed from `HARDWARE.toml`.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
28pub enum Vendor {
29    /// NVIDIA CUDA — compiles `.cu` → PTX via nvcc.
30    Nvidia,
31    /// AMD ROCm — compiles `.cu`/`.hip` → HSACO via hipcc (future).
32    Amd,
33    /// Apple Metal — compiles `.metal` → metallib via xcrun (future).
34    Apple,
35    /// Intel oneAPI — compiles `.cl`/`.sycl` → SPIR-V via icpx (future).
36    Intel,
37}
38
39impl Vendor {
40    pub fn parse(s: &str) -> Option<Self> {
41        match s.to_lowercase().as_str() {
42            "nvidia" | "cuda" => Some(Self::Nvidia),
43            "amd" | "rocm" | "hip" => Some(Self::Amd),
44            "apple" | "metal" => Some(Self::Apple),
45            "intel" | "oneapi" | "sycl" => Some(Self::Intel),
46            _ => None,
47        }
48    }
49}
50
51impl std::fmt::Display for Vendor {
52    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53        match self {
54            Self::Nvidia => write!(f, "nvidia"),
55            Self::Amd => write!(f, "amd"),
56            Self::Apple => write!(f, "apple"),
57            Self::Intel => write!(f, "intel"),
58        }
59    }
60}
61
62/// Build-time compilation target contract.
63///
64/// Each hardware vendor implements this trait to describe how kernel source
65/// files are compiled into loadable binary modules. The build script
66/// (`atlas-kernels/build.rs`) uses this trait to compile kernels without
67/// knowing the specific compiler or binary format.
68///
69/// # Lifecycle
70///
71/// ```text
72/// [build.rs]                                [runtime]
73///
74/// .cu / .metal / .cl                        GpuBackend::new(modules)
75///        │                                         │
76///        ▼                                         ▼
77///  ComputeTarget::compile()              GpuBackend::kernel(name, fn)
78///        │                                         │
79///        ▼                                         ▼
80///   .ptx / .metallib / .spv              GpuBackend::launch(handle, ...)
81///        │
82///        ▼
83///  include_str!() / include_bytes!()
84///        │
85///        ▼
86///  Embedded in binary as &str / &[u8]
87/// ```
88pub trait ComputeTarget {
89    /// File extension for kernel source files (without dot).
90    ///
91    /// Examples: `"cu"` (NVIDIA), `"metal"` (Apple), `"cl"` (OpenCL).
92    fn source_extension(&self) -> &str;
93
94    /// File extension for compiled kernel modules (without dot).
95    ///
96    /// Examples: `"ptx"` (NVIDIA), `"metallib"` (Apple), `"spv"` (SPIR-V).
97    fn output_extension(&self) -> &str;
98
99    /// Whether compiled output is text (can use `include_str!`) or binary
100    /// (must use `include_bytes!`).
101    ///
102    /// PTX is text; SPIR-V and metallib are binary.
103    fn output_is_text(&self) -> bool;
104
105    /// Find the compiler executable path.
106    ///
107    /// Returns `None` if the compiler is not installed.
108    fn find_compiler(&self) -> Option<PathBuf>;
109
110    /// Compile a single kernel source file to the target binary format.
111    ///
112    /// - `source`: path to the kernel source file (e.g., `rms_norm.cu`)
113    /// - `output`: path to write the compiled output (e.g., `rms_norm.ptx`)
114    /// - `arch`: target architecture string from HARDWARE.toml (e.g., `"sm_121f"`)
115    /// - `extra_flags`: additional compiler flags from KERNEL.toml
116    ///
117    /// Returns `Ok(())` on success, `Err` with compiler output on failure.
118    fn compile(
119        &self,
120        source: &Path,
121        output: &Path,
122        arch: &str,
123        extra_flags: &[String],
124    ) -> Result<(), String>;
125
126    /// Hardware vendor for this target.
127    fn vendor(&self) -> Vendor;
128}
129
130/// NVIDIA CUDA compilation target: `.cu` → PTX via `nvcc`.
131///
132/// This is the only concrete implementation today. Other vendors can be
133/// added by implementing [`ComputeTarget`] and wiring into `build.rs`.
134pub struct NvidiaTarget {
135    nvcc_path: PathBuf,
136}
137
138impl NvidiaTarget {
139    /// Create a new NVIDIA target, locating nvcc from CUDA_HOME or PATH.
140    pub fn new() -> Option<Self> {
141        let nvcc = find_nvcc()?;
142        Some(Self { nvcc_path: nvcc })
143    }
144
145    /// Create with an explicit nvcc path.
146    pub fn with_compiler(nvcc_path: PathBuf) -> Self {
147        Self { nvcc_path }
148    }
149}
150
151impl ComputeTarget for NvidiaTarget {
152    fn source_extension(&self) -> &str {
153        "cu"
154    }
155
156    fn output_extension(&self) -> &str {
157        "ptx"
158    }
159
160    fn output_is_text(&self) -> bool {
161        true // PTX is human-readable text
162    }
163
164    fn find_compiler(&self) -> Option<PathBuf> {
165        Some(self.nvcc_path.clone())
166    }
167
168    fn compile(
169        &self,
170        source: &Path,
171        output: &Path,
172        arch: &str,
173        extra_flags: &[String],
174    ) -> Result<(), String> {
175        let arch_flag = format!("-arch={arch}");
176        let mut args = vec!["--ptx".to_string(), arch_flag, "-O3".to_string()];
177        args.extend(extra_flags.iter().cloned());
178        args.push(source.to_str().unwrap().to_string());
179        args.push("-o".to_string());
180        args.push(output.to_str().unwrap().to_string());
181
182        let result = std::process::Command::new(&self.nvcc_path)
183            .args(&args)
184            .output()
185            .map_err(|e| format!("Failed to run nvcc: {e}"))?;
186
187        if result.status.success() {
188            Ok(())
189        } else {
190            let stderr = String::from_utf8_lossy(&result.stderr);
191            Err(format!(
192                "nvcc --ptx failed for {}: {}",
193                source.display(),
194                stderr
195            ))
196        }
197    }
198
199    fn vendor(&self) -> Vendor {
200        Vendor::Nvidia
201    }
202}
203
204/// Locate nvcc from CUDA_HOME, CUDA_PATH, or standard install locations.
205fn find_nvcc() -> Option<PathBuf> {
206    // Check CUDA_HOME / CUDA_PATH environment variables
207    for var in ["CUDA_HOME", "CUDA_PATH", "CUDA_ROOT"] {
208        if let Ok(dir) = std::env::var(var) {
209            let nvcc = PathBuf::from(dir).join("bin/nvcc");
210            if nvcc.exists() {
211                return Some(nvcc);
212            }
213        }
214    }
215    // Check standard install locations
216    for path in [
217        "/usr/local/cuda/bin/nvcc",
218        "/usr/local/cuda-13.0/bin/nvcc",
219        "/usr/local/cuda-12.0/bin/nvcc",
220        "/opt/cuda/bin/nvcc",
221    ] {
222        let p = PathBuf::from(path);
223        if p.exists() {
224            return Some(p);
225        }
226    }
227    // Check PATH
228    which_in_path("nvcc")
229}
230
231fn which_in_path(name: &str) -> Option<PathBuf> {
232    std::env::var_os("PATH").and_then(|paths| {
233        std::env::split_paths(&paths)
234            .map(|dir| dir.join(name))
235            .find(|p| p.exists())
236    })
237}
238
239/// Resolve the appropriate [`ComputeTarget`] from a HARDWARE.toml vendor field.
240///
241/// Falls back to [`NvidiaTarget`] if no vendor is specified (backward compat).
242pub fn target_for_vendor(vendor: Option<&str>) -> Box<dyn ComputeTarget> {
243    match vendor.and_then(Vendor::parse) {
244        Some(Vendor::Nvidia) | None => {
245            Box::new(NvidiaTarget::new().expect("nvcc not found — install CUDA toolkit"))
246        }
247        Some(v) => {
248            panic!("Compute target '{v}' is not yet implemented. Only 'nvidia' is supported.")
249        }
250    }
251}
252
253#[cfg(test)]
254mod tests {
255    use super::*;
256
257    #[test]
258    fn vendor_parse_accepts_supported_names() {
259        assert_eq!(Vendor::parse("nvidia"), Some(Vendor::Nvidia));
260        assert_eq!(Vendor::parse("CUDA"), Some(Vendor::Nvidia));
261        assert_eq!(Vendor::parse("amd"), Some(Vendor::Amd));
262        assert_eq!(Vendor::parse("rocm"), Some(Vendor::Amd));
263        assert_eq!(Vendor::parse("hip"), Some(Vendor::Amd));
264        assert_eq!(Vendor::parse("apple"), Some(Vendor::Apple));
265        assert_eq!(Vendor::parse("metal"), Some(Vendor::Apple));
266        assert_eq!(Vendor::parse("intel"), Some(Vendor::Intel));
267        assert_eq!(Vendor::parse("oneapi"), Some(Vendor::Intel));
268        assert_eq!(Vendor::parse("sycl"), Some(Vendor::Intel));
269        assert_eq!(Vendor::parse("unknown"), None);
270    }
271
272    #[test]
273    fn nvidia_target_metadata() {
274        let target = NvidiaTarget::with_compiler(PathBuf::from("nvcc"));
275        assert_eq!(target.source_extension(), "cu");
276        assert_eq!(target.output_extension(), "ptx");
277        assert!(target.output_is_text());
278        assert_eq!(target.vendor(), Vendor::Nvidia);
279    }
280}