--- 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 [ ...] --frames --output # Profile with Nsight Compute (kernel-level GPU metrics) python -m uipc.cli.benchmark profile --scene --frames --output --count --skip --ncu-set # Analyze bottlenecks from a benchmark result directory python -m uipc.cli.benchmark analyze --ncu-csv # Compare two benchmark result directories python -m uipc.cli.benchmark compare --output ``` **`profile` flags:** - `--ncu-set default` — basic metrics (SM%, occupancy, registers). Use `full` for duration/memory but requires admin/root. - `--count ` — profile first K kernel launches. - `--skip ` — skip initial warmup/init kernels. - `--ncu-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** (`_report.md` / `_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** | | `_report.md` | `profile` | Kernel GPU metrics — **what's inefficient** | | `_report.json` | `profile` | Structured kernel data with `source_hint` and `optimization_hints` | | `.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 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 ::) ``` Naming convention: `_[_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` (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`, `buffer_copy_kernel` 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`) ``` // benchmark.json # metadata (name, wall_time, num_frames, summary) timer_frames.json # per-frame timer tree (JSON) — read this for hierarchy workspace_/ # 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`) ``` / _report.md # AGENT: read this — kernel hotspot table _report.json # AGENT: read this — structured metrics + source_hints .ncu-rep # binary — do NOT read .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, "avg_occupancy_pct": 48.0, "source_hint": "src/backends/cuda/collision_detection/", "optimization_hints": ["Low occupancy - consider reducing registers"], "full_names": ["void muda::parallel_for_kernel<...>"] } ```