Files
rustytorch/crates/specialized/rtx-cfd/src/solvers/incompressible/piso_gpu.rs
T
Omar Sobh 5e402918bb
CI / Build (macos-latest) (push) Waiting to run
CI / Test (macos-latest) (push) Blocked by required conditions
CI / Test (ubuntu-latest) (push) Blocked by required conditions
CI / Python Bindings (maturin) (macos-latest) (push) Blocked by required conditions
CI / Python Bindings (maturin) (ubuntu-latest) (push) Blocked by required conditions
CI / WASM Build + Size Check (push) Blocked by required conditions
CI / Distributed Training Tests (push) Blocked by required conditions
CI / CI Success (push) Blocked by required conditions
CI / Build (ubuntu-latest) (push) Failing after 5s
CI / Clippy Check (push) Failing after 5s
Documentation / Build User Guide (push) Successful in 6s
Documentation / Build API Documentation (push) Failing after 17s
CI / Build CPU-Only (Explicit) (push) Failing after 32s
CI / Format Check (push) Failing after 42s
Performance Benchmarks / Run Benchmarks (push) Successful in 1m59s
rtx-cfd legacy GPU modules: cudarc 0.18 API — unsafe launches, the stream accessor, memcpy_htod argument order; the renamed k-ε constants and ::standard(); get_module clones; CfdConfig test literals take ..Default
2026-09-16 07:16:39 -05:00

738 lines
23 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! GPU-accelerated PISO solver implementation
//!
//! This module provides a CUDA-accelerated version of the PISO algorithm
//! for incompressible Navier-Stokes equations. It uses real CUDA kernels
//! for momentum prediction, pressure correction, and velocity correction operations.
use super::{
BoundaryConditions, FlowField, IncompressibleSolver, PisoParameters, PisoResult, SolverResult,
};
use crate::kernels::*;
use crate::{CfdConfig, CfdError, CfdResult};
use async_trait::async_trait;
use cudarc::driver::{CudaSlice, LaunchConfig, PushKernelArg};
use std::sync::Arc;
use std::time::Instant;
/// GPU-accelerated PISO solver
pub struct PisoGpuSolver {
/// Base CPU solver for reference and fallback
cpu_solver: super::piso::PisoSolver,
/// GPU kernel manager
kernel_manager: Arc<CudaKernelManager>,
/// GPU kernels
advection_kernel: AdvectionKernel,
diffusion_kernel: DiffusionKernel,
poisson_kernel: PoissonKernel,
matrix_ops_kernel: MatrixOpsKernel,
/// GPU memory buffers
gpu_buffers: Option<PisoGpuBuffers>,
}
/// GPU memory buffers for PISO flow field variables
struct PisoGpuBuffers {
/// Grid dimensions
nx: usize,
ny: usize,
/// Velocity components
u: CudaSlice<f32>,
v: CudaSlice<f32>,
u_star: CudaSlice<f32>,
v_star: CudaSlice<f32>,
u_old: CudaSlice<f32>,
v_old: CudaSlice<f32>,
/// Pressure fields
p: CudaSlice<f32>,
p_prime: CudaSlice<f32>,
p_old: CudaSlice<f32>,
/// Source terms and temporary arrays
mass_source: CudaSlice<f32>,
momentum_source_u: CudaSlice<f32>,
momentum_source_v: CudaSlice<f32>,
/// Working arrays for corrections
u_correction: CudaSlice<f32>,
v_correction: CudaSlice<f32>,
pressure_correction: CudaSlice<f32>,
/// Temporary working arrays
temp1: CudaSlice<f32>,
temp2: CudaSlice<f32>,
residual: CudaSlice<f32>,
}
impl PisoGpuSolver {
/// Create new GPU-accelerated PISO solver
pub fn new(config: CfdConfig, parameters: PisoParameters) -> CfdResult<Self> {
// Create CPU solver for fallback and validation
let cpu_solver = super::piso::PisoSolver::new(config.clone(), parameters.clone())?;
// Initialize GPU components
let kernel_manager = Arc::new(CudaKernelManager::new(&config)?);
let advection_kernel = AdvectionKernel::new(&kernel_manager, AdvectionScheme::Upwind)?;
let diffusion_kernel = DiffusionKernel::new(&kernel_manager, DiffusionScheme::Explicit)?;
let poisson_kernel = PoissonKernel::new(&kernel_manager)?;
let matrix_ops_kernel = MatrixOpsKernel::new(&kernel_manager)?;
Ok(Self {
cpu_solver,
kernel_manager,
advection_kernel,
diffusion_kernel,
poisson_kernel,
matrix_ops_kernel,
gpu_buffers: None,
})
}
/// Initialize GPU buffers for given flow field dimensions
fn initialize_gpu_buffers(&mut self, flow_field: &FlowField) -> CfdResult<()> {
let nx = flow_field.nx;
let ny = flow_field.ny;
// Allocate GPU memory for all flow variables
let u = self.kernel_manager.allocate_f32(nx * ny)?;
let v = self.kernel_manager.allocate_f32(nx * ny)?;
let u_star = self.kernel_manager.allocate_f32(nx * ny)?;
let v_star = self.kernel_manager.allocate_f32(nx * ny)?;
let u_old = self.kernel_manager.allocate_f32(nx * ny)?;
let v_old = self.kernel_manager.allocate_f32(nx * ny)?;
let p = self.kernel_manager.allocate_f32(nx * ny)?;
let p_prime = self.kernel_manager.allocate_f32(nx * ny)?;
let p_old = self.kernel_manager.allocate_f32(nx * ny)?;
let mass_source = self.kernel_manager.allocate_f32(nx * ny)?;
let momentum_source_u = self.kernel_manager.allocate_f32(nx * ny)?;
let momentum_source_v = self.kernel_manager.allocate_f32(nx * ny)?;
let u_correction = self.kernel_manager.allocate_f32(nx * ny)?;
let v_correction = self.kernel_manager.allocate_f32(nx * ny)?;
let pressure_correction = self.kernel_manager.allocate_f32(nx * ny)?;
let temp1 = self.kernel_manager.allocate_f32(nx * ny)?;
let temp2 = self.kernel_manager.allocate_f32(nx * ny)?;
let residual = self.kernel_manager.allocate_f32(nx * ny)?;
self.gpu_buffers = Some(PisoGpuBuffers {
nx,
ny,
u,
v,
u_star,
v_star,
u_old,
v_old,
p,
p_prime,
p_old,
mass_source,
momentum_source_u,
momentum_source_v,
u_correction,
v_correction,
pressure_correction,
temp1,
temp2,
residual,
});
Ok(())
}
/// Copy flow field data from CPU to GPU
fn copy_to_gpu(&self, flow_field: &FlowField) -> CfdResult<()> {
let buffers = self
.gpu_buffers
.as_ref()
.ok_or_else(|| CfdError::gpu_error("GPU buffers not initialized"))?;
// Convert nalgebra matrices to flat vectors
let u_flat = matrix_to_flat(&flow_field.u, buffers.nx, buffers.ny)?;
let v_flat = matrix_to_flat(&flow_field.v, buffers.nx, buffers.ny)?;
let p_flat = matrix_to_flat(&flow_field.p, buffers.nx, buffers.ny)?;
let u_old_flat = matrix_to_flat(&flow_field.u_old, buffers.nx, buffers.ny)?;
let v_old_flat = matrix_to_flat(&flow_field.v_old, buffers.nx, buffers.ny)?;
// Copy to GPU (simplified - in real implementation would use memcpy to existing buffers)
let _u_gpu = self.kernel_manager.copy_to_device(&u_flat)?;
let _v_gpu = self.kernel_manager.copy_to_device(&v_flat)?;
let _p_gpu = self.kernel_manager.copy_to_device(&p_flat)?;
let _u_old_gpu = self.kernel_manager.copy_to_device(&u_old_flat)?;
let _v_old_gpu = self.kernel_manager.copy_to_device(&v_old_flat)?;
Ok(())
}
/// Copy flow field data from GPU to CPU
fn copy_from_gpu(&self, flow_field: &mut FlowField) -> CfdResult<()> {
let buffers = self
.gpu_buffers
.as_ref()
.ok_or_else(|| CfdError::gpu_error("GPU buffers not initialized"))?;
// Copy from GPU
let u_flat = self.kernel_manager.copy_from_device(&buffers.u)?;
let v_flat = self.kernel_manager.copy_from_device(&buffers.v)?;
let p_flat = self.kernel_manager.copy_from_device(&buffers.p)?;
// Convert back to nalgebra matrices
flat_to_matrix(&u_flat, &mut flow_field.u, buffers.nx, buffers.ny)?;
flat_to_matrix(&v_flat, &mut flow_field.v, buffers.nx, buffers.ny)?;
flat_to_matrix(&p_flat, &mut flow_field.p, buffers.nx, buffers.ny)?;
Ok(())
}
/// GPU-accelerated momentum prediction step
async fn gpu_momentum_prediction(&mut self, dt: f32) -> CfdResult<()> {
let buffers = self
.gpu_buffers
.as_mut()
.ok_or_else(|| CfdError::gpu_error("GPU buffers not initialized"))?;
let config = self.cpu_solver.config();
let dx = (config.lx / config.nx as f64) as f32;
let dy = (config.ly / config.ny as f64) as f32;
let viscosity = config.viscosity as f32;
let density = config.density as f32;
let nu = viscosity / density; // kinematic viscosity
// Step 1: Solve u-momentum equation
// ∂u/∂t + ∇·(u⊗u) = -∇p^n/ρ + ν∇²u
// Advection term for u-momentum
self.advection_kernel.apply_2d(
&buffers.u_old,
&mut buffers.temp1,
&buffers.u,
&buffers.v,
dt,
dx,
dy,
buffers.nx,
buffers.ny,
)?;
// Diffusion term for u-momentum
self.diffusion_kernel.apply_2d(
&buffers.temp1,
&mut buffers.u_star,
nu,
dt,
dx,
dy,
buffers.nx,
buffers.ny,
)?;
// Step 2: Solve v-momentum equation
// ∂v/∂t + ∇·(v⊗u) = -∇p^n/ρ + ν∇²v
// Advection term for v-momentum
self.advection_kernel.apply_2d(
&buffers.v_old,
&mut buffers.temp2,
&buffers.u,
&buffers.v,
dt,
dx,
dy,
buffers.nx,
buffers.ny,
)?;
// Diffusion term for v-momentum
self.diffusion_kernel.apply_2d(
&buffers.temp2,
&mut buffers.v_star,
nu,
dt,
dx,
dy,
buffers.nx,
buffers.ny,
)?;
Ok(())
}
/// GPU-accelerated pressure correction step
async fn gpu_pressure_correction(&mut self, dt: f32, corrector_step: usize) -> CfdResult<f32> {
let config = self.cpu_solver.config();
let dx = (config.lx / config.nx as f64) as f32;
let dy = (config.ly / config.ny as f64) as f32;
let density = config.density as f32;
// Get buffer info first
let (nx, ny) = {
let buffers = self
.gpu_buffers
.as_ref()
.ok_or_else(|| CfdError::gpu_error("GPU buffers not initialized"))?;
(buffers.nx, buffers.ny)
};
// Step 1: Compute mass source term from velocity divergence
// For PISO, we use the current predicted velocities (u*, v*)
self.compute_mass_source_for_step(corrector_step, dt, dx, dy, density)?;
// Step 2: Solve pressure Poisson equation ∇²p' = mass_source
let iterations = {
let buffers = self
.gpu_buffers
.as_mut()
.ok_or_else(|| CfdError::gpu_error("GPU buffers not initialized"))?;
self.poisson_kernel.solve_2d(
&mut buffers.pressure_correction,
&buffers.mass_source,
nx,
ny,
dx,
dy,
50, // max iterations
1e-6, // tolerance
)?
};
// Step 3: Update pressure field
// p^(n+1) = p^n + p' (no relaxation for PISO)
{
let buffers = self
.gpu_buffers
.as_mut()
.ok_or_else(|| CfdError::gpu_error("GPU buffers not initialized"))?;
self.matrix_ops_kernel
.axpy(1.0, &buffers.pressure_correction, &mut buffers.p)?;
}
// Step 4: Compute residual for convergence check
let residual_norm = {
let buffers = self
.gpu_buffers
.as_ref()
.ok_or_else(|| CfdError::gpu_error("GPU buffers not initialized"))?;
self.matrix_ops_kernel.vector_norm(&buffers.mass_source)?
};
Ok(residual_norm)
}
/// Helper to compute mass source for a specific corrector step
fn compute_mass_source_for_step(
&mut self,
corrector_step: usize,
dt: f32,
dx: f32,
dy: f32,
density: f32,
) -> CfdResult<()> {
let buffers = self
.gpu_buffers
.as_mut()
.ok_or_else(|| CfdError::gpu_error("GPU buffers not initialized"))?;
// Clone the references we need for velocity field
let (u_ref, v_ref) = if corrector_step == 0 {
(buffers.u_star.clone(), buffers.v_star.clone())
} else {
(buffers.u.clone(), buffers.v.clone())
};
// Now compute divergence using the cloned references
self.compute_mass_source(&u_ref, &v_ref, dt, dx, dy, density)
}
/// Compute mass source term from velocity divergence
fn compute_mass_source(
&mut self,
u: &CudaSlice<f32>,
v: &CudaSlice<f32>,
dt: f32,
dx: f32,
dy: f32,
density: f32,
) -> CfdResult<()> {
let buffers = self
.gpu_buffers
.as_mut()
.ok_or_else(|| CfdError::gpu_error("GPU buffers not initialized"))?;
// Get divergence computation kernel
let module = self.kernel_manager.get_module("matrix_kernels")?;
let func = module
.load_function("compute_divergence_2d")
.map_err(|e| CfdError::gpu_error(&format!("Failed to get divergence kernel: {}", e)))?;
let grid_dim_x = (buffers.nx as u32 + 15) / 16;
let grid_dim_y = (buffers.ny as u32 + 15) / 16;
let config = LaunchConfig {
grid_dim: (grid_dim_x, grid_dim_y, 1),
block_dim: (16, 16, 1),
shared_mem_bytes: 0,
};
unsafe {
unsafe {
self.kernel_manager
.stream()
.launch_builder(&func)
.arg(&mut buffers.mass_source)
.arg(u)
.arg(v)
.arg(&(density / dt))
.arg(&dx)
.arg(&dy)
.arg(&(buffers.nx as i32))
.arg(&(buffers.ny as i32))
.launch(config)
.map_err(|e| {
CfdError::gpu_error(&format!("Divergence kernel launch failed: {}", e))
})?;
}
}
self.kernel_manager.synchronize()?;
Ok(())
}
/// GPU-accelerated velocity correction step
async fn gpu_velocity_correction(&mut self, dt: f32) -> CfdResult<()> {
let config = self.cpu_solver.config();
let dx = (config.lx / config.nx as f64) as f32;
let dy = (config.ly / config.ny as f64) as f32;
let density = config.density as f32;
// Compute velocity corrections
// u^(n+1) = u* - (dt/ρ) * ∂p'/∂x
// v^(n+1) = v* - (dt/ρ) * ∂p'/∂y
self.compute_velocity_corrections(dt, dx, dy, density)?;
// Apply velocity corrections
// u = u* + u_correction
// v = v* + v_correction
{
let buffers = self
.gpu_buffers
.as_mut()
.ok_or_else(|| CfdError::gpu_error("GPU buffers not initialized"))?;
let u_correction = buffers.u_correction.clone();
let v_correction = buffers.v_correction.clone();
self.matrix_ops_kernel
.axpy(1.0, &u_correction, &mut buffers.u)?;
self.matrix_ops_kernel
.axpy(1.0, &v_correction, &mut buffers.v)?;
}
Ok(())
}
/// Compute velocity corrections from pressure gradients
fn compute_velocity_corrections(
&mut self,
dt: f32,
dx: f32,
dy: f32,
density: f32,
) -> CfdResult<()> {
let buffers = self
.gpu_buffers
.as_mut()
.ok_or_else(|| CfdError::gpu_error("GPU buffers not initialized"))?;
// Get gradient computation kernel
let module = self.kernel_manager.get_module("matrix_kernels")?;
let func = module
.load_function("compute_gradient_2d")
.map_err(|e| CfdError::gpu_error(&format!("Failed to get gradient kernel: {}", e)))?;
let grid_dim_x = (buffers.nx as u32 + 15) / 16;
let grid_dim_y = (buffers.ny as u32 + 15) / 16;
let config = LaunchConfig {
grid_dim: (grid_dim_x, grid_dim_y, 1),
block_dim: (16, 16, 1),
shared_mem_bytes: 0,
};
let correction_factor = -dt / density;
unsafe {
unsafe {
self.kernel_manager
.stream()
.launch_builder(&func)
.arg(&buffers.pressure_correction)
.arg(&mut buffers.u_correction.clone())
.arg(&mut buffers.v_correction.clone())
.arg(&correction_factor)
.arg(&dx)
.arg(&dy)
.arg(&(buffers.nx as i32))
.arg(&(buffers.ny as i32))
.launch(config)
.map_err(|e| {
CfdError::gpu_error(&format!("Gradient kernel launch failed: {}", e))
})?;
}
}
self.kernel_manager.synchronize()?;
Ok(())
}
/// Solve one GPU-accelerated PISO time step
pub async fn solve_gpu_piso_time_step(
&mut self,
flow_field: &mut FlowField,
_boundary_conditions: &BoundaryConditions,
dt: f64,
) -> CfdResult<PisoResult> {
// Initialize GPU buffers if needed
if self.gpu_buffers.is_none() {
self.initialize_gpu_buffers(flow_field)?;
}
// Copy current state to GPU
self.copy_to_gpu(flow_field)?;
let dt_f32 = dt as f32;
let corrector_steps = self.cpu_solver.parameters().corrector_steps;
let tolerance = self.cpu_solver.parameters().tolerance as f32;
let start_time = Instant::now();
let mut residual_history = Vec::new();
// Step 1: Momentum predictor
self.gpu_momentum_prediction(dt_f32).await?;
let mut corrector_steps_performed = 0;
// Step 2-4: Pressure-velocity correction loop
for corrector in 0..corrector_steps {
// Pressure correction
let pressure_residual = self.gpu_pressure_correction(dt_f32, corrector).await?;
residual_history.push(pressure_residual as f64);
// Velocity correction
self.gpu_velocity_correction(dt_f32).await?;
corrector_steps_performed += 1;
// Check convergence
if pressure_residual < tolerance {
break;
}
}
// Copy results back to CPU
self.copy_from_gpu(flow_field)?;
// Apply boundary conditions on CPU (could be moved to GPU)
// In a full GPU implementation, boundary conditions would also be on GPU
let solve_time = start_time.elapsed();
let final_residual = residual_history.last().copied().unwrap_or(0.0);
let converged = final_residual < tolerance as f64;
Ok(PisoResult {
solver_result: SolverResult {
converged,
iterations: corrector_steps_performed,
final_residual,
residual_history,
solve_time,
},
corrector_steps_performed,
})
}
}
#[async_trait]
impl IncompressibleSolver for PisoGpuSolver {
type Parameters = PisoParameters;
type Result = PisoResult;
fn new(config: CfdConfig, params: Self::Parameters) -> CfdResult<Self> {
PisoGpuSolver::new(config, params)
}
async fn solve_time_step(
&mut self,
flow_field: &mut FlowField,
boundary_conditions: &BoundaryConditions,
dt: f64,
) -> CfdResult<Self::Result> {
self.solve_gpu_piso_time_step(flow_field, boundary_conditions, dt)
.await
}
async fn solve(
&mut self,
flow_field: &mut FlowField,
boundary_conditions: &BoundaryConditions,
) -> CfdResult<Self::Result> {
let parameters = self.cpu_solver.parameters();
self.solve_time_step(flow_field, boundary_conditions, parameters.time_step)
.await
}
fn config(&self) -> &CfdConfig {
self.cpu_solver.config()
}
fn parameters(&self) -> &Self::Parameters {
self.cpu_solver.parameters()
}
}
/// Utility functions for matrix/GPU data conversion (reused from simple_gpu.rs)
/// Convert nalgebra matrix to flat array for GPU
fn matrix_to_flat(matrix: &nalgebra::DMatrix<f64>, nx: usize, ny: usize) -> CfdResult<Vec<f32>> {
let mut flat = Vec::with_capacity(nx * ny);
for j in 0..ny {
for i in 0..nx {
if j < matrix.nrows() && i < matrix.ncols() {
flat.push(matrix[(j, i)] as f32);
} else {
flat.push(0.0);
}
}
}
Ok(flat)
}
/// Convert flat array from GPU to nalgebra matrix
fn flat_to_matrix(
flat: &[f32],
matrix: &mut nalgebra::DMatrix<f64>,
nx: usize,
ny: usize,
) -> CfdResult<()> {
if flat.len() != nx * ny {
return Err(CfdError::gpu_error("Array size mismatch"));
}
for j in 0..ny {
for i in 0..nx {
if j < matrix.nrows() && i < matrix.ncols() {
matrix[(j, i)] = flat[j * nx + i] as f64;
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::CfdConfig;
#[tokio::test]
async fn test_gpu_piso_solver_creation() -> CfdResult<()> {
let config = CfdConfig {
nx: 32,
ny: 32,
nz: 1,
lx: 1.0,
ly: 1.0,
lz: 1.0,
dt: 0.001,
viscosity: 0.01,
density: 1.0,
device_id: 0,
..CfdConfig::default()
};
let params = PisoParameters::default();
// This will only work if CUDA is available
if let Ok(_solver) = PisoGpuSolver::new(config, params) {
println!("GPU PISO solver created successfully");
} else {
println!("GPU not available, skipping GPU PISO test");
}
Ok(())
}
#[test]
fn test_matrix_conversions() -> CfdResult<()> {
let nx = 4;
let ny = 3;
// Create test matrix
let mut matrix = nalgebra::DMatrix::zeros(ny, nx);
for j in 0..ny {
for i in 0..nx {
matrix[(j, i)] = (j * nx + i) as f64;
}
}
// Convert to flat
let flat = matrix_to_flat(&matrix, nx, ny)?;
assert_eq!(flat.len(), nx * ny);
// Convert back to matrix
let mut matrix2 = nalgebra::DMatrix::zeros(ny, nx);
flat_to_matrix(&flat, &mut matrix2, nx, ny)?;
// Check that they match
for j in 0..ny {
for i in 0..nx {
assert!((matrix[(j, i)] - matrix2[(j, i)]).abs() < 1e-6);
}
}
Ok(())
}
#[tokio::test]
async fn test_gpu_piso_momentum_prediction() -> CfdResult<()> {
let config = CfdConfig {
nx: 16,
ny: 16,
nz: 1,
lx: 1.0,
ly: 1.0,
lz: 1.0,
dt: 0.001,
viscosity: 0.01,
density: 1.0,
device_id: 0,
..CfdConfig::default()
};
let params = PisoParameters::default();
if let Ok(mut solver) = PisoGpuSolver::new(config.clone(), params) {
let mut flow_field = FlowField::new(
config.nx,
config.ny,
config.lx / config.nx as f64,
config.ly / config.ny as f64,
)?;
// Initialize buffers
solver.initialize_gpu_buffers(&flow_field)?;
// Test momentum prediction step
let result = solver.gpu_momentum_prediction(0.001).await;
assert!(result.is_ok(), "Momentum prediction should succeed");
println!("GPU PISO momentum prediction test passed");
} else {
println!("GPU not available, skipping momentum prediction test");
}
Ok(())
}
}