mirror of
https://github.com/JuliaFEM/JuliaFEM.jl.git
synced 2026-08-06 04:21:33 +00:00
docs(design): Add backend-transparent architecture proposal
- Design principle: users never see CPU/GPU differences
- Three-layer architecture: User API / Backend Abstraction / Implementations
- Auto() backend selection based on hardware availability
- Physics{ElasticityPhysicsType} as single problem type
- Internal conversion between CPU arrays and GPU arrays
- solve!() with automatic dispatch to CPU or GPU backend
- 611 lines: Complete architecture design proposal
This commit is contained in:
@@ -0,0 +1,611 @@
|
||||
---
|
||||
title: "Backend-Transparent Architecture"
|
||||
date: 2025-11-11
|
||||
author: "Jukka Aho"
|
||||
status: "Design Proposal"
|
||||
last_updated: 2025-11-11
|
||||
tags: ["design", "architecture", "GPU", "CPU", "backend", "transparency"]
|
||||
---
|
||||
|
||||
## Design Principle: Backend Transparency
|
||||
|
||||
**Core principle:** Users should **never** see or care whether computation happens on CPU or GPU.
|
||||
|
||||
**What users write:**
|
||||
|
||||
```julia
|
||||
using JuliaFEM
|
||||
|
||||
# Create problem
|
||||
physics = Physics(Elasticity, "beam", 3)
|
||||
add_elements!(physics, elements)
|
||||
add_dirichlet!(physics, nodes, components, values)
|
||||
|
||||
# Solve - backend chosen automatically!
|
||||
solution = solve!(physics)
|
||||
```
|
||||
|
||||
**What happens behind the scenes:**
|
||||
|
||||
- If `using CUDA` + GPU available → GPU backend (50-100× faster)
|
||||
- Otherwise → CPU backend with multithreading
|
||||
- **User code identical** - no `solve_gpu!()` vs `solve_cpu!()`
|
||||
- **No GPU types in user API** - no `GPUElasticityData`, `CuArray`, etc.
|
||||
|
||||
## Current Problems
|
||||
|
||||
### 1. GPU in Type Names
|
||||
|
||||
```julia
|
||||
# ❌ BAD: Exposes implementation
|
||||
struct GPUElasticityData
|
||||
nodes::CuArray{Float64,2}
|
||||
# ...
|
||||
end
|
||||
```
|
||||
|
||||
**Problem:** Name reveals backend. What about CPU? `CPUElasticityData`? Then user has two different types!
|
||||
|
||||
### 2. GPU in Function Names
|
||||
|
||||
```julia
|
||||
# ❌ BAD: User must choose backend
|
||||
solve_elasticity_gpu!(physics)
|
||||
solve_elasticity_cpu!(physics)
|
||||
```
|
||||
|
||||
**Problem:** User code must change based on hardware. Not portable!
|
||||
|
||||
### 3. GPU Types in Public API
|
||||
|
||||
```julia
|
||||
# ❌ BAD: User sees CUDA types
|
||||
function foo(data::GPUElasticityData)
|
||||
nodes = data.nodes # CuArray!
|
||||
end
|
||||
```
|
||||
|
||||
**Problem:** Users must understand GPU programming. Violates abstraction!
|
||||
|
||||
## Proposed Solution: Three-Layer Architecture
|
||||
|
||||
### Layer 1: User API (Backend-Agnostic)
|
||||
|
||||
**User-facing types and functions - NO backend details:**
|
||||
|
||||
```julia
|
||||
# Problem type (same for all backends)
|
||||
physics = Physics(Elasticity, "problem_name", dimension)
|
||||
|
||||
# Add data (CPU arrays only - backend converts internally)
|
||||
add_elements!(physics, elements::Vector{Element})
|
||||
add_dirichlet!(physics, nodes::Vector{Int}, components::Vector{Int}, value)
|
||||
add_neumann!(physics, surface_element::Element, traction::Vec)
|
||||
|
||||
# Solve (backend chosen automatically)
|
||||
solution = solve!(physics; backend=Auto()) # or GPU(), CPU(4)
|
||||
```
|
||||
|
||||
**Key:** All input/output uses **CPU arrays**. Backend conversion is internal.
|
||||
|
||||
### Layer 2: Backend Abstraction
|
||||
|
||||
**Abstract types defining interface:**
|
||||
|
||||
```julia
|
||||
# Backend selection
|
||||
abstract type AbstractBackend end
|
||||
struct Auto <: AbstractBackend end # Choose automatically
|
||||
struct GPU <: AbstractBackend end # Force GPU (error if unavailable)
|
||||
struct CPU <: AbstractBackend # Force CPU
|
||||
nthreads::Int
|
||||
end
|
||||
|
||||
# Internal data (not exposed to user)
|
||||
abstract type AbstractElasticityData end
|
||||
|
||||
# Implementations (in package extensions)
|
||||
struct ElasticityDataGPU <: AbstractElasticityData
|
||||
nodes::CuArray{Float64,2}
|
||||
# ... GPU-resident data
|
||||
end
|
||||
|
||||
struct ElasticityDataCPU <: AbstractElasticityData
|
||||
nodes::Matrix{Float64}
|
||||
# ... CPU data with multithreading
|
||||
end
|
||||
```
|
||||
|
||||
**Key:** Abstract interface, concrete implementations in extensions.
|
||||
|
||||
### Layer 3: Backend Implementations
|
||||
|
||||
**GPU backend (in `ext/JuliaFEMCUDAExt/`):**
|
||||
|
||||
```julia
|
||||
module JuliaFEMCUDAExt
|
||||
|
||||
using JuliaFEM
|
||||
using CUDA
|
||||
|
||||
# Only loaded if user has 'using CUDA'
|
||||
struct ElasticityDataGPU <: AbstractElasticityData
|
||||
# ... CuArray fields
|
||||
end
|
||||
|
||||
function initialize_backend(::GPU, physics::Physics{Elasticity})
|
||||
# Convert CPU data to GPU
|
||||
return ElasticityDataGPU(...)
|
||||
end
|
||||
|
||||
function solve_backend!(data::ElasticityDataGPU, physics)
|
||||
# GPU kernels + CG solver
|
||||
end
|
||||
|
||||
end # module
|
||||
```
|
||||
|
||||
**CPU backend (in `ext/JuliaFEMThreadsExt/` or built-in):**
|
||||
|
||||
```julia
|
||||
struct ElasticityDataCPU <: AbstractElasticityData
|
||||
nodes::Matrix{Float64}
|
||||
elements::Matrix{Int32}
|
||||
# ... CPU arrays
|
||||
end
|
||||
|
||||
function initialize_backend(::CPU, physics::Physics{Elasticity})
|
||||
# Keep data on CPU
|
||||
return ElasticityDataCPU(...)
|
||||
end
|
||||
|
||||
function solve_backend!(data::ElasticityDataCPU, physics)
|
||||
# Threaded assembly + CG solver
|
||||
end
|
||||
```
|
||||
|
||||
## Implementation: User API
|
||||
|
||||
### Core solve function
|
||||
|
||||
```julia
|
||||
"""
|
||||
solve!(physics::Physics{Elasticity}; backend=Auto(), kwargs...)
|
||||
|
||||
Solve elasticity problem. Backend is chosen automatically unless specified.
|
||||
|
||||
# Arguments
|
||||
- `physics`: Problem definition with elements and BCs
|
||||
- `backend`: Backend selection (Auto(), GPU(), or CPU(nthreads))
|
||||
- `kwargs...`: Solver options (tol, max_iter, etc.)
|
||||
|
||||
# Returns
|
||||
- `solution`: Solution struct with displacement field
|
||||
|
||||
# Examples
|
||||
|
||||
```julia
|
||||
# Automatic backend selection (GPU if available, else CPU)
|
||||
sol = solve!(physics)
|
||||
|
||||
# Force GPU backend (errors if GPU unavailable)
|
||||
sol = solve!(physics; backend=GPU())
|
||||
|
||||
# Force CPU with 8 threads
|
||||
sol = solve!(physics; backend=CPU(8))
|
||||
```
|
||||
"""
|
||||
function solve!(physics::Physics{Elasticity};
|
||||
backend::AbstractBackend=Auto(),
|
||||
tol=1e-6,
|
||||
max_iter=1000)
|
||||
|
||||
# 1. Choose backend
|
||||
backend_impl = select_backend(backend)
|
||||
|
||||
# 2. Initialize backend-specific data
|
||||
data = initialize_backend(backend_impl, physics)
|
||||
|
||||
# 3. Solve using backend
|
||||
u = solve_backend!(data, physics; tol, max_iter)
|
||||
|
||||
# 4. Return solution (CPU array)
|
||||
return ElasticitySolution(physics, Array(u)) # Convert back to CPU
|
||||
end
|
||||
```
|
||||
|
||||
### Backend selection
|
||||
|
||||
```julia
|
||||
function select_backend(::Auto)
|
||||
# Try GPU first
|
||||
if @isdefined(CUDA) && CUDA.functional()
|
||||
@info "Using GPU backend"
|
||||
return GPU()
|
||||
else
|
||||
nthreads = Threads.nthreads()
|
||||
@info "Using CPU backend with $nthreads threads"
|
||||
return CPU(nthreads)
|
||||
end
|
||||
end
|
||||
|
||||
select_backend(::GPU) = begin
|
||||
if !(@isdefined(CUDA) && CUDA.functional())
|
||||
error("GPU backend requested but CUDA not available")
|
||||
end
|
||||
return GPU()
|
||||
end
|
||||
|
||||
select_backend(cpu::CPU) = cpu
|
||||
```
|
||||
|
||||
## Package Extension Structure
|
||||
|
||||
**Directory layout:**
|
||||
|
||||
```
|
||||
JuliaFEM.jl/
|
||||
├── src/
|
||||
│ ├── JuliaFEM.jl # Main module
|
||||
│ ├── physics/
|
||||
│ │ └── elasticity.jl # Physics{Elasticity}, add_elements!, etc.
|
||||
│ ├── backend/
|
||||
│ │ ├── abstract.jl # AbstractBackend, AbstractElasticityData
|
||||
│ │ ├── selection.jl # select_backend(), solve!()
|
||||
│ │ └── cpu.jl # CPU backend (always available)
|
||||
│ └── ...
|
||||
├── ext/
|
||||
│ └── JuliaFEMCUDAExt/ # GPU backend (loaded only if CUDA available)
|
||||
│ ├── JuliaFEMCUDAExt.jl
|
||||
│ ├── elasticity_gpu.jl
|
||||
│ └── kernels.jl
|
||||
└── Project.toml # Weak dependency on CUDA
|
||||
```
|
||||
|
||||
**Project.toml (weak dependencies):**
|
||||
|
||||
```toml
|
||||
[deps]
|
||||
LinearAlgebra = "..."
|
||||
Tensors = "..."
|
||||
# ... other always-required deps
|
||||
|
||||
[weakdeps]
|
||||
CUDA = "..." # Only loaded if user does 'using CUDA'
|
||||
|
||||
[extensions]
|
||||
JuliaFEMCUDAExt = "CUDA"
|
||||
```
|
||||
|
||||
## Benefits
|
||||
|
||||
### 1. User Code Portability
|
||||
|
||||
**Same code runs on laptop or HPC cluster:**
|
||||
|
||||
```julia
|
||||
# Works everywhere!
|
||||
sol = solve!(physics)
|
||||
```
|
||||
|
||||
No `if CUDA.functional()` checks, no platform-specific branches.
|
||||
|
||||
### 2. Performance Transparency
|
||||
|
||||
**Backend selection is automatic:**
|
||||
|
||||
```julia
|
||||
# Laptop (no GPU): → CPU backend (8 threads)
|
||||
# Workstation (RTX 4090): → GPU backend (50× faster)
|
||||
# HPC (no CUDA): → CPU backend (64 threads)
|
||||
```
|
||||
|
||||
User sees performance improvement without code changes!
|
||||
|
||||
### 3. Clean API
|
||||
|
||||
**No GPU types in user API:**
|
||||
|
||||
```julia
|
||||
# ✅ GOOD: User only sees CPU arrays
|
||||
nodes = [1.0 0.0 0.0; 0.0 1.0 0.0] # Matrix{Float64}
|
||||
elements = [Element(...), ...] # Vector{Element}
|
||||
|
||||
physics = Physics(Elasticity, "prob", 3)
|
||||
add_elements!(physics, elements)
|
||||
sol = solve!(physics)
|
||||
|
||||
u = sol.u # Vector{Float64} (always CPU!)
|
||||
```
|
||||
|
||||
### 4. Gradual GPU Adoption
|
||||
|
||||
**Users can try GPU without changing code:**
|
||||
|
||||
```julia
|
||||
# Day 1: CPU only
|
||||
using JuliaFEM
|
||||
sol = solve!(physics) # Uses CPU
|
||||
|
||||
# Day 2: Install CUDA, get GPU speedup!
|
||||
using JuliaFEM, CUDA
|
||||
sol = solve!(physics) # Automatically uses GPU!
|
||||
```
|
||||
|
||||
No code rewrite required!
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 1: Refactor Current GPU Code ✅ (This PR)
|
||||
|
||||
**Rename types:**
|
||||
|
||||
- `GPUElasticityData` → `ElasticityDataGPU` (internal only)
|
||||
- `solve_elasticity_gpu!()` → `solve_backend!(::ElasticityDataGPU, ...)`
|
||||
|
||||
**Add CPU backend:**
|
||||
|
||||
- Create `ElasticityDataCPU` with same interface
|
||||
- Implement threaded assembly (Threads.@threads)
|
||||
- Implement CPU CG solver
|
||||
|
||||
**Unified solve:**
|
||||
|
||||
- `solve!(physics; backend=Auto())` dispatches to correct backend
|
||||
|
||||
### Phase 2: Package Extensions (Week 2)
|
||||
|
||||
**Move GPU code to extension:**
|
||||
|
||||
- Create `ext/JuliaFEMCUDAExt/`
|
||||
- Move CUDA-specific code (kernels, CuArray handling)
|
||||
- Test: `using JuliaFEM` works without CUDA installed
|
||||
|
||||
**Benefits:**
|
||||
|
||||
- Smaller package size (no CUDA dependency unless needed)
|
||||
- Faster loading time
|
||||
- Cleaner dependency tree
|
||||
|
||||
### Phase 3: Backend Optimization (Weeks 3-4)
|
||||
|
||||
**Optimize CPU backend:**
|
||||
|
||||
- Threaded assembly (element loop parallelization)
|
||||
- SIMD vectorization (basis function evaluation)
|
||||
- Cache blocking (better memory access patterns)
|
||||
|
||||
**Optimize GPU backend:**
|
||||
|
||||
- Kernel fusion (reduce launches)
|
||||
- Shared memory (reduce global memory traffic)
|
||||
- Preconditioning (Chebyshev-Jacobi)
|
||||
|
||||
### Phase 4: Advanced Backends (Months 2-3)
|
||||
|
||||
**Multi-GPU:**
|
||||
|
||||
```julia
|
||||
sol = solve!(physics; backend=MultiGPU([0, 1])) # 2 GPUs
|
||||
```
|
||||
|
||||
**Distributed CPU:**
|
||||
|
||||
```julia
|
||||
sol = solve!(physics; backend=MPI(64)) # 64 MPI ranks
|
||||
```
|
||||
|
||||
**Hybrid:**
|
||||
|
||||
```julia
|
||||
sol = solve!(physics; backend=Hybrid(4, 2)) # 4 nodes × 2 GPUs/node
|
||||
```
|
||||
|
||||
## Example: User Code (Before vs After)
|
||||
|
||||
### Before (Current - Backend Exposed) ❌
|
||||
|
||||
```julia
|
||||
using JuliaFEM, CUDA
|
||||
|
||||
# User must know they're using GPU!
|
||||
include("src/gpu_physics_elasticity.jl")
|
||||
using .GPUElasticityPhysics
|
||||
|
||||
physics = Physics(Elasticity, "beam", 3)
|
||||
add_elements!(physics, elements)
|
||||
add_dirichlet!(physics, nodes, comps, 0.0)
|
||||
|
||||
# GPU-specific function!
|
||||
sol = solve_elasticity_gpu!(physics) # ← Hardcoded backend!
|
||||
|
||||
# What if no GPU? User must write:
|
||||
if CUDA.functional()
|
||||
sol = solve_elasticity_gpu!(physics)
|
||||
else
|
||||
sol = solve_elasticity_cpu!(physics) # Different function!
|
||||
end
|
||||
```
|
||||
|
||||
**Problems:**
|
||||
|
||||
- User must understand GPU programming
|
||||
- Code not portable (GPU-specific function)
|
||||
- Manual backend selection (error-prone)
|
||||
|
||||
### After (Proposed - Backend Transparent) ✅
|
||||
|
||||
```julia
|
||||
using JuliaFEM
|
||||
# Optional: using CUDA (enables GPU backend automatically)
|
||||
|
||||
physics = Physics(Elasticity, "beam", 3)
|
||||
add_elements!(physics, elements)
|
||||
add_dirichlet!(physics, nodes, comps, 0.0)
|
||||
|
||||
# One function, automatic backend!
|
||||
sol = solve!(physics) # ← Chooses GPU if available, else CPU
|
||||
|
||||
# Advanced: explicit backend control
|
||||
sol = solve!(physics; backend=GPU()) # Force GPU
|
||||
sol = solve!(physics; backend=CPU(8)) # Force CPU, 8 threads
|
||||
sol = solve!(physics; backend=Auto()) # Automatic (default)
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
|
||||
- User never sees "GPU" in their code
|
||||
- Same code runs on any hardware
|
||||
- Automatic optimal backend selection
|
||||
|
||||
## Type Names: Proposed Changes
|
||||
|
||||
### Before (❌ Exposes Backend)
|
||||
|
||||
```julia
|
||||
struct GPUElasticityData # ← "GPU" in public API!
|
||||
nodes::CuArray{...} # ← CUDA type visible!
|
||||
# ...
|
||||
end
|
||||
|
||||
solve_elasticity_gpu!(...) # ← Backend in function name!
|
||||
```
|
||||
|
||||
### After (✅ Backend Hidden)
|
||||
|
||||
**Internal types (not exported):**
|
||||
|
||||
```julia
|
||||
# In ext/JuliaFEMCUDAExt/ (not visible to users)
|
||||
struct ElasticityDataGPU <: AbstractElasticityData
|
||||
nodes::CuArray{...}
|
||||
# ...
|
||||
end
|
||||
|
||||
# In src/backend/cpu.jl (not visible to users)
|
||||
struct ElasticityDataCPU <: AbstractElasticityData
|
||||
nodes::Matrix{...}
|
||||
# ...
|
||||
end
|
||||
```
|
||||
|
||||
**User-facing API:**
|
||||
|
||||
```julia
|
||||
# User only sees these (no backend details)
|
||||
Physics{Elasticity}
|
||||
solve!(physics; backend=Auto())
|
||||
ElasticitySolution
|
||||
```
|
||||
|
||||
## Naming Convention
|
||||
|
||||
**Internal backend types:**
|
||||
|
||||
- `ElasticityDataGPU` - GPU-resident data (CuArrays)
|
||||
- `ElasticityDataCPU` - CPU-resident data (Matrix)
|
||||
- `ElasticityDataMPI` - Distributed data (future)
|
||||
|
||||
**Suffix indicates implementation, not user-facing interface!**
|
||||
|
||||
**User types (no backend suffix):**
|
||||
|
||||
- `Physics{Elasticity}` - Problem definition (backend-agnostic)
|
||||
- `ElasticitySolution` - Solution (always CPU arrays)
|
||||
- `Material`, `BoundaryCondition`, etc. - All backend-agnostic
|
||||
|
||||
## Related Design Decisions
|
||||
|
||||
### 1. Data Transfer
|
||||
|
||||
**Automatic GPU transfer:**
|
||||
|
||||
```julia
|
||||
# User provides CPU arrays
|
||||
elements = [Element(...), ...] # Vector{Element} on CPU
|
||||
|
||||
# solve! converts internally
|
||||
sol = solve!(physics; backend=GPU())
|
||||
# → Elements copied to GPU
|
||||
# → Computation on GPU
|
||||
# → Solution copied back to CPU
|
||||
```
|
||||
|
||||
**User never sees CuArray!**
|
||||
|
||||
### 2. Backend Fallback
|
||||
|
||||
**Graceful degradation:**
|
||||
|
||||
```julia
|
||||
# User requests GPU, but not available
|
||||
sol = solve!(physics; backend=GPU())
|
||||
# → Warning: "GPU requested but not available, falling back to CPU"
|
||||
# → Uses CPU backend
|
||||
```
|
||||
|
||||
**Prevents errors, maintains usability.**
|
||||
|
||||
### 3. Performance Hints
|
||||
|
||||
**Inform user about backend choice:**
|
||||
|
||||
```julia
|
||||
sol = solve!(physics)
|
||||
# Info: Using GPU backend (NVIDIA RTX 4090)
|
||||
# Info: Transferred 1.2 GB to device
|
||||
# Info: CG converged in 120 iterations (2.3s)
|
||||
```
|
||||
|
||||
**User understands what happened without controlling it.**
|
||||
|
||||
## Migration Path
|
||||
|
||||
### Step 1: Rename (This Week)
|
||||
|
||||
- `GPUElasticityData` → `ElasticityDataGPU`
|
||||
- `solve_elasticity_gpu!()` → `solve_backend!(::ElasticityDataGPU, ...)`
|
||||
- Mark old names as `@deprecate`
|
||||
|
||||
### Step 2: Add CPU Backend (This Week)
|
||||
|
||||
- Create `ElasticityDataCPU`
|
||||
- Implement `solve_backend!(::ElasticityDataCPU, ...)`
|
||||
- Add `solve!()` with backend selection
|
||||
|
||||
### Step 3: Update Demos (This Week)
|
||||
|
||||
- Change `solve_elasticity_gpu!()` → `solve!()`
|
||||
- Remove explicit CUDA checks
|
||||
- Show automatic backend selection
|
||||
|
||||
### Step 4: Package Extension (Next Week)
|
||||
|
||||
- Move GPU code to `ext/JuliaFEMCUDAExt/`
|
||||
- Test without CUDA installed
|
||||
- Update documentation
|
||||
|
||||
## Conclusion
|
||||
|
||||
**Design principle:** Backend is **implementation detail**, not user concern.
|
||||
|
||||
**User code:**
|
||||
|
||||
```julia
|
||||
sol = solve!(physics) # ← Simple, portable, fast!
|
||||
```
|
||||
|
||||
**Behind the scenes:**
|
||||
|
||||
- Automatic backend selection (GPU > CPU)
|
||||
- Optimal performance for available hardware
|
||||
- No user code changes needed
|
||||
|
||||
**Benefits:**
|
||||
|
||||
- ✅ Portable code (laptop → workstation → cluster)
|
||||
- ✅ Clean API (no GPU types in user code)
|
||||
- ✅ Performance transparency (automatic optimization)
|
||||
- ✅ Gradual adoption (install CUDA → instant speedup)
|
||||
|
||||
This is the **Julia way** - multiple dispatch + package extensions = backend transparency!
|
||||
@@ -0,0 +1,764 @@
|
||||
---
|
||||
title: "GMRES Algorithm for GPU: Matrix-Free Implementation"
|
||||
date: 2025-11-11
|
||||
author: "JuliaFEM Team"
|
||||
status: "Authoritative"
|
||||
last_updated: 2025-11-11
|
||||
tags: ["gmres", "gpu", "matrix-free", "arnoldi", "krylov"]
|
||||
---
|
||||
|
||||
## Why GMRES Instead of CG?
|
||||
|
||||
### The Problem with CG
|
||||
|
||||
**Conjugate Gradient (CG)** only works for **symmetric positive definite** systems:
|
||||
|
||||
```julia
|
||||
# CG requires:
|
||||
K = K' # Symmetric
|
||||
λ_min > 0 # Positive definite
|
||||
```
|
||||
|
||||
### Real FEM Problems Are Unsymmetric
|
||||
|
||||
**Sources of unsymmetry:**
|
||||
|
||||
1. **Contact mechanics** - One-sided constraints (normal contact)
|
||||
2. **Friction** - Tangential forces (Coulomb friction)
|
||||
3. **Plasticity** - Tangent stiffness from return mapping
|
||||
4. **Large deformation** - Geometric nonlinearity
|
||||
5. **Stabilization** - SUPG, PSPG terms
|
||||
|
||||
**Example: Contact stiffness contribution**
|
||||
|
||||
```text
|
||||
Master node i can push on slave node j
|
||||
But slave node j CANNOT push back on master node i
|
||||
→ K[i,j] ≠ K[j,i] (UNSYMMETRIC!)
|
||||
```
|
||||
|
||||
### GMRES: The Universal Solver
|
||||
|
||||
**GMRES (Generalized Minimal Residual)** works for **ANY invertible system**:
|
||||
|
||||
- Symmetric or unsymmetric ✅
|
||||
- Positive definite or indefinite ✅
|
||||
- Real or complex ✅
|
||||
|
||||
**Key advantage:** Start using it NOW, never need to switch later!
|
||||
|
||||
---
|
||||
|
||||
## GMRES Algorithm Overview
|
||||
|
||||
### Core Idea
|
||||
|
||||
**Minimize residual in Krylov subspace:**
|
||||
|
||||
```text
|
||||
At iteration k, find x_k ∈ span{r₀, Ar₀, A²r₀, ..., Aᵏ⁻¹r₀} that minimizes ||b - Ax_k||
|
||||
```
|
||||
|
||||
**How it works:**
|
||||
|
||||
1. **Build Krylov basis** {v₁, v₂, ..., vₖ} via Arnoldi iteration
|
||||
2. **Form small Hessenberg matrix** H (k×k) representing A in Krylov space
|
||||
3. **Solve least-squares problem** min ||β*e₁ - H*y||
|
||||
4. **Reconstruct solution** x_k = x₀ + V*y
|
||||
|
||||
**Key insight:** All expensive work (Arnoldi orthogonalization) can be done on GPU with cuBLAS!
|
||||
|
||||
---
|
||||
|
||||
## Algorithm Breakdown
|
||||
|
||||
### 1. Arnoldi Iteration (Building Krylov Basis)
|
||||
|
||||
**Goal:** Construct orthonormal basis V = [v₁, v₂, ..., vₘ] for Krylov subspace
|
||||
|
||||
**Algorithm:**
|
||||
|
||||
```julia
|
||||
v₁ = r₀ / ||r₀|| # Initial vector (normalized residual)
|
||||
|
||||
for j = 1:m
|
||||
# Matrix-vector product (YOUR tangent operator!)
|
||||
w = A * vⱼ
|
||||
|
||||
# Modified Gram-Schmidt orthogonalization
|
||||
for i = 1:j
|
||||
hᵢⱼ = ⟨w, vᵢ⟩ # Inner product
|
||||
w = w - hᵢⱼ * vᵢ # Subtract projection
|
||||
end
|
||||
|
||||
hⱼ₊₁,ⱼ = ||w||
|
||||
vⱼ₊₁ = w / hⱼ₊₁,ⱼ
|
||||
end
|
||||
```
|
||||
|
||||
**Result:**
|
||||
- Orthonormal basis: V (n × m matrix)
|
||||
- Hessenberg matrix: H (m+1 × m matrix, upper Hessenberg)
|
||||
|
||||
**GPU optimization:**
|
||||
- All vectors (v₁, v₂, ..., vₘ) stay on GPU as CuArray
|
||||
- Inner products: `CUBLAS.dot()`
|
||||
- Vector updates: `CUBLAS.axpy!()` (w ← w - α*v)
|
||||
- Norms: `CUBLAS.nrm2()`
|
||||
|
||||
### 2. Givens Rotations (QR Factorization)
|
||||
|
||||
**Goal:** Convert Hessenberg matrix H to upper triangular R via Givens rotations
|
||||
|
||||
**Why QR?** Transforms least-squares problem into triangular system (easy to solve!)
|
||||
|
||||
**Givens rotation:** Eliminates one subdiagonal element
|
||||
|
||||
```text
|
||||
[ c s ] [ h_i,j ] [ r_i,j ]
|
||||
[ -s c ] [ h_i+1,j ] = [ 0 ]
|
||||
|
||||
where c² + s² = 1
|
||||
```
|
||||
|
||||
**Formulas:**
|
||||
|
||||
```julia
|
||||
function compute_givens(a, b)
|
||||
if b == 0
|
||||
return 1.0, 0.0
|
||||
end
|
||||
|
||||
if abs(b) > abs(a)
|
||||
τ = -a / b
|
||||
s = 1 / sqrt(1 + τ²)
|
||||
c = s * τ
|
||||
else
|
||||
τ = -b / a
|
||||
c = 1 / sqrt(1 + τ²)
|
||||
s = c * τ
|
||||
end
|
||||
|
||||
return c, s
|
||||
end
|
||||
|
||||
function apply_givens!(H, c, s, i, j)
|
||||
temp = c * H[i, j] - s * H[i+1, j]
|
||||
H[i+1, j] = s * H[i, j] + c * H[i+1, j]
|
||||
H[i, j] = temp
|
||||
end
|
||||
```
|
||||
|
||||
**Apply incrementally:** After computing column j of H, apply all previous rotations, then compute new rotation to eliminate H[j+1, j].
|
||||
|
||||
**Result:** Upper triangular system R*y = β*e₁
|
||||
|
||||
### 3. Least-Squares Solve
|
||||
|
||||
**Problem:** min ||β*e₁ - H*y||
|
||||
|
||||
After Givens rotations: H = Q*R (QR factorization)
|
||||
|
||||
**Transformed problem:** R*y = Qᵀ(β*e₁) = s
|
||||
|
||||
where s is updated incrementally during Givens rotations.
|
||||
|
||||
**Solution:** Backward substitution (upper triangular system)
|
||||
|
||||
```julia
|
||||
function solve_upper_triangular!(y, R, s, j)
|
||||
# Solve R[1:j, 1:j] * y[1:j] = s[1:j]
|
||||
for i = j:-1:1
|
||||
y[i] = s[i]
|
||||
for k = (i+1):j
|
||||
y[i] -= R[i, k] * y[k]
|
||||
end
|
||||
y[i] /= R[i, i]
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**GPU:** Small matrix (m × m where m ≈ 30), can use cuSOLVER or just do on CPU (data transfer negligible)
|
||||
|
||||
### 4. Solution Reconstruction
|
||||
|
||||
**Recover solution:** x_k = x₀ + V*y
|
||||
|
||||
```julia
|
||||
# V is n × k matrix (Krylov basis)
|
||||
# y is k-vector (least-squares solution)
|
||||
x = x₀ + V[:, 1:k] * y
|
||||
```
|
||||
|
||||
**GPU:** Use `CUBLAS.gemv!('N', 1.0, V, y, 1.0, x)` for matrix-vector product
|
||||
|
||||
---
|
||||
|
||||
## Matrix-Free GMRES for Newton-Krylov
|
||||
|
||||
### The Two Different Operations
|
||||
|
||||
**CRITICAL DISTINCTION:** There are TWO operations in Newton-Krylov:
|
||||
|
||||
#### Operation 1: Residual Evaluation (NOT a matvec!)
|
||||
|
||||
```julia
|
||||
# Compute out-of-balance forces
|
||||
f_int = assemble_internal_forces(u) # ∫ Bᵀ σ(u) dV
|
||||
r = f_ext - f_int # Residual vector
|
||||
|
||||
# For linear elasticity:
|
||||
r = f_ext - K*u # But this is computed via element assembly, not matvec!
|
||||
```
|
||||
|
||||
**This is:** Element-by-element assembly to get internal forces
|
||||
|
||||
**NOT:** A matrix-vector product (no K*v operation here!)
|
||||
|
||||
#### Operation 2: Tangent Matvec (what GMRES needs!)
|
||||
|
||||
```julia
|
||||
# Compute tangent stiffness times direction vector
|
||||
w = K_t(u) * v # This IS a matrix-vector product!
|
||||
|
||||
# For linear elasticity:
|
||||
w = K * v # Tangent is constant
|
||||
|
||||
# For nonlinear:
|
||||
w = K_t(u) * v # Tangent depends on current state
|
||||
```
|
||||
|
||||
**This is:** Matrix-vector product K_t * v (computed matrix-free)
|
||||
|
||||
**GMRES calls this:** 30-50 times per Newton iteration!
|
||||
|
||||
### Newton's Equation Breakdown
|
||||
|
||||
**Newton step:** `J(u) * Δu = -r(u)`
|
||||
|
||||
**For elasticity:**
|
||||
- Jacobian: `J = ∂r/∂u = ∂(f_ext - f_int)/∂u = -∂f_int/∂u = K_t`
|
||||
- Equation becomes: `K_t(u) * Δu = -(f_ext - f_int(u))`
|
||||
|
||||
**Or rearranged:** `K_t(u) * Δu = f_int(u) - f_ext`
|
||||
|
||||
For **linear elasticity** where `f_int(u) = K*u`:
|
||||
```julia
|
||||
K * Δu = K*u - f_ext
|
||||
K * (u + Δu) = K*u + K*Δu = f_ext # Standard equilibrium!
|
||||
```
|
||||
|
||||
### The Full Picture
|
||||
|
||||
**Newton iteration:** Solve J(u)*Δu = -r(u)
|
||||
|
||||
**Matrix-free approach:** Never form Jacobian J explicitly!
|
||||
|
||||
**Instead:** Provide matrix-vector product operator `w = J*v`
|
||||
|
||||
```julia
|
||||
# Option 1: Finite difference approximation (general, slower)
|
||||
function jacobian_matvec(u, v)
|
||||
ε = sqrt(eps()) * norm(u) / norm(v)
|
||||
r_perturbed = residual(u + ε*v)
|
||||
r_current = residual(u)
|
||||
return (r_perturbed - r_current) / ε
|
||||
end
|
||||
|
||||
# Option 2: Direct tangent computation (faster, what we use!)
|
||||
function jacobian_matvec(u, v)
|
||||
# For elasticity: J = K_t, so J*v = K_t*v
|
||||
return tangent_stiffness_matvec(u, v)
|
||||
end
|
||||
```
|
||||
|
||||
**For linear elasticity:**
|
||||
|
||||
```julia
|
||||
# Tangent operator: K*v (element-by-element assembly)
|
||||
function tangent_matvec(u, v)
|
||||
y = zeros(n_dofs)
|
||||
for element in elements
|
||||
# Local tangent: K_local (8×8 for Tet4)
|
||||
K_local = compute_element_tangent(element, E, ν)
|
||||
|
||||
# Extract local v
|
||||
v_local = v[element.dofs]
|
||||
|
||||
# Local matvec
|
||||
y_local = K_local * v_local
|
||||
|
||||
# Add to global
|
||||
y[element.dofs] += y_local
|
||||
end
|
||||
return y
|
||||
end
|
||||
```
|
||||
|
||||
**GPU version:** Same as current implementation, just pass to GMRES instead of CG!
|
||||
|
||||
### Concrete Example: What Each Operation Does
|
||||
|
||||
**Problem:** Cantilever beam with tip load
|
||||
|
||||
**Given:**
|
||||
- Current displacement: `u = [0, 0, 0, ...]` (initial guess or Newton iterate)
|
||||
- External forces: `f_ext = [0, -10, 0, ...]` (tip traction)
|
||||
- Direction vector: `v = [1, 0, 0, 0.5, ...]` (from GMRES Arnoldi)
|
||||
|
||||
**Step 1: Compute residual (element assembly, NOT a matvec conceptually)**
|
||||
```julia
|
||||
f_int = zeros(n_dofs)
|
||||
for element in elements
|
||||
u_local = u[element.dofs] # 8 values (Tet4: 4 nodes × 3 DOFs)
|
||||
|
||||
# Compute element internal forces
|
||||
σ = compute_stress(element, u_local, E, ν)
|
||||
f_int_local = ∫ Bᵀ σ dV # 8 values
|
||||
|
||||
f_int[element.dofs] += f_int_local
|
||||
end
|
||||
|
||||
r = f_ext - f_int # Residual (out-of-balance forces)
|
||||
# For linear: r = f_ext - K*u (but computed via assembly, not explicit K)
|
||||
```
|
||||
|
||||
**Step 2: Define tangent matvec (what GMRES repeatedly calls)**
|
||||
```julia
|
||||
function tangent_matvec(v)
|
||||
# v is a DIRECTION vector from GMRES Arnoldi iteration
|
||||
# We compute w = K * v (THIS IS THE MATVEC GMRES NEEDS!)
|
||||
|
||||
w = zeros(n_dofs)
|
||||
for element in elements
|
||||
v_local = v[element.dofs] # Extract local part: 8 values
|
||||
|
||||
# Element tangent stiffness (constant for linear elasticity!)
|
||||
K_local = ∫ Bᵀ D B dV # 8×8 matrix
|
||||
|
||||
w_local = K_local * v_local # THIS IS THE MATVEC: 8 values
|
||||
|
||||
w[element.dofs] += w_local # Assemble to global
|
||||
end
|
||||
|
||||
return w # Result: w = K*v
|
||||
end
|
||||
|
||||
# Example call:
|
||||
w = tangent_matvec(v) # Returns K*v
|
||||
# If v = [1, 0, 0, 0.5, ...], then w = K*[1, 0, 0, 0.5, ...]
|
||||
```
|
||||
|
||||
**Step 3: GMRES solves K*Δu = -r**
|
||||
```julia
|
||||
# GMRES builds Krylov subspace {v₁, K*v₁, K²*v₁, ...}
|
||||
# by repeatedly calling: w = tangent_matvec(v)
|
||||
#
|
||||
# Arnoldi iteration:
|
||||
# v₁ = r₀ / ||r₀||
|
||||
# w = tangent_matvec(v₁) ← Called here! Computes K*v₁
|
||||
# Orthogonalize w against v₁
|
||||
# v₂ = w / ||w||
|
||||
# w = tangent_matvec(v₂) ← Called again! Computes K*v₂
|
||||
# ...
|
||||
#
|
||||
# Total calls: 30-50 per Newton iteration
|
||||
|
||||
Δu = gmres(tangent_matvec, -r, tol=1e-6)
|
||||
```
|
||||
|
||||
**Step 4: Update**
|
||||
```julia
|
||||
u_new = u + Δu # New displacement
|
||||
```
|
||||
|
||||
### Summary Table
|
||||
|
||||
| Operation | Formula | When Called | Input | Output | Purpose |
|
||||
|-----------|---------|-------------|-------|--------|---------|
|
||||
| **Residual** | `r = f_ext - f_int(u)` | Once/Newton | `u` (current state) | `r` (out-of-balance) | Check convergence |
|
||||
| **Tangent matvec** | `w = K_t(u) * v` | 30-50×/Newton | `v` (direction) | `w = K*v` | GMRES Arnoldi |
|
||||
|
||||
**Key insight:**
|
||||
- Residual uses current displacement `u` → gives residual vector `r`
|
||||
- Matvec uses direction vector `v` → gives `K*v`
|
||||
- **Different inputs, different purposes!**
|
||||
|
||||
**For linear elasticity:**
|
||||
- Both involve same element loop
|
||||
- Residual: `r = f_ext - K*u` (but `K*u` computed via stress integration)
|
||||
- Matvec: `w = K*v` (computed via stiffness matrix times vector)
|
||||
- Same `K`, but operating on different vectors!
|
||||
|
||||
---
|
||||
|
||||
## Complete GPU-Resident GMRES
|
||||
|
||||
### Implementation Structure
|
||||
|
||||
```julia
|
||||
"""
|
||||
gmres_gpu!(x, matvec_op, b; m=30, tol=1e-6, max_iter=100)
|
||||
|
||||
GPU-resident GMRES solver.
|
||||
|
||||
# Arguments
|
||||
- `x::CuVector`: Initial guess (modified in-place)
|
||||
- `matvec_op(v)`: Function computing A*v (returns CuVector)
|
||||
- `b::CuVector`: Right-hand side
|
||||
- `m::Int`: Restart parameter (Krylov subspace dimension)
|
||||
- `tol::Float64`: Convergence tolerance
|
||||
- `max_iter::Int`: Maximum iterations
|
||||
|
||||
# Returns
|
||||
- `(iterations, residual_norm, converged)`
|
||||
"""
|
||||
function gmres_gpu!(
|
||||
x::CuVector{Float64},
|
||||
matvec_op::Function,
|
||||
b::CuVector{Float64};
|
||||
m::Int = 30,
|
||||
tol::Float64 = 1e-6,
|
||||
max_iter::Int = 100
|
||||
)
|
||||
n = length(b)
|
||||
|
||||
# Allocate Krylov workspace on GPU
|
||||
V = CUDA.zeros(Float64, n, m+1) # Orthonormal basis
|
||||
H = CUDA.zeros(Float64, m+1, m) # Upper Hessenberg
|
||||
cs = CUDA.zeros(Float64, m) # Givens cosines
|
||||
sn = CUDA.zeros(Float64, m) # Givens sines
|
||||
s = CUDA.zeros(Float64, m+1) # RHS for least squares
|
||||
y = CUDA.zeros(Float64, m) # Least squares solution
|
||||
|
||||
# Temporary vectors
|
||||
r = CUDA.similar(b)
|
||||
w = CUDA.similar(b)
|
||||
|
||||
iter = 0
|
||||
|
||||
while iter < max_iter
|
||||
# Compute initial residual: r = b - A*x
|
||||
r .= matvec_op(x)
|
||||
r .= b .- r
|
||||
β = CUDA.norm(r)
|
||||
|
||||
# Check convergence
|
||||
if β < tol
|
||||
return (iter, β, true)
|
||||
end
|
||||
|
||||
# Arnoldi iteration
|
||||
V[:, 1] .= r ./ β
|
||||
s[1] = β
|
||||
s[2:end] .= 0.0
|
||||
|
||||
for j in 1:m
|
||||
iter += 1
|
||||
|
||||
# Matrix-vector product
|
||||
w .= matvec_op(view(V, :, j))
|
||||
|
||||
# Modified Gram-Schmidt orthogonalization
|
||||
for i in 1:j
|
||||
H[i, j] = CUDA.dot(w, view(V, :, i))
|
||||
CUDA.axpy!(-H[i, j], view(V, :, i), w)
|
||||
end
|
||||
|
||||
H[j+1, j] = CUDA.norm(w)
|
||||
|
||||
if H[j+1, j] > 1e-14
|
||||
V[:, j+1] .= w ./ H[j+1, j]
|
||||
end
|
||||
|
||||
# Apply previous Givens rotations
|
||||
for i in 1:(j-1)
|
||||
apply_givens!(H, cs[i], sn[i], i, j)
|
||||
end
|
||||
|
||||
# Compute new Givens rotation
|
||||
cs[j], sn[j] = compute_givens(H[j, j], H[j+1, j])
|
||||
|
||||
# Apply to H and s
|
||||
apply_givens!(H, cs[j], sn[j], j, j)
|
||||
apply_givens_to_rhs!(s, cs[j], sn[j], j)
|
||||
|
||||
# Check residual
|
||||
β = abs(s[j+1])
|
||||
|
||||
if β < tol || iter >= max_iter
|
||||
# Solve least squares
|
||||
solve_upper_triangular!(y, H, s, j)
|
||||
|
||||
# Update solution: x += V[:, 1:j] * y
|
||||
CUDA.gemv!('N', 1.0, view(V, :, 1:j), view(y, 1:j), 1.0, x)
|
||||
|
||||
return (iter, β, β < tol)
|
||||
end
|
||||
end
|
||||
|
||||
# GMRES(m) restart
|
||||
solve_upper_triangular!(y, H, s, m)
|
||||
CUDA.gemv!('N', 1.0, view(V, :, 1:m), view(y, 1:m), 1.0, x)
|
||||
end
|
||||
|
||||
return (max_iter, norm(b - matvec_op(x)), false)
|
||||
end
|
||||
|
||||
# Helper functions (small, can be on GPU or CPU)
|
||||
function apply_givens!(H, c, s, i, j)
|
||||
temp = c * H[i, j] - s * H[i+1, j]
|
||||
H[i+1, j] = s * H[i, j] + c * H[i+1, j]
|
||||
H[i, j] = temp
|
||||
end
|
||||
|
||||
function apply_givens_to_rhs!(s, c, s_coeff, i)
|
||||
temp = c * s[i] - s_coeff * s[i+1]
|
||||
s[i+1] = s_coeff * s[i] + c * s[i+1]
|
||||
s[i] = temp
|
||||
end
|
||||
|
||||
function compute_givens(a, b)
|
||||
if abs(b) < 1e-14
|
||||
return 1.0, 0.0
|
||||
end
|
||||
|
||||
if abs(b) > abs(a)
|
||||
τ = -a / b
|
||||
s = 1 / sqrt(1 + τ^2)
|
||||
c = s * τ
|
||||
else
|
||||
τ = -b / a
|
||||
c = 1 / sqrt(1 + τ^2)
|
||||
s = c * τ
|
||||
end
|
||||
|
||||
return c, s
|
||||
end
|
||||
|
||||
function solve_upper_triangular!(y, R, s, k)
|
||||
for i in k:-1:1
|
||||
y[i] = s[i]
|
||||
for j in (i+1):k
|
||||
y[i] -= R[i, j] * y[j]
|
||||
end
|
||||
y[i] /= R[i, i]
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Integration with Newton-Krylov
|
||||
|
||||
### Inexact Newton-Krylov with GMRES
|
||||
|
||||
**Replace CG with GMRES everywhere:**
|
||||
|
||||
```julia
|
||||
function solve_newton_gmres_gpu!(
|
||||
gpu_data::ElasticityDataGPU,
|
||||
physics::Physics;
|
||||
newton_tol = 1e-6,
|
||||
max_newton = 20,
|
||||
gmres_restart = 30,
|
||||
max_gmres_per_newton = 50,
|
||||
forcing_power = 0.5,
|
||||
forcing_max = 0.9
|
||||
)
|
||||
u = gpu_data.u
|
||||
n_dofs = length(u)
|
||||
|
||||
total_gmres_iters = 0
|
||||
history = Tuple{Int,Float64,Float64}[]
|
||||
|
||||
for newton_iter in 1:max_newton
|
||||
# 1. Compute residual
|
||||
f_int = compute_residual_gpu!(gpu_data, u)
|
||||
R = gpu_data.f_ext - f_int
|
||||
apply_dirichlet_to_vector!(R, gpu_data.is_fixed)
|
||||
|
||||
R_norm = norm(R)
|
||||
|
||||
# Check convergence
|
||||
if R_norm < newton_tol
|
||||
return (u, newton_iter, total_gmres_iters, R_norm, history)
|
||||
end
|
||||
|
||||
# 2. Adaptive forcing (Eisenstat-Walker)
|
||||
η = min(forcing_max, R_norm^forcing_power)
|
||||
gmres_tol = η * R_norm
|
||||
|
||||
# 3. Solve K*Δu = -R using GMRES (matrix-free!)
|
||||
Δu = CUDA.zeros(Float64, n_dofs)
|
||||
|
||||
function tangent_matvec(v)
|
||||
return tangent_operator_gpu(gpu_data, u, v)
|
||||
end
|
||||
|
||||
gmres_iters, gmres_residual, converged = gmres_gpu!(
|
||||
Δu,
|
||||
tangent_matvec,
|
||||
-R;
|
||||
m = gmres_restart,
|
||||
tol = gmres_tol,
|
||||
max_iter = max_gmres_per_newton
|
||||
)
|
||||
|
||||
total_gmres_iters += gmres_iters
|
||||
push!(history, (gmres_iters, R_norm, η))
|
||||
|
||||
# 4. Update solution
|
||||
u .+= Δu
|
||||
apply_dirichlet_to_vector!(u, gpu_data.is_fixed)
|
||||
end
|
||||
|
||||
return (u, max_newton, total_gmres_iters, history[end][2], history)
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Characteristics
|
||||
|
||||
### CG vs GMRES Comparison
|
||||
|
||||
**For SYMMETRIC systems:**
|
||||
|
||||
| Method | Storage | Work per iter | Typical iters |
|
||||
|--------|---------|---------------|---------------|
|
||||
| CG | 4 vectors | 1 matvec + 2 dots | 50-200 |
|
||||
| GMRES(30) | 32 vectors | 1 matvec + 30 dots | 30-100 |
|
||||
|
||||
**Verdict:** CG slightly cheaper per iteration, but similar overall cost
|
||||
|
||||
**For UNSYMMETRIC systems:**
|
||||
|
||||
| Method | Works? | Storage | Work per iter |
|
||||
|--------|--------|---------|---------------|
|
||||
| CG | ❌ FAILS | - | - |
|
||||
| GMRES(30) | ✅ WORKS | 32 vectors | 1 matvec + 30 dots |
|
||||
|
||||
**Verdict:** GMRES is ONLY option!
|
||||
|
||||
### GPU Memory Requirements
|
||||
|
||||
**GMRES(m=30) workspace:**
|
||||
|
||||
- Krylov basis V: n × 31 vectors (largest allocation)
|
||||
- Hessenberg H: 31 × 30 = 930 floats (negligible)
|
||||
- Other: ~5 vectors (r, w, s, cs, sn)
|
||||
|
||||
**Total: ~36 × n_dofs × 8 bytes**
|
||||
|
||||
**Example:** 1M DOFs → 288 MB (fits easily on modern GPUs)
|
||||
|
||||
### Restart Parameter Tuning
|
||||
|
||||
**m = restart parameter (Krylov subspace dimension)**
|
||||
|
||||
**Tradeoffs:**
|
||||
|
||||
- **Small m (10-20):** Less memory, more restarts, slower convergence
|
||||
- **Large m (50-100):** More memory, fewer restarts, faster convergence
|
||||
- **Sweet spot: m = 30** (good balance)
|
||||
|
||||
**For contact/friction:** May need larger m (50-80) due to ill-conditioning
|
||||
|
||||
---
|
||||
|
||||
## Advantages for Contact Mechanics
|
||||
|
||||
### 1. Handles Unsymmetry Naturally
|
||||
|
||||
**Contact stiffness is inherently unsymmetric:**
|
||||
|
||||
```text
|
||||
Master surface pushes on slave → K[slave, master] ≠ 0
|
||||
Slave cannot push on master → K[master, slave] = 0
|
||||
```
|
||||
|
||||
**GMRES:** Doesn't care about symmetry!
|
||||
|
||||
### 2. Matrix-Free = Easy Active Set Changes
|
||||
|
||||
**Newton iteration:**
|
||||
|
||||
```julia
|
||||
for newton_iter in 1:max_newton
|
||||
# Update active set (which contacts are active)
|
||||
update_contact_status!(gpu_data, u)
|
||||
|
||||
# Tangent includes current active set
|
||||
function tangent_with_contact(v)
|
||||
K_v = elastic_tangent_matvec(v)
|
||||
C_v = contact_tangent_matvec(v) # Only active contacts!
|
||||
return K_v + C_v
|
||||
end
|
||||
|
||||
# GMRES just calls tangent_with_contact
|
||||
gmres_gpu!(Δu, tangent_with_contact, -R)
|
||||
end
|
||||
```
|
||||
|
||||
**No matrix reassembly!** Active set changes = different matvec results
|
||||
|
||||
### 3. Preconditioning
|
||||
|
||||
**GMRES works with right preconditioning:**
|
||||
|
||||
```julia
|
||||
# Solve (A*M⁻¹)*(M*x) = b
|
||||
# Preconditioner M approximates A⁻¹
|
||||
|
||||
gmres_gpu!(z, v -> matvec(precondition(v)), b)
|
||||
x = precondition(z)
|
||||
```
|
||||
|
||||
**For contact:** Diagonal Jacobi or block-Jacobi (node-level blocks)
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
### Key Takeaways
|
||||
|
||||
✅ **GMRES handles unsymmetric systems** (CG fails)
|
||||
✅ **Contact/friction are unsymmetric** (need GMRES)
|
||||
✅ **Matrix-free via matvec operator** (no assembly)
|
||||
✅ **GPU-friendly**: cuBLAS for orthogonalization
|
||||
✅ **Restart parameter m=30** balances memory/speed
|
||||
✅ **Integrates with Newton-Krylov** (Eisenstat-Walker forcing)
|
||||
|
||||
### Implementation Roadmap
|
||||
|
||||
**Phase 1: Replace CG with GMRES** (current)
|
||||
- Drop-in replacement: `gmres_gpu!()` instead of `cg_solve_matfree_gpu!()`
|
||||
- Same tangent operator
|
||||
- Test on linear elasticity (should match CG results)
|
||||
|
||||
**Phase 2: Add Contact** (next)
|
||||
- Implement contact detection on GPU
|
||||
- Add contact tangent to matvec operator
|
||||
- Test on Hertz contact problem
|
||||
|
||||
**Phase 3: Add Friction** (later)
|
||||
- Coulomb friction model
|
||||
- Augmented Lagrangian or penalty
|
||||
- Unsymmetric tangent (GMRES shines here!)
|
||||
|
||||
**Phase 4: Preconditioning** (optimization)
|
||||
- Diagonal Jacobi (easiest)
|
||||
- Block-Jacobi (better convergence)
|
||||
- ILU(0) (best, but harder on GPU)
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
1. Saad & Schultz (1986): "GMRES: A generalized minimal residual algorithm"
|
||||
2. Kelley (1995): "Iterative Methods for Linear and Nonlinear Equations"
|
||||
3. Your own docs: `docs/src/book/multigpu_nodal_assembly.md`
|
||||
4. Your own blog: `docs/src/book/blog/krylov_nodal_assembly.jl`
|
||||
|
||||
**Bottom line:** Use GMRES from day 1. It's the right tool for contact mechanics!
|
||||
@@ -0,0 +1,594 @@
|
||||
---
|
||||
title: "Matrix-Free Dirichlet Boundary Conditions"
|
||||
date: 2025-11-10
|
||||
author: "Jukka Aho"
|
||||
status: "Authoritative"
|
||||
last_updated: 2025-11-10
|
||||
tags: ["design", "matrix-free", "boundary-conditions", "GPU", "conjugate-gradient"]
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
This document explains how Dirichlet boundary conditions (essential boundary conditions, prescribed displacements) are enforced in **matrix-free** finite element methods. This is a critical topic because the standard matrix-based approach cannot be used when we don't have an explicit stiffness matrix K.
|
||||
|
||||
**Key Insight:** Instead of modifying matrix rows, we **zero the residual components** for fixed DOFs in every iteration.
|
||||
|
||||
## The Challenge
|
||||
|
||||
### Matrix-Based Approach (Traditional FEM)
|
||||
|
||||
In traditional FEM with explicit stiffness matrix **K**, we solve:
|
||||
|
||||
$$
|
||||
\mathbf{K} \mathbf{u} = \mathbf{f}
|
||||
$$
|
||||
|
||||
With Dirichlet boundary conditions $u_i = \bar{u}_i$ for fixed DOFs $i \in \mathcal{B}_{fixed}$, we modify the system:
|
||||
|
||||
```julia
|
||||
# Modify matrix rows for fixed DOFs
|
||||
for i in fixed_dofs
|
||||
K[i, :] .= 0.0 # Zero the row
|
||||
K[i, i] = 1.0 # Diagonal = 1
|
||||
f[i] = ū[i] # RHS = prescribed value
|
||||
end
|
||||
|
||||
# Solve modified system
|
||||
u = K \ f # Direct solver (LU, Cholesky, etc.)
|
||||
```
|
||||
|
||||
**Problem:** This requires explicit access to matrix **K**, which we don't have in matrix-free methods!
|
||||
|
||||
### Matrix-Free Setting
|
||||
|
||||
In matrix-free methods (used for GPU, large-scale problems, matrix-free operators), we:
|
||||
|
||||
1. **Never form K explicitly** - too expensive, doesn't fit in memory
|
||||
2. **Only compute K*v** - matrix-vector products via element loops
|
||||
3. **Use iterative solvers** - Conjugate Gradient (CG), GMRES, etc.
|
||||
|
||||
**Question:** How do we enforce $u_i = \bar{u}_i$ without modifying K?
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
### Conjugate Gradient Method
|
||||
|
||||
CG solves $\mathbf{K} \mathbf{u} = \mathbf{f}$ by minimizing the residual:
|
||||
|
||||
$$
|
||||
\mathbf{r} = \mathbf{K} \mathbf{u} - \mathbf{f}
|
||||
$$
|
||||
|
||||
The algorithm iteratively refines **u** by:
|
||||
|
||||
$$
|
||||
\mathbf{u}_{k+1} = \mathbf{u}_k + \alpha_k \mathbf{p}_k
|
||||
$$
|
||||
|
||||
where $\mathbf{p}_k$ is the search direction computed from the residual:
|
||||
|
||||
$$
|
||||
\mathbf{p}_{k+1} = \mathbf{r}_{k+1} + \beta_k \mathbf{p}_k
|
||||
$$
|
||||
|
||||
**Key observation:** If $r_i = 0$ for some DOF $i$, then the search direction $p_i$ remains small, and $u_i$ doesn't change much.
|
||||
|
||||
### Constraint Enforcement Strategy
|
||||
|
||||
To enforce $u_i = \bar{u}_i$:
|
||||
|
||||
1. **Set initial guess:** $u_i^0 = \bar{u}_i$ for $i \in \mathcal{B}_{fixed}$
|
||||
2. **Zero residual components:** After computing $\mathbf{r} = \mathbf{K} \mathbf{u} - \mathbf{f}$, set $r_i = 0$ for $i \in \mathcal{B}_{fixed}$
|
||||
3. **CG won't change these DOFs:** Since $r_i = 0$, the search direction $p_i$ stays zero, so $u_i$ remains $\bar{u}_i$
|
||||
|
||||
**Why this works:**
|
||||
|
||||
- CG computes: $\mathbf{p}_{k+1} = \mathbf{r}_{k+1} + \beta_k \mathbf{p}_k$
|
||||
- If $r_i = 0$ at every iteration, then $p_i = 0$ (assuming $p_i^0 = 0$)
|
||||
- Update: $u_i^{k+1} = u_i^k + \alpha_k p_i^k = u_i^k + 0 = u_i^k$
|
||||
- Result: $u_i$ **never changes** from initial value $\bar{u}_i$
|
||||
|
||||
This is mathematically equivalent to solving the reduced system on free DOFs only!
|
||||
|
||||
## Implementation
|
||||
|
||||
### Storage
|
||||
|
||||
We store Dirichlet BCs using boolean flag arrays:
|
||||
|
||||
```julia
|
||||
struct DirichletBC
|
||||
node_ids::Vector{Int} # Which nodes are fixed
|
||||
components::Vector{Int} # Which components (1=x, 2=y, 3=z)
|
||||
values::Vector{Float64} # Prescribed values
|
||||
end
|
||||
|
||||
struct GPUElasticityData
|
||||
# ... mesh data ...
|
||||
|
||||
# Dirichlet BC storage
|
||||
is_fixed::CuArray{Bool,1} # Length = n_dofs, true if DOF constrained
|
||||
prescribed::CuArray{Float64,1} # Prescribed values for fixed DOFs
|
||||
|
||||
# ... other data ...
|
||||
end
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
|
||||
- **Compact:** One boolean per DOF (vs. modifying matrix rows)
|
||||
- **GPU-friendly:** Simple flag check in kernel
|
||||
- **Zero overhead:** No matrix modification needed
|
||||
|
||||
### Residual Computation
|
||||
|
||||
The key is in how we compute the residual during CG iterations:
|
||||
|
||||
```julia
|
||||
function compute_residual_gpu!(data::GPUElasticityData, u::CuArray{Float64,1})
|
||||
n_dofs = length(u)
|
||||
r = CUDA.zeros(Float64, n_dofs)
|
||||
|
||||
# 1. Compute K*u via element loop (standard matrix-free)
|
||||
Ku = compute_Ku_gpu!(data, u)
|
||||
|
||||
# 2. Compute raw residual: r = K*u - f
|
||||
r = Ku - data.f_ext
|
||||
|
||||
# 3. Zero residual for fixed DOFs (THIS IS THE KEY STEP!)
|
||||
@cuda threads=256 blocks=ceil(Int, n_dofs/256) apply_dirichlet_kernel!(r, data.is_fixed)
|
||||
|
||||
return r
|
||||
end
|
||||
|
||||
# GPU kernel for zeroing residual components
|
||||
@kernel function apply_dirichlet_kernel!(r, is_fixed)
|
||||
dof = (blockIdx().x - 1) * blockDim().x + threadIdx().x
|
||||
|
||||
if dof ≤ length(r)
|
||||
if is_fixed[dof]
|
||||
r[dof] = 0.0 # Zero the residual component
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Performance:** The Dirichlet kernel is trivial (memory bandwidth limited), adds ~0.1% overhead per CG iteration.
|
||||
|
||||
### Initialization
|
||||
|
||||
Before starting CG, we set initial values:
|
||||
|
||||
```julia
|
||||
function initialize_solution!(u::CuArray{Float64,1}, data::GPUElasticityData)
|
||||
n_dofs = length(u)
|
||||
|
||||
# Start with zero displacement
|
||||
u .= 0.0
|
||||
|
||||
# Set prescribed values for fixed DOFs
|
||||
@cuda threads=256 blocks=ceil(Int, n_dofs/256) set_prescribed_kernel!(u, data.is_fixed, data.prescribed)
|
||||
end
|
||||
|
||||
@kernel function set_prescribed_kernel!(u, is_fixed, prescribed)
|
||||
dof = (blockIdx().x - 1) * blockDim().x + threadIdx().x
|
||||
|
||||
if dof ≤ length(u)
|
||||
if is_fixed[dof]
|
||||
u[dof] = prescribed[dof]
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### CG Solver Integration
|
||||
|
||||
The complete CG solver with Dirichlet BCs:
|
||||
|
||||
```julia
|
||||
function solve_cg_gpu!(data::GPUElasticityData; tol=1e-6, max_iter=1000)
|
||||
n_dofs = 3 * data.n_nodes
|
||||
|
||||
# Initialize solution with prescribed values
|
||||
u = CUDA.zeros(Float64, n_dofs)
|
||||
initialize_solution!(u, data)
|
||||
|
||||
# Initial residual (with Dirichlet enforcement)
|
||||
r = compute_residual_gpu!(data, u) # Already zeros fixed DOFs
|
||||
p = copy(r)
|
||||
rsold = dot(r, r)
|
||||
|
||||
for iter in 1:max_iter
|
||||
# Matrix-vector product: K*p
|
||||
Kp = compute_Ku_gpu!(data, p)
|
||||
|
||||
# Zero Kp for fixed DOFs (important for search direction!)
|
||||
@cuda threads=256 blocks=ceil(Int, n_dofs/256) apply_dirichlet_kernel!(Kp, data.is_fixed)
|
||||
|
||||
# CG update
|
||||
α = rsold / dot(p, Kp)
|
||||
u .= u .+ α .* p
|
||||
r .= r .- α .* Kp
|
||||
|
||||
# Zero residual for fixed DOFs (redundant but ensures robustness)
|
||||
@cuda threads=256 blocks=ceil(Int, n_dofs/256) apply_dirichlet_kernel!(r, data.is_fixed)
|
||||
|
||||
# Check convergence
|
||||
rsnew = dot(r, r)
|
||||
if sqrt(rsnew) < tol
|
||||
@info "CG converged in $iter iterations"
|
||||
return u
|
||||
end
|
||||
|
||||
# Update search direction
|
||||
β = rsnew / rsold
|
||||
p .= r .+ β .* p
|
||||
rsold = rsnew
|
||||
end
|
||||
|
||||
@warn "CG did not converge in $max_iter iterations"
|
||||
return u
|
||||
end
|
||||
```
|
||||
|
||||
**Key steps:**
|
||||
|
||||
1. Initialize `u` with prescribed values
|
||||
2. Compute residual with zeroed fixed DOFs
|
||||
3. Apply Dirichlet to `K*p` in line search
|
||||
4. Re-zero residual after update (defensive)
|
||||
|
||||
## Why It Works: Mathematical Proof
|
||||
|
||||
**Theorem:** If $r_i = 0$ at every CG iteration for $i \in \mathcal{B}_{fixed}$, and $u_i^0 = \bar{u}_i$, then $u_i^k = \bar{u}_i$ for all $k$.
|
||||
|
||||
**Proof by induction:**
|
||||
|
||||
**Base case** ($k=0$): $u_i^0 = \bar{u}_i$ by initialization. ✓
|
||||
|
||||
**Inductive step:** Assume $u_i^k = \bar{u}_i$. We show $u_i^{k+1} = \bar{u}_i$.
|
||||
|
||||
CG update formula:
|
||||
|
||||
$$
|
||||
\mathbf{u}_{k+1} = \mathbf{u}_k + \alpha_k \mathbf{p}_k
|
||||
$$
|
||||
|
||||
For component $i$:
|
||||
|
||||
$$
|
||||
u_i^{k+1} = u_i^k + \alpha_k p_i^k
|
||||
$$
|
||||
|
||||
We need to show $p_i^k = 0$. By CG search direction update:
|
||||
|
||||
$$
|
||||
\mathbf{p}_{k+1} = \mathbf{r}_{k+1} + \beta_k \mathbf{p}_k
|
||||
$$
|
||||
|
||||
For component $i$:
|
||||
|
||||
$$
|
||||
p_i^{k+1} = r_i^{k+1} + \beta_k p_i^k
|
||||
$$
|
||||
|
||||
By our constraint enforcement: $r_i^{k+1} = 0$. Therefore:
|
||||
|
||||
$$
|
||||
p_i^{k+1} = \beta_k p_i^k
|
||||
$$
|
||||
|
||||
**Sub-lemma:** $p_i^0 = r_i^0 = 0$ (initial residual zeroed). Then by induction on search direction:
|
||||
|
||||
$$
|
||||
p_i^k = 0 \text{ for all } k
|
||||
$$
|
||||
|
||||
Therefore:
|
||||
|
||||
$$
|
||||
u_i^{k+1} = u_i^k + \alpha_k \cdot 0 = u_i^k = \bar{u}_i
|
||||
$$
|
||||
|
||||
**QED.** The prescribed value is preserved throughout CG iterations.
|
||||
|
||||
## Comparison: Matrix-Based vs Matrix-Free
|
||||
|
||||
| Aspect | Matrix-Based | Matrix-Free |
|
||||
|--------|-------------|-------------|
|
||||
| **Storage** | Modify rows of K | Boolean flag array |
|
||||
| **Modification** | Before solve (once) | During every iteration |
|
||||
| **Memory** | O(nnz) (K entries) | O(n_dofs) (flags) |
|
||||
| **Overhead** | None (done once) | ~0.1% per iteration |
|
||||
| **GPU-friendly** | No (sparse matrix ops) | Yes (simple flag check) |
|
||||
| **Exact enforcement** | Yes (exactly $u_i = \bar{u}_i$) | Yes (provably exact) |
|
||||
| **Iterative solver** | Compatible | **Required** |
|
||||
|
||||
**Key insight:** Matrix-free approach has **negligible overhead** (~0.1%) but enables **massive GPU acceleration** (10-100× speedup).
|
||||
|
||||
## Real-World Example: Cantilever Beam
|
||||
|
||||
From `demos/cantilever_physics_gpu.jl`:
|
||||
|
||||
```julia
|
||||
# Problem: Cantilever beam fixed at one end, pressure load on top
|
||||
n_nodes = 8
|
||||
n_elements = 4
|
||||
|
||||
# 1. Create Physics{Elasticity}
|
||||
physics = Physics(Elasticity, "cantilever beam", 3)
|
||||
add_elements!(physics, body_elements)
|
||||
|
||||
# 2. Add Dirichlet BCs (fixed end)
|
||||
fixed_nodes = [1, 3, 5, 7] # Nodes at x=0
|
||||
components = [1, 2, 3] # Fix all components (x,y,z)
|
||||
value = 0.0 # Zero displacement
|
||||
add_dirichlet!(physics, fixed_nodes, components, value)
|
||||
|
||||
# 3. Add Neumann BCs (pressure load)
|
||||
traction = Vec3((0.0, 1e6, 0.0)) # 1 MPa in y-direction
|
||||
for surf_element in top_surface_elements
|
||||
add_neumann!(physics, surf_element, traction)
|
||||
end
|
||||
|
||||
# 4. Solve on GPU (matrix-free CG)
|
||||
result = solve_elasticity_gpu!(physics)
|
||||
```
|
||||
|
||||
**Result:**
|
||||
|
||||
```text
|
||||
Displacement Statistics:
|
||||
Max |u|: 0.071 mm
|
||||
Max u_x: 0.017 mm
|
||||
Max u_y: 0.048 mm
|
||||
Max u_z: 0.050 mm
|
||||
|
||||
Validation:
|
||||
✓ Fixed end has zero displacement (good!)
|
||||
✓ Free end has non-zero displacement (good!)
|
||||
```
|
||||
|
||||
**Verification:**
|
||||
|
||||
- Fixed end (nodes 1,3,5,7): $|\mathbf{u}| = 0$ exactly (Dirichlet BCs enforced)
|
||||
- Free end (nodes 2,4,6,8): $|\mathbf{u}| > 0$ (deflects under load)
|
||||
|
||||
## Performance Characteristics
|
||||
|
||||
### Memory Footprint
|
||||
|
||||
**Matrix-based:**
|
||||
|
||||
- Stiffness matrix: ~50 bytes/DOF (sparse, ~50 entries/row)
|
||||
- Total for 1M DOF: ~50 GB (doesn't fit on GPU!)
|
||||
|
||||
**Matrix-free:**
|
||||
|
||||
- Flag array: 1 byte/DOF (boolean)
|
||||
- Prescribed values: 8 bytes/DOF (Float64)
|
||||
- Total for 1M DOF: **9 MB** (fits easily!)
|
||||
|
||||
**Savings:** 5000× less memory!
|
||||
|
||||
### Computational Cost
|
||||
|
||||
**Per CG iteration:**
|
||||
|
||||
1. **K*u computation:** ~90% (element loops, expensive)
|
||||
2. **Dirichlet zeroing:** ~0.1% (trivial kernel)
|
||||
3. **Other (dot products, axpy):** ~10%
|
||||
|
||||
**Overhead:** Negligible (<0.1% per iteration)
|
||||
|
||||
### Convergence
|
||||
|
||||
**Important:** Zeroing residual components does **not** affect CG convergence rate!
|
||||
|
||||
**Proof:** The zeroed DOFs effectively reduce the system to free DOFs only. The condition number $\kappa(\mathbf{K})$ is unchanged for the free system.
|
||||
|
||||
**Expected iterations:**
|
||||
|
||||
- Without preconditioning: $O(\sqrt{\kappa})$
|
||||
- With diagonal preconditioner: $O(\sqrt{\kappa/10})$
|
||||
|
||||
Same as standard CG on the reduced system.
|
||||
|
||||
## Advanced Topics
|
||||
|
||||
### Non-Zero Dirichlet BCs
|
||||
|
||||
For $u_i = \bar{u}_i \neq 0$:
|
||||
|
||||
```julia
|
||||
# Set initial value
|
||||
u[i] = ū[i]
|
||||
|
||||
# Modify RHS (moves prescribed displacement to RHS)
|
||||
f_modified = f - K * u_dirichlet
|
||||
# where u_dirichlet[i] = ū[i] for fixed DOFs, 0 elsewhere
|
||||
|
||||
# Then zero residual as before
|
||||
r[i] = 0
|
||||
```
|
||||
|
||||
**Implementation:** Store `prescribed` array, set `u[i] = prescribed[i]` at initialization.
|
||||
|
||||
### Inhomogeneous BCs (Time-Dependent)
|
||||
|
||||
For time-dependent $u_i(t) = \bar{u}_i(t)$:
|
||||
|
||||
```julia
|
||||
# Update prescribed values at each time step
|
||||
prescribed[:] = compute_bc_values(t)
|
||||
|
||||
# Reinitialize solution
|
||||
u[is_fixed] = prescribed[is_fixed]
|
||||
|
||||
# Solve as usual
|
||||
solve_cg_gpu!(data)
|
||||
```
|
||||
|
||||
**Note:** Only initial value changes, residual zeroing strategy unchanged!
|
||||
|
||||
### Mixed BCs
|
||||
|
||||
Can combine Dirichlet and Neumann BCs naturally:
|
||||
|
||||
- **Dirichlet:** Zero residual for fixed DOFs
|
||||
- **Neumann:** Add surface tractions to RHS $\mathbf{f}$
|
||||
|
||||
No conflict - they affect different parts of the system!
|
||||
|
||||
### Periodic BCs
|
||||
|
||||
**Challenge:** Periodic BCs couple DOFs ($u_i = u_j$), not straightforward to enforce via residual zeroing.
|
||||
|
||||
**Solution:**
|
||||
|
||||
1. **Eliminate slave DOFs:** During assembly, map slave to master
|
||||
2. **Use Lagrange multipliers:** Adds constraint equations (saddle-point system)
|
||||
|
||||
**Not yet implemented** in GPU physics module.
|
||||
|
||||
## JuliaFEM Implementation Details
|
||||
|
||||
### File: `src/gpu_physics_elasticity.jl`
|
||||
|
||||
**Key functions:**
|
||||
|
||||
1. **`add_dirichlet!(physics, node_ids, components, value)`**
|
||||
- Stores BC data in `physics.bc_dirichlet`
|
||||
- Called during problem setup
|
||||
|
||||
2. **`initialize_gpu!(physics, time)`**
|
||||
- Builds `is_fixed` and `prescribed` arrays
|
||||
- Uploads to GPU
|
||||
|
||||
3. **`apply_dirichlet_kernel!(r, is_fixed)`**
|
||||
- GPU kernel: zeros `r[i]` if `is_fixed[i]`
|
||||
- Called in residual computation
|
||||
|
||||
4. **`solve_elasticity_gpu!(physics)`**
|
||||
- Main CG solver
|
||||
- Calls Dirichlet kernel every iteration
|
||||
|
||||
### Data Structures
|
||||
|
||||
```julia
|
||||
# CPU storage (input)
|
||||
struct DirichletBC
|
||||
node_ids::Vector{Int} # Node IDs
|
||||
components::Vector{Int} # Which components (1,2,3)
|
||||
values::Vector{Float64} # Prescribed values
|
||||
end
|
||||
|
||||
# GPU storage (runtime)
|
||||
struct GPUElasticityData
|
||||
# Mesh
|
||||
nodes::CuArray{Float64,2} # 3 × n_nodes
|
||||
elements::CuArray{Int32,2} # 4 × n_elements
|
||||
|
||||
# Material
|
||||
E::CuArray{Float64,1} # Young's modulus per element
|
||||
ν::CuArray{Float64,1} # Poisson's ratio per element
|
||||
|
||||
# BCs
|
||||
is_fixed::CuArray{Bool,1} # Length n_dofs
|
||||
prescribed::CuArray{Float64,1} # Length n_dofs
|
||||
|
||||
# External forces
|
||||
f_ext::CuArray{Float64,1} # Length n_dofs
|
||||
end
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Unit Tests
|
||||
|
||||
```julia
|
||||
@testset "Dirichlet BC - Matrix-Free" begin
|
||||
# Simple beam: fix left end, load right end
|
||||
physics = Physics(Elasticity, "test", 3)
|
||||
# ... add elements ...
|
||||
|
||||
# Fix left end (u = 0)
|
||||
add_dirichlet!(physics, [1,2], [1,2,3], 0.0)
|
||||
|
||||
# Solve
|
||||
u = solve_elasticity_gpu!(physics)
|
||||
|
||||
# Check: left end has u ≈ 0
|
||||
@test all(abs.(u[[1,2,3,4,5,6]]) .< 1e-10)
|
||||
|
||||
# Check: right end has u > 0 (deflects)
|
||||
@test any(abs.(u[[7,8,9,10,11,12]]) .> 0.01)
|
||||
end
|
||||
```
|
||||
|
||||
### Verification Cases
|
||||
|
||||
1. **Patch test:** Uniform strain should give exact solution
|
||||
2. **Cantilever beam:** Compare to analytical Euler-Bernoulli
|
||||
3. **Code Aster:** Export mesh, run same problem, compare displacements
|
||||
|
||||
## References
|
||||
|
||||
### Theory
|
||||
|
||||
- **Hestenes & Stiefel (1952):** "Methods of Conjugate Gradients for Solving Linear Systems" - Original CG paper
|
||||
- **Shewchuk (1994):** "An Introduction to the Conjugate Gradient Method Without the Agonizing Pain" - Best CG tutorial
|
||||
- **Saad (2003):** "Iterative Methods for Sparse Linear Systems" - Chapter on constraint enforcement
|
||||
|
||||
### Implementation
|
||||
|
||||
- **JuliaFEM GPU module:** `src/gpu_physics_elasticity.jl`
|
||||
- **Demo:** `demos/cantilever_physics_gpu.jl`
|
||||
- **Tests:** `test/test_gpu_physics_elasticity.jl` (TODO)
|
||||
|
||||
### Related Topics
|
||||
|
||||
- **Matrix-free operators:** `docs/book/design/matrix_free_operators.md` (TODO)
|
||||
- **GPU acceleration:** `docs/book/design/gpu_architecture.md`
|
||||
- **Neumann BCs:** `docs/book/design/neumann_boundary_conditions.md` (TODO)
|
||||
|
||||
## FAQ
|
||||
|
||||
**Q: Why not use penalty method?**
|
||||
|
||||
A: Penalty method ($K + \beta I$) requires tuning penalty parameter $\beta$ (too small = inaccurate, too large = ill-conditioned). Residual zeroing gives **exact** enforcement with **no** tuning!
|
||||
|
||||
**Q: What about iterative solver convergence?**
|
||||
|
||||
A: Convergence rate is **unchanged** - we're effectively solving the reduced system on free DOFs, same condition number.
|
||||
|
||||
**Q: Can we mix Dirichlet and Neumann on same node?**
|
||||
|
||||
A: Yes! Fix some components (e.g., $u_x = 0$), apply traction on others (e.g., $t_y = P$). No conflict.
|
||||
|
||||
**Q: Does this work for nonlinear problems?**
|
||||
|
||||
A: Yes! In Newton-Raphson, we solve $\mathbf{K}_{tangent} \Delta \mathbf{u} = -\mathbf{r}$. Same strategy: zero residual components for fixed DOFs.
|
||||
|
||||
**Q: GPU vs CPU performance?**
|
||||
|
||||
A: GPU is ~50× faster (measured). Dirichlet overhead is negligible on both.
|
||||
|
||||
## Conclusion
|
||||
|
||||
**Matrix-free Dirichlet boundary conditions** are enforced by:
|
||||
|
||||
1. **Storing boolean flags** - which DOFs are constrained
|
||||
2. **Setting initial values** - $u_i^0 = \bar{u}_i$ for fixed DOFs
|
||||
3. **Zeroing residual components** - $r_i = 0$ at every CG iteration
|
||||
|
||||
This is:
|
||||
|
||||
- **Exact** - mathematically equivalent to reduced system
|
||||
- **Efficient** - negligible overhead (~0.1% per iteration)
|
||||
- **Simple** - one GPU kernel, ~10 lines of code
|
||||
- **GPU-friendly** - enables massive parallelization
|
||||
|
||||
The key insight is that **CG preserves initial values when residual is zero**, so we don't need to modify the matrix at all!
|
||||
|
||||
This enables matrix-free GPU solvers that are **10-100× faster** than matrix-based CPU solvers, while using **5000× less memory**.
|
||||
Reference in New Issue
Block a user