Skip to content

Optimize HashGrid candidate traversal with matching cell groups - #282

Open
Huangzizhou wants to merge 3 commits into
ipc-sim:mainfrom
Huangzizhou:codex/hashgrid-cell-group-traversal
Open

Huangzizhou wants to merge 3 commits into
ipc-sim:mainfrom
Huangzizhou:codex/hashgrid-cell-group-traversal

Conversation

@Huangzizhou

@Huangzizhou Huangzizhou commented Oct 30, 2026 •

Copy link
Copy Markdown
Collaborator

Description

Optimize HashGrid candidate traversal by joining already-sorted equal-key cell runs and enumerating only pairs inside matching cells. Same-type relations enumerate the upper triangle.

Cell descriptors are processed in batches of at most 65,536 cells. Same-type descriptors store one range instead of two; descriptor payload is bounded to 1 MiB for same-type queries and 2 MiB for cross-type queries on LP64, independent of occupied-cell count. Candidate buffers and deduplication storage still scale with generated candidates. Small cells share an outer parallel loop; dense cells retain the two-dimensional split with a grain size of 64 entries per axis.

Collision filters run before AABB overlap tests, preserving the legacy rejection order and avoiding the reject-heavy performance regression found during review. All new control statements have braces.

Applies to all six candidate relations, including static and swept bounds. Public API/ABI, C++17 requirements, broad-phase selection, and candidate sorting/deduplication remain unchanged. No ESP evaluator or PolyFEM solver changes are included.

Base: b9d03258956c94f7bc8d861929430dc7a9473458. Reviewed version: b2b9769. Updated version: cc352d1.

Type of change

  • Enhancement

Validation

  • Release rebuild, macOS arm64, Apple Clang 17, repository CMake dependencies.
  • ipc_toolkit_tests '[hash_grid][regression]' --rng-seed 282: 4 cases / 2,222 assertions passed. Includes exact ordered IDs, all six relations against brute force, 2D/3D, static/swept bounds, filters, inflation, empty relations, clear/reuse, and full/partial cell batches at 65,535 / 65,536 / 65,537 cells.
  • ipc_toolkit_tests '[broad_phase]' --skip-benchmarks --rng-seed 282: 23 cases passed / 1 skipped / 2,930,505 assertions passed. The skip needs private benchmark meshes.
  • Direct side-by-side base-versus-updated HashGrid tests: 5 cases / 76,612 assertions passed, covering 1,608 builds, 10,944 relation comparisons, and 15,862,452 returned candidate pairs. Compares raw IDs, pair orientation, and original output order without sorting/canonicalizing either result; also checks against brute force. Thread limits: 1 / 2 / 16.
  • Exact ordered-ID equality across base, reviewed, and updated versions on all six fixed-state replay cases described below, for all six relations.
  • Added repository traversal benchmarks for sparse and reject-all cells; all 4 benchmark configurations return zero candidates as expected.
  • clang-format 23.1.3 dry-run and git diff --check.

Performance on identical inputs

The following results measure traversal separately from grid construction and nonlinear-solver trajectories. A/B/C denote base / reviewed / updated versions. All use the same cached Release dependencies, with the side-by-side source variants compiled using C++17 and -O3 (without -march=native). Numbers are medians of three process-level median timings, alternating process execution order, on macOS arm64.

Sparse and reject-heavy regression workloads

Prebuilt sorted cell runs isolate candidate traversal. Each process performs one warmup query and then 7 measured sparse queries or 9 measured reject-all queries. Both workloads return zero candidates:

  • Sparse: 1,048,578 vertex items, 524,289 cells, two disjoint point AABBs per cell, accept-all filter.
  • Reject-all: 4,096 coincident point AABBs in one cell, cheap reject-all filter.
Workload Thread limit A: base B: reviewed C: updated C reduction vs A
Sparse 1 27.041 ms 3.847 ms 3.565 ms 86.82%
Sparse 16 2.736 ms 1.741 ms 1.216 ms 55.56%
Reject-all 1 7.724 ms 11.240 ms 6.001 ms 22.31%
Reject-all 16 0.705 ms 0.973 ms 0.466 ms 33.91%

Peak memory: whole-process peak RSS for the sparse workload at the 16-thread limit is 82.98 / 131.16 / 84.08 MiB for A/B/C. B's growing cell array caused a large transient allocation; C is within approximately 1.09 MiB of the base in this case. This is a workload-specific RSS measurement, not a general memory-reduction claim.

The repository contains the same input patterns in tests/src/tests/broad_phase/benchmark_broad_phase.cpp. To run its Catch2 benchmarks:

ipc_toolkit_tests '[hash_grid][traversal]' --benchmark-samples 9 --benchmark-resamples 100 --benchmark-warmup-time 100

Catch2 reports its own statistical estimates; the table above uses the separate fixed-query process-median protocol described above.

Fixed-state mat-twist replay

Replay saved surface geometry from the baseline trajectory of the pinned PolyFEM test-data scene contact/examples/3D/stress-tests/mat-twist.json (c103d197b7f4d5474208c9b0a470c81349e75dd7). Each input has 3,200 vertices / 9,594 edges / 6,396 boundary triangles. Static queries use the saved state at the indicated step; swept queries use the previous and indicated saved states. All bounds use inflation radius 0.0005. Each process builds the same input once, warms up once, and measures 9 repetitions of all six candidate relations. The reported time is for the combined six queries, excluding construction, input loading, and the solver.

Saved step Bounds A: base B: reviewed C: updated C reduction vs A
1 Static 1.864 ms 1.206 ms 1.225 ms 34.27%
1 Swept 2.210 ms 1.546 ms 1.531 ms 30.70%
150 Static 8.446 ms 5.997 ms 6.240 ms 26.12%
150 Swept 11.294 ms 8.237 ms 8.589 ms 23.95%
300 Static 16.424 ms 11.995 ms 12.559 ms 23.53%
300 Swept 31.890 ms 23.955 ms 24.804 ms 22.22%

Every returned candidate pair, its orientation, and its sequence position match exactly across all three implementations for these six replay cases. Restoring filter-first order trades a few percent relative to B on some replay inputs for the reject-heavy improvement and legacy filter behavior; C remains faster than A in all measured cases. These are traversal measurements on fixed inputs, not new end-to-end simulation timings.

Earlier end-to-end run

The full 300-step simulation results below belong to the previous implementation at b2b9769. They are retained as historical measurements; the updated cc352d1 revision is measured above using identical-input replay. The historical simulations take different trajectories, so their total times and RSS do not isolate broad-phase cost.

Historical 300-step benchmark at b2b9769

Plain IPC performance: PolyFEM mat-twist

Both variants completed 300/300 timesteps from rest, t=0–12 s at dt=0.04 s, using 16 threads. Only PolyFEM test-data contact/examples/3D/stress-tests/mat-twist.json was run, pinned to c103d197b7f4d5474208c9b0a470c81349e75dd7: 3,200 vertices / 9,126 tetrahedra, plain IPC, ESP/GCP disabled, HashGrid.

Metric A: base HashGrid B: PR HashGrid Reduction in B
Process wall time 667.79 s 631.01 s 5.51%
Program-reported time 664.50 s 627.50 s 5.57%
Forward solves, steps 1–300 626.15 s 592.56 s 5.36%
CCD broad phase 50.12 s 40.98 s 18.24%
CCD narrow phase 51.64 s 46.46 s 10.02%
Timestep constraint-set updates 1.26 s 1.02 s 19.16%
Line-search constraint-set updates 7.51 s 7.33 s 2.38%
Nonlinear iterations 11,050 11,705 -5.93%
Solver peak RSS 708.77 MiB 610.31 MiB 13.89%

Positive percentages mean reductions in B; negative values mean increases. Forward time is summed from CSV statistics; component timers use rounded trace values and overlap. Process timings include startup, output, and shutdown.

Solver configuration and validation

Both use the same reduced Newton settings:

{
  "solver": {
    "nonlinear": {
      "Newton": {
        "use_psd_projection": false,
        "use_psd_projection_in_regularized": true
      }
    }
  }
}

Ordinary projected fallback is disabled; regularized Newton uses PSD projection. Materials, boundary conditions, mass lumping, stopping tolerances, the 500-iteration nonlinear limit, and augmented-Lagrangian settings are unchanged. Linear solver: Eigen::AccelerateLDLT. These settings apply only to the benchmark; no PolyFEM solver changes are included in this PR.

Each run logged 300 successful nonlinear solves, all stopping on gradient norm, with no solver errors. All 301 saved states per run had finite fields, positive tetrahedron volumes, prescribed boundary motion, and no surface intersections detected with independent SweepAndPrune. Minimum volume ratios: A 0.383577894, B 0.382036348; maximum boundary error 9.71e-17. All 300 native checkpoints per run passed finite-state and timestep-metadata validation.

Reference meshes and topology match, but trajectories differ: nonlinear iteration counts differ at 258/300 steps, and maximum vertex-displacement difference is 0.164897149 scene units (step 300: 0.0755031643). These are not numerically identical trajectories.

@codecov

codecov Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.98%. Comparing base (b9d0325) to head (db79844).
⚠️ Report is 5 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #282   +/-   ##
=======================================
  Coverage   96.97%   96.98%           
=======================================
  Files         221      221           
  Lines       22426    22435    +9     
  Branches     1540     1538    -2     
=======================================
+ Hits        21748    21758   +10     
+ Misses        678      677    -1     
Flag Coverage Δ
unittests 96.98% <100.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Huangzizhou
Huangzizhou marked this pull request as ready for review October 31, 2026 18:32
@Huangzizhou
Huangzizhou marked this pull request as draft October 31, 2026 18:32
@Huangzizhou
Huangzizhou marked this pull request as ready for review November 2, 2026 03:17
@Huangzizhou
Huangzizhou requested a review from zfergus November 2, 2026 03:17
@zfergus

zfergus commented Nov 3, 2026

Copy link
Copy Markdown
Member

Hi @Huangzizhou, could you enable Allow edits by maintainers on this PR? I’d like to push a merge of main into this branch, along with a small update for the new trivially copyable candidates (#286). HashGrid now writes into CandidateVector<Candidate> instead of std::vector<Candidate>.

To turn it on, open the PR page. In the right sidebar, tick the Allow edits by maintainers checkbox. The box is only visible to you as the PR author.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants