Files
rustytorch/crates/core/rtx-backend-cuda/src/kernels.rs
T
2026-03-04 00:08:42 +00:00

324 lines
10 KiB
Rust

//! CUDA kernel management for rtx-backend-cuda.
//!
//! This module provides kernel loading, caching, and launch utilities
//! for GPU-accelerated tensor operations.
#[cfg(feature = "cuda")]
use cudarc::driver::{CudaFunction, CudaModule, LaunchConfig};
#[cfg(feature = "cuda")]
use cudarc::nvrtc::{CompileOptions, Ptx, compile_ptx_with_opts};
use parking_lot::RwLock;
use std::collections::HashMap;
use std::sync::Arc;
use crate::{CudaDevice, CudaError, CudaResult};
/// CUDA source for element-wise kernels (embedded for runtime compilation).
pub const ELEMENT_WISE_CUDA_SRC: &str =
include_str!("../../rtx-tensor/src/cuda_kernels/element_wise.cu");
/// CUDA source preamble with missing defines for NVRTC.
const CUDA_PREAMBLE: &str = r#"
#ifndef INFINITY
#define INFINITY __int_as_float(0x7f800000)
#endif
#ifndef NAN
#define NAN __int_as_float(0x7fffffff)
#endif
"#;
#[cfg(feature = "cuda")]
/// Compile CUDA source to PTX at runtime using NVRTC.
/// This ensures PTX compatibility with the current driver.
fn compile_cuda_to_ptx(cuda_src: &str) -> CudaResult<Ptx> {
// Add preamble with missing definitions
let full_source = format!("{}\n{}", CUDA_PREAMBLE, cuda_src);
// Use NVRTC with architecture targeting current GPU
// Add CUDA include paths for standard headers
let opts = CompileOptions {
arch: Some("sm_86"), // Target RTX 3050/3090 (Ampere)
include_paths: vec![
"/usr/local/cuda/include".to_string(),
"/usr/local/cuda-13.1/include".to_string(),
],
..Default::default()
};
compile_ptx_with_opts(&full_source, opts)
.map_err(|e| CudaError::KernelCompilation(format!("NVRTC compilation failed: {:?}", e)))
}
#[cfg(feature = "cuda")]
/// Lazily compiled PTX for element-wise kernels.
static COMPILED_ELEMENT_WISE_PTX: once_cell::sync::Lazy<Result<Ptx, String>> =
once_cell::sync::Lazy::new(|| {
compile_cuda_to_ptx(ELEMENT_WISE_CUDA_SRC).map_err(|e| format!("{:?}", e))
});
#[cfg(feature = "cuda")]
/// Get the element-wise PTX (compiled at runtime).
pub fn get_element_wise_ptx() -> CudaResult<&'static Ptx> {
COMPILED_ELEMENT_WISE_PTX
.as_ref()
.map_err(|e| CudaError::KernelCompilation(e.clone()))
}
#[cfg(feature = "cuda")]
/// Global module cache: (device_index, module_id) -> CudaModule
static MODULE_CACHE: once_cell::sync::Lazy<
RwLock<HashMap<(usize, &'static str), Arc<CudaModule>>>,
> = once_cell::sync::Lazy::new(|| RwLock::new(HashMap::new()));
#[cfg(feature = "cuda")]
/// Get or load the element-wise CUDA module (uses runtime-compiled PTX).
fn get_or_load_element_wise_module(device: &CudaDevice) -> CudaResult<Arc<CudaModule>> {
let cache_key = (device.index, "element_wise");
// Check cache first (read lock)
{
let cache = MODULE_CACHE.read();
if let Some(module) = cache.get(&cache_key) {
return Ok(module.clone());
}
}
// Load and cache module (write lock)
let mut cache = MODULE_CACHE.write();
// Double-check after acquiring write lock
if let Some(module) = cache.get(&cache_key) {
return Ok(module.clone());
}
// Get runtime-compiled PTX
let ptx = get_element_wise_ptx()?;
// Load PTX module
let module = device
.context
.load_module(ptx.clone())
.map_err(|e| CudaError::KernelCompilation(format!("Failed to load PTX module: {:?}", e)))?;
cache.insert(cache_key, module.clone());
Ok(module)
}
#[cfg(feature = "cuda")]
/// Get a CUDA kernel function from the element-wise module.
///
/// This function caches modules per device to avoid redundant PTX compilation.
/// The PTX is compiled at runtime using NVRTC for driver compatibility.
pub fn get_kernel(
device: &CudaDevice,
kernel_name: &str,
_ptx: &str, // Ignored - we use runtime-compiled PTX
) -> CudaResult<CudaFunction> {
let module = get_or_load_element_wise_module(device)?;
module.load_function(kernel_name).map_err(|e| {
CudaError::KernelCompilation(format!(
"Failed to load kernel function '{}': {:?}",
kernel_name, e
))
})
}
/// Constant for backwards compatibility (not used, we compile at runtime).
pub const ELEMENT_WISE_PTX: &str = "";
// Stub implementations for non-CUDA builds
#[cfg(not(feature = "cuda"))]
/// Stub launch config type.
#[derive(Clone, Debug)]
pub struct LaunchConfig {
pub grid_dim: (u32, u32, u32),
pub block_dim: (u32, u32, u32),
pub shared_mem_bytes: u32,
}
#[cfg(not(feature = "cuda"))]
/// Stub kernel function type.
pub struct CudaFunction;
#[cfg(not(feature = "cuda"))]
pub fn get_kernel(
_device: &crate::CudaDevice,
_kernel_name: &str,
_ptx: &str,
) -> crate::CudaResult<CudaFunction> {
Err(crate::CudaError::KernelCompilation(
"CUDA support not compiled".to_string(),
))
}
#[cfg(not(feature = "cuda"))]
pub fn element_wise_config(_numel: usize) -> LaunchConfig {
LaunchConfig {
grid_dim: (0, 0, 0),
block_dim: (0, 0, 0),
shared_mem_bytes: 0,
}
}
#[cfg(not(feature = "cuda"))]
pub fn reduction_config(_numel: usize) -> LaunchConfig {
LaunchConfig {
grid_dim: (0, 0, 0),
block_dim: (0, 0, 0),
shared_mem_bytes: 0,
}
}
#[cfg(not(feature = "cuda"))]
pub fn row_wise_config(_num_rows: usize, _row_size: usize) -> LaunchConfig {
LaunchConfig {
grid_dim: (0, 0, 0),
block_dim: (0, 0, 0),
shared_mem_bytes: 0,
}
}
#[cfg(not(feature = "cuda"))]
pub fn transpose_2d_config(_rows: usize, _cols: usize) -> LaunchConfig {
LaunchConfig {
grid_dim: (0, 0, 0),
block_dim: (0, 0, 0),
shared_mem_bytes: 0,
}
}
#[cfg(not(feature = "cuda"))]
pub fn dim_reduction_config(
_outer_size: usize,
_inner_size: usize,
_reduce_size: usize,
) -> LaunchConfig {
LaunchConfig {
grid_dim: (0, 0, 0),
block_dim: (0, 0, 0),
shared_mem_bytes: 0,
}
}
#[cfg(feature = "cuda")]
/// Standard launch configuration for element-wise operations.
///
/// Uses 256 threads per block (optimal for memory coalescing on modern GPUs).
pub fn element_wise_config(numel: usize) -> LaunchConfig {
let block_size = 256u32;
let grid_size = ((numel as u32) + block_size - 1) / block_size;
LaunchConfig {
grid_dim: (grid_size, 1, 1),
block_dim: (block_size, 1, 1),
shared_mem_bytes: 0,
}
}
#[cfg(feature = "cuda")]
/// Launch configuration for reduction operations with shared memory.
pub fn reduction_config(numel: usize) -> LaunchConfig {
let block_size = 256u32;
let grid_size = ((numel as u32) + block_size - 1) / block_size;
// Limit grid size for reduction (we'll do multi-pass for large tensors)
let grid_size = grid_size.min(1024);
LaunchConfig {
grid_dim: (grid_size, 1, 1),
block_dim: (block_size, 1, 1),
shared_mem_bytes: block_size * std::mem::size_of::<f32>() as u32,
}
}
#[cfg(feature = "cuda")]
/// Launch configuration for row-wise operations (softmax, layer_norm, rms_norm).
///
/// Each row is processed by one block using shared memory for reductions.
/// - `num_rows`: Number of rows to process (grid dimension)
/// - `row_size`: Size of each row (used for shared memory allocation)
pub fn row_wise_config(num_rows: usize, row_size: usize) -> LaunchConfig {
// Block size must be a power of 2 for the parallel reduction to work correctly.
// Use the next power of 2 >= row_size, capped at 256.
let block_size = next_power_of_2(row_size as u32).min(256);
// Shared memory: floats for parallel reductions
let shared_mem = block_size * std::mem::size_of::<f32>() as u32;
LaunchConfig {
grid_dim: (num_rows as u32, 1, 1),
block_dim: (block_size, 1, 1),
shared_mem_bytes: shared_mem,
}
}
#[cfg(feature = "cuda")]
/// Round up to the next power of 2.
fn next_power_of_2(n: u32) -> u32 {
if n == 0 {
return 1;
}
let mut v = n - 1;
v |= v >> 1;
v |= v >> 2;
v |= v >> 4;
v |= v >> 8;
v |= v >> 16;
v + 1
}
#[cfg(feature = "cuda")]
/// Launch configuration for 2D matrix transpose using 32x32 tiles.
///
/// Uses shared memory for coalesced access pattern.
/// - `rows`: Number of rows in input matrix
/// - `cols`: Number of columns in input matrix
pub fn transpose_2d_config(rows: usize, cols: usize) -> LaunchConfig {
let tile_size = 32u32;
let grid_x = ((cols as u32) + tile_size - 1) / tile_size;
let grid_y = ((rows as u32) + tile_size - 1) / tile_size;
LaunchConfig {
grid_dim: (grid_x, grid_y, 1),
block_dim: (tile_size, tile_size, 1),
shared_mem_bytes: 0, // Shared memory is statically allocated in kernel
}
}
#[cfg(feature = "cuda")]
/// Launch configuration for dimensional reduction (sum_dim, max_dim, min_dim).
///
/// Each output element is computed by one block.
/// - `outer_size`: Product of dimensions before the reduction dimension
/// - `inner_size`: Product of dimensions after the reduction dimension
/// - `reduce_size`: Size of the dimension being reduced
pub fn dim_reduction_config(
outer_size: usize,
inner_size: usize,
reduce_size: usize,
) -> LaunchConfig {
// Block size must be a power of 2 for the parallel reduction to work correctly.
let block_size = next_power_of_2(reduce_size as u32).min(256).max(32);
let shared_mem = block_size * std::mem::size_of::<f32>() as u32;
LaunchConfig {
grid_dim: (outer_size as u32, inner_size as u32, 1),
block_dim: (block_size, 1, 1),
shared_mem_bytes: shared_mem,
}
}
#[cfg(all(test, feature = "cuda"))]
mod tests {
use super::*;
#[test]
fn test_element_wise_config() {
let config = element_wise_config(1000);
assert_eq!(config.block_dim.0, 256);
assert_eq!(config.grid_dim.0, 4); // ceil(1000/256) = 4
}
#[test]
fn test_reduction_config() {
let config = reduction_config(1_000_000);
assert_eq!(config.block_dim.0, 256);
assert!(config.grid_dim.0 <= 1024); // Capped at 1024
assert!(config.shared_mem_bytes > 0);
}
}