From 0eb580e5011b93df56f9e67514534542c2ccc9a6 Mon Sep 17 00:00:00 2001 From: haixuantao Date: Mon, 8 Jun 2026 15:00:36 +0200 Subject: [PATCH 1/6] fix(mir-importer): lower libm float ops to libdevice intrinsics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit glam (with `nostd-libm`) lowers float ops to `libm::math::*` (`sqrtf`, `sinf`, `cosf`, `expf`, `powf`, `floor`, ...). Intercept these at the call site and map them to the libdevice intrinsics (hardware math), and tell the collector NOT to translate the `libm` bodies (otherwise `-Zalways-encode-mir` drags in their generic software implementations and trips importer gaps). Adds `from_libm_path` (final-segment match -> intrinsic) and an `emit_sincos` handler for `libm::sincos{f}` (returns `(sin, cos)`, so no single scalar intrinsic fits — emits a `sinf`/`cosf` pair packed into the destination tuple). --- .../terminator/intrinsics/float_math.rs | 197 ++++++++++++++++++ .../src/translator/terminator/mod.rs | 16 ++ crates/rustc-codegen-cuda/src/collector.rs | 12 ++ 3 files changed, 225 insertions(+) diff --git a/crates/mir-importer/src/translator/terminator/intrinsics/float_math.rs b/crates/mir-importer/src/translator/terminator/intrinsics/float_math.rs index 7bfd720bbd..b1e5d8c7fb 100644 --- a/crates/mir-importer/src/translator/terminator/intrinsics/float_math.rs +++ b/crates/mir-importer/src/translator/terminator/intrinsics/float_math.rs @@ -182,6 +182,61 @@ impl RustFloatMathIntrinsic { "std::sys::cmath::atan2" => Some(Self::Atan2F64), "std::sys::cmath::atanf" => Some(Self::AtanF32), "std::sys::cmath::atan" => Some(Self::AtanF64), + other => Self::from_libm_path(other), + } + } + + /// Recognize `libm` crate float functions (the `nostd-libm` lowering used + /// by glam on nvptx, e.g. `libm::math::sqrt::sqrtf`). These dependency-rlib + /// functions frequently lack exportable MIR downstream, so we intercept them + /// and lower to the same libdevice intrinsics as the core-intrinsic forms, + /// giving hardware math instead of software libm. Match on the final path + /// segment so both the canonical (`libm::math::sqrt::sqrtf`) and re-exported + /// (`libm::sqrtf`) spellings are caught. + fn from_libm_path(name: &str) -> Option { + if !name.contains("libm") { + return None; + } + let seg = name.rsplit("::").next().unwrap_or(name); + match seg { + "sqrtf" => Some(Self::SqrtF32), + "sqrt" => Some(Self::SqrtF64), + "sinf" => Some(Self::SinF32), + "sin" => Some(Self::SinF64), + "cosf" => Some(Self::CosF32), + "cos" => Some(Self::CosF64), + "tanf" => Some(Self::TanF32), + "tan" => Some(Self::TanF64), + "expf" => Some(Self::ExpF32), + "exp" => Some(Self::ExpF64), + "exp2f" => Some(Self::Exp2F32), + "exp2" => Some(Self::Exp2F64), + "logf" => Some(Self::LogF32), + "log" => Some(Self::LogF64), + "log2f" => Some(Self::Log2F32), + "log2" => Some(Self::Log2F64), + "log10f" => Some(Self::Log10F32), + "log10" => Some(Self::Log10F64), + // libm: `powf` is the f32 power, `pow` is the f64 power. + "powf" => Some(Self::PowfF32), + "pow" => Some(Self::PowfF64), + "floorf" => Some(Self::FloorF32), + "floor" => Some(Self::FloorF64), + "ceilf" => Some(Self::CeilF32), + "ceil" => Some(Self::CeilF64), + "truncf" => Some(Self::TruncF32), + "trunc" => Some(Self::TruncF64), + "roundf" => Some(Self::RoundF32), + "round" => Some(Self::RoundF64), + "fmaf" => Some(Self::FmaF32), + "fma" => Some(Self::FmaF64), + "fabsf" | "fabs" => Some(Self::Fabs), + "copysignf" => Some(Self::CopysignF32), + "copysign" => Some(Self::CopysignF64), + "atan2f" => Some(Self::Atan2F32), + "atan2" => Some(Self::Atan2F64), + "atanf" => Some(Self::AtanF32), + "atan" => Some(Self::AtanF64), _ => None, } } @@ -240,6 +295,148 @@ impl RustFloatMathIntrinsic { } } +/// Recognize `libm::sincosf` / `libm::sincos` (glam's `nostd-libm` lowering of +/// `f32::sin_cos`). These return a `(sin, cos)` tuple, so they do not fit the +/// scalar `RustFloatMathIntrinsic` dispatch; [`emit_sincos`] handles them. +/// Returns `Some(is_f64)` when `name` is a libm sincos function. +pub fn libm_sincos_is_f64(name: &str) -> Option { + if !name.contains("libm") { + return None; + } + match name.rsplit("::").next().unwrap_or(name) { + "sincosf" => Some(false), + "sincos" => Some(true), + _ => None, + } +} + +/// Emit `libm::sincos{f}(x) -> (sin, cos)` as a `sinf`/`cosf` placeholder pair +/// (lowered to libdevice `__nv_sin/cosf`) packed into the destination tuple. +#[allow(clippy::too_many_arguments)] +pub fn emit_sincos( + ctx: &mut Context, + body: &mir::Body, + is_f64: bool, + args: &[mir::Operand], + destination: &mir::Place, + target: &Option, + block_ptr: Ptr, + prev_op: Option>, + value_map: &mut ValueMap, + block_map: &[Ptr], + loc: Location, +) -> TranslationResult> { + use crate::error::TranslationErr; + use crate::translator::rvalue; + use dialect_mir::ops::{MirCallOp, MirConstructTupleOp}; + use pliron::builtin::attributes::StringAttr; + use pliron::input_err; + use pliron::location::Located; + use pliron::op::Op; + + // Destination tuple type and its scalar element type. + let tuple_ty = types::translate_type(ctx, &body.locals()[destination.local].ty)?; + let scalar_ty = { + let r = tuple_ty.deref(ctx); + match r.downcast_ref::() { + Some(t) => t.get_types()[0], + None => { + return input_err!( + loc.clone(), + TranslationErr::unsupported( + "libm::sincos destination is not a tuple".to_string() + ) + ); + } + } + }; + + // Translate the angle argument once and reuse it for both calls. + let (arg_value, last_op) = rvalue::translate_operand( + ctx, + body, + &args[0], + value_map, + block_ptr, + prev_op, + loc.clone(), + )?; + + let (sin_callee, cos_callee) = if is_f64 { + (rust_intrinsics::CALLEE_SIN_F64, rust_intrinsics::CALLEE_COS_F64) + } else { + (rust_intrinsics::CALLEE_SIN_F32, rust_intrinsics::CALLEE_COS_F32) + }; + + let callee_id = pliron::identifier::Identifier::try_from("callee").unwrap(); + + // sin(x) + let sin_op = Operation::new( + ctx, + MirCallOp::get_concrete_op_info(), + vec![scalar_ty], + vec![arg_value], + vec![], + 0, + ); + sin_op.deref_mut(ctx).set_loc(loc.clone()); + sin_op + .deref_mut(ctx) + .attributes + .0 + .insert(callee_id.clone(), StringAttr::new(sin_callee.into()).into()); + if let Some(prev) = last_op { + sin_op.insert_after(ctx, prev); + } else { + sin_op.insert_at_front(block_ptr, ctx); + } + let sin_val = sin_op.deref(ctx).get_result(0); + + // cos(x) + let cos_op = Operation::new( + ctx, + MirCallOp::get_concrete_op_info(), + vec![scalar_ty], + vec![arg_value], + vec![], + 0, + ); + cos_op.deref_mut(ctx).set_loc(loc.clone()); + cos_op + .deref_mut(ctx) + .attributes + .0 + .insert(callee_id, StringAttr::new(cos_callee.into()).into()); + cos_op.insert_after(ctx, sin_op); + let cos_val = cos_op.deref(ctx).get_result(0); + + // Pack (sin, cos) into the destination tuple. + let tuple_op = Operation::new( + ctx, + MirConstructTupleOp::get_concrete_op_info(), + vec![tuple_ty], + vec![sin_val, cos_val], + vec![], + 0, + ); + tuple_op.deref_mut(ctx).set_loc(loc.clone()); + tuple_op.insert_after(ctx, cos_op); + let tuple_val = tuple_op.deref(ctx).get_result(0); + + let goto_prev = value_map + .store_local(ctx, destination.local, tuple_val, block_ptr, Some(tuple_op)) + .unwrap_or(tuple_op); + + if let Some(target_idx) = target { + Ok(helpers::emit_goto(ctx, *target_idx, goto_prev, block_map, loc)) + } else { + input_err!( + loc.clone(), + TranslationErr::unsupported("libm::sincos call without target not supported".to_string()) + ) + } +} + /// Emit a placeholder `mir.call` for a rustc float math intrinsic. #[allow(clippy::too_many_arguments)] pub fn emit_rust_float_math_intrinsic( diff --git a/crates/mir-importer/src/translator/terminator/mod.rs b/crates/mir-importer/src/translator/terminator/mod.rs index fd6c4f7508..7fa76a82b6 100644 --- a/crates/mir-importer/src/translator/terminator/mod.rs +++ b/crates/mir-importer/src/translator/terminator/mod.rs @@ -1514,6 +1514,22 @@ fn try_dispatch_intrinsic( )); } + if let Some(is_f64) = intrinsics::float_math::libm_sincos_is_f64(name) { + return Ok(Some(intrinsics::float_math::emit_sincos( + ctx, + body, + is_f64, + args, + destination, + target, + block_ptr, + prev_op, + value_map, + block_map, + loc, + )?)); + } + if let Some(intrinsic) = intrinsics::float_math::RustFloatMathIntrinsic::from_core_path(name) { return Ok(Some( intrinsics::float_math::emit_rust_float_math_intrinsic( diff --git a/crates/rustc-codegen-cuda/src/collector.rs b/crates/rustc-codegen-cuda/src/collector.rs index 4d4694eeb2..bea31e18ea 100644 --- a/crates/rustc-codegen-cuda/src/collector.rs +++ b/crates/rustc-codegen-cuda/src/collector.rs @@ -1025,6 +1025,18 @@ impl<'tcx> DeviceCollector<'tcx> { let crate_name = self.tcx.crate_name(def_id.krate); let name_str = crate_name.as_str(); + // The `libm` crate (glam's `nostd-libm` float backend) is intercepted at + // every call site by mir-importer's float-math dispatch and lowered to + // libdevice intrinsics (`__nv_sqrtf`, `__nv_sinf`, ...). Its bodies must + // therefore NOT be collected: translating libm's generic software-float + // implementations (e.g. `libm::math::generic::sqrt::sqrt_round`) would + // both be wasted work and trip importer gaps. Skip the whole crate; any + // libm function we don't yet intercept will surface as a missing symbol, + // signalling that `from_libm_path` needs another entry. + if name_str == "libm" { + return CollectDecision::SkipIntentional; + } + // Check if this is a kernel entry point. Kernels can come from ANY // crate — this enables library crates to export generic kernels that // get monomorphized when used in an application. From b1e37b3b0a9cf2277a8096ffb47ef1c19920ad5c Mon Sep 17 00:00:00 2001 From: nihalpasham Date: Fri, 12 Jun 2026 12:10:58 +0530 Subject: [PATCH 2/6] fix(mir-importer): anchor libm interception to the libm crate root The libm match guard accepted any path containing the substring "libm", so a user function that shares a libm function name (for example an `expf` helper in a `libm_compat` module, or anything in a crate whose name mentions libm) was silently rerouted to the `__nv_expf` libdevice call, replacing the user's body with hardware math: a miscompile, proven on IR during review. Anchor both `from_libm_path` and `libm_sincos_is_f64` on the first path segment being exactly `libm`, and pin the adversarial cases in unit tests. --- .../terminator/intrinsics/float_math.rs | 59 ++++++++++++++++++- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/crates/mir-importer/src/translator/terminator/intrinsics/float_math.rs b/crates/mir-importer/src/translator/terminator/intrinsics/float_math.rs index b1e5d8c7fb..003379adb7 100644 --- a/crates/mir-importer/src/translator/terminator/intrinsics/float_math.rs +++ b/crates/mir-importer/src/translator/terminator/intrinsics/float_math.rs @@ -194,7 +194,7 @@ impl RustFloatMathIntrinsic { /// segment so both the canonical (`libm::math::sqrt::sqrtf`) and re-exported /// (`libm::sqrtf`) spellings are caught. fn from_libm_path(name: &str) -> Option { - if !name.contains("libm") { + if !is_libm_path(name) { return None; } let seg = name.rsplit("::").next().unwrap_or(name); @@ -295,12 +295,21 @@ impl RustFloatMathIntrinsic { } } +/// Whether `name` is a path rooted in the `libm` crate: the first path +/// segment must be exactly `libm`. A bare substring test would also match +/// user functions whose path merely mentions libm (e.g. +/// `my_app::libm_compat::expf`), silently replacing the user's body with a +/// libdevice call. +pub fn is_libm_path(name: &str) -> bool { + name.split("::").next() == Some("libm") +} + /// Recognize `libm::sincosf` / `libm::sincos` (glam's `nostd-libm` lowering of /// `f32::sin_cos`). These return a `(sin, cos)` tuple, so they do not fit the /// scalar `RustFloatMathIntrinsic` dispatch; [`emit_sincos`] handles them. /// Returns `Some(is_f64)` when `name` is a libm sincos function. pub fn libm_sincos_is_f64(name: &str) -> Option { - if !name.contains("libm") { + if !is_libm_path(name) { return None; } match name.rsplit("::").next().unwrap_or(name) { @@ -525,6 +534,52 @@ mod tests { ); } + /// Libm interception must be anchored to the `libm` crate root. A user + /// function that shares a libm function name, inside a path that merely + /// mentions "libm", must stay a regular call: a bare `contains("libm")` + /// test rerouted such calls to libdevice, silently replacing the user's + /// body (miscompile caught in PR #142 review). + #[test] + fn libm_interception_is_anchored_to_the_libm_crate_root() { + // Canonical and re-exported libm spellings are intercepted. + for (path, expected) in [ + ("libm::math::expf::expf", RustFloatMathIntrinsic::ExpF32), + ("libm::expf", RustFloatMathIntrinsic::ExpF32), + ("libm::math::sqrt::sqrt", RustFloatMathIntrinsic::SqrtF64), + ] { + assert_eq!( + RustFloatMathIntrinsic::from_core_path(path), + Some(expected), + "`{path}` should be intercepted as a libm function" + ); + } + + // Adversarial: a user `expf` under a path containing "libm" is NOT + // the libm crate and must not be rerouted. + for path in [ + "my_app::libm_compat::expf", + "my_app::libm::expf", + "libmath::expf", + "libm_math::lookalike::expf", + "not_libm::expf", + ] { + assert_eq!( + RustFloatMathIntrinsic::from_core_path(path), + None, + "user function `{path}` was wrongly rerouted to libdevice" + ); + } + } + + /// Same anchoring requirement for the tuple-returning sincos detector. + #[test] + fn libm_sincos_detection_is_anchored_to_the_libm_crate_root() { + assert_eq!(libm_sincos_is_f64("libm::sincosf"), Some(false)); + assert_eq!(libm_sincos_is_f64("libm::math::sincos::sincos"), Some(true)); + assert_eq!(libm_sincos_is_f64("my_app::libm_compat::sincosf"), None); + assert_eq!(libm_sincos_is_f64("libmath::sincos"), None); + } + #[test] fn maxnum_minnum_nsz_placeholders_round_trip_through_dialect_mir() { // The placeholder names must match between this importer crate and From 887e0be97d9d410e5779c0551281e979ca51e2df Mon Sep 17 00:00:00 2001 From: nihalpasham Date: Fri, 12 Jun 2026 12:11:53 +0530 Subject: [PATCH 3/6] feat(mir-importer): map libm fmax/fmin and rint/roundeven to libdevice These libm entry points already have lowering variants on the core-intrinsic path, so the table extension is free: fmax{f}/fmin{f} reuse the _nsz maxNum/minNum lowering that backs f32::max and f32::min, and rint{f}/roundeven{f} reuse the round-ties-even lowering (the device has no dynamic rounding mode, so rint and roundeven coincide). --- .../terminator/intrinsics/float_math.rs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/crates/mir-importer/src/translator/terminator/intrinsics/float_math.rs b/crates/mir-importer/src/translator/terminator/intrinsics/float_math.rs index 003379adb7..412d8af39f 100644 --- a/crates/mir-importer/src/translator/terminator/intrinsics/float_math.rs +++ b/crates/mir-importer/src/translator/terminator/intrinsics/float_math.rs @@ -228,11 +228,19 @@ impl RustFloatMathIntrinsic { "trunc" => Some(Self::TruncF64), "roundf" => Some(Self::RoundF32), "round" => Some(Self::RoundF64), + // libm's `rint` documents round-half-to-even; the device has no + // dynamic rounding mode, so it shares the roundeven lowering. + "rintf" | "roundevenf" => Some(Self::RoundevenF32), + "rint" | "roundeven" => Some(Self::RoundevenF64), "fmaf" => Some(Self::FmaF32), "fma" => Some(Self::FmaF64), "fabsf" | "fabs" => Some(Self::Fabs), "copysignf" => Some(Self::CopysignF32), "copysign" => Some(Self::CopysignF64), + "fmaxf" => Some(Self::MaxNumNszF32), + "fmax" => Some(Self::MaxNumNszF64), + "fminf" => Some(Self::MinNumNszF32), + "fmin" => Some(Self::MinNumNszF64), "atan2f" => Some(Self::Atan2F32), "atan2" => Some(Self::Atan2F64), "atanf" => Some(Self::AtanF32), @@ -571,6 +579,29 @@ mod tests { } } + /// Pin the libm names that reuse existing enum variants: fmax/fmin map + /// to the `_nsz` maxNum/minNum lowering (same as `f32::max`/`f32::min`), + /// and rint/roundeven map to the round-ties-even lowering. + #[test] + fn libm_fmax_fmin_rint_roundeven_map_to_existing_variants() { + for (path, expected) in [ + ("libm::fmaxf", RustFloatMathIntrinsic::MaxNumNszF32), + ("libm::fmax", RustFloatMathIntrinsic::MaxNumNszF64), + ("libm::fminf", RustFloatMathIntrinsic::MinNumNszF32), + ("libm::fmin", RustFloatMathIntrinsic::MinNumNszF64), + ("libm::rintf", RustFloatMathIntrinsic::RoundevenF32), + ("libm::rint", RustFloatMathIntrinsic::RoundevenF64), + ("libm::roundevenf", RustFloatMathIntrinsic::RoundevenF32), + ("libm::roundeven", RustFloatMathIntrinsic::RoundevenF64), + ] { + assert_eq!( + RustFloatMathIntrinsic::from_core_path(path), + Some(expected), + "`{path}` did not map to the expected intrinsic" + ); + } + } + /// Same anchoring requirement for the tuple-returning sincos detector. #[test] fn libm_sincos_detection_is_anchored_to_the_libm_crate_root() { From d81de7253f1de35c616f442d95ca38477d4cd152 Mon Sep 17 00:00:00 2001 From: nihalpasham Date: Fri, 12 Jun 2026 12:12:54 +0530 Subject: [PATCH 4/6] feat(mir-importer): actionable error for unmapped libm functions The collector skips the whole libm crate, so a libm call the float-math dispatch does not recognize can never resolve: it used to surface only at module verification as a bare "Symbol libm__cbrtf not found", far from the offending kernel. Bail out at the call site instead, naming the libm function and where to add the mapping, following the per-site diagnostics direction of #168. --- .../src/translator/terminator/mod.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/crates/mir-importer/src/translator/terminator/mod.rs b/crates/mir-importer/src/translator/terminator/mod.rs index 29f639a0ae..0749fc9177 100644 --- a/crates/mir-importer/src/translator/terminator/mod.rs +++ b/crates/mir-importer/src/translator/terminator/mod.rs @@ -1170,6 +1170,25 @@ fn translate_call( ); } + // The collector skips the entire `libm` crate: every libm call must be + // intercepted by the float-math dispatch above and rerouted to a + // libdevice intrinsic, so no definition for a libm symbol ever exists in + // the module. A libm function the dispatch does not recognize would only + // fail much later, as a bare "Symbol libm__cbrtf not found" verifier + // error on the LLVM dialect module. Fail here instead, with the + // function's name and source location. + if let Some(ref name) = pattern_name + && intrinsics::float_math::is_libm_path(name) + { + return input_err!( + loc, + TranslationErr::unsupported(format!( + "libm function `{name}` is not yet mapped to a libdevice intrinsic; \ + add it to `from_libm_path` in the mir-importer float-math dispatch" + )) + ); + } + // Not an intrinsic - emit regular function call let raw_name = call_name.unwrap_or_else(|| "unknown_function".to_string()); let legal_name = legaliser.legalise(&raw_name); From 919a34728dde492f0fddd2ae2a533b9021426ad6 Mon Sep 17 00:00:00 2001 From: nihalpasham Date: Fri, 12 Jun 2026 12:16:31 +0530 Subject: [PATCH 5/6] style(mir-importer): rustfmt float_math The workspace fmt check gates CI; the sincos helper's callee-pair tuples needed wrapping. --- .../terminator/intrinsics/float_math.rs | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/crates/mir-importer/src/translator/terminator/intrinsics/float_math.rs b/crates/mir-importer/src/translator/terminator/intrinsics/float_math.rs index 412d8af39f..34e8ab2ee1 100644 --- a/crates/mir-importer/src/translator/terminator/intrinsics/float_math.rs +++ b/crates/mir-importer/src/translator/terminator/intrinsics/float_math.rs @@ -380,9 +380,15 @@ pub fn emit_sincos( )?; let (sin_callee, cos_callee) = if is_f64 { - (rust_intrinsics::CALLEE_SIN_F64, rust_intrinsics::CALLEE_COS_F64) + ( + rust_intrinsics::CALLEE_SIN_F64, + rust_intrinsics::CALLEE_COS_F64, + ) } else { - (rust_intrinsics::CALLEE_SIN_F32, rust_intrinsics::CALLEE_COS_F32) + ( + rust_intrinsics::CALLEE_SIN_F32, + rust_intrinsics::CALLEE_COS_F32, + ) }; let callee_id = pliron::identifier::Identifier::try_from("callee").unwrap(); @@ -445,11 +451,19 @@ pub fn emit_sincos( .unwrap_or(tuple_op); if let Some(target_idx) = target { - Ok(helpers::emit_goto(ctx, *target_idx, goto_prev, block_map, loc)) + Ok(helpers::emit_goto( + ctx, + *target_idx, + goto_prev, + block_map, + loc, + )) } else { input_err!( loc.clone(), - TranslationErr::unsupported("libm::sincos call without target not supported".to_string()) + TranslationErr::unsupported( + "libm::sincos call without target not supported".to_string() + ) ) } } From f7b229fff3fb65cc55e3dca85af16a5ac7fc92ff Mon Sep 17 00:00:00 2001 From: nihalpasham Date: Fri, 12 Jun 2026 12:18:33 +0530 Subject: [PATCH 6/6] examples: add libm_math (libm crate float ops via libdevice) Covers the f32 and f64 libm entry points, the tuple-returning sincosf, and the new fmax/fmin/rint lanes, comparing device results against host stdlib math with a SUCCESS marker. Also pins the interception anchor end to end: a user expf in a libm-named module must stay a regular device call; the +1000.0 offset turns any wrong rerouting to __nv_expf into a host-comparison failure. libm sqrt/sqrtf are excluded: libm routes sqrt through an arch-specific inline-asm wrapper that MIR-inlines into the kernel before call-site interception, a known follow-up. --- .../examples/libm_math/Cargo.lock | 431 ++++++++++++++++++ .../examples/libm_math/Cargo.toml | 23 + .../examples/libm_math/src/main.rs | 156 +++++++ 3 files changed, 610 insertions(+) create mode 100644 crates/rustc-codegen-cuda/examples/libm_math/Cargo.lock create mode 100644 crates/rustc-codegen-cuda/examples/libm_math/Cargo.toml create mode 100644 crates/rustc-codegen-cuda/examples/libm_math/src/main.rs diff --git a/crates/rustc-codegen-cuda/examples/libm_math/Cargo.lock b/crates/rustc-codegen-cuda/examples/libm_math/Cargo.lock new file mode 100644 index 0000000000..ed03e3b59d --- /dev/null +++ b/crates/rustc-codegen-cuda/examples/libm_math/Cargo.lock @@ -0,0 +1,431 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "bindgen" +version = "0.71.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "cuda-bindings" +version = "0.2.1" +dependencies = [ + "bindgen", +] + +[[package]] +name = "cuda-core" +version = "0.2.1" +dependencies = [ + "anyhow", + "cuda-bindings", + "half", + "oxide-artifacts", +] + +[[package]] +name = "cuda-device" +version = "0.2.1" +dependencies = [ + "cuda-macros", +] + +[[package]] +name = "cuda-host" +version = "0.2.1" +dependencies = [ + "cuda-core", + "cuda-macros", + "half", + "libnvvm-sys", + "nvjitlink-sys", + "thiserror", +] + +[[package]] +name = "cuda-macros" +version = "0.2.1" +dependencies = [ + "proc-macro2", + "quote", + "reserved-oxide-symbols", + "syn", +] + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libm_math" +version = "0.1.0" +dependencies = [ + "cuda-core", + "cuda-device", + "cuda-host", + "libm", +] + +[[package]] +name = "libnvvm-sys" +version = "0.2.1" +dependencies = [ + "libloading", + "thiserror", +] + +[[package]] +name = "log" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nvjitlink-sys" +version = "0.2.1" +dependencies = [ + "libloading", + "thiserror", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "crc32fast", + "hashbrown 0.15.5", + "indexmap", + "memchr", +] + +[[package]] +name = "oxide-artifacts" +version = "0.2.1" +dependencies = [ + "object", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reserved-oxide-symbols" +version = "0.2.1" + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "zerocopy" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/crates/rustc-codegen-cuda/examples/libm_math/Cargo.toml b/crates/rustc-codegen-cuda/examples/libm_math/Cargo.toml new file mode 100644 index 0000000000..0cfbfac3f2 --- /dev/null +++ b/crates/rustc-codegen-cuda/examples/libm_math/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "libm_math" +version = "0.1.0" +edition = "2024" +license = "Apache-2.0" + +[workspace] + +# Feature example for PR #142: kernels calling `libm` crate float +# functions (the path glam's `nostd-libm` feature emits on nvptx). +# Each call site is intercepted by mir-importer's float-math dispatch +# and lowered to the matching libdevice intrinsic (__nv_sinf, +# __nv_expf, ...); the libm crate's software-float bodies are never +# translated. +# +# Run with: +# cargo oxide run libm_math + +[dependencies] +cuda-device = { path = "../../../cuda-device" } +cuda-host = { path = "../../../cuda-host" } +cuda-core = { path = "../../../cuda-core" } +libm = "0.2" diff --git a/crates/rustc-codegen-cuda/examples/libm_math/src/main.rs b/crates/rustc-codegen-cuda/examples/libm_math/src/main.rs new file mode 100644 index 0000000000..9a4375f1c4 --- /dev/null +++ b/crates/rustc-codegen-cuda/examples/libm_math/src/main.rs @@ -0,0 +1,156 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +//! Kernels calling `libm` crate float functions (PR #142). +//! +//! This is the path glam's `nostd-libm` feature emits on nvptx: float +//! math goes through `libm::sinf`, `libm::expf`, ... instead of the +//! `f32`/`f64` inherent methods. Each call site is intercepted by +//! mir-importer's float-math dispatch and lowered to the matching +//! libdevice intrinsic (`__nv_sinf`, `__nv_expf`, ...); libm's +//! software-float bodies are never translated. +//! +//! Exercises the f32 ("...f") and f64 (bare) entry points, the +//! tuple-returning `sincosf`, and the fmax/fmin/rint lanes. The host +//! computes the same expressions with stdlib float methods and compares. +//! +//! Also pins the interception anchor: `libm_lookalike::expf` below is a +//! user function whose path contains "libm", and it must compile as a +//! regular device call, never be rerouted to `__nv_expf`. +//! +//! Run with: +//! cargo oxide run libm_math + +use cuda_core::{CudaContext, DeviceBuffer, LaunchConfig}; +use cuda_device::{DisjointSlice, cuda_module, kernel, thread}; + +/// Adversarial guard for the libm interception anchor: a user function +/// named like a libm entry point, in a module path containing "libm". +mod libm_lookalike { + /// The +1000.0 offset makes any wrong rerouting to `__nv_expf` show + /// up in the host comparison. `inline(never)` keeps the call visible + /// to the importer's dispatch instead of being MIR-inlined away. + #[inline(never)] + pub fn expf(x: f32) -> f32 { + x + 1000.0 + } +} + +#[cuda_module] +mod kernels { + use super::*; + + /// f32 libm entry points. + // libm::sqrtf / libm::sqrt are not exercised: libm routes sqrt + // through an arch-specific inline-asm wrapper that rustc MIR-inlines + // into the kernel before the call-site interception can fire, so + // sqrt is not yet supported via libm (f32::sqrt / f64::sqrt work). + #[kernel] + pub fn libm_f32(x: &[f32], mut out: DisjointSlice) { + let idx = thread::index_1d(); + let i = idx.get(); + if let Some(o) = out.get_mut(idx) { + let v = x[i]; + let trig = libm::sinf(v) + libm::cosf(v); + let e = libm::expf(v); + let p = libm::powf(v, 1.5f32); + let r = libm::floorf(v) + libm::fabsf(v) + libm::rintf(v * 2.5); + let mm = libm::fmaxf(v, 1.0) + libm::fminf(v, 2.0); + let (sn, cs) = libm::sincosf(v); + let guard = libm_lookalike::expf(v); + *o = trig + e + p + r + mm + sn * cs + guard; + } + } + + /// f64 libm entry points (bare names must dispatch to the f64 + /// libdevice symbols, not the f32 ones). + #[kernel] + pub fn libm_f64(x: &[f64], mut out: DisjointSlice) { + let idx = thread::index_1d(); + let i = idx.get(); + if let Some(o) = out.get_mut(idx) { + let v = x[i]; + let trig = libm::sin(v) + libm::cos(v); + let e = libm::exp(v); + let p = libm::pow(v, 1.5f64); + let t = libm::atan2(v, 2.0f64); + let r = libm::rint(v * 2.5); + let mm = libm::fmax(v, 1.0) + libm::fmin(v, 2.0); + *o = trig + e + p + t + r + mm; + } + } +} + +fn main() { + println!("=== libm_math: libm crate float ops on the device ==="); + + let ctx = CudaContext::new(0).expect("Failed to create CUDA context"); + let stream = ctx.default_stream(); + + const N: usize = 64; + let x32: Vec = (0..N).map(|i| 0.25 + i as f32 * 0.1).collect(); + let x64: Vec = (0..N).map(|i| 0.25 + i as f64 * 0.1).collect(); + + let x32_dev = DeviceBuffer::from_host(&stream, &x32).unwrap(); + let mut o32_dev = DeviceBuffer::::zeroed(&stream, N).unwrap(); + let x64_dev = DeviceBuffer::from_host(&stream, &x64).unwrap(); + let mut o64_dev = DeviceBuffer::::zeroed(&stream, N).unwrap(); + + let module = kernels::load(&ctx).expect("Failed to load embedded CUDA module"); + module + .libm_f32( + &stream, + LaunchConfig::for_num_elems(N as u32), + &x32_dev, + &mut o32_dev, + ) + .expect("libm_f32 launch failed"); + module + .libm_f64( + &stream, + LaunchConfig::for_num_elems(N as u32), + &x64_dev, + &mut o64_dev, + ) + .expect("libm_f64 launch failed"); + + let o32 = o32_dev.to_host_vec(&stream).unwrap(); + let o64 = o64_dev.to_host_vec(&stream).unwrap(); + + let mut ok = true; + for i in 0..N { + let v = x32[i]; + let (sn, cs) = (v.sin(), v.cos()); + let want = (v.sin() + v.cos()) + + v.exp() + + v.powf(1.5) + + (v.floor() + v.abs() + (v * 2.5).round_ties_even()) + + (v.max(1.0) + v.min(2.0)) + + sn * cs + + (v + 1000.0); + if (o32[i] - want).abs() > 1e-3 * want.abs().max(1.0) { + println!("f32 mismatch at {i}: got {} want {want}", o32[i]); + ok = false; + } + let v = x64[i]; + let want = (v.sin() + v.cos()) + + v.exp() + + v.powf(1.5) + + v.atan2(2.0) + + (v * 2.5).round_ties_even() + + (v.max(1.0) + v.min(2.0)); + if (o64[i] - want).abs() > 1e-9 * want.abs().max(1.0) { + println!("f64 mismatch at {i}: got {} want {want}", o64[i]); + ok = false; + } + } + + if ok { + println!("SUCCESS: all libm lanes match host math"); + } else { + println!("FAIL: device results diverge from host math"); + std::process::exit(1); + } +}