Skip to content

perf(rbd): one-launch segmented bitonic sort for small per-batch key counts#20

Open
haixuanTao wants to merge 1 commit into
dimforge:mainfrom
haixuanTao:perf/small-batch-sort
Open

perf(rbd): one-launch segmented bitonic sort for small per-batch key counts#20
haixuanTao wants to merge 1 commit into
dimforge:mainfrom
haixuanTao:perf/small-batch-sort

Conversation

@haixuanTao

Copy link
Copy Markdown
Contributor

What

The LBVH morton sort currently runs the full radix cascade — ceil(bits/4) passes × 5 kernels each, plus the batch-id flattening passes — even when each batch only holds a handful of keys. For a robot-RL style scene (thousands of environments, a dozen colliders each) that's ~55 kernel launches per step to sort 12-element segments.

This adds a fast path in RadixSort::dispatch: when the per-batch capacity is ≤ SMALL_SORT_MAX (128), a single segmented bitonic-sort launch (one 64-wide workgroup per batch, keys held in workgroup memory) replaces the whole cascade.

Correctness

  • Stable, and output bit-identical to the radix path: ties are broken by original index, which reproduces exactly what a stable LSD radix sort produces. Verified on our training stack — iteration-0 statistics of a deterministic PPO reference are bit-identical with the fast path on and off.
  • Falls back to the radix cascade whenever per-batch capacity exceeds the threshold; NEXUS_SMALL_SORT=0 env-var escape hatch for A/B.

Measured

Host encode time for the broad phase 12.3 → 9.5 ms/step on a 2048-env, 12-DOF-robot scene (RTX 5090, WebGPU and CUDA backends alike — the win is launch count, not kernel time).

cargo check clean on nexus_rbd3d and nexus_rbd2d.

🤖 Generated with Claude Code

https://claude.ai/code/session_01U2n9RqmxTJb8UG5d1Sjw4W

…cities

The batched radix path costs ceil(bits/4) passes x 5 kernels plus extra
batch-id passes (~55 launches per broad-phase rebuild) even when each
batch only holds a dozen colliders. For per-batch capacities <= 128,
sort each batch with a single 64-wide workgroup doing a shared-memory
bitonic sort instead.

Stability: the comparator orders by (key, original_index), so equal keys
keep their input order and the output is identical to the stable radix
path. NEXUS_SMALL_SORT=0 restores the radix path.

Robot-RL biped scene (2048 envs, 14 colliders/batch, RTX 5090): host
pipeline time 12.3 -> 9.5 ms/step (12-DOF G1), 21.2 -> 17.1 ms/step
(29-DOF G1); training stats bit-identical either path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Dbpu5NhqXK2Xt3YzUbJRM
@haixuanTao
haixuanTao force-pushed the perf/small-batch-sort branch from fcd1ed7 to 7e0eec2 Compare July 16, 2026 16:04
haixuanTao added a commit to haixuanTao/nexus that referenced this pull request Jul 17, 2026
…capture

CUDA-graph capture of the physics step was failing with
STREAM_CAPTURE_INVALIDATED on the unified stack (production captured fine;
these are unified-added regressions from the dimforge#20 small-sort and dimforge#22
fused-color merges). Any allocation (cuMemAlloc) or pageable host→device
copy inside the captured region invalidates the capture. Two offenders,
localized with a cuStreamGetCaptureInfo probe after each op:

1. Small-sort fast path (radix_sort): allocated a fresh SortUniforms tensor
   every dispatch (`pass_uniforms.clear(); push(Tensor::scalar(..))`),
   bypassing the `uniforms_key` cache the radix path already uses for exactly
   this reason. Now cached/keyed by per_batch_max — allocate once, reuse.

2. num_colors_uniform sync (rbd_step): updated the fused-solver color-count
   uniform inside step() — first as a realloc, then (mistakenly) as an
   in-place write_buffer, both illegal during capture. Moved the sync to
   auto_resize_buffers, which runs OUTSIDE the captured region; max_colors
   only changes there (the ratchet) and in set_max_colors (setup), so the
   value is stable across a capture.

With these + the 1-D fixed-grid fix (daf5a43), BIPED_GRAPH=1 now captures and
replays on the unified stack (verified: 12 iters @2048 g1_29dof_agile terrain
full-parity, falls within baseline noise). Unblocks the env-per-lane LᵀDL
wall-clock win, whose per-kernel gain was hidden by eager launch overhead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U2n9RqmxTJb8UG5d1Sjw4W
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.

1 participant