Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 88 additions & 4 deletions cuda-oxide/crates/cuda-intrinsics-gen/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

Extractor and deterministic source generator for cuda-oxide's intrinsics.

It reads `intrinsics/catalog.json` and writes every generated file in the
intrinsic pipeline. `MANAGED_GENERATED_ROOTS` in `src/generate.rs` lists the
nine directories it owns, where `generate` writes outputs and removes stale
It reads `intrinsics/catalog.json` and the libdevice math shard, and writes every
generated file in the intrinsic pipeline. `MANAGED_GENERATED_ROOTS` in
`src/generate.rs` lists the
eleven directories it owns, where `generate` writes outputs and removes stale
marker-bearing files:

- `cuda-oxide/crates/cuda-intrinsics/src/generated/`: the raw intrinsic declarations;
Expand All @@ -17,16 +18,20 @@ marker-bearing files:
requirements), `cuda-oxide/crates/mir-lower/src/convert` (conversion interfaces), and
`cuda-oxide/crates/rustc-codegen-cuda/src` (collector predicates);
- `intrinsics/probes/`: the per-intrinsic PTX probe evidence.
- `intrinsics/generated/`: libm-to-libdevice mapping metadata;
- `crates/dialect-mir/src/generated/`: shared compiler math dispatch.

Generated files carry a
`// @generated by cuda-intrinsics-gen ... DO NOT EDIT.` banner; edit the
catalog and regenerate instead.
catalog or shard and regenerate instead.

```
cuda-intrinsics-gen extract --intrinsics-json FILE --nvptx-json FILE
cuda-intrinsics-gen extract --llvm-src DIR --llvm-tblgen FILE
cuda-intrinsics-gen generate
cuda-intrinsics-gen check
cuda-intrinsics-gen extract-libdevice --libdevice FILE --llvm-dis FILE
cuda-intrinsics-gen verify-libdevice --libdevice FILE --llvm-dis FILE
cuda-intrinsics-gen coverage [--family NAME]
cuda-intrinsics-gen check-abi-history --base-ref REF
cuda-intrinsics-gen probe [--all | --intrinsic ID] [--llc FILE] [--skip-terminal] [--per-target]
Expand All @@ -40,3 +45,82 @@ by each route, and
`check-abi-history` keeps the intrinsic ABI ledger append-only.

Generation shells out to `rustfmt`, which the pinned toolchain provides.

## Libdevice math family

`intrinsics/overlay/libdevice_math.toml` is the schema-2 source of compiler math
dispatch. It preserves all 82 existing f32/f64 libm exports, including the
hand-landed tgamma, ilogb, ldexp/scalbn, and frexp routes, plus the core-only
powi and fmuladd intrinsics. This migration adds no new math exports.

The renderer writes three outputs:

- `crates/dialect-mir/src/generated/libdevice_math.rs`: the shared math enum,
placeholder constants, compiler-path and alias dispatch, operand counts,
libdevice/native LLVM symbols, exact cmath interception, and link requirements;
- `intrinsics/generated/libdevice_math.rs`: mapping metadata used by generator
tests;
- `intrinsics/libdevice-math.md`: the mapping reference.

The importer, MIR lowerer, and rustc collector consume the shared generated
dispatch instead of maintaining their own copies. Type-aware generic core
intrinsics and the semantic expansions for frexp, sincos, min/max, and fast
arithmetic remain handwritten. For example, before and after regeneration,
`libm::math::scalbn::scalbnf` resolves to `LdexpF32`, emits the same ldexp
placeholder, and lowers to `__nv_ldexpf` with an i32 exponent.

`generate` owns and reconciles all three outputs; `check` rejects drift in any
of them, including the compiler dispatch. A regression fixture records the 214
scalar compiler paths before migration and compares them against the compiled
consumer's identities, placeholders, symbols, and link classification. Separate
controls cover sincos, generic fabs, native LLVM routes, and unknown names.

This input is read separately from the hardware shard index in
`intrinsics/overlay.toml`. Its evidence comes from `libdevice.10.bc`, so it does
not receive hardware ABI IDs or change the hardware catalog hash. Ordinary
unindexed hardware shards remain errors.

Rows name a Rust signature class, optional libm aliases, core intrinsic names,
compatibility paths, cmath spelling, placeholder suffixes, libdevice symbol,
native LLVM alternative, link classification, and handwritten handler. By
default, a row exports its name from libm; `libm = []` explicitly describes a
core-only operation. `{width}` expands to `f32`/`f64`, `{Width}` and `{WIDTH}` to
`F32`/`F64`, and `{suffix}` to `f`/the empty string. Variant and constant overrides
preserve existing compiler identities such as `PowfF32` and the generic `Fabs`.

Unknown fields, duplicate paths/identities/placeholders, invalid templates,
ambiguous C ABIs, incompatible link classifications, and unsupported handwritten
handlers fail before outputs are written. Tuple signatures require a supported
handwritten adapter.

Generation checks all 74 referenced C symbols and signatures against the pinned
capture in `intrinsics/evidence/libdevice_math/cuda-13.0.json`. Its provenance
and reproduction instructions are recorded beside it. Ordinary `generate` and
`check` do not need a CUDA installation.

To verify the same ABI against a locally installed toolkit:

```bash
libdevice_llvm_dis="$(rustc --print sysroot)/lib/rustlib/$(rustc -vV | sed -n 's/^host: //p')/bin/llvm-dis"
cargo run -p cuda-intrinsics-gen --locked -- verify-libdevice \
--libdevice "${CUDA_PATH:-/usr/local/cuda}/nvvm/libdevice/libdevice.10.bc" \
--llvm-dis "$libdevice_llvm_dis"
```

Verification accepts compatible newer toolkit bitcode and reports its hash.
To refresh evidence, review and update the shard's bitcode hash pin, then run
`extract-libdevice` with the same arguments, followed by `generate` and `check`.
Extraction hashes and disassembles the same bytes, requires exported definitions
with exact return/argument types, and writes only after all referenced signatures
pass. Opaque pointers do not encode their pointee type; out-parameter semantics
remain the handwritten adapter's responsibility.

Validation from `cuda-oxide/`:

```bash
cargo test -p cuda-intrinsics-gen -p dialect-mir -p mir-importer -p mir-lower --locked --all-targets
cargo clippy -p cuda-intrinsics-gen -p dialect-mir -p mir-importer -p mir-lower --locked --all-targets -- -D warnings
cargo run -p cuda-intrinsics-gen --locked -- check
cargo test --manifest-path crates/rustc-codegen-cuda/Cargo.toml --locked --lib
just check
```
23 changes: 22 additions & 1 deletion cuda-oxide/crates/cuda-intrinsics-gen/src/generate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ const MANAGED_GENERATED_ROOTS: &[&str] = &[
"crates/mir-lower/src/convert",
"crates/rustc-codegen-cuda/src",
"intrinsics/probes",
"intrinsics/generated",
"crates/dialect-mir/src/generated",
];
const GENERATED_MARKER: &[u8] = b"@generated by cuda-intrinsics-gen";

Expand Down Expand Up @@ -72,7 +74,14 @@ fn unindexed_overlay_shards(repo_root: &Path) -> Result<Vec<String>> {
"{} lists no shards; refusing to report every overlay file as unindexed",
manifest_path.display()
);
let listed: BTreeSet<&str> = index.shards.iter().map(String::as_str).collect();
// Libdevice is a separate evidence domain, read unconditionally by
// libdevice_math::outputs rather than folded into the hardware ABI catalog.
let listed: BTreeSet<&str> = index
.shards
.iter()
.map(String::as_str)
.chain(std::iter::once(crate::libdevice_math::SHARD))
.collect();

let intrinsics_root = repo_root.join("intrinsics");
let overlay_dir = intrinsics_root.join("overlay");
Expand Down Expand Up @@ -142,6 +151,7 @@ pub fn run(repo_root: &Path, check: bool) -> Result<()> {
let catalog_json = pretty_json(&catalog)?;
let catalog_hash = sha256_bytes(catalog_json.as_bytes());
let mut outputs = all_outputs(&catalog, catalog_json, &catalog_hash)?;
outputs.extend(crate::libdevice_math::outputs(repo_root)?);
let expected: BTreeSet<PathBuf> = outputs.keys().cloned().collect();
for (path, contents) in &mut outputs {
if path.extension().and_then(|extension| extension.to_str()) == Some("rs") {
Expand Down Expand Up @@ -300,6 +310,17 @@ mod tests {
fs::remove_dir_all(root).unwrap();
}

#[test]
fn libdevice_shard_has_an_explicit_separate_reader() {
let root = overlay_fixture(
"libdevice-indexed",
&["listed.toml"],
&["listed.toml", "libdevice_math.toml"],
);
assert!(unindexed_overlay_shards(&root).unwrap().is_empty());
fs::remove_dir_all(root).unwrap();
}

/// The manifest grammar accepts nested shard paths, so the walk must
/// descend: a forgotten file is no less forgotten for sitting in a
/// subdirectory.
Expand Down
179 changes: 179 additions & 0 deletions cuda-oxide/crates/cuda-intrinsics-gen/src/libdevice_math/compiler.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
/*
* SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

//! Render the shared dispatch used by the importer, lowerer, and collector.

use super::{Link, Mapping};
use std::collections::{BTreeMap, BTreeSet};
use std::fmt::Write;

// These polymorphic arithmetic operations have handwritten semantic lowerings,
// no libm aliases, and no libdevice ABI. They are controls, not shard rows.
pub(super) const FAST: &[(&str, &str, &str)] = &[
("FaddFast", "CALLEE_FADD_FAST", "fadd_fast"),
("FsubFast", "CALLEE_FSUB_FAST", "fsub_fast"),
("FmulFast", "CALLEE_FMUL_FAST", "fmul_fast"),
("FdivFast", "CALLEE_FDIV_FAST", "fdiv_fast"),
("FremFast", "CALLEE_FREM_FAST", "frem_fast"),
];

pub(super) fn render(rows: &[Mapping]) -> String {
let mut out = String::from(
"/*\n * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\n\n// @generated by cuda-intrinsics-gen from overlay/libdevice_math.toml. DO NOT EDIT.\n\n",
);
let mut constants = BTreeMap::new();
let mut variants: BTreeMap<&str, BTreeMap<&str, &Mapping>> = BTreeMap::new();
let mut paths = BTreeMap::new();
let mut exports = BTreeMap::new();
for row in rows {
let Some(variant) = row.variant.as_deref() else {
continue;
};
let constant = row.constant.as_deref().unwrap();
constants.insert(constant, &row.placeholders[0]);
variants.entry(variant).or_default().insert(&row.width, row);
for path in &row.paths {
paths.insert(path, variant);
}
if let Some(libm) = &row.libm {
exports.insert(libm, variant);
}
}
for (constant, suffix) in constants {
writeln!(out, "/// Internal math placeholder generated from the libdevice shard.\npub const {constant}: &str = placeholder!({suffix:?});").unwrap();
}
out.push_str("\n/// Shared math identity. Semantic expansions remain in the importer and lowerer.\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\npub enum RustFloatMathIntrinsic {\n");
for variant in variants
.keys()
.copied()
.chain(FAST.iter().map(|entry| entry.0))
{
writeln!(out, "{variant},").unwrap();
}
out.push_str("}\n\n/// Whether a math placeholder needs libdevice under the selected backend.\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\npub enum MathLink { Always, Libnvvm, Never }\n\nimpl RustFloatMathIntrinsic {\n");

out.push_str("/// Recognize exact core, cmath, or libm compiler paths.\npub fn from_core_path(name: &str) -> Option<Self> { match name {\n");
for (path, variant) in paths {
writeln!(out, "{path:?} => Some(Self::{variant}),").unwrap();
}
for (variant, _, name) in FAST {
writeln!(
out,
"\"core::intrinsics::{name}\" | \"std::intrinsics::{name}\" => Some(Self::{variant}),"
)
.unwrap();
}
out.push_str("_ => Self::from_libm_path(name),\n} }\n\n/// Resolve aliases only within the libm crate namespace.\npub fn from_libm_path(name: &str) -> Option<Self> {\nif !is_libm_path(name) { return None; }\nmatch name.rsplit(\"::\").next().unwrap_or(name) {\n");
for (export, variant) in exports {
writeln!(out, "{export:?} => Some(Self::{variant}),").unwrap();
}
out.push_str("_ => None,\n} }\n\n/// Return the placeholder emitted for this math identity.\npub fn placeholder_callee(self) -> &'static str { match self {\n");
for (variant, widths) in &variants {
writeln!(
out,
"Self::{variant} => {},",
widths.values().next().unwrap().constant.as_deref().unwrap()
)
.unwrap();
}
for (variant, constant, _) in FAST {
writeln!(out, "Self::{variant} => {constant},").unwrap();
}
out.push_str("} }\n\n/// Recover an identity from an exact known placeholder.\npub fn from_placeholder_callee(callee: &str) -> Option<Self> { match callee {\n");
for (variant, widths) in &variants {
writeln!(
out,
"{} => Some(Self::{variant}),",
widths.values().next().unwrap().constant.as_deref().unwrap()
)
.unwrap();
}
for (variant, constant, _) in FAST {
writeln!(out, "{constant} => Some(Self::{variant}),").unwrap();
}
out.push_str("_ => None,\n} }\n\n/// Operand count of the Rust placeholder, before C ABI adaptation.\npub fn arg_count(self) -> usize { match self {\n");
for (variant, widths) in &variants {
let signature = &widths.values().next().unwrap().signature;
let arity = signature.split(" -> ").next().unwrap().matches(',').count() + 1;
writeln!(out, "Self::{variant} => {arity},").unwrap();
}
for (variant, _, _) in FAST {
writeln!(out, "Self::{variant} => 2,").unwrap();
}
out.push_str("} }\n\n/// Classify libdevice linking without accepting unknown placeholder prefixes.\npub fn link_requirement(self) -> MathLink { match self {\n");
for (variant, widths) in &variants {
let link = match widths.values().next().unwrap().link {
Link::Always => "Always",
Link::Libnvvm => "Libnvvm",
Link::Never => "Never",
};
writeln!(out, "Self::{variant} => MathLink::{link},").unwrap();
}
for (variant, _, _) in FAST {
writeln!(out, "Self::{variant} => MathLink::Never,").unwrap();
}
out.push_str("} }\n");
for (method, llvm) in [("libdevice_symbol", false), ("llvm_symbol", true)] {
writeln!(out, "\n/// Resolve the symbol; only generic placeholders consult the concrete width.\npub fn {method}(self, is_f64: bool) -> Option<&'static str> {{ match self {{").unwrap();
for (variant, widths) in &variants {
if widths.len() == 2 {
writeln!(
out,
"Self::{variant} => if is_f64 {{ {:?} }} else {{ {:?} }},",
symbol(widths["f64"], llvm),
symbol(widths["f32"], llvm)
)
.unwrap();
} else {
writeln!(
out,
"Self::{variant} => {:?},",
symbol(widths.values().next().unwrap(), llvm)
)
.unwrap();
}
}
for (variant, _, _) in FAST {
writeln!(out, "Self::{variant} => None,").unwrap();
}
out.push_str("} }\n");
}
out.push_str("}\n\n/// Whether the path belongs to the libm crate, rather than a user namespace.\npub fn is_libm_path(name: &str) -> bool { name.split(\"::\").next() == Some(\"libm\") }\n\n/// Exact std shims intercepted before the collector's forbidden-std guard.\npub fn is_intrinsic_lowered_cmath_shim(name: &str) -> bool { matches!(name,\n");
let cmath: BTreeSet<_> = rows.iter().filter_map(|row| row.cmath.as_deref()).collect();
if cmath.is_empty() {
out.push_str("_ if false");
} else {
out.push_str(
&cmath
.into_iter()
.map(|path| format!("{path:?}"))
.collect::<Vec<_>>()
.join(" | "),
);
}
out.push_str(") }\n\n/// Sincos retains its importer tuple expansion into two scalar placeholders.\npub fn libm_sincos_is_f64(name: &str) -> Option<bool> {\nif !is_libm_path(name) { return None; }\nmatch name.rsplit(\"::\").next().unwrap_or(name) {\n");
for row in rows
.iter()
.filter(|row| row.lowering.as_deref() == Some("handwritten:emit_sincos"))
{
writeln!(
out,
"{:?} => Some({}),",
row.libm.as_deref().unwrap(),
row.width == "f64"
)
.unwrap();
}
out.push_str("_ => None,\n} }\n");
out
}

fn symbol(row: &Mapping, llvm: bool) -> Option<&str> {
if llvm {
row.llvm.as_deref()
} else {
row.libdevice.as_deref()
}
}
Loading