289 lines
9.5 KiB
Rust
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]);
|
|
}
|
|
}
|