Files
rustytorch/examples/pinn_mre_helmholtz/src/cuda_fused.rs
T
2026-03-04 00:08:42 +00:00

289 lines
9.5 KiB
Rust

//! Fused CUDA kernels for PINN acceleration
//!
//! This module provides highly optimized fused CUDA kernels that combine multiple
//! operations into a single kernel launch, dramatically reducing GPU overhead.
//!
//! Key optimizations:
//! - Fused Fourier features: matmul + scale + sin/cos + concat in ONE kernel
//! - Shared memory tiling for weight matrix reuse
//! - Coalesced memory access patterns
//! - Eliminates 4+ kernel launches and synchronization barriers
use rtx_tensor::{Tensor, TensorError, Result, Device};
use std::sync::Arc;
// NOTE: CUDA contexts are now managed by the global singleton cache in rtx-tensor
// (rtx_tensor::storage::cuda_manager::get_or_create_context)
#[cfg(feature = "cuda")]
use cudarc::driver::{LaunchConfig, PushKernelArg};
#[cfg(feature = "cuda")]
use cudarc::nvrtc::Ptx;
/// Fused Fourier features computation
///
/// Combines: matmul(x, B) * scale -> sin/cos -> concatenate
/// into a single CUDA kernel with shared memory optimization.
///
/// Input: x [batch, 1]
/// Weights: B [1, ff_dim]
/// Output: out [batch, ff_dim * 2] where out = [sin(x @ B * scale), cos(x @ B * scale)]
#[cfg(feature = "cuda")]
pub fn fused_fourier_features(
x: &Tensor,
b_weights: &Tensor,
scale: f32,
output: &mut Tensor,
) -> Result<()> {
// Validate inputs
let x_shape = x.shape();
let b_shape = b_weights.shape();
let out_shape = output.shape();
let batch_size = x_shape.dims()[0];
let ff_dim = b_shape.dims()[1];
// Validate shapes
if x_shape.dims().len() != 2 || x_shape.dims()[1] != 1 {
return Err(TensorError::shape(format!(
"x must be [batch, 1], got {:?}", x_shape.dims()
)));
}
if b_shape.dims().len() != 2 || b_shape.dims()[0] != 1 {
return Err(TensorError::shape(format!(
"B must be [1, ff_dim], got {:?}", b_shape.dims()
)));
}
if out_shape.dims() != [batch_size, ff_dim * 2] {
return Err(TensorError::shape(format!(
"output must be [{}, {}], got {:?}", batch_size, ff_dim * 2, out_shape.dims()
)));
}
// Get device ID
let device_id = match x.device() {
Device::Cuda(id) => *id,
_ => return Err(TensorError::device("fused_fourier_features requires CUDA device")),
};
// Launch the fused kernel
launch_fused_fourier_kernel(x, b_weights, scale, output, batch_size, ff_dim, device_id)
}
#[cfg(feature = "cuda")]
fn launch_fused_fourier_kernel(
x: &Tensor,
b_weights: &Tensor,
scale: f32,
output: &mut Tensor,
batch_size: usize,
ff_dim: usize,
device_id: usize,
) -> Result<()> {
// PTX for fused Fourier features kernel
// This kernel:
// 1. Loads B weights into shared memory (reused across all batch elements)
// 2. For each batch element: computes x * B[j] * scale
// 3. Applies sin() and cos()
// 4. Writes directly to concatenated output buffer
static FUSED_FOURIER_PTX: &str = r#"
.version 7.0
.target sm_50
.address_size 64
// Fused Fourier features kernel
// Input: x[batch, 1], B[1, ff_dim]
// Output: out[batch, ff_dim*2] = [sin(x*B*scale), cos(x*B*scale)]
//
// Grid: (batch_size, 1, 1)
// Block: (ff_dim, 1, 1) - one thread per output pair
.visible .entry fused_fourier_kernel(
.param .u64 x_ptr, // Input tensor [batch, 1]
.param .u64 b_ptr, // Weight matrix [1, ff_dim]
.param .u64 out_ptr, // Output tensor [batch, ff_dim*2]
.param .f32 scale, // Scale factor (2*pi)
.param .u32 batch_size,
.param .u32 ff_dim
) {
// Shared memory for B weights - loaded once, reused for all batch elements
.shared .align 4 .f32 B_shared[256]; // Max ff_dim = 256
.reg .u32 %tid, %bid, %ff;
.reg .u64 %x_addr, %b_addr, %out_addr;
.reg .u64 %offset, %sin_offset, %cos_offset;
.reg .f32 %x_val, %b_val, %prod, %scaled, %s, %c, %scale_val;
.reg .pred %p_bounds, %p_load;
.reg .u32 %ff_dim_reg, %batch_reg, %ff_dim_2;
// Get thread and block indices
mov.u32 %tid, %tid.x; // Thread ID = which ff_dim element
mov.u32 %bid, %ctaid.x; // Block ID = which batch element
// Load parameters
ld.param.u32 %ff_dim_reg, [ff_dim];
ld.param.u32 %batch_reg, [batch_size];
ld.param.f32 %scale_val, [scale];
// Bounds check
setp.ge.u32 %p_bounds, %tid, %ff_dim_reg;
setp.ge.u32 %p_load, %bid, %batch_reg;
@%p_bounds bra DONE;
@%p_load bra DONE;
// === Phase 1: Load B weights into shared memory ===
// Each thread loads one element
ld.param.u64 %b_addr, [b_ptr];
cvt.u64.u32 %offset, %tid;
shl.b64 %offset, %offset, 2; // * sizeof(float)
add.u64 %b_addr, %b_addr, %offset;
ld.global.f32 %b_val, [%b_addr];
// Store to shared memory
mov.u32 %ff, %tid;
st.shared.f32 [B_shared + %ff * 4], %b_val;
// Synchronize - ensure all B weights are loaded
bar.sync 0;
// === Phase 2: Compute x * B * scale for this batch element ===
// Load x[bid]
ld.param.u64 %x_addr, [x_ptr];
cvt.u64.u32 %offset, %bid;
shl.b64 %offset, %offset, 2; // * sizeof(float)
add.u64 %x_addr, %x_addr, %offset;
ld.global.f32 %x_val, [%x_addr];
// Load B[tid] from shared memory
ld.shared.f32 %b_val, [B_shared + %ff * 4];
// Compute: x * B * scale
mul.f32 %prod, %x_val, %b_val;
mul.f32 %scaled, %prod, %scale_val;
// === Phase 3: Compute sin and cos ===
sin.approx.f32 %s, %scaled;
cos.approx.f32 %c, %scaled;
// === Phase 4: Write to output [batch, ff_dim*2] ===
// sin goes to out[bid, tid]
// cos goes to out[bid, ff_dim + tid]
ld.param.u64 %out_addr, [out_ptr];
mul.lo.u32 %ff_dim_2, %ff_dim_reg, 2;
// Calculate sin output address: out_ptr + (bid * ff_dim * 2 + tid) * 4
cvt.u64.u32 %sin_offset, %bid;
cvt.u64.u32 %offset, %ff_dim_2;
mul.lo.u64 %sin_offset, %sin_offset, %offset;
cvt.u64.u32 %offset, %tid;
add.u64 %sin_offset, %sin_offset, %offset;
shl.b64 %sin_offset, %sin_offset, 2;
add.u64 %out_addr, %out_addr, %sin_offset;
st.global.f32 [%out_addr], %s;
// Calculate cos output address: sin_addr + ff_dim * 4
cvt.u64.u32 %cos_offset, %ff_dim_reg;
shl.b64 %cos_offset, %cos_offset, 2;
add.u64 %out_addr, %out_addr, %cos_offset;
st.global.f32 [%out_addr], %c;
DONE:
ret;
}
"#;
// Use global singleton context cache from rtx-tensor for CUDA graph capture compatibility
let ctx = rtx_tensor::storage::cuda_manager::get_or_create_context(device_id)?;
// Load PTX module
let module = ctx.load_module(Ptx::from_src(FUSED_FOURIER_PTX))
.map_err(|e| TensorError::kernel(format!("Failed to load fused_fourier PTX: {:?}", e)))?;
let func = module.load_function("fused_fourier_kernel")
.map_err(|e| TensorError::kernel(format!("Failed to load fused_fourier kernel: {:?}", e)))?;
// Get exclusive access to output buffer
output.make_exclusive()?;
// Get CUDA slices
let x_slice = x.storage().cuda_slice_clone()
.map_err(|e| TensorError::device(format!("Failed to get x CUDA slice: {}", e)))?;
let b_slice = b_weights.storage().cuda_slice_clone()
.map_err(|e| TensorError::device(format!("Failed to get B CUDA slice: {}", e)))?;
let mut out_slice = Arc::get_mut(&mut output.storage_mut())
.ok_or_else(|| TensorError::runtime("Failed to get exclusive output access"))?
.cuda_slice_clone()
.map_err(|e| TensorError::device(format!("Failed to get output CUDA slice: {}", e)))?;
// Launch kernel
// Grid: one block per batch element
// Block: one thread per ff_dim element
let block_size = ff_dim.min(256) as u32; // Max 256 threads per block
let grid_size = batch_size as u32;
let cfg = LaunchConfig {
grid_dim: (grid_size, 1, 1),
block_dim: (block_size, 1, 1),
shared_mem_bytes: (ff_dim * 4) as u32, // B weights in shared memory
};
let stream = ctx.default_stream();
unsafe {
stream.launch_builder(&func)
.arg(&x_slice)
.arg(&b_slice)
.arg(&mut out_slice)
.arg(&scale)
.arg(&(batch_size as u32))
.arg(&(ff_dim as u32))
.launch(cfg)
.map_err(|e| TensorError::kernel(format!("fused_fourier kernel launch failed: {:?}", e)))?;
}
// REMOVED: stream.synchronize() - unnecessary because:
// 1. CUDA streams guarantee operation ordering within the same stream
// 2. Subsequent operations (linear layers) use the same stream
// 3. Sync is only needed when reading data back to CPU (which we defer)
// This removal saves ~5-10µs per forward pass.
Ok(())
}
#[cfg(not(feature = "cuda"))]
pub fn fused_fourier_features(
_x: &Tensor,
_b_weights: &Tensor,
_scale: f32,
_output: &mut Tensor,
) -> Result<()> {
Err(TensorError::device("fused_fourier_features requires CUDA feature"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[cfg(feature = "cuda")]
fn test_fused_fourier_features() {
use rtx_tensor::Device;
let device = Device::cuda(0);
let batch_size = 100;
let ff_dim = 64;
// Create test tensors
let x = Tensor::randn([batch_size, 1], &device).unwrap();
let b = Tensor::randn([1, ff_dim], &device).unwrap();
let mut output = Tensor::zeros([batch_size, ff_dim * 2], &device).unwrap();
// Run fused kernel
fused_fourier_features(&x, &b, 2.0 * std::f32::consts::PI, &mut output).unwrap();
// Verify output shape
assert_eq!(output.shape().dims(), &[batch_size, ff_dim * 2]);
}
}