- name
- gpu-optimization
- description
- GPU optimization workflow using uipc.profile, uipc.profile.nsight, and Nsight Compute CLI. Use when profiling, optimizing, or benchmarking CUDA kernels.
# GPU Optimization Pipeline
Workflow for profiling and optimizing CUDA kernels in libuipc using `uipc.profile` + `uipc.profile.nsight` + Nsight Compute CLI (`ncu`).
For building and installing, see [cmake-workflow](mdc:.cursor/skills/cmake-workflow/SKILL.md).
**Build type**: Always use `Release` for benchmarking and profiling. `Debug` is too slow; `RelWithDebInfo` adds debug info overhead that skews results.
## Agent Rules
- Do NOT read `.ncu-rep` files — they are binary, for Nsight Compute GUI only.
- Do NOT benchmark or profile with `Debug` builds — results are meaningless.
- Do NOT optimize kernels in stages that take <5% of total frame time.
- Always read `timer_frames.json` for the actual timer hierarchy — it is dynamic and varies per scene/run.
## CLI Commands
```bash
# List available benchmark scenes
python -m uipc.cli.benchmark list
# Run a benchmark (headless, timer-based); --scene accepts multiple names
python -m uipc.cli.benchmark run --scene <scene> [<scene2> ...] --frames <N> --output <dir>
# Profile with Nsight Compute (kernel-level GPU metrics)
python -m uipc.cli.benchmark profile --scene <scene> --frames <N> --output <dir> --count <K> --skip <S> --ncu-set <set>
# Analyze bottlenecks from a benchmark result directory
python -m uipc.cli.benchmark analyze <result_dir> --ncu-csv <csv_path>
# Compare two benchmark result directories
python -m uipc.cli.benchmark compare <before_dir> <after_dir> --output <out_dir>
```
**`profile` flags:**
- `--ncu-set default` — basic metrics (SM%, occupancy, registers). Use `full` for duration/memory but requires admin/root.
- `--count <K>` — profile first K kernel launches.
- `--skip <S>` — skip initial warmup/init kernels.
- `--ncu-path <path>` — explicit path to `ncu` executable. Auto-detected via `NCU_PATH` env var, `PATH`, or default NVIDIA install locations.
**Note**: The CLI `profile` defaults to `--ncu-set default`. The Python API `nsight.run()` defaults to `ncu_set='full'`. Be explicit to avoid confusion.
## Agent Workflow
### Step 1: Run Benchmark + Profile
Run both `run` (for `SimulationStats` timer data) and `profile` (for Nsight Compute kernel metrics). Both are needed to make good optimization decisions.
### Step 2: Read Both Reports
**Benchmark report** (`report/report.md` from `run`):
- Shows **wall-clock time** per simulation stage as a hierarchical tree (e.g., `Newton Iteration: 45%`, `Line Search: 30%`)
- Tells you **which stages dominate total frame time** — this is where optimization has the most impact
**Nsight Compute report** (`<scene>_report.md` / `<scene>_report.json` from `profile`):
- Shows **per-kernel GPU metrics** (duration, SM%, occupancy, registers)
- Tells you **which kernels are inefficient** and why (high register usage, low occupancy, memory-bound, etc.)
| File | Source | What it tells you |
|------|--------|-------------------|
| `report/report.md` | `run` | Wall-clock time breakdown by stage — **what matters most** |
| `timer_frames.json` | `run` | Per-frame timer tree as JSON — **source of truth for hierarchy** |
| `<scene>_report.md` | `profile` | Kernel GPU metrics — **what's inefficient** |
| `<scene>_report.json` | `profile` | Structured kernel data with `source_hint` and `optimization_hints` |
| `<scene>.ncu-rep` | `profile` | Binary — do NOT read, for Nsight Compute GUI only |
### Step 3: Cross-Reference to Prioritize
**A kernel is worth optimizing only if it's both inefficient AND in a hot stage.**
1. Read `timer_frames.json` or `report/report.md` to find which stages take the most wall-clock time (e.g., `Detect DCD Candidates: 40%`).
2. Read the ncu report to find which kernels in those stages have bad metrics.
3. Match kernel names to stages using the class/function names (e.g., `StacklessBVH::*` -> collision detection stage). Use the `source_hint` field in the JSON report.
4. **Ignore** kernels with bad metrics in stages that take <5% of frame time — not worth optimizing.
5. **Prioritize** kernels that are both in a hot stage AND have clear inefficiencies (high registers, low occupancy).
### Step 4: Optimize
1. Follow `source_hint` in the JSON report to find the CUDA source.
2. Search for the kernel's class/method name in that directory.
3. Read the `.cu` file and optimize the kernel.
4. Rebuild with `Release` (see [cmake-workflow](mdc:.cursor/skills/cmake-workflow/SKILL.md)), re-benchmark, and compare.
### Adding Finer-Grained Timers in C++
If the benchmark report shows a hot stage but you need more detail to locate the bottleneck within it, add `Timer` scopes in the C++ source:
```cpp
#include <uipc/common/timer.h>
void MySystem::do_something()
{
{
Timer timer{"MySystem::phase_A"};
// ... GPU work ...
}
{
Timer timer{"MySystem::phase_B"};
// ... GPU work ...
}
}
```
The `Timer` is scoped — it starts on construction and stops on destruction. Nested timers form a tree. The names appear in `timer_frames.json` and `report/report.md`, letting you drill down into which sub-phase of a hot stage is the actual bottleneck before running the more expensive `ncu` profiling.
## Simulation Timer Hierarchy
The timer tree is **dynamic** — it varies per run depending on which simulation features are active (contact, friction, animation, etc.). The actual tree for any run is stored in `timer_frames.json` (written by `run`). Always read that file for the real hierarchy.
Below is a **representative example** with all features enabled, showing the typical nesting from [sim_engine_do_advance.cu](mdc:src/backends/cuda/engine/sim_engine_do_advance.cu), [global_linear_system.cu](mdc:src/backends/cuda/linear_system/global_linear_system.cu), and [linear_pcg.cu](mdc:src/backends/cuda/linear_system/linear_pcg.cu):
```
Pipeline engine/
├── Rebuild Scene engine/
│ └── Update Diff Parm diff_sim/ (conditional)
└── Simulation engine/
├── Clear External Forces external_force/ (conditional)
├── Step Animation animator/ (conditional)
├── Compute External Force Accel. external_force/ (conditional)
├── Detect DCD Candidates collision_detection/ (conditional)
├── Newton Iteration engine/ (LOOP)
│ ├── Detect DCD Candidates collision_detection/ (iter > 0)
│ ├── Compute DyTopo Effect dytopo_effect_system/ (conditional)
│ │ ├── Assemble Dytopo Effect
│ │ ├── Convert Dytopo Matrix
│ │ └── Distribute Dytopo Effect
│ ├── Solve Global Linear System linear_system/
│ │ ├── Build Linear System
│ │ └── Solve Linear System
│ │ └── PCG
│ │ ├── Apply Preconditioner
│ │ └── SpMV (per PCG iteration)
│ └── Line Search line_search/
│ ├── Detect Trajectory Cand. collision_detection/
│ ├── Compute Energy line_search/ (initial E0)
│ ├── Filter CCD TOI collision_detection/
│ ├── Compute CFL Condition contact_system/
│ └── Line Search Iteration line_search/ (LOOP)
│ ├── Filter Contact Cand. collision_detection/
│ └── Compute Energy line_search/
└── Update Velocity time_integrator/
```
Parent timer durations **include** their children. E.g., if `Newton Iteration` is 80% of frame time, `PCG` and `Line Search` durations are already counted inside that 80%.
## Identifying Kernels in ncu Output
All backend kernels are named `__global__` functions (defined in anonymous namespaces) launched with raw `<<<>>>` — kernel symbols are directly readable in ncu reports, e.g.:
```
InfoStacklessBVHSimplexTrajectoryFilter_detect_k1_kernel
FEMLineSearchReporter_step_forward_kernel
StacklessBVH::... (older helpers may still appear as <Class>::<method>)
```
Naming convention: `<OwningClass>_<original_function>[_kN]_kernel` (`_kN` = Nth launch point in the same function). To find the source of a kernel: search the kernel name (or its suffix) in `src/backends/cuda/`.
Legacy note: before the named-kernel migration, kernels were launched via `muda::ParallelFor().apply(N, lambda)` and appeared as `parallel_for_kernel<Class::method()::lambda>` (with `#N` for the Nth lambda in a function). The `_shorten_kernel_name()` function in [nsight.py](mdc:python/src/uipc/profile/nsight.py) was written for that scheme; with named kernels it passes the readable name through unchanged.
**Buffer operations** (`buffer_fill_kernel<T>`, `buffer_copy_kernel<T>` in `cuda_tool`) are memory operations, usually not optimization targets.
## Python API
### Package layout
```
uipc.profile — benchmark runner (timer-based, in-process)
uipc.profile.nsight — Nsight Compute profiler (kernel-level, ncu subprocess)
```
The primary input for both is a `World`. A `Scene` is also accepted as a convenience shortcut (a temporary Engine + World is created internally).
### Setup
```python
from uipc import Scene, Engine, World
from uipc.assets import load
scene = Scene(Scene.default_config())
load('cube_ground', scene)
engine = Engine('cuda', 'my_workspace')
world = World(engine)
world.init(scene)
```
### `uipc.profile` — benchmark runner
```python
from uipc import profile
# Simple: benchmark 10 frames from the world's current frame
result = profile.run(world, num_frames=10, name='baseline', output_dir='bench')
print(result['summary'])
# Flexible: mix warmup and benchmarking
with profile.session(world, name='baseline', output_dir='bench') as s:
s.advance(50) # warmup 50 frames (no stats)
s.profile(10) # benchmark 10 frames (collect stats)
print(s.result['summary'])
# Compare two saved benchmark directories
md = profile.compare('bench/baseline', 'bench/optimized', output_dir='comparison')
print(md)
# Load a saved result for later analysis
data = profile.load_result('bench/baseline')
```
### `uipc.profile.nsight` — Nsight Compute profiler
```python
from uipc.profile import nsight
# Simple: profile 2 frames under ncu
result = nsight.run(world, num_frames=2, name='cube_ground',
output_dir='ncu_results', ncu_set='default')
# Flexible: mix warmup and profiling
with nsight.session(world, name='cube_ground') as s:
s.profile(10) # profiles from world.frame()
print(s.result)
```
When a `World` is passed, `nsight` automatically calls `world.dump()` and uses `world.recover()` in the subprocess for instant state restoration (no replay cost). The engine workspace is obtained via `WorldVisitor(world).engine().workspace()`.
### `SimulationStats` visualization tools
Available on the `stats` object in benchmark results (`result['stats']`):
- `stats.profiler_heatmap()` — sunburst chart of timer breakdown
- `stats.system_dependency_graph(workspace)` — directed graph of backend system dependencies
- `stats.plot(keys, metric='duration')` — per-frame line/bar chart
- `stats.to_markdown(keys)` — Markdown table of per-frame values
## Key CUDA Kernel Directories
All relative to `src/backends/cuda/`:
- `finite_element/` — FEM constitutions, gradient/Hessian assembly
- `affine_body/` — Affine body dynamics
- `linear_system/` — PCG solver, SpMV, preconditioners
- `collision_detection/` — BVH, trajectory filtering, DCD
- `contact_system/` — Contact forces (IPC barrier)
- `global_geometry/` — Vertex management, bounding boxes
- `time_integrator/` — Time integration, velocity update
- `dytopo_effect_system/` — Dynamic topology effects
- `line_search/` — Line search energy evaluation
- `external_force/` — External force computation
- `animator/` — Animation stepping
- `diff_sim/` — Differentiable simulation
- `coupling_system/` — Multi-body coupling
- `implicit_geometry/` — Implicit geometry representations
- `inter_primitive_effect_system/` — Inter-primitive effects
- `newton_tolerance/` — Newton convergence tolerance
- `engine/` — Core pipeline orchestration
- `utils/` — Shared utilities
## Optimization Checklist
When the bottleneck report identifies a hot kernel:
1. **Memory-bound** (high Mem%, low SM%): Coalesced access, shared memory, reduce data movement.
2. **Compute-bound** (high SM%, low Mem%): Algorithmic improvements, intrinsics.
3. **Low occupancy**: Reduce register pressure (`__launch_bounds__`), adjust block size.
4. **Many small launches**: Kernel fusion or batching via `muda::ParallelFor`.
5. **High launch overhead**: Reduce host-device sync points.
6. **High register count** (>64/thread): Simplify kernel logic, split into multiple passes, use `__launch_bounds__(blockSize, minBlocksPerSM)`.
## Scenes
Scenes are loaded from [HuggingFace: MuGdxy/uipc-assets](https://huggingface.co/datasets/MuGdxy/uipc-assets). Each asset has a `scene.py` with `build_scene(scene)`. The asset module is at [assets/__init__.py](mdc:python/src/uipc/assets/__init__.py).
## Output Structure
### Benchmark (`run`)
```
<output_dir>/<scene_name>/
benchmark.json # metadata (name, wall_time, num_frames, summary)
timer_frames.json # per-frame timer tree (JSON) — read this for hierarchy
workspace_<name>/ # engine workspace (contains systems.json)
report/
report.md # summary with charts — AGENT: read this
profiler_heatmap.svg # sunburst chart
system_deps.svg # system dependency graph (if workspace exists)
*.svg # per-timer charts
```
### Nsight Compute (`profile`)
```
<output_dir>/
<scene>_report.md # AGENT: read this — kernel hotspot table
<scene>_report.json # AGENT: read this — structured metrics + source_hints
<scene>.ncu-rep # binary — do NOT read
<scene>.csv # raw CSV (already parsed into reports)
```
JSON report per kernel:
```json
{
"name": "StacklessBVH::calcExtNodeSplitMetrics",
"launches": 3,
"total_duration_ns": 123456.0,
"registers_per_thread": 40,
"avg_sm_pct": 100.0,
View on GitHub