Initial commit

This commit is contained in:
redclawsystems
2026-03-04 00:08:42 +00:00
commit 4d88dc0584
4449 changed files with 1556714 additions and 0 deletions
@@ -0,0 +1,731 @@
//! 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 {
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 {
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,
};
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,
};
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(())
}
}