Haider TohaProjects

parallel navier-stokes solver

by ·

a high-performance c++ solver for 2d incompressible fluid dynamics. built during my aeronautics degree when i wanted to understand both the physics and the computational techniques at a low level.

the problem

simulating fluid flow is computationally expensive. the navier-stokes equations describe how velocity fields evolve:

$\frac{\partial \mathbf{u}}{\partial t} + (\mathbf{u} \cdot \nabla)\mathbf{u} = -\frac{1}{\rho}\nabla p + \nu \nabla^2 \mathbf{u} + \mathbf{f}$

$\nabla \cdot \mathbf{u} = 0$

where \mathbf{u} is velocity, p is pressure, \rho is density, \nu is kinematic viscosity and \mathbf{f} represents external forces. solving them at high resolution requires millions of calculations per timestep. sequential code hits a wall fast.

approach

i implemented a pressure-projection method: 1. advect velocity field (move fluid) 2. apply external forces (gravity, etc.) 3. solve pressure poisson equation (enforce incompressibility) 4. project velocity to be divergence-free

the expensive part is step 3, solving the pressure poisson equation:

$\nabla^2 p = \frac{\rho}{\Delta t} \nabla \cdot \mathbf{u}^*$

i used a jacobi iterative solver where each cell update only depends on its neighbours:

$p_{i,j}^{n+1} = \frac{1}{4}(p_{i+1,j}^n + p_{i-1,j}^n + p_{i,j+1}^n + p_{i,j-1}^n - \Delta x^2 b_{i,j})$

this is embarrassingly parallel since each cell can be updated independently.

parallelisation strategy

  • mpi for distributed memory: split the grid across nodes. each node owns a horizontal slice. ghost cell exchanges happen at boundaries
  • openmp for shared memory: within each node, parallelise the inner loops with pragma directives. cache-aware iteration order (row-major) reduced l1/l2 misses by ~40%
  • hybrid approach: mpi between nodes, openmp within nodes. this matched the cluster architecture (16 cores per node, 8 nodes)

optimisations

  • aligned memory allocation for simd vectorisation
  • loop tiling to improve cache locality
  • asynchronous mpi communication overlapped with computation
  • red-black gauss-seidel for faster convergence (2x fewer iterations than jacobi)

results

  • achieved near-linear speedup up to 64 cores, with parallel efficiency E = S/P \approx 0.88 where S is speedup and P is processor count
  • 512x512 grid at 1000 timesteps: 45 minutes sequential → 48 seconds parallel (S \approx 56\times)
  • learned more about memory hierarchies than any textbook could teach

stack: c++, mpi, openmp, hpc cluster, vtk for visualisation

the code is messy (academic code always is), but the experience shaped how i think about performance-critical systems.