Batch pose NMS tiles and skip suppressed references - #617
Conversation
Profiling OpenDoor planning showed 90% of a 10.6 s compile inside pose NMS: the greedy pass computed pairwise tiles through one Warp kernel launch per host-bounded reference block, and the block budget shrank as the candidate count grew, giving a launch count that scales cubically (N^3/chunk^3 - about 10,000 launches at the ~43k grasp candidates the antipodal sampler produces). On CPU pose inputs each launch also ran the tile single-threaded. Rewrite the pairwise stage as batched torch tiles with identical threshold math and reduction order, and visit references through an alive filter: a reference suppressed before its block starts can never be kept, so its row is never computed and the cost scales with the number of survivors instead of the raw candidate count (43k -> ~900 on the door handle). CPU inputs offload the elementwise float32 pairwise math to CUDA when available; indices are returned on the input device. Public API, thresholds, visit order, tie-breaking, and outputs are unchanged. Measured: OpenDoor NMS 10.27 s -> 0.21-0.31 s (~40x), full plan compile 10.6 s -> 0.72 s (~15x). Equivalence is enforced by tests against a literal O(N^2) reference over clustered random poses (both orderings, chunk sizes 1/7/128/2048, rotation-always-close branch, heavy-suppression profile, CUDA), and old-vs-new index sequences match exactly on synthetic sweeps up to 44k poses. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
| # Make them visible to Warp before launching on its stream. | ||
| torch.cuda.synchronize(positions.device) | ||
| close_counts_wp = wp.zeros(num_poses, dtype=wp.int32, device=positions_wp.device) | ||
| close = torch.empty(num_refs, num_poses, dtype=torch.bool, device=positions.device) |
There was a problem hiding this comment.
_close_block retains an (R, N) matrix where R can equal chunk_size, so peak storage grows as chunk_size × num_poses during both neighbor counting and suppression. Large valid sample configurations can therefore exhaust CPU or GPU memory even though chunk_size is documented as bounding both dimensions of a pairwise tile.
Knowledge Base Used: Runtime configuration and utilities
Prompt To Fix With AI
This is a comment left during a code review.
Path: embodichain/utils/nms.py
Line: 77
Comment:
**Tiles No Longer Bound Memory**
`_close_block` retains an `(R, N)` matrix where `R` can equal `chunk_size`, so peak storage grows as `chunk_size × num_poses` during both neighbor counting and suppression. Large valid sample configurations can therefore exhaust CPU or GPU memory even though `chunk_size` is documented as bounding both dimensions of a pairwise tile.
**Knowledge Base Used:** [Runtime configuration and utilities](https://app.greptile.com/dexforce/-/custom-context/knowledge-base/dexforce/embodichain/-/docs/runtime-configuration-and-utilities.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.There was a problem hiding this comment.
Fixed in 17234c8. The greedy pass sizes reference blocks by the same chunk_size**2 // num_poses entry budget the pre-rewrite implementation used (the old slowness came from per-tile kernel launches, not block granularity — with batched torch tiles the extra outer iterations are negligible: OpenDoor end-to-end stays at 0.30 s). Neighbor counting no longer materializes any (rows, num_poses) matrix at all: counts accumulate per bounded tile, with the diagonal cleared per tile by index. chunk_size again bounds both dimensions of every pairwise allocation.
Address three review findings on the pose-NMS acceleration: 1. Memory bounds restored: the greedy pass sizes reference blocks by the chunk_size**2 entry budget (as before the rewrite), and neighbor counting accumulates per bounded tile without materializing any (rows, num_poses) matrix, so chunk_size again bounds both dimensions of every pairwise tile. 2. Deterministic threshold arithmetic: tiles use explicit per-component multiplies with left-to-right addition — the exact association of the previous scalar implementation — instead of backend-selected sum reductions, so closeness decisions are identical across CPU and CUDA. Diagonal self-pairs are cleared by index, matching the old ref==target guard rather than relying on floating-point identity. 3. CUDA offload of CPU requests now falls back to the CPU path with a warning on any CUDA RuntimeError (initialization or memory pressure) instead of failing a request the CPU path can serve. OpenDoor end-to-end after the fixes: NMS 0.30 s, compile 0.83 s (previously 10.27 s / 10.6 s). All 37 equivalence and behavior tests pass unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Description
This PR accelerates grasp-pose NMS (
embodichain/utils/nms.py) by ~40×, cutting OpenDoor plan compilation from 10.6 s to 0.72 s end-to-end. Semantics are frozen: public API, thresholds, visit order, tie-breaking, and output index sequences are unchanged.Why NMS dominated planning
Profiling
OpenDoorcompilation (UR5, default microwave tutorial) showed the motion stack itself is healthy — the 10.6 s went almost entirely elsewhere:pose_nms)The antipodal sampler produces ~43k grasp candidates on the door handle; NMS reduces them to a few hundred survivors.
Root cause: cubic launch scaling + single-threaded CPU tiles
The greedy pass computed pairwise tiles via one Warp launch per host-bounded reference block, with the block budget
chunk²//Nshrinking as N grows — total launches scale as N³/chunk³ (≈10,200 at N=43k, matching the 9,959 observed in the profile). Two additional measurements rule out a machine-specific cause:What changed
Measured results
engine.compileend-to-endEvery skill that samples grasps through graspkit (pick_up, place, hand_over, axis_align, open_door, slide, twist, …) benefits; larger
--n_samplesettings benefit the most.Equivalence enforcement
tests/utils/test_nms.pygains a literal O(N²) reference implementation of the documented semantics and asserts exact index-sequence equality across: both orderings (preserve_orderand neighbor-count priority with index tie-break), chunk sizes 1/7/128/2048, theangle_th > πrotation-always-close branch, an all-duplicates input, a 6k heavy-suppression profile matching real grasp-candidate statistics (also validating the CUDA offload against the CPU reference), and CUDA-resident inputs. The 7 pre-existing behavioral tests pass unchanged, as do the 6 graspkitpg_grasptests downstream.Dependencies: none (removes the module's Warp dependency).
Type of change
Screenshots
N/A
Checklist
black .command to format the code base.utils/nms.py; behavior unchanged)python docs/scripts/check_api_docs.py: 1925/1925)Validation