From 7f4995735795ea8d3a767777d89b50e806c1ae20 Mon Sep 17 00:00:00 2001 From: Raul Estrada Date: Sun, 25 Oct 2026 01:39:05 -0600 Subject: [PATCH 1/2] feat: lower tgamma, ilogb and ldexp through libdevice Signed-off-by: Raul Estrada --- .../crates/dialect-mir/src/rust_intrinsics.rs | 25 + .../terminator/intrinsics/float_math.rs | 77 +- .../crates/mir-lower/src/convert/ops/call.rs | 129 +++ .../tests/lowering_test/math_conversions.rs | 261 ++++++ .../examples/math_special/Cargo.lock | 761 ++++++++++++++++++ .../examples/math_special/Cargo.toml | 16 + .../examples/math_special/README.md | 70 ++ .../examples/math_special/src/main.rs | 233 ++++++ .../rustc-codegen-cuda/src/collector.rs | 28 +- 9 files changed, 1596 insertions(+), 4 deletions(-) create mode 100644 cuda-oxide/crates/rustc-codegen-cuda/examples/math_special/Cargo.lock create mode 100644 cuda-oxide/crates/rustc-codegen-cuda/examples/math_special/Cargo.toml create mode 100644 cuda-oxide/crates/rustc-codegen-cuda/examples/math_special/README.md create mode 100644 cuda-oxide/crates/rustc-codegen-cuda/examples/math_special/src/main.rs diff --git a/cuda-oxide/crates/dialect-mir/src/rust_intrinsics.rs b/cuda-oxide/crates/dialect-mir/src/rust_intrinsics.rs index 43d7241f52..4d4b5387ea 100644 --- a/cuda-oxide/crates/dialect-mir/src/rust_intrinsics.rs +++ b/cuda-oxide/crates/dialect-mir/src/rust_intrinsics.rs @@ -211,6 +211,19 @@ pub const CALLEE_HYPOT_F32: &str = placeholder!("hypotf32"); /// Placeholder call used for `f64::hypot` / `std::sys::cmath::hypot` (binary). pub const CALLEE_HYPOT_F64: &str = placeholder!("hypotf64"); +/// Placeholder call used for `libm::tgammaf`. +pub const CALLEE_TGAMMA_F32: &str = placeholder!("tgammaf32"); +/// Placeholder call used for `libm::tgamma`. +pub const CALLEE_TGAMMA_F64: &str = placeholder!("tgammaf64"); +/// Placeholder call used for `libm::ilogbf`. +pub const CALLEE_ILOGB_F32: &str = placeholder!("ilogbf32"); +/// Placeholder call used for `libm::ilogb`. +pub const CALLEE_ILOGB_F64: &str = placeholder!("ilogbf64"); +/// Placeholder call used for `libm::ldexpf`. +pub const CALLEE_LDEXP_F32: &str = placeholder!("ldexpf32"); +/// Placeholder call used for `libm::ldexp`. +pub const CALLEE_LDEXP_F64: &str = placeholder!("ldexpf64"); + /// Placeholder call used for `core::intrinsics::fadd_fast` (generic over float type). /// /// Lowered to `llvm.fadd` with explicit `fast` fast-math flags. The `f*_fast` intrinsics @@ -350,6 +363,12 @@ pub fn is_libdevice_backed_placeholder(callee: &str) -> bool { | CALLEE_LOG1P_F64 | CALLEE_HYPOT_F32 | CALLEE_HYPOT_F64 + | CALLEE_TGAMMA_F32 + | CALLEE_TGAMMA_F64 + | CALLEE_ILOGB_F32 + | CALLEE_ILOGB_F64 + | CALLEE_LDEXP_F32 + | CALLEE_LDEXP_F64 ) } @@ -452,6 +471,12 @@ mod tests { CALLEE_LOG1P_F64, CALLEE_HYPOT_F32, CALLEE_HYPOT_F64, + CALLEE_TGAMMA_F32, + CALLEE_TGAMMA_F64, + CALLEE_ILOGB_F32, + CALLEE_ILOGB_F64, + CALLEE_LDEXP_F32, + CALLEE_LDEXP_F64, ]; /// Every placeholder that is libdevice-backed only under the libNVVM diff --git a/cuda-oxide/crates/mir-importer/src/translator/terminator/intrinsics/float_math.rs b/cuda-oxide/crates/mir-importer/src/translator/terminator/intrinsics/float_math.rs index 77140b8dcb..3eb562bed4 100644 --- a/cuda-oxide/crates/mir-importer/src/translator/terminator/intrinsics/float_math.rs +++ b/cuda-oxide/crates/mir-importer/src/translator/terminator/intrinsics/float_math.rs @@ -16,7 +16,7 @@ use pliron::location::Location; use pliron::operation::Operation; use rustc_public::mir; -/// Floating-point math intrinsic from libcore. +/// Floating-point math intrinsic from core, std, or libm. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum RustFloatMathIntrinsic { /// `core::intrinsics::sqrtf32`. @@ -161,6 +161,18 @@ pub enum RustFloatMathIntrinsic { HypotF32, /// `f64::hypot` / `std::sys::cmath::hypot` (binary). HypotF64, + /// `libm::tgammaf`. + TgammaF32, + /// `libm::tgamma`. + TgammaF64, + /// `libm::ilogbf`. + IlogbF32, + /// `libm::ilogb`. + IlogbF64, + /// `libm::ldexpf`. + LdexpF32, + /// `libm::ldexp`. + LdexpF64, /// Generic `core::intrinsics::fadd_fast` (lowered to `llvm.fadd` + fast-math). FaddFast, /// Generic `core::intrinsics::fsub_fast` (lowered to `llvm.fsub` + fast-math). @@ -341,6 +353,8 @@ impl RustFloatMathIntrinsic { "std::sys::cmath::log1p" => Some(Self::Log1pF64), "std::sys::cmath::hypotf" => Some(Self::HypotF32), "std::sys::cmath::hypot" => Some(Self::HypotF64), + "std::sys::cmath::tgammaf" => Some(Self::TgammaF32), + "std::sys::cmath::tgamma" => Some(Self::TgammaF64), other => Self::from_libm_path(other).or_else(|| Self::from_fast_intrinsic_path(other)), } } @@ -428,6 +442,12 @@ impl RustFloatMathIntrinsic { "log1p" => Some(Self::Log1pF64), "hypotf" => Some(Self::HypotF32), "hypot" => Some(Self::HypotF64), + "tgammaf" => Some(Self::TgammaF32), + "tgamma" => Some(Self::TgammaF64), + "ilogbf" => Some(Self::IlogbF32), + "ilogb" => Some(Self::IlogbF64), + "ldexpf" | "scalbnf" => Some(Self::LdexpF32), + "ldexp" | "scalbn" => Some(Self::LdexpF64), _ => None, } } @@ -523,6 +543,12 @@ impl RustFloatMathIntrinsic { Self::Log1pF64 => rust_intrinsics::CALLEE_LOG1P_F64, Self::HypotF32 => rust_intrinsics::CALLEE_HYPOT_F32, Self::HypotF64 => rust_intrinsics::CALLEE_HYPOT_F64, + Self::TgammaF32 => rust_intrinsics::CALLEE_TGAMMA_F32, + Self::TgammaF64 => rust_intrinsics::CALLEE_TGAMMA_F64, + Self::IlogbF32 => rust_intrinsics::CALLEE_ILOGB_F32, + Self::IlogbF64 => rust_intrinsics::CALLEE_ILOGB_F64, + Self::LdexpF32 => rust_intrinsics::CALLEE_LDEXP_F32, + Self::LdexpF64 => rust_intrinsics::CALLEE_LDEXP_F64, Self::FaddFast => rust_intrinsics::CALLEE_FADD_FAST, Self::FsubFast => rust_intrinsics::CALLEE_FSUB_FAST, Self::FmulFast => rust_intrinsics::CALLEE_FMUL_FAST, @@ -736,6 +762,55 @@ mod tests { use super::*; use dialect_mir::rust_intrinsics; + #[test] + fn special_math_paths_resolve_to_typed_placeholders() { + use RustFloatMathIntrinsic as I; + for (name, intrinsic, callee) in [ + ("tgammaf", I::TgammaF32, rust_intrinsics::CALLEE_TGAMMA_F32), + ("tgamma", I::TgammaF64, rust_intrinsics::CALLEE_TGAMMA_F64), + ("ilogbf", I::IlogbF32, rust_intrinsics::CALLEE_ILOGB_F32), + ("ilogb", I::IlogbF64, rust_intrinsics::CALLEE_ILOGB_F64), + ("ldexpf", I::LdexpF32, rust_intrinsics::CALLEE_LDEXP_F32), + ("ldexp", I::LdexpF64, rust_intrinsics::CALLEE_LDEXP_F64), + ("scalbnf", I::LdexpF32, rust_intrinsics::CALLEE_LDEXP_F32), + ("scalbn", I::LdexpF64, rust_intrinsics::CALLEE_LDEXP_F64), + ] { + for path in [ + format!("libm::{name}"), + format!("libm::math::{name}::{name}"), + ] { + assert_eq!(I::from_core_path(&path), Some(intrinsic), "{path}"); + assert_eq!(intrinsic.placeholder_callee(), callee); + } + for path in [ + format!("my_app::libm_compat::{name}"), + format!("my_app::libm::{name}"), + format!("libm_lookalike::{name}"), + format!("std::sys::cmath::{name}_unsupported"), + ] { + assert_eq!(I::from_core_path(&path), None, "{path}"); + } + } + assert_eq!( + I::from_core_path("std::sys::cmath::tgammaf"), + Some(I::TgammaF32) + ); + assert_eq!( + I::from_core_path("std::sys::cmath::tgamma"), + Some(I::TgammaF64) + ); + // Existing scalar and tuple dispatch must keep their identities. + assert_eq!(I::from_core_path("libm::hypot"), Some(I::HypotF64)); + assert_eq!( + I::from_core_path("core::intrinsics::powif32"), + Some(I::PowiF32) + ); + assert_eq!(libm_sincos_is_f64("libm::sincosf"), Some(false)); + for name in ["frexp", "modf", "remquo", "lgamma_r", "tgammaf128"] { + assert_eq!(I::from_core_path(&format!("libm::{name}")), None); + } + } + #[test] fn generic_float_intrinsics_resolve_only_the_declared_float_type() { use RustFloatMathIntrinsic as I; diff --git a/cuda-oxide/crates/mir-lower/src/convert/ops/call.rs b/cuda-oxide/crates/mir-lower/src/convert/ops/call.rs index bc19168b0c..b18e43b12c 100644 --- a/cuda-oxide/crates/mir-lower/src/convert/ops/call.rs +++ b/cuda-oxide/crates/mir-lower/src/convert/ops/call.rs @@ -262,6 +262,12 @@ enum RustFloatMathIntrinsic { Log1pF64, HypotF32, HypotF64, + TgammaF32, + TgammaF64, + IlogbF32, + IlogbF64, + LdexpF32, + LdexpF64, FaddFast, FsubFast, FmulFast, @@ -344,6 +350,12 @@ impl RustFloatMathIntrinsic { rust_intrinsics::CALLEE_LOG1P_F64 => Some(Self::Log1pF64), rust_intrinsics::CALLEE_HYPOT_F32 => Some(Self::HypotF32), rust_intrinsics::CALLEE_HYPOT_F64 => Some(Self::HypotF64), + rust_intrinsics::CALLEE_TGAMMA_F32 => Some(Self::TgammaF32), + rust_intrinsics::CALLEE_TGAMMA_F64 => Some(Self::TgammaF64), + rust_intrinsics::CALLEE_ILOGB_F32 => Some(Self::IlogbF32), + rust_intrinsics::CALLEE_ILOGB_F64 => Some(Self::IlogbF64), + rust_intrinsics::CALLEE_LDEXP_F32 => Some(Self::LdexpF32), + rust_intrinsics::CALLEE_LDEXP_F64 => Some(Self::LdexpF64), rust_intrinsics::CALLEE_FADD_FAST => Some(Self::FaddFast), rust_intrinsics::CALLEE_FSUB_FAST => Some(Self::FsubFast), rust_intrinsics::CALLEE_FMUL_FAST => Some(Self::FmulFast), @@ -448,6 +460,12 @@ impl RustFloatMathIntrinsic { Self::Log1pF64 => Ok("__nv_log1p"), Self::HypotF32 => Ok("__nv_hypotf"), Self::HypotF64 => Ok("__nv_hypot"), + Self::TgammaF32 => Ok("__nv_tgammaf"), + Self::TgammaF64 => Ok("__nv_tgamma"), + Self::IlogbF32 => Ok("__nv_ilogbf"), + Self::IlogbF64 => Ok("__nv_ilogb"), + Self::LdexpF32 => Ok("__nv_ldexpf"), + Self::LdexpF64 => Ok("__nv_ldexp"), Self::FaddFast | Self::FsubFast | Self::FmulFast | Self::FdivFast | Self::FremFast => { // The `f*_fast` intrinsics lower directly to LLVM `fadd`/`fsub`/ // `fmul`/`fdiv`/`frem` with fast-math flags, not to a libdevice @@ -479,6 +497,8 @@ impl RustFloatMathIntrinsic { | Self::Atan2F64 | Self::HypotF32 | Self::HypotF64 + | Self::LdexpF32 + | Self::LdexpF64 | Self::FaddFast | Self::FsubFast | Self::FmulFast @@ -1300,6 +1320,35 @@ fn convert_rust_float_math_intrinsic( let result_mir_ty = op.deref(ctx).get_result(0).get_type(ctx); let result_ty = convert_type(ctx, result_mir_ty).map_err(anyhow_to_pliron)?; + // These libdevice signatures mix integer and float types. Validate the + // complete signature before declaring the callee: deriving it solely from + // a malformed placeholder would silently declare the wrong libdevice ABI. + let special_float_ty: Option = match intrinsic { + RustFloatMathIntrinsic::TgammaF32 + | RustFloatMathIntrinsic::IlogbF32 + | RustFloatMathIntrinsic::LdexpF32 => Some(FP32Type::get(ctx).into()), + RustFloatMathIntrinsic::TgammaF64 + | RustFloatMathIntrinsic::IlogbF64 + | RustFloatMathIntrinsic::LdexpF64 => Some(FP64Type::get(ctx).into()), + _ => None, + }; + if let Some(float_ty) = special_float_ty { + let i32_ty: TypeHandle = IntegerType::get(ctx, 32, Signedness::Signless).into(); + let expected_result = match intrinsic { + RustFloatMathIntrinsic::IlogbF32 | RustFloatMathIntrinsic::IlogbF64 => i32_ty, + _ => float_ty, + }; + if result_ty != expected_result + || args[0].get_type(ctx) != float_ty + || (matches!( + intrinsic, + RustFloatMathIntrinsic::LdexpF32 | RustFloatMathIntrinsic::LdexpF64 + ) && args[1].get_type(ctx) != i32_ty) + { + return pliron::input_err!(loc, "Rust special math intrinsic signature mismatch"); + } + } + // The rounding and sign intrinsics have direct LLVM equivalents that the // NVPTX backend selects without a libdevice call; everything else (and // every op under the libNVVM backend) stays on libdevice. See @@ -2005,6 +2054,86 @@ fn resolve_device_extern_symbol(callee_name: &str) -> String { mod tests { use super::*; + #[test] + fn special_math_symbols_and_arities_match_on_both_backends() { + use RustFloatMathIntrinsic as I; + for backend in [ + crate::IntrinsicBackend::LlvmNvptx, + crate::IntrinsicBackend::LibNvvm, + ] { + let mut ctx = Context::new(); + crate::context::set_lowering_options( + &mut ctx, + crate::LoweringOptions { + intrinsic_backend: backend, + ..Default::default() + }, + ); + let f32_ty = FP32Type::get(&ctx).into(); + let f64_ty = FP64Type::get(&ctx).into(); + let i32_ty = IntegerType::get(&ctx, 32, Signedness::Signless).into(); + for (callee, intrinsic, result_ty, symbol, arity) in [ + ( + rust_intrinsics::CALLEE_TGAMMA_F32, + I::TgammaF32, + f32_ty, + "__nv_tgammaf", + 1, + ), + ( + rust_intrinsics::CALLEE_TGAMMA_F64, + I::TgammaF64, + f64_ty, + "__nv_tgamma", + 1, + ), + ( + rust_intrinsics::CALLEE_ILOGB_F32, + I::IlogbF32, + i32_ty, + "__nv_ilogbf", + 1, + ), + ( + rust_intrinsics::CALLEE_ILOGB_F64, + I::IlogbF64, + i32_ty, + "__nv_ilogb", + 1, + ), + ( + rust_intrinsics::CALLEE_LDEXP_F32, + I::LdexpF32, + f32_ty, + "__nv_ldexpf", + 2, + ), + ( + rust_intrinsics::CALLEE_LDEXP_F64, + I::LdexpF64, + f64_ty, + "__nv_ldexp", + 2, + ), + ] { + assert_eq!(I::from_placeholder_callee(callee), Some(intrinsic)); + assert_eq!(intrinsic.arg_count(), arity); + assert_eq!( + float_math_intrinsic_symbol( + &ctx, + intrinsic, + result_ty, + pliron::location::Location::Unknown, + ) + .unwrap(), + symbol + ); + assert!(rust_intrinsics::is_libdevice_backed_placeholder(callee)); + assert!(!rust_intrinsics::is_backend_dependent_libdevice_placeholder(callee)); + } + } + } + #[test] fn no_fma_policy_removes_only_contract_from_fast_float_intrinsics() { let mut ctx = Context::new(); diff --git a/cuda-oxide/crates/mir-lower/tests/lowering_test/math_conversions.rs b/cuda-oxide/crates/mir-lower/tests/lowering_test/math_conversions.rs index 95fbf7bd31..940af0bf53 100644 --- a/cuda-oxide/crates/mir-lower/tests/lowering_test/math_conversions.rs +++ b/cuda-oxide/crates/mir-lower/tests/lowering_test/math_conversions.rs @@ -15,6 +15,267 @@ use pliron::operation::Operation; use crate::common::{append_return, build_test_kernel, lowered_kernel_body, make_test_ctx}; +#[test] +fn test_special_math_mixed_signatures_on_both_backends() -> Result<(), anyhow::Error> { + use dialect_mir::rust_intrinsics as ri; + use pliron::builtin::attributes::StringAttr; + use pliron::builtin::types::{FP32Type, FP64Type, IntegerType, Signedness}; + use pliron::r#type::{TypeHandle, Typed}; + + for backend in [ + mir_lower::IntrinsicBackend::LlvmNvptx, + mir_lower::IntrinsicBackend::LibNvvm, + ] { + let mut ctx = make_test_ctx(); + let f32_ty: TypeHandle = FP32Type::get(&ctx).into(); + let f64_ty: TypeHandle = FP64Type::get(&ctx).into(); + let i32_mir: TypeHandle = IntegerType::get(&ctx, 32, Signedness::Signed).into(); + let i32_llvm: TypeHandle = IntegerType::get(&ctx, 32, Signedness::Signless).into(); + let (module_ptr, entry) = build_test_kernel(&mut ctx, vec![f32_ty, f64_ty, i32_mir]); + let x32 = entry.deref(&ctx).get_argument(0); + let x64 = entry.deref(&ctx).get_argument(1); + let exponent = entry.deref(&ctx).get_argument(2); + let cases = [ + ( + ri::CALLEE_TGAMMA_F32, + "__nv_tgammaf", + f32_ty, + vec![x32], + vec![f32_ty], + ), + ( + ri::CALLEE_TGAMMA_F64, + "__nv_tgamma", + f64_ty, + vec![x64], + vec![f64_ty], + ), + ( + ri::CALLEE_ILOGB_F32, + "__nv_ilogbf", + i32_mir, + vec![x32], + vec![f32_ty], + ), + ( + ri::CALLEE_ILOGB_F64, + "__nv_ilogb", + i32_mir, + vec![x64], + vec![f64_ty], + ), + ( + ri::CALLEE_LDEXP_F32, + "__nv_ldexpf", + f32_ty, + vec![x32, exponent], + vec![f32_ty, i32_llvm], + ), + ( + ri::CALLEE_LDEXP_F64, + "__nv_ldexp", + f64_ty, + vec![x64, exponent], + vec![f64_ty, i32_llvm], + ), + // Control: the pre-existing mixed float/integer powi path. + ( + ri::CALLEE_POWI_F32, + "__nv_powif", + f32_ty, + vec![x32, exponent], + vec![f32_ty, i32_llvm], + ), + ( + ri::CALLEE_POWI_F64, + "__nv_powi", + f64_ty, + vec![x64, exponent], + vec![f64_ty, i32_llvm], + ), + ]; + for (callee, _, result_ty, operands, _) in &cases { + let call = Operation::new( + &mut ctx, + mir::MirCallOp::get_concrete_op_info(), + vec![*result_ty], + operands.clone(), + vec![], + 0, + ); + mir::MirCallOp::new(call).set_attr_callee(&ctx, StringAttr::new((*callee).to_string())); + call.insert_at_back(entry, &ctx); + } + append_return(&mut ctx, entry); + mir_lower::lower_mir_to_llvm_with_options( + &mut ctx, + module_ptr, + mir_lower::LoweringOptions { + intrinsic_backend: backend, + ..Default::default() + }, + ) + .map_err(|error| anyhow::anyhow!("{error}"))?; + + let calls = lowered_kernel_body(&ctx, module_ptr) + .into_iter() + .filter_map(|op| Operation::get_op::(op, &ctx)) + .collect::>(); + assert_eq!(calls.len(), cases.len()); + for call in calls { + let CallOpCallable::Direct(symbol) = call.callee(&ctx) else { + panic!("special math must lower to a direct call"); + }; + let name = symbol.to_string(); + let (_, _, mir_result, _, expected_args) = cases + .iter() + .find(|(_, symbol, _, _, _)| *symbol == name) + .expect("unexpected libdevice symbol"); + let expected_result = if *mir_result == i32_mir { + i32_llvm + } else { + *mir_result + }; + let op = call.get_operation().deref(&ctx); + assert_eq!(op.get_num_results(), 1, "{name}"); + assert_eq!(op.get_result(0).get_type(&ctx), expected_result, "{name}"); + assert_eq!( + op.operands() + .map(|arg| arg.get_type(&ctx)) + .collect::>(), + *expected_args, + "{name}" + ); + } + let module = Operation::get_op::(module_ptr, &ctx).unwrap(); + let ir = llvm_export::export::export_module_to_string(&ctx, &module) + .expect("mixed-signature math must export to LLVM IR"); + for declaration in [ + "declare float @__nv_tgammaf(float", + "declare double @__nv_tgamma(double", + "declare i32 @__nv_ilogbf(float", + "declare i32 @__nv_ilogb(double", + "declare float @__nv_ldexpf(float", + "declare double @__nv_ldexp(double", + ] { + assert!(ir.contains(declaration), "missing {declaration}:\n{ir}"); + } + assert!(!ir.contains(ri::PLACEHOLDER_PREFIX), "{ir}"); + } + Ok(()) +} + +#[test] +fn test_special_math_lowering_rejects_bad_arity_and_types() { + use dialect_mir::rust_intrinsics as ri; + use pliron::builtin::attributes::StringAttr; + use pliron::builtin::types::{FP32Type, FP64Type, IntegerType, Signedness}; + use pliron::r#type::TypeHandle; + + for backend in [ + mir_lower::IntrinsicBackend::LlvmNvptx, + mir_lower::IntrinsicBackend::LibNvvm, + ] { + for bad_case in 0..10 { + let mut ctx = make_test_ctx(); + let f32_ty: TypeHandle = FP32Type::get(&ctx).into(); + let f64_ty: TypeHandle = FP64Type::get(&ctx).into(); + let i32_ty: TypeHandle = IntegerType::get(&ctx, 32, Signedness::Signed).into(); + let i64_ty: TypeHandle = IntegerType::get(&ctx, 64, Signedness::Signed).into(); + let (module_ptr, entry) = + build_test_kernel(&mut ctx, vec![f32_ty, f64_ty, i32_ty, i64_ty]); + let x32 = entry.deref(&ctx).get_argument(0); + let x64 = entry.deref(&ctx).get_argument(1); + let exponent = entry.deref(&ctx).get_argument(2); + let wide_exponent = entry.deref(&ctx).get_argument(3); + let (callee, results, args, diagnostic) = match bad_case { + 0 => ( + ri::CALLEE_TGAMMA_F32, + vec![f32_ty], + vec![], + "requires 1 operand(s)", + ), + 1 => ( + ri::CALLEE_LDEXP_F64, + vec![f64_ty], + vec![x64], + "requires 2 operand(s)", + ), + 2 => ( + ri::CALLEE_ILOGB_F32, + vec![f32_ty], + vec![x32], + "signature mismatch", + ), + 3 => ( + ri::CALLEE_LDEXP_F32, + vec![f32_ty], + vec![x32, x32], + "signature mismatch", + ), + 4 => ( + ri::CALLEE_LDEXP_F64, + vec![f64_ty], + vec![x64, wide_exponent], + "signature mismatch", + ), + 5 => ( + ri::CALLEE_TGAMMA_F32, + vec![f32_ty], + vec![x64], + "signature mismatch", + ), + 6 => ( + ri::CALLEE_TGAMMA_F64, + vec![f32_ty], + vec![x64], + "signature mismatch", + ), + 7 => ( + ri::CALLEE_ILOGB_F64, + vec![i64_ty], + vec![x64], + "signature mismatch", + ), + 8 => ( + ri::CALLEE_LDEXP_F32, + vec![f64_ty], + vec![x32, exponent], + "signature mismatch", + ), + _ => ( + ri::CALLEE_TGAMMA_F64, + vec![], + vec![x64], + "must have one result", + ), + }; + let call = Operation::new( + &mut ctx, + mir::MirCallOp::get_concrete_op_info(), + results, + args, + vec![], + 0, + ); + mir::MirCallOp::new(call).set_attr_callee(&ctx, StringAttr::new(callee.to_string())); + call.insert_at_back(entry, &ctx); + append_return(&mut ctx, entry); + let error = mir_lower::lower_mir_to_llvm_with_options( + &mut ctx, + module_ptr, + mir_lower::LoweringOptions { + intrinsic_backend: backend, + ..Default::default() + }, + ) + .expect_err("malformed special math calls must fail") + .to_string(); + assert!(error.contains(diagnostic), "case {bad_case}: {error}"); + } + } +} + #[test] fn test_fast_float_intrinsics_lower_to_explicit_fast_binops() -> Result<(), anyhow::Error> { use dialect_mir::rust_intrinsics; diff --git a/cuda-oxide/crates/rustc-codegen-cuda/examples/math_special/Cargo.lock b/cuda-oxide/crates/rustc-codegen-cuda/examples/math_special/Cargo.lock new file mode 100644 index 0000000000..71036a0499 --- /dev/null +++ b/cuda-oxide/crates/rustc-codegen-cuda/examples/math_special/Cargo.lock @@ -0,0 +1,761 @@ +# 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.69.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "lazy_static", + "lazycell", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", + "which", +] + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[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 = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[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 = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cuda-artifact-finalizer" +version = "0.2.1" +dependencies = [ + "libnvvm-sys", + "nvjitlink-sys", + "serde", + "sha2", + "thiserror", +] + +[[package]] +name = "cuda-bindings" +version = "0.5.0" +dependencies = [ + "bindgen", + "libloading", + "prettyplease", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "cuda-core" +version = "0.5.0" +dependencies = [ + "anyhow", + "cuda-bindings", + "cuda-core-derive", + "half", + "oxide-artifacts", +] + +[[package]] +name = "cuda-core-derive" +version = "0.5.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "cuda-device" +version = "0.2.1" +dependencies = [ + "cuda-macros", +] + +[[package]] +name = "cuda-host" +version = "0.2.1" +dependencies = [ + "cuda-artifact-finalizer", + "cuda-core", + "cuda-device", + "cuda-macros", + "half", + "libc", + "oxide-artifacts", + "ptx-parse", + "sha2", + "thiserror", +] + +[[package]] +name = "cuda-macros" +version = "0.2.1" +dependencies = [ + "oxide-tile-export", + "proc-macro2", + "quote", + "reserved-oxide-symbols", + "syn", +] + +[[package]] +name = "cuda-target-spec" +version = "0.1.0" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[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 = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[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 = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[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.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[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 = "libnvvm-sys" +version = "0.2.1" +dependencies = [ + "cuda-target-spec", + "libloading", + "thiserror", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "math_special" +version = "0.1.0" +dependencies = [ + "cuda-core", + "cuda-device", + "cuda-host", + "libm", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[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 = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "oxide-artifacts" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74d9a941c0c43d2dceaf25f5c9ca4c9f9cceb28dfa5e4e3cfd14e476f003e7c9" +dependencies = [ + "object", +] + +[[package]] +name = "oxide-tile-export" +version = "0.2.1" +dependencies = [ + "serde", + "serde_json", + "thiserror", +] + +[[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 = "ptx-parse" +version = "0.1.0" + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +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 = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[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 = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[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", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/cuda-oxide/crates/rustc-codegen-cuda/examples/math_special/Cargo.toml b/cuda-oxide/crates/rustc-codegen-cuda/examples/math_special/Cargo.toml new file mode 100644 index 0000000000..6e87dfe5a4 --- /dev/null +++ b/cuda-oxide/crates/rustc-codegen-cuda/examples/math_special/Cargo.toml @@ -0,0 +1,16 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "math_special" +version = "0.1.0" +edition = "2024" +license = "Apache-2.0" + +[workspace] + +[dependencies] +cuda-device = { path = "../../../cuda-device" } +cuda-host = { path = "../../../cuda-host" } +cuda-core = { path = "../../../../../cuda-core" } +libm = "=0.2.16" diff --git a/cuda-oxide/crates/rustc-codegen-cuda/examples/math_special/README.md b/cuda-oxide/crates/rustc-codegen-cuda/examples/math_special/README.md new file mode 100644 index 0000000000..a21149f0ab --- /dev/null +++ b/cuda-oxide/crates/rustc-codegen-cuda/examples/math_special/README.md @@ -0,0 +1,70 @@ + + + +# Special scalar math + +Exercises the first scalar additions for issue #1224: + +| Rust call | f32 libdevice symbol | f64 libdevice symbol | +| --- | --- | --- | +| `libm::tgamma{f}` and `f32/f64::gamma` | `__nv_tgammaf` | `__nv_tgamma` | +| `libm::ilogb{f}` | `__nv_ilogbf` | `__nv_ilogb` | +| `libm::ldexp{f}`, `libm::scalbn{f}` | `__nv_ldexpf` | `__nv_ldexp` | + +`ilogb` returns an `i32`; the exponent argument of `ldexp`/`scalbn` is +an `i32`. `float_gamma` is enabled to exercise the standard-library cmath +shim as well as the `libm` entry points. Use the repository's pinned toolchain +and its existing `RUSTC_BOOTSTRAP` configuration. + +Before these mappings are added, the standard gamma path is rejected by the +collector and the libm paths have no dedicated libdevice lowering. Capture the +baseline diagnostic on the unmodified tree; no local baseline/GPU run is claimed +by this source package. + +The example checks 264 results: 22 inputs, six checks per input, and two widths. +It compares gamma to host libm within a relative tolerance, compares integer +logarithms exactly, and compares scaled values bit for bit. The inputs include +negative non-integers, both exponent signs, subnormals, scaling overflow and +underflow. A user function with a libm-like module name must retain its own +body. `ilogb` inputs are finite and nonzero; its domain-error sentinels are +outside this example's comparison. + +The lowering tests independently verify all six libdevice signatures on both +backends, reject malformed calls, and retain `powi` as a mixed-signature control. +Importer tests check public/internal libm spellings, aliases, cmath gamma, +unrelated names, and functions deferred to later PRs. + +## Validation + +Run from `cuda-oxide/` after copying the files into the existing checkout: + +```bash +cargo oxide fmt +cargo oxide fmt --check +git diff --check +cargo test -p dialect-mir -p mir-importer -p mir-lower --all-targets +cargo clippy -p dialect-mir -p mir-importer -p mir-lower --all-targets -- -D warnings +(cd crates/rustc-codegen-cuda && cargo test --lib) +(cd crates/rustc-codegen-cuda && cargo clippy --all-targets -- -D warnings) +(cd crates/rustc-codegen-cuda/examples/math_special && cargo clippy --locked --all-targets -- -D warnings) +cargo oxide build math_special +cargo oxide run math_special +cargo oxide run libm_math +cargo oxide run math_hyperbolic +bash scripts/check-example-smoketest-contract.sh +bash scripts/sync-example-locks.sh --check +``` + +`math_special` must print `SUCCESS: math_special passed 264 checks across f32 and f64`. +GPU commands require a working CUDA toolkit, driver and device. The example is +a separate workspace; the parent workspace's tests do not build it. + +Before marking the PR ready, run the repository gate from the repository root: + +```bash +just -f cuda-oxide/Justfile check +``` + +This package has undergone static source and archive checks only. Rust, Cargo, +and CUDA were unavailable in the preparation environment; compile, clippy and +GPU validation are pending execution on a configured checkout. diff --git a/cuda-oxide/crates/rustc-codegen-cuda/examples/math_special/src/main.rs b/cuda-oxide/crates/rustc-codegen-cuda/examples/math_special/src/main.rs new file mode 100644 index 0000000000..89c688e14f --- /dev/null +++ b/cuda-oxide/crates/rustc-codegen-cuda/examples/math_special/src/main.rs @@ -0,0 +1,233 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +//! tgamma, ilogb, ldexp and scalbn in f32/f64 device code. +//! Run with `cargo oxide run math_special`. +#![feature(float_gamma)] +// The width-generic host macro casts both f32 and f64 results to f64. +#![allow(clippy::too_many_arguments, clippy::unnecessary_cast)] + +use cuda_core::simt::LaunchConfig; +use cuda_core::{CudaContext, DeviceBuffer}; +use cuda_device::{DisjointSlice, cuda_module, kernel, thread}; + +// An unrelated function with a libm-like path must retain its own body. +mod libm_lookalike { + #[inline(never)] + pub fn tgammaf(x: f32) -> f32 { + x + 1000.0 + } + + #[inline(never)] + pub fn tgamma(x: f64) -> f64 { + x + 1000.0 + } +} + +#[cuda_module] +mod kernels { + use super::*; + + #[kernel] + pub fn special_f32( + xs: &[f32], + exponents: &[i32], + mut gamma: DisjointSlice, + mut std_gamma: DisjointSlice, + mut logarithm: DisjointSlice, + mut scaled: DisjointSlice, + mut alias: DisjointSlice, + mut control: DisjointSlice, + ) { + let i = thread::index_1d().get(); + if i < xs.len() { + let x = xs[i]; + if let Some(out) = gamma.get_mut(thread::index_1d()) { + *out = libm::tgammaf(x); + } + if let Some(out) = std_gamma.get_mut(thread::index_1d()) { + *out = x.gamma(); + } + if let Some(out) = logarithm.get_mut(thread::index_1d()) { + *out = libm::ilogbf(x); + } + if let Some(out) = scaled.get_mut(thread::index_1d()) { + *out = libm::ldexpf(x, exponents[i]); + } + if let Some(out) = alias.get_mut(thread::index_1d()) { + *out = libm::scalbnf(x, exponents[i]); + } + if let Some(out) = control.get_mut(thread::index_1d()) { + *out = libm_lookalike::tgammaf(x); + } + } + } + + #[kernel] + pub fn special_f64( + xs: &[f64], + exponents: &[i32], + mut gamma: DisjointSlice, + mut std_gamma: DisjointSlice, + mut logarithm: DisjointSlice, + mut scaled: DisjointSlice, + mut alias: DisjointSlice, + mut control: DisjointSlice, + ) { + let i = thread::index_1d().get(); + if i < xs.len() { + let x = xs[i]; + if let Some(out) = gamma.get_mut(thread::index_1d()) { + *out = libm::tgamma(x); + } + if let Some(out) = std_gamma.get_mut(thread::index_1d()) { + *out = x.gamma(); + } + if let Some(out) = logarithm.get_mut(thread::index_1d()) { + *out = libm::ilogb(x); + } + if let Some(out) = scaled.get_mut(thread::index_1d()) { + *out = libm::ldexp(x, exponents[i]); + } + if let Some(out) = alias.get_mut(thread::index_1d()) { + *out = libm::scalbn(x, exponents[i]); + } + if let Some(out) = control.get_mut(thread::index_1d()) { + *out = libm_lookalike::tgamma(x); + } + } + } +} + +fn close(got: f64, expected: f64, rtol: f64) -> bool { + if expected.is_nan() { + got.is_nan() + } else if expected.is_infinite() || expected == 0.0 { + got.to_bits() == expected.to_bits() + } else { + got.is_finite() && (got - expected).abs() <= rtol * expected.abs() + } +} + +fn main() -> Result<(), Box> { + let ctx = CudaContext::new(0)?; + let stream = ctx.default_stream(); + let module = kernels::load(&ctx)?; + let mut failures = 0usize; + let mut checks = 0usize; + + macro_rules! check_width { + ($ty:ty, $kernel:ident, $gamma:ident, $ilogb:ident, $ldexp:ident, $rtol:expr, $extra:expr) => {{ + // Nonzero finite inputs keep ilogb away from C's domain-error + // sentinels. Both gamma signs and both exponent signs are covered. + let mut xs: Vec<$ty> = vec![ + -3.5, -2.5, -1.25, -0.5, 0.125, 0.5, 0.75, 1.0, 1.5, 2.0, 3.0, 4.5, 6.0, 8.0, 10.0, + 0.33, + ]; + let mut exponents = vec![-12, -4, -1, 0, 1, 4, 8, 24, -24, 100, -100, 3, 2, -3, 5, -5]; + // Width-specific subnormal inputs and scaling overflow/underflow. + for (x, exponent) in $extra { + xs.push(x); + exponents.push(exponent); + } + let n = xs.len(); + let inputs = DeviceBuffer::from_host(&stream, &xs)?; + let powers = DeviceBuffer::from_host(&stream, &exponents)?; + let mut gamma = DeviceBuffer::<$ty>::zeroed(&stream, n)?; + let mut std_gamma = DeviceBuffer::<$ty>::zeroed(&stream, n)?; + let mut logarithm = DeviceBuffer::::zeroed(&stream, n)?; + let mut scaled = DeviceBuffer::<$ty>::zeroed(&stream, n)?; + let mut alias = DeviceBuffer::<$ty>::zeroed(&stream, n)?; + let mut control = DeviceBuffer::<$ty>::zeroed(&stream, n)?; + // SAFETY: every buffer has n elements; the kernel bounds-checks i. + unsafe { + module.$kernel( + &stream, + LaunchConfig::for_num_elems(n as u32), + &inputs, + &powers, + &mut gamma, + &mut std_gamma, + &mut logarithm, + &mut scaled, + &mut alias, + &mut control, + )?; + } + let gamma = gamma.to_host_vec(&stream)?; + let std_gamma = std_gamma.to_host_vec(&stream)?; + let logarithm = logarithm.to_host_vec(&stream)?; + let scaled = scaled.to_host_vec(&stream)?; + let alias = alias.to_host_vec(&stream)?; + let control = control.to_host_vec(&stream)?; + for (i, &x) in xs.iter().enumerate() { + let expected_gamma = libm::$gamma(x); + let expected_logarithm = libm::$ilogb(x); + let expected_scaled = libm::$ldexp(x, exponents[i]); + for (name, valid) in [ + ( + "libm gamma", + close(gamma[i] as f64, expected_gamma as f64, $rtol), + ), + ( + "std gamma", + close(std_gamma[i] as f64, expected_gamma as f64, $rtol), + ), + ("ilogb", logarithm[i] == expected_logarithm), + ("ldexp", scaled[i].to_bits() == expected_scaled.to_bits()), + ("scalbn", alias[i].to_bits() == expected_scaled.to_bits()), + ("control", control[i].to_bits() == (x + 1000.0).to_bits()), + ] { + checks += 1; + if !valid { + failures += 1; + eprintln!( + "{} {name}[{i}]: x={x:?}, exponent={}", + stringify!($ty), + exponents[i] + ); + } + } + } + }}; + } + check_width!( + f32, + special_f32, + tgammaf, + ilogbf, + ldexpf, + 4e-6, + [ + (f32::from_bits(1), 149), + (f32::MIN_POSITIVE, -1), + (1.0, -149), + (-1.0, -150), + (1.0, 128), + (-1.0, 128), + ] + ); + check_width!( + f64, + special_f64, + tgamma, + ilogb, + ldexp, + 2e-13, + [ + (f64::from_bits(1), 1074), + (f64::MIN_POSITIVE, -1), + (1.0, -1074), + (-1.0, -1075), + (1.0, 1024), + (-1.0, 1024), + ] + ); + if failures != 0 { + return Err(format!("FAILED: {failures} of {checks} checks failed").into()); + } + println!("SUCCESS: math_special passed {checks} checks across f32 and f64"); + Ok(()) +} diff --git a/cuda-oxide/crates/rustc-codegen-cuda/src/collector.rs b/cuda-oxide/crates/rustc-codegen-cuda/src/collector.rs index 078099ebaf..239f34060b 100644 --- a/cuda-oxide/crates/rustc-codegen-cuda/src/collector.rs +++ b/cuda-oxide/crates/rustc-codegen-cuda/src/collector.rs @@ -776,6 +776,8 @@ fn is_intrinsic_lowered_cmath_shim(fn_path: &str) -> bool { | "std::sys::cmath::log1p" | "std::sys::cmath::hypotf" | "std::sys::cmath::hypot" + | "std::sys::cmath::tgammaf" + | "std::sys::cmath::tgamma" ) } @@ -795,9 +797,9 @@ fn is_intrinsic_lowered_cmath_shim(fn_path: &str) -> bool { /// /// Nothing in these wrappers touches the OS or the heap, so collect them like /// any other reachable pure function. Every stable method's shim is in -/// `is_intrinsic_lowered_cmath_shim`; the unstable `gamma`/`erf` family and -/// `f128` still stop one level deeper, at their shim, with the guard naming -/// that shim. +/// `is_intrinsic_lowered_cmath_shim`; `gamma` is also intercepted for f32/f64. +/// Unsupported methods such as `erf`, and f128 shims, still stop one level +/// deeper with the guard naming that shim. fn is_std_float_inherent_method(fn_path: &str) -> bool { ["f16", "f32", "f64", "f128"] .iter() @@ -3212,6 +3214,26 @@ mod tests { }; use rustc_middle::mir::BasicBlock; + #[test] + fn special_math_cmath_shims_are_an_exact_allowlist() { + for path in ["std::sys::cmath::tgammaf", "std::sys::cmath::tgamma"] { + assert!(super::is_intrinsic_lowered_cmath_shim(path), "{path}"); + } + for path in [ + "user::std::sys::cmath::tgamma", + "std::sys::cmath::tgammaf128", + "std::sys::cmath::lgamma_r", + "std::sys::cmath::frexp", + "std::sys::cmath::erf", + "std::io::stdio::_print", + ] { + assert!(!super::is_intrinsic_lowered_cmath_shim(path), "{path}"); + } + assert!(super::is_intrinsic_lowered_cmath_shim( + "std::sys::cmath::hypot" + )); + } + #[test] fn std_float_wrappers_are_collected_not_forbidden() { for ty in ["f16", "f32", "f64", "f128"] { From fb37561679b28f5820ec1646c49016dc50c6fb60 Mon Sep 17 00:00:00 2001 From: Raul Estrada Date: Sun, 25 Oct 2026 17:53:48 -0600 Subject: [PATCH 2/2] feat: add libdevice frexp tuple adapter Signed-off-by: Raul Estrada --- .../crates/dialect-mir/src/rust_intrinsics.rs | 9 + .../terminator/intrinsics/float_math.rs | 46 +++- .../crates/mir-lower/src/convert/ops/call.rs | 185 ++++++++++++- .../tests/lowering_test/math_conversions.rs | 259 ++++++++++++++++++ .../examples/math_special/README.md | 94 +++---- .../examples/math_special/src/main.rs | 245 ++++++++++++++++- 6 files changed, 785 insertions(+), 53 deletions(-) diff --git a/cuda-oxide/crates/dialect-mir/src/rust_intrinsics.rs b/cuda-oxide/crates/dialect-mir/src/rust_intrinsics.rs index 4d4b5387ea..9b640c64ea 100644 --- a/cuda-oxide/crates/dialect-mir/src/rust_intrinsics.rs +++ b/cuda-oxide/crates/dialect-mir/src/rust_intrinsics.rs @@ -224,6 +224,11 @@ pub const CALLEE_LDEXP_F32: &str = placeholder!("ldexpf32"); /// Placeholder call used for `libm::ldexp`. pub const CALLEE_LDEXP_F64: &str = placeholder!("ldexpf64"); +/// Placeholder call used for `libm::frexpf`, returning `(f32, i32)`. +pub const CALLEE_FREXP_F32: &str = placeholder!("frexpf32"); +/// Placeholder call used for `libm::frexp`, returning `(f64, i32)`. +pub const CALLEE_FREXP_F64: &str = placeholder!("frexpf64"); + /// Placeholder call used for `core::intrinsics::fadd_fast` (generic over float type). /// /// Lowered to `llvm.fadd` with explicit `fast` fast-math flags. The `f*_fast` intrinsics @@ -369,6 +374,8 @@ pub fn is_libdevice_backed_placeholder(callee: &str) -> bool { | CALLEE_ILOGB_F64 | CALLEE_LDEXP_F32 | CALLEE_LDEXP_F64 + | CALLEE_FREXP_F32 + | CALLEE_FREXP_F64 ) } @@ -477,6 +484,8 @@ mod tests { CALLEE_ILOGB_F64, CALLEE_LDEXP_F32, CALLEE_LDEXP_F64, + CALLEE_FREXP_F32, + CALLEE_FREXP_F64, ]; /// Every placeholder that is libdevice-backed only under the libNVVM diff --git a/cuda-oxide/crates/mir-importer/src/translator/terminator/intrinsics/float_math.rs b/cuda-oxide/crates/mir-importer/src/translator/terminator/intrinsics/float_math.rs index 3eb562bed4..92bbb5cef0 100644 --- a/cuda-oxide/crates/mir-importer/src/translator/terminator/intrinsics/float_math.rs +++ b/cuda-oxide/crates/mir-importer/src/translator/terminator/intrinsics/float_math.rs @@ -173,6 +173,10 @@ pub enum RustFloatMathIntrinsic { LdexpF32, /// `libm::ldexp`. LdexpF64, + /// `libm::frexpf`, returning `(f32, i32)`. + FrexpF32, + /// `libm::frexp`, returning `(f64, i32)`. + FrexpF64, /// Generic `core::intrinsics::fadd_fast` (lowered to `llvm.fadd` + fast-math). FaddFast, /// Generic `core::intrinsics::fsub_fast` (lowered to `llvm.fsub` + fast-math). @@ -448,6 +452,8 @@ impl RustFloatMathIntrinsic { "ilogb" => Some(Self::IlogbF64), "ldexpf" | "scalbnf" => Some(Self::LdexpF32), "ldexp" | "scalbn" => Some(Self::LdexpF64), + "frexpf" => Some(Self::FrexpF32), + "frexp" => Some(Self::FrexpF64), _ => None, } } @@ -549,6 +555,8 @@ impl RustFloatMathIntrinsic { Self::IlogbF64 => rust_intrinsics::CALLEE_ILOGB_F64, Self::LdexpF32 => rust_intrinsics::CALLEE_LDEXP_F32, Self::LdexpF64 => rust_intrinsics::CALLEE_LDEXP_F64, + Self::FrexpF32 => rust_intrinsics::CALLEE_FREXP_F32, + Self::FrexpF64 => rust_intrinsics::CALLEE_FREXP_F64, Self::FaddFast => rust_intrinsics::CALLEE_FADD_FAST, Self::FsubFast => rust_intrinsics::CALLEE_FSUB_FAST, Self::FmulFast => rust_intrinsics::CALLEE_FMUL_FAST, @@ -568,8 +576,8 @@ pub fn is_libm_path(name: &str) -> bool { } /// 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. +/// `f32::sin_cos`). [`emit_sincos`] handles these separately by emitting +/// two scalar placeholders and constructing their tuple in the importer. /// Returns `Some(is_f64)` when `name` is a libm sincos function. pub fn libm_sincos_is_f64(name: &str) -> Option { if !is_libm_path(name) { @@ -762,6 +770,38 @@ mod tests { use super::*; use dialect_mir::rust_intrinsics; + #[test] + fn frexp_paths_resolve_only_libm_entry_points() { + use RustFloatMathIntrinsic as I; + for (name, variant, placeholder) in [ + ("frexpf", I::FrexpF32, rust_intrinsics::CALLEE_FREXP_F32), + ("frexp", I::FrexpF64, rust_intrinsics::CALLEE_FREXP_F64), + ] { + for path in [ + format!("libm::{name}"), + format!("libm::math::{name}::{name}"), + ] { + assert_eq!(I::from_core_path(&path), Some(variant), "{path}"); + assert_eq!(variant.placeholder_callee(), placeholder); + assert!(rust_intrinsics::is_libdevice_backed_placeholder( + placeholder + )); + } + for root in [ + "user::libm", + "libm_lookalike", + "std::sys::cmath", + "core::intrinsics", + ] { + let path = format!("{root}::{name}"); + assert_eq!(I::from_core_path(&path), None, "{path}"); + } + } + assert_eq!(I::from_core_path("libm::sincos"), None); + assert_eq!(libm_sincos_is_f64("libm::sincos"), Some(true)); + assert_eq!(I::from_core_path("libm::ldexp"), Some(I::LdexpF64)); + } + #[test] fn special_math_paths_resolve_to_typed_placeholders() { use RustFloatMathIntrinsic as I; @@ -806,7 +846,7 @@ mod tests { Some(I::PowiF32) ); assert_eq!(libm_sincos_is_f64("libm::sincosf"), Some(false)); - for name in ["frexp", "modf", "remquo", "lgamma_r", "tgammaf128"] { + for name in ["modf", "remquo", "lgamma_r", "tgammaf128"] { assert_eq!(I::from_core_path(&format!("libm::{name}")), None); } } diff --git a/cuda-oxide/crates/mir-lower/src/convert/ops/call.rs b/cuda-oxide/crates/mir-lower/src/convert/ops/call.rs index b18e43b12c..8e98b2031b 100644 --- a/cuda-oxide/crates/mir-lower/src/convert/ops/call.rs +++ b/cuda-oxide/crates/mir-lower/src/convert/ops/call.rs @@ -87,7 +87,7 @@ use pliron::builtin::type_interfaces::FunctionTypeInterface; use pliron::builtin::types::{FP32Type, FP64Type, IntegerType, Signedness}; use pliron::context::{Context, Ptr}; use pliron::irbuild::dialect_conversion::{DialectConversionRewriter, OperandsInfo}; -use pliron::irbuild::inserter::Inserter; +use pliron::irbuild::inserter::{Inserter, OpInsertionPoint}; use pliron::irbuild::rewriter::Rewriter; use pliron::linked_list::ContainsLinkedList; use pliron::op::Op; @@ -268,6 +268,8 @@ enum RustFloatMathIntrinsic { IlogbF64, LdexpF32, LdexpF64, + FrexpF32, + FrexpF64, FaddFast, FsubFast, FmulFast, @@ -356,6 +358,8 @@ impl RustFloatMathIntrinsic { rust_intrinsics::CALLEE_ILOGB_F64 => Some(Self::IlogbF64), rust_intrinsics::CALLEE_LDEXP_F32 => Some(Self::LdexpF32), rust_intrinsics::CALLEE_LDEXP_F64 => Some(Self::LdexpF64), + rust_intrinsics::CALLEE_FREXP_F32 => Some(Self::FrexpF32), + rust_intrinsics::CALLEE_FREXP_F64 => Some(Self::FrexpF64), rust_intrinsics::CALLEE_FADD_FAST => Some(Self::FaddFast), rust_intrinsics::CALLEE_FSUB_FAST => Some(Self::FsubFast), rust_intrinsics::CALLEE_FMUL_FAST => Some(Self::FmulFast), @@ -466,6 +470,8 @@ impl RustFloatMathIntrinsic { Self::IlogbF64 => Ok("__nv_ilogb"), Self::LdexpF32 => Ok("__nv_ldexpf"), Self::LdexpF64 => Ok("__nv_ldexp"), + Self::FrexpF32 => Ok("__nv_frexpf"), + Self::FrexpF64 => Ok("__nv_frexp"), Self::FaddFast | Self::FsubFast | Self::FmulFast | Self::FdivFast | Self::FremFast => { // The `f*_fast` intrinsics lower directly to LLVM `fadd`/`fsub`/ // `fmul`/`fdiv`/`frem` with fast-math flags, not to a libdevice @@ -480,7 +486,7 @@ impl RustFloatMathIntrinsic { } } - /// Number of operands expected by the libdevice function. + /// Number of operands expected by the Rust placeholder. fn arg_count(self) -> usize { match self { Self::PowiF32 @@ -1317,6 +1323,13 @@ fn convert_rust_float_math_intrinsic( return lower_float_minmax_nsz(ctx, rewriter, op, &args, predicate); } + if matches!( + intrinsic, + RustFloatMathIntrinsic::FrexpF32 | RustFloatMathIntrinsic::FrexpF64 + ) { + return convert_rust_frexp(ctx, rewriter, op, intrinsic, args[0]); + } + let result_mir_ty = op.deref(ctx).get_result(0).get_type(ctx); let result_ty = convert_type(ctx, result_mir_ty).map_err(anyhow_to_pliron)?; @@ -1377,6 +1390,174 @@ fn convert_rust_float_math_intrinsic( Ok(()) } +/// Emit a libdevice scalar return plus one out-parameter. The caller supplies +/// the initial out value and handles its Rust tuple and semantic adjustments. +/// Keeping storage and ABI construction here lets modf/remquo share the path. +fn emit_libdevice_out_param_call( + ctx: &mut Context, + rewriter: &mut DialectConversionRewriter, + op: Ptr, + symbol: &str, + args: &[Value], + scalar_ty: TypeHandle, + initial_out: Value, +) -> Result<(Value, Value)> { + let loc = op.deref(ctx).loc().clone(); + let block = op.deref(ctx).get_parent_block().ok_or_else(|| { + pliron::input_error!( + loc.clone(), + "libdevice out-parameter call has no parent block" + ) + })?; + let parent = helpers::get_parent_func(ctx, block).map_err(anyhow_to_pliron)?; + let entry = parent + .deref(ctx) + .get_region(0) + .deref(ctx) + .iter(ctx) + .next() + .ok_or_else(|| { + pliron::input_error!( + loc.clone(), + "libdevice out-parameter call has no entry block" + ) + })?; + let first = entry.deref(ctx).iter(ctx).next().ok_or_else(|| { + pliron::input_error!( + loc.clone(), + "libdevice out-parameter call has an empty entry block" + ) + })?; + // A call inside a loop must reuse a static slot, rather than allocate + // additional stack storage on every iteration. Its size constant must + // dominate it too; initialization stays at the actual call site. + let one_attr = IntegerAttr::new( + IntegerType::get(ctx, 32, Signedness::Signless), + APInt::from_u64(1, NonZeroUsize::new(32).unwrap()), + ); + let one = llvm::ConstantOp::new(ctx, Box::new(one_attr)); + let saved_ip = rewriter.get_insertion_point(); + rewriter.set_insertion_point(OpInsertionPoint::BeforeOperation(first)); + rewriter.insert_operation(ctx, one.get_operation()); + let out_ty = initial_out.get_type(ctx); + let one_value = one.get_operation().deref(ctx).get_result(0); + let slot = llvm::AllocaOp::new(ctx, out_ty, one_value, 0); + rewriter.insert_operation(ctx, slot.get_operation()); + rewriter.set_insertion_point(saved_ip); + let ptr = slot.get_operation().deref(ctx).get_result(0); + let initialize = llvm::StoreOp::new(ctx, initial_out, ptr); + rewriter.insert_operation(ctx, initialize.get_operation()); + let mut call_args = args.to_vec(); + call_args.push(ptr); + let arg_types = call_args.iter().map(|arg| arg.get_type(ctx)).collect(); + let func_ty = llvm_types::FuncType::get(ctx, scalar_ty, arg_types, false); + helpers::ensure_intrinsic_declared(ctx, block, symbol, func_ty).map_err(|error| { + pliron::input_error!(loc.clone(), "Failed to declare {symbol}: {error}") + })?; + let symbol = symbol + .try_into() + .map_err(|error| pliron::input_error!(loc.clone(), "Invalid libdevice name: {error:?}"))?; + let call = llvm::CallOp::new(ctx, CallOpCallable::Direct(symbol), func_ty, call_args); + crate::convert::preserve_location(ctx, op, call.get_operation()); + rewriter.insert_operation(ctx, call.get_operation()); + let out = llvm::LoadOp::new(ctx, ptr, out_ty); + rewriter.insert_operation(ctx, out.get_operation()); + Ok(( + call.get_operation().deref(ctx).get_result(0), + out.get_operation().deref(ctx).get_result(0), + )) +} + +/// Adapt libdevice's `(F, i32*) -> F` ABI to libm's `(F) -> (F, i32)`. +fn convert_rust_frexp( + ctx: &mut Context, + rewriter: &mut DialectConversionRewriter, + op: Ptr, + intrinsic: RustFloatMathIntrinsic, + x: Value, +) -> Result<()> { + let loc = op.deref(ctx).loc().clone(); + let (float_ty, width, exponent_mask): (TypeHandle, u32, u64) = match intrinsic { + RustFloatMathIntrinsic::FrexpF32 => (FP32Type::get(ctx).into(), 32, 0x7f80_0000), + RustFloatMathIntrinsic::FrexpF64 => (FP64Type::get(ctx).into(), 64, 0x7ff0_0000_0000_0000), + _ => unreachable!("frexp converter requires a frexp placeholder"), + }; + let result_mir_ty = op.deref(ctx).get_result(0).get_type(ctx); + let layout = { + let ty = result_mir_ty.deref(ctx); + let Some(tuple) = ty.downcast_ref::() else { + return pliron::input_err!(loc, "Rust frexp intrinsic requires a (float, i32) result"); + }; + let fields = tuple.get_types(); + let i32_mir: TypeHandle = IntegerType::get(ctx, 32, Signedness::Signed).into(); + if fields != [float_ty, i32_mir] || x.get_type(ctx) != float_ty { + return pliron::input_err!(loc, "Rust frexp intrinsic signature mismatch"); + } + StructLayoutInfo::of_tuple(tuple) + }; + let map = build_struct_slot_map(ctx, &layout).map_err(anyhow_to_pliron)?; + let mut constant = |ctx: &mut Context, value: u64, bits: u32| { + let ty = IntegerType::get(ctx, bits, Signedness::Signless); + let attr = IntegerAttr::new( + ty, + APInt::from_u64(value, NonZeroUsize::new(bits as usize).unwrap()), + ); + let constant = llvm::ConstantOp::new(ctx, Box::new(attr)); + rewriter.insert_operation(ctx, constant.get_operation()); + constant.get_operation().deref(ctx).get_result(0) + }; + let zero = constant(ctx, 0, 32); + let mask = constant(ctx, exponent_mask, width); + let symbol = intrinsic.libdevice_name(ctx, float_ty, loc.clone())?; + let (fraction, exponent) = + emit_libdevice_out_param_call(ctx, rewriter, op, symbol, &[x], float_ty, zero)?; + // libm returns (x, 0) for infinities and NaNs; libdevice leaves the + // exponent unspecified. Bit tests preserve NaN payloads without floating + // comparisons or fast-math assumptions. The select preserves x itself. + let bits_ty = IntegerType::get(ctx, width, Signedness::Signless).into(); + let bits = llvm::BitcastOp::new(ctx, x, bits_ty); + rewriter.insert_operation(ctx, bits.get_operation()); + let bits_value = bits.get_operation().deref(ctx).get_result(0); + let masked = llvm::AndOp::new(ctx, bits_value, mask); + rewriter.insert_operation(ctx, masked.get_operation()); + let masked_value = masked.get_operation().deref(ctx).get_result(0); + let non_finite = llvm::ICmpOp::new( + ctx, + llvm_export::attributes::ICmpPredicateAttr::EQ, + masked_value, + mask, + ); + rewriter.insert_operation(ctx, non_finite.get_operation()); + let predicate = non_finite.get_operation().deref(ctx).get_result(0); + let fraction = llvm::SelectOp::new(ctx, predicate, x, fraction); + rewriter.insert_operation(ctx, fraction.get_operation()); + let exponent = llvm::SelectOp::new(ctx, predicate, zero, exponent); + rewriter.insert_operation(ctx, exponent.get_operation()); + let fields = [ + fraction.get_operation().deref(ctx).get_result(0), + exponent.get_operation().deref(ctx).get_result(0), + ]; + let undef = llvm::UndefOp::new(ctx, map.llvm_struct_ty); + rewriter.insert_operation(ctx, undef.get_operation()); + let mut value = undef.get_operation().deref(ctx).get_result(0); + let mut last = undef.get_operation(); + for (field, operand) in fields.into_iter().enumerate() { + let slot = map.decl_to_llvm[field].ok_or_else(|| { + pliron::input_error!( + loc.clone(), + "Rust frexp result field has no LLVM storage slot" + ) + })?; + let insert = llvm::InsertValueOp::new(ctx, value, operand, vec![slot]); + rewriter.insert_operation(ctx, insert.get_operation()); + value = insert.get_operation().deref(ctx).get_result(0); + last = insert.get_operation(); + } + crate::convert::preserve_location(ctx, op, last); + rewriter.replace_operation(ctx, op, last); + Ok(()) +} + /// Resolve the callee symbol a float-math intrinsic lowers to under the /// active intrinsic backend. /// diff --git a/cuda-oxide/crates/mir-lower/tests/lowering_test/math_conversions.rs b/cuda-oxide/crates/mir-lower/tests/lowering_test/math_conversions.rs index 940af0bf53..0c7e406400 100644 --- a/cuda-oxide/crates/mir-lower/tests/lowering_test/math_conversions.rs +++ b/cuda-oxide/crates/mir-lower/tests/lowering_test/math_conversions.rs @@ -15,6 +15,265 @@ use pliron::operation::Operation; use crate::common::{append_return, build_test_kernel, lowered_kernel_body, make_test_ctx}; +#[test] +fn test_frexp_out_parameter_lowering_on_both_backends() -> Result<(), anyhow::Error> { + use dialect_mir::rust_intrinsics as ri; + use dialect_mir::types::MirTupleType; + use pliron::basic_block::BasicBlock; + use pliron::builtin::attributes::StringAttr; + use pliron::builtin::types::{FP32Type, FP64Type, IntegerType, Signedness}; + use pliron::r#type::{TypeHandle, Typed}; + + for backend in [ + mir_lower::IntrinsicBackend::LlvmNvptx, + mir_lower::IntrinsicBackend::LibNvvm, + ] { + for wide in [false, true] { + // The reordered case also carries real padding. Tuple packing + // must use the declared-field map rather than hardcoded slots. + for reordered in [false, true] { + let mut ctx = make_test_ctx(); + let float_ty: TypeHandle = if wide { + FP64Type::get(&ctx).into() + } else { + FP32Type::get(&ctx).into() + }; + let i32_mir: TypeHandle = IntegerType::get(&ctx, 32, Signedness::Signed).into(); + let tuple_ty: TypeHandle = if reordered { + MirTupleType::get_with_layout( + &mut ctx, + vec![float_ty, i32_mir], + vec![1, 0], + vec![if wide { 8 } else { 4 }, 0], + if wide { 16 } else { 8 }, + if wide { 8 } else { 4 }, + ) + .into() + } else { + MirTupleType::get(&mut ctx, vec![float_ty, i32_mir]).into() + }; + let (module_ptr, entry) = build_test_kernel(&mut ctx, vec![float_ty]); + let x = entry.deref(&ctx).get_argument(0); + let parent = entry.deref(&ctx).get_parent_op(&ctx).unwrap(); + let region = parent.deref(&ctx).get_region(0); + let body = BasicBlock::new(&mut ctx, None, vec![]); + body.insert_at_back(region, &ctx); + Operation::new( + &mut ctx, + mir::MirGotoOp::get_concrete_op_info(), + vec![], + vec![], + vec![body], + 0, + ) + .insert_at_back(entry, &ctx); + let placeholder = if wide { + ri::CALLEE_FREXP_F64 + } else { + ri::CALLEE_FREXP_F32 + }; + let call = Operation::new( + &mut ctx, + mir::MirCallOp::get_concrete_op_info(), + vec![tuple_ty], + vec![x], + vec![], + 0, + ); + mir::MirCallOp::new(call) + .set_attr_callee(&ctx, StringAttr::new(placeholder.to_string())); + call.insert_at_back(body, &ctx); + append_return(&mut ctx, body); + mir_lower::lower_mir_to_llvm_with_options( + &mut ctx, + module_ptr, + mir_lower::LoweringOptions { + intrinsic_backend: backend, + ..Default::default() + }, + ) + .map_err(|error| anyhow::anyhow!("{error}"))?; + pliron::operation::verify_operation(module_ptr, &ctx) + .map_err(|error| anyhow::anyhow!("{error}"))?; + let module_block = module_ptr + .deref(&ctx) + .get_region(0) + .deref(&ctx) + .iter(&ctx) + .next() + .unwrap(); + let func = module_block + .deref(&ctx) + .iter(&ctx) + .find_map(|op| { + let func = Operation::get_op::(op, &ctx)?; + (func.get_symbol_name(&ctx).to_string() == "kernel_func").then_some(func) + }) + .unwrap(); + let blocks = func + .get_operation() + .deref(&ctx) + .get_region(0) + .deref(&ctx) + .iter(&ctx) + .collect::>(); + let operations = blocks + .iter() + .flat_map(|block| block.deref(&ctx).iter(&ctx)) + .collect::>(); + let allocas = operations + .iter() + .filter(|&&op| Operation::get_op::(op, &ctx).is_some()) + .copied() + .collect::>(); + assert_eq!(allocas.len(), 1); + assert_eq!( + allocas[0].deref(&ctx).get_parent_block(), + Some(blocks[0]), + "out slot must be static even for a call outside the entry block" + ); + let calls = operations + .iter() + .filter_map(|&op| Operation::get_op::(op, &ctx)) + .collect::>(); + assert_eq!(calls.len(), 1); + let call = &calls[0]; + let CallOpCallable::Direct(symbol) = call.callee(&ctx) else { + panic!("expected a direct libdevice call") + }; + assert_eq!( + symbol.to_string(), + if wide { "__nv_frexp" } else { "__nv_frexpf" } + ); + let call_op = call.get_operation().deref(&ctx); + assert_eq!(call_op.get_num_operands(), 2); + assert_eq!(call_op.get_result(0).get_type(&ctx), float_ty); + let out_ptr = call_op.get_operand(1); + assert_eq!(out_ptr, allocas[0].deref(&ctx).get_result(0)); + assert!( + operations + .iter() + .any(|&op| Operation::get_op::(op, &ctx).is_some() + && op.deref(&ctx).get_operand(1) == out_ptr) + ); + assert!( + operations + .iter() + .any(|&op| Operation::get_op::(op, &ctx).is_some() + && op.deref(&ctx).get_operand(0) == out_ptr) + ); + assert_eq!( + operations + .iter() + .filter(|&&op| Operation::get_op::(op, &ctx).is_some()) + .count(), + 2, + "non-finite fraction and exponent must both be normalized" + ); + let module = Operation::get_op::(module_ptr, &ctx).unwrap(); + let ir = llvm_export::export::export_module_to_string(&ctx, &module) + .expect("frexp module must export"); + assert!( + ir.contains(if wide { + "declare double @__nv_frexp(double, ptr)" + } else { + "declare float @__nv_frexpf(float, ptr)" + }), + "{ir}" + ); + assert!( + ir.contains(if wide { "and i64" } else { "and i32" }), + "{ir}" + ); + assert!(!ir.contains(ri::PLACEHOLDER_PREFIX), "{ir}"); + } + } + } + Ok(()) +} + +#[test] +fn test_frexp_rejects_malformed_calls() { + use dialect_mir::rust_intrinsics as ri; + use dialect_mir::types::MirTupleType; + use pliron::builtin::attributes::StringAttr; + use pliron::builtin::types::{FP32Type, FP64Type, IntegerType, Signedness}; + use pliron::r#type::TypeHandle; + + for backend in [ + mir_lower::IntrinsicBackend::LlvmNvptx, + mir_lower::IntrinsicBackend::LibNvvm, + ] { + for wide in [false, true] { + for invalid in 0..7 { + let mut ctx = make_test_ctx(); + let float_ty: TypeHandle = if wide { + FP64Type::get(&ctx).into() + } else { + FP32Type::get(&ctx).into() + }; + let other_ty: TypeHandle = if wide { + FP32Type::get(&ctx).into() + } else { + FP64Type::get(&ctx).into() + }; + let i32_ty: TypeHandle = IntegerType::get(&ctx, 32, Signedness::Signed).into(); + let i64_ty: TypeHandle = IntegerType::get(&ctx, 64, Signedness::Signed).into(); + let (module_ptr, entry) = build_test_kernel(&mut ctx, vec![float_ty, other_ty]); + let x = entry.deref(&ctx).get_argument(0); + let other = entry.deref(&ctx).get_argument(1); + let fields = match invalid { + 3 => vec![float_ty, i64_ty], + 4 => vec![other_ty, i32_ty], + 5 => vec![float_ty], + _ => vec![float_ty, i32_ty], + }; + let tuple: TypeHandle = MirTupleType::get(&mut ctx, fields).into(); + let result = if invalid == 2 { float_ty } else { tuple }; + let args = match invalid { + 0 => vec![], + 1 => vec![x, x], + 6 => vec![other], + _ => vec![x], + }; + let placeholder = if wide { + ri::CALLEE_FREXP_F64 + } else { + ri::CALLEE_FREXP_F32 + }; + let call = Operation::new( + &mut ctx, + mir::MirCallOp::get_concrete_op_info(), + vec![result], + args, + vec![], + 0, + ); + mir::MirCallOp::new(call) + .set_attr_callee(&ctx, StringAttr::new(placeholder.to_string())); + call.insert_at_back(entry, &ctx); + append_return(&mut ctx, entry); + let error = mir_lower::lower_mir_to_llvm_with_options( + &mut ctx, + module_ptr, + mir_lower::LoweringOptions { + intrinsic_backend: backend, + ..Default::default() + }, + ) + .expect_err("malformed frexp ABI must fail") + .to_string(); + let expected = match invalid { + 0 | 1 => "requires 1 operand(s)", + 2 => "requires a (float, i32) result", + _ => "signature mismatch", + }; + assert!(error.contains(expected), "case {invalid}: {error}"); + } + } + } +} + #[test] fn test_special_math_mixed_signatures_on_both_backends() -> Result<(), anyhow::Error> { use dialect_mir::rust_intrinsics as ri; diff --git a/cuda-oxide/crates/rustc-codegen-cuda/examples/math_special/README.md b/cuda-oxide/crates/rustc-codegen-cuda/examples/math_special/README.md index a21149f0ab..9fe14eaf72 100644 --- a/cuda-oxide/crates/rustc-codegen-cuda/examples/math_special/README.md +++ b/cuda-oxide/crates/rustc-codegen-cuda/examples/math_special/README.md @@ -1,42 +1,49 @@ -# Special scalar math - -Exercises the first scalar additions for issue #1224: - -| Rust call | f32 libdevice symbol | f64 libdevice symbol | -| --- | --- | --- | -| `libm::tgamma{f}` and `f32/f64::gamma` | `__nv_tgammaf` | `__nv_tgamma` | -| `libm::ilogb{f}` | `__nv_ilogbf` | `__nv_ilogb` | -| `libm::ldexp{f}`, `libm::scalbn{f}` | `__nv_ldexpf` | `__nv_ldexp` | - -`ilogb` returns an `i32`; the exponent argument of `ldexp`/`scalbn` is -an `i32`. `float_gamma` is enabled to exercise the standard-library cmath -shim as well as the `libm` entry points. Use the repository's pinned toolchain -and its existing `RUSTC_BOOTSTRAP` configuration. - -Before these mappings are added, the standard gamma path is rejected by the -collector and the libm paths have no dedicated libdevice lowering. Capture the -baseline diagnostic on the unmodified tree; no local baseline/GPU run is claimed -by this source package. - -The example checks 264 results: 22 inputs, six checks per input, and two widths. -It compares gamma to host libm within a relative tolerance, compares integer -logarithms exactly, and compares scaled values bit for bit. The inputs include -negative non-integers, both exponent signs, subnormals, scaling overflow and -underflow. A user function with a libm-like module name must retain its own -body. `ilogb` inputs are finite and nonzero; its domain-error sentinels are -outside this example's comparison. - -The lowering tests independently verify all six libdevice signatures on both -backends, reject malformed calls, and retain `powi` as a mixed-signature control. -Importer tests check public/internal libm spellings, aliases, cmath gamma, -unrelated names, and functions deferred to later PRs. +# Special math + +The scalar tests cover `tgamma`, standard-library `gamma()`, `ilogb`, and +`ldexp`/`scalbn` for f32/f64. The frexp tests additionally exercise +`libm::frexpf(f32) -> (f32, i32)` and `libm::frexp(f64) -> (f64, i32)`. + +Libdevice returns a scalar fraction and writes an exponent through a pointer. +The lowering adapts this ABI to Rust's tuple using a function-local slot, +a libdevice call, a load and tuple construction. The slot lives in the function's +entry block so loop iterations reuse static storage. Tuple fields use the +existing layout map, including reordered fields and padding. + +For infinities and NaNs, libm returns `(x, 0)`, whereas libdevice leaves the +exponent unspecified. The adapter initializes the exponent slot and explicitly +selects the original input and zero exponent for non-finite inputs. Integer +bit tests preserve NaN payloads and do not introduce floating-point comparisons. + +References: + +- [libm frexp implementation](https://github.com/rust-lang/libm/blob/master/libm/src/math/frexp.rs) +- [libm frexpf implementation](https://github.com/rust-lang/libm/blob/master/libm/src/math/frexpf.rs) +- [NVIDIA libdevice frexp contract](https://docs.nvidia.com/cuda/libdevice-users-guide/libdevice_users_guide/function_device__functions__decls_8h_1addcff9bac6691e5d398a2f8e684ae441.html) + +## Coverage + +The example retains the 264 scalar checks and adds 512 frexp checks, for an +expected total of 776. Each width has 32 frexp inputs: finite values, signed +zeros, normal limits, subnormal limits, infinities and positive/negative quiet +and signaling NaNs with payloads. Four routes check both tuple components: + +- A device-function boundary returning the tuple. +- Assignment into a nested tuple field. +- A call inside a loop. +- An unrelated user function with a libm-like name, which must retain its body. + +The host compares both frexp components exactly, including fraction bits. +Lowering tests cover both intrinsic backends, an out-of-entry-block call, +reordered tuple layouts, and rejected arities and types. Importer tests cover +public/internal libm spellings and unrelated names. ## Validation -Run from `cuda-oxide/` after copying the files into the existing checkout: +Run from `cuda-oxide/` using the pinned toolchain: ```bash cargo oxide fmt @@ -45,26 +52,19 @@ git diff --check cargo test -p dialect-mir -p mir-importer -p mir-lower --all-targets cargo clippy -p dialect-mir -p mir-importer -p mir-lower --all-targets -- -D warnings (cd crates/rustc-codegen-cuda && cargo test --lib) -(cd crates/rustc-codegen-cuda && cargo clippy --all-targets -- -D warnings) (cd crates/rustc-codegen-cuda/examples/math_special && cargo clippy --locked --all-targets -- -D warnings) -cargo oxide build math_special cargo oxide run math_special cargo oxide run libm_math cargo oxide run math_hyperbolic -bash scripts/check-example-smoketest-contract.sh -bash scripts/sync-example-locks.sh --check +just check ``` -`math_special` must print `SUCCESS: math_special passed 264 checks across f32 and f64`. -GPU commands require a working CUDA toolkit, driver and device. The example is -a separate workspace; the parent workspace's tests do not build it. +Expected output from the extended example: -Before marking the PR ready, run the repository gate from the repository root: - -```bash -just -f cuda-oxide/Justfile check +```text +SUCCESS: math_special passed 776 checks across f32 and f64 ``` -This package has undergone static source and archive checks only. Rust, Cargo, -and CUDA were unavailable in the preparation environment; compile, clippy and -GPU validation are pending execution on a configured checkout. +The tuple-returning helpers are marked `#[inline(never)]` to exercise the +device-function ABI in the optimized run, together with libdevice linking. +`cargo oxide run` uses the release profile and does not accept `--profile`. diff --git a/cuda-oxide/crates/rustc-codegen-cuda/examples/math_special/src/main.rs b/cuda-oxide/crates/rustc-codegen-cuda/examples/math_special/src/main.rs index 89c688e14f..867691e554 100644 --- a/cuda-oxide/crates/rustc-codegen-cuda/examples/math_special/src/main.rs +++ b/cuda-oxide/crates/rustc-codegen-cuda/examples/math_special/src/main.rs @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -//! tgamma, ilogb, ldexp and scalbn in f32/f64 device code. +//! Scalar math and tuple-returning frexp in f32/f64 device code. //! Run with `cargo oxide run math_special`. #![feature(float_gamma)] // The width-generic host macro casts both f32 and f64 results to f64. @@ -24,6 +24,29 @@ mod libm_lookalike { pub fn tgamma(x: f64) -> f64 { x + 1000.0 } + + #[inline(never)] + pub fn frexpf(x: f32) -> (f32, i32) { + (x, 12345) + } + + #[inline(never)] + pub fn frexp(x: f64) -> (f64, i32) { + (x, 12345) + } +} + +// Exercise an ordinary device-function boundary with a tuple return. +mod frexp_helpers { + #[inline(never)] + pub fn split_f32(x: f32) -> (f32, i32) { + libm::frexpf(x) + } + + #[inline(never)] + pub fn split_f64(x: f64) -> (f64, i32) { + libm::frexp(x) + } } #[cuda_module] @@ -99,6 +122,108 @@ mod kernels { } } } + + #[kernel] + pub fn frexp_f32( + xs: &[f32], + mut direct_fraction: DisjointSlice, + mut direct_exponent: DisjointSlice, + mut projected_fraction: DisjointSlice, + mut projected_exponent: DisjointSlice, + mut loop_fraction: DisjointSlice, + mut loop_exponent: DisjointSlice, + mut control_fraction: DisjointSlice, + mut control_exponent: DisjointSlice, + ) { + let i = thread::index_1d().get(); + if i < xs.len() { + let x = xs[i]; + let direct = frexp_helpers::split_f32(x); + let mut projected = (17_u32, (x, 0_i32)); + projected.1 = libm::frexpf(projected.1.0); + let mut repeated = (0.0, 0_i32); + for step in 0..3 { + let input = if step == 1 { -x } else { x }; + repeated = libm::frexpf(input); + } + let control = libm_lookalike::frexpf(x); + if let Some(out) = direct_fraction.get_mut(thread::index_1d()) { + *out = direct.0; + } + if let Some(out) = direct_exponent.get_mut(thread::index_1d()) { + *out = direct.1; + } + if let Some(out) = projected_fraction.get_mut(thread::index_1d()) { + *out = projected.1.0; + } + if let Some(out) = projected_exponent.get_mut(thread::index_1d()) { + *out = projected.1.1; + } + if let Some(out) = loop_fraction.get_mut(thread::index_1d()) { + *out = repeated.0; + } + if let Some(out) = loop_exponent.get_mut(thread::index_1d()) { + *out = repeated.1; + } + if let Some(out) = control_fraction.get_mut(thread::index_1d()) { + *out = control.0; + } + if let Some(out) = control_exponent.get_mut(thread::index_1d()) { + *out = control.1; + } + } + } + + #[kernel] + pub fn frexp_f64( + xs: &[f64], + mut direct_fraction: DisjointSlice, + mut direct_exponent: DisjointSlice, + mut projected_fraction: DisjointSlice, + mut projected_exponent: DisjointSlice, + mut loop_fraction: DisjointSlice, + mut loop_exponent: DisjointSlice, + mut control_fraction: DisjointSlice, + mut control_exponent: DisjointSlice, + ) { + let i = thread::index_1d().get(); + if i < xs.len() { + let x = xs[i]; + let direct = frexp_helpers::split_f64(x); + let mut projected = (17_u32, (x, 0_i32)); + projected.1 = libm::frexp(projected.1.0); + let mut repeated = (0.0, 0_i32); + for step in 0..3 { + let input = if step == 1 { -x } else { x }; + repeated = libm::frexp(input); + } + let control = libm_lookalike::frexp(x); + if let Some(out) = direct_fraction.get_mut(thread::index_1d()) { + *out = direct.0; + } + if let Some(out) = direct_exponent.get_mut(thread::index_1d()) { + *out = direct.1; + } + if let Some(out) = projected_fraction.get_mut(thread::index_1d()) { + *out = projected.1.0; + } + if let Some(out) = projected_exponent.get_mut(thread::index_1d()) { + *out = projected.1.1; + } + if let Some(out) = loop_fraction.get_mut(thread::index_1d()) { + *out = repeated.0; + } + if let Some(out) = loop_exponent.get_mut(thread::index_1d()) { + *out = repeated.1; + } + if let Some(out) = control_fraction.get_mut(thread::index_1d()) { + *out = control.0; + } + if let Some(out) = control_exponent.get_mut(thread::index_1d()) { + *out = control.1; + } + } + } } fn close(got: f64, expected: f64, rtol: f64) -> bool { @@ -225,6 +350,124 @@ fn main() -> Result<(), Box> { (-1.0, 1024), ] ); + macro_rules! check_frexp_width { + ($ty:ty, $kernel:ident, $frexp:ident, $extra:expr) => {{ + let mut xs: Vec<$ty> = vec![ + 0.0, -0.0, 0.5, -0.5, 1.0, -1.0, 1.5, -1.5, 2.0, -2.0, 3.25, -3.25, 0.75, -0.75, + 42.0, -42.0, 0.125, -0.125, + ]; + xs.extend($extra); + let n = xs.len(); + let inputs = DeviceBuffer::from_host(&stream, &xs)?; + let mut direct_fraction = DeviceBuffer::<$ty>::zeroed(&stream, n)?; + let mut direct_exponent = DeviceBuffer::::zeroed(&stream, n)?; + let mut projected_fraction = DeviceBuffer::<$ty>::zeroed(&stream, n)?; + let mut projected_exponent = DeviceBuffer::::zeroed(&stream, n)?; + let mut loop_fraction = DeviceBuffer::<$ty>::zeroed(&stream, n)?; + let mut loop_exponent = DeviceBuffer::::zeroed(&stream, n)?; + let mut control_fraction = DeviceBuffer::<$ty>::zeroed(&stream, n)?; + let mut control_exponent = DeviceBuffer::::zeroed(&stream, n)?; + // SAFETY: every buffer contains n elements and each output is + // written only by its matching thread, with an input bounds check. + unsafe { + module.$kernel( + &stream, + LaunchConfig::for_num_elems(n as u32), + &inputs, + &mut direct_fraction, + &mut direct_exponent, + &mut projected_fraction, + &mut projected_exponent, + &mut loop_fraction, + &mut loop_exponent, + &mut control_fraction, + &mut control_exponent, + )?; + } + let fractions = [ + direct_fraction.to_host_vec(&stream)?, + projected_fraction.to_host_vec(&stream)?, + loop_fraction.to_host_vec(&stream)?, + control_fraction.to_host_vec(&stream)?, + ]; + let exponents = [ + direct_exponent.to_host_vec(&stream)?, + projected_exponent.to_host_vec(&stream)?, + loop_exponent.to_host_vec(&stream)?, + control_exponent.to_host_vec(&stream)?, + ]; + for (i, &x) in xs.iter().enumerate() { + let expected = libm::$frexp(x); + for (route, name) in ["tuple return", "projected tuple", "loop", "user control"] + .iter() + .enumerate() + { + let (fraction, exponent) = if route == 3 { (x, 12345) } else { expected }; + checks += 2; + if fractions[route][i].to_bits() != fraction.to_bits() { + failures += 1; + eprintln!( + "{} frexp {name}[{i}] fraction: got={:#x} expected={:#x}", + stringify!($ty), + fractions[route][i].to_bits(), + fraction.to_bits() + ); + } + if exponents[route][i] != exponent { + failures += 1; + eprintln!( + "{} frexp {name}[{i}] exponent: got={} expected={exponent}", + stringify!($ty), + exponents[route][i] + ); + } + } + } + }}; + } + check_frexp_width!( + f32, + frexp_f32, + frexpf, + [ + f32::MIN_POSITIVE, + -f32::MIN_POSITIVE, + f32::MAX, + -f32::MAX, + f32::from_bits(1), + f32::from_bits(0x8000_0001), + f32::from_bits(0x007f_ffff), + f32::from_bits(0x807f_ffff), + f32::INFINITY, + f32::NEG_INFINITY, + f32::from_bits(0x7fc1_2345), + f32::from_bits(0xffc1_2345), + f32::from_bits(0x7f81_2345), + f32::from_bits(0xff81_2345), + ] + ); + check_frexp_width!( + f64, + frexp_f64, + frexp, + [ + f64::MIN_POSITIVE, + -f64::MIN_POSITIVE, + f64::MAX, + -f64::MAX, + f64::from_bits(1), + f64::from_bits(0x8000_0000_0000_0001), + f64::from_bits(0x000f_ffff_ffff_ffff), + f64::from_bits(0x800f_ffff_ffff_ffff), + f64::INFINITY, + f64::NEG_INFINITY, + f64::from_bits(0x7ff8_0000_0001_2345), + f64::from_bits(0xfff8_0000_0001_2345), + f64::from_bits(0x7ff0_0000_0001_2345), + f64::from_bits(0xfff0_0000_0001_2345), + ] + ); + if failures != 0 { return Err(format!("FAILED: {failures} of {checks} checks failed").into()); }