perf(rbd): flat 1-D narrow-phase dispatch — pack warps across batches#21
perf(rbd): flat 1-D narrow-phase dispatch — pack warps across batches#21haixuanTao wants to merge 2 commits into
Conversation
The narrow-phase kernels dispatched [max_len/64, num_batches, 1]: one 64-lane workgroup per batch rounded up from that batch's live pair count. A batched robot env has ~7 pairs, so at 2048+ envs the GPU ran thousands of workgroups at ~11% lane occupancy and narrow-phase scaled ~linearly with env count (0.02 ms at 1 env -> 18.6 ms at 4096). gpu_flatten_batches_dispatch (one thread, same style as the existing max-scan init kernels) now builds exclusive prefix offsets over the per-batch work-lists plus a flat [total/64, 1, 1] grid; the classify, deferred and PFM kernels walk 0..total and recover (batch, item) with a binary search over the offsets. Warps fill with real pairs from consecutive batches. Buffer layout is unchanged — only the dispatch shape and index math moved. RTX 5090, 12-DOF biped batch, dt=5ms, steady-state contacts: narrow-phase 2048 envs: 10.31 -> 5.09 ms 4096 envs: 18.57 -> 6.94 ms whole step 2048 envs: 22.45 -> 16.50 ms/step (91k -> 124k env-steps/s) Physics bit-identical (robot trajectory + cube settle unchanged to printed precision). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…narrow phase Collision pairs carry env-local collider ids, and collider_parent is batch-strided like every other per-collider buffer (its construction comment says so: 'Env-local body slot; the kernels apply the per-batch stride') — but the classify and pfm_pfm kernels read it unsliced, resolving every batch's parents through batch 0's table. With identical environments the tables coincide and nothing observable goes wrong — which is why every bit-exactness check passed. With heterogeneous environments (the point of per-env MJCF insertion) contacts are silently mis-parented: a pair that is same-body in batch 0 gets skipped in batches where it isn't, and solved impulses can target the wrong bodies. Repro (now a stacking test): env0 = body with two glued boxes + a single box; env1 = single box dropped onto a two-glued-box body (equal body and collider counts, different parent tables). Pre-fix the falling box never rests on the stack; post-fix it settles on top (z=0.75). Identical-env scenes are bit-identical before/after, as the aliasing argument predicts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Pushed an additional commit with a correctness fix found while stress-testing heterogeneous environments: the classify and pfm_pfm kernels read |
The flat narrow-phase kernels (post-dimforge#21 flatten) thread purely off `global_invocation_id.x` with `num_threads = num_workgroups.x * WORKGROUP_SIZE` — the grid y/z dims are never read. But the FIXED fallback grid was still the stale 2-D `[x, num_batches, 1]` from before the flatten (the indirect path was updated to 1-D by flatten_batches; the fixed path was not). Every one of the `num_batches` y-slices re-ran the identical flat grid-stride loop, so the atomic-append emitters — deferred (site 7) and pfm_pfm (site 8) — wrote each contact `num_batches` times. That exploded the contact buffers and graph coloring: ~19s/step at 2048 batches with the GPU pinned (the long-standing "fixed-grid pathology" that forced sites 7+8 onto indirect dispatch; also the historical >16-batch illegal address). Fix: size the fixed grid 1-D over the whole flat capacity, matching the indirect path. Verified: sites 7+8 on fixed dispatch now run at 2.0 s/iter (was pathological), iter-0 falls within baseline noise. Flips both sites back to fixed dispatch, removing their per-step host sync. Also hoist the fused-color `num_colors_uniform` update out of a per-step `Tensor::scalar` realloc (a cudaMalloc inside step(), illegal under CUDA-graph capture) to an in-place stream-ordered `write_buffer`. Both are prerequisites for restoring graph capture on the unified stack. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U2n9RqmxTJb8UG5d1Sjw4W
Problem
The narrow-phase kernels dispatch
[max_len/64, num_batches, 1]: one 64-lane workgroup per batch, rounded up from that batch's live pair count. A batched robot environment has ~7 collision pairs, so at 2048+ envs the GPU runs thousands of workgroups at ~11% lane occupancy, and narrow-phase time scales almost linearly with env count (0.02 ms at 1 env → 18.6 ms at 4096 on the scene below) — it goes from negligible to the single largest pass in the frame.Change
A new one-thread kernel (
gpu_flatten_batches_dispatch, same style and cost as the existing max-scan init kernels) builds exclusive prefix offsets over the per-batch work-lists plus a flat[total/64, 1, 1]indirect grid. The classify, deferred, and PFM kernels walk0..totaland recover(batch, item)with a binary search over the offsets (≤12 steps at 4096 batches). Warps fill with real pairs from consecutive batches instead of one batch's handful plus idle lanes.Buffer layout is unchanged — per-batch capacity-strided storage stays as is; only the dispatch shape and index math moved. The same flatten kernel is reused for both the collision-pair and the PFM work-lists, repurposing the two existing indirect buffers (nothing else consumes their batched form).
Results
RTX 5090, batched 12-DOF biped (one robot per env, mesh feet), dt = 5 ms, steady-state contacts, wgpu/Vulkan:
Physics is bit-identical before/after (robot trajectory and cube-settle checks match to printed precision), as expected — the same items are processed, only warp packing changes. Single-env scenes see no change (the flat grid degenerates to the old one).
The remaining narrow-phase cost is the per-pair GJK/EPA divergence itself; the same per-batch dispatch pattern also exists in the contact-solver kernels (
contacts_grid), which this PR deliberately leaves untouched — planned as a follow-up.🤖 Generated with Claude Code