You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
BVH/mesh AABB queries: packed-leaf candidate-bound loads leave performance on the table #1840
In the BVH and mesh AABB query traversal, when a packed leaf (leaf_size > 1) is enumerated, each candidate primitive's bounds are fetched lazily inside the short-circuiting intersect_aabb_aabb test. That compiles to a per-component load / test / branch dependency chain. The primitive-level reads (primitive_indices, item_lowers/item_uppers, mesh face bounds) also bypass the CUDA read-only data path (__ldg) that node loads already use.
Opportunity
Load each candidate's AABB into locals up front so the overlap test issues as a single predicate chain (the dominant effect), and route the primitive reads through the read-only path. Gated to the AABB query kind â applying the eager load to the heavier sphere/capsule narrow-phase instantiations regresses them via register pressure.
Measured up to ~21% faster on packed-leaf (leaf_size = 8) BVH AABB queries and ~5-9% on mesh AABB, on an L40 (122k-triangle scene, lbvh/cubql/sah constructors). Singleton-leaf trees (leaf_size = 1, the wp.Bvh default) and non-AABB query kinds are unaffected. Query results are bitwise-identical.
Notes
First of a short series of BVH/mesh query hot-path performance improvements.
The change routes primitive indices and AABB candidate bounds through CUDA read-only loads and eagerly materializes packed-leaf candidate bounds before overlap testing. The eager-load optimization is compile-time-gated to AABB queries, so sphere and capsule paths avoid the reported register-pressure regression.
Public API behavior
Python scope
No signatures change. Existing wp.Bvh(..., leaf_size=...) and wp.Mesh(..., bvh_leaf_size=...) objects benefit without caller changes.
Kernel scope
Existing wp.bvh_query_aabb() and wp.mesh_query_aabb() queries are faster for packed leaves while preserving their returned hit sets.
Test coverage
No test files were changed in this commit. Existing warp/tests/geometry/test_bvh.py::test_bvh_query_aabb and warp/tests/geometry/test_mesh_query_aabb.py::test_mesh_query_aabb_count_overlap_with_checksum cover packed-leaf correctness, and asv/benchmarks/spatial_query.py::BvhAABBQuery covers leaf-size 8 performance for LBVH and cuBQL.
Behavioral verification
A CUDA probe with leaf_size=8 compared complete BVH and mesh hit matrices against brute-force AABB overlap for LBVH, SAH, and cuBQL; all six cases matched exactly.
Follow-up to consider
Non-blocking: Extend BvhAABBQueryâs GPU constructor matrix to include SAH so the existing ASV benchmark covers all three constructors named in this issue.
Additional testing of fcc86dd on Jetson AGX Thor found an architecture-dependent mesh-ray regression in the read-only load conversion. The packed-leaf AABB improvement described in this issue still reproduces, so a full revert of the commit is not recommended.
The isolated AABB results on Thor vary with leaf size:
Query
Parent
Commit
Change
Raw BVH AABB, radius .008, leaf 8, CuBQL
3.94 ms
3.46 ms
-12.2%
Raw BVH AABB, radius .008, leaf 0, CuBQL
2.42 ms
2.54 ms
+5.0%
Packed leaves benefit from the eager and read-only loads because the fixed work is spread across several primitives. Some single-primitive leaves pay that cost without enough reuse.
The larger regression is in the primary closest-hit mesh_query_ray() path:
Query
Parent
Commit
Change
Mesh ray, 1080, leaf 8, LBVH
35.4 ms
40.9 ms
+15.5%
Mesh ray, 1080, leaf 8, CuBQL
27.3 ms
31.6 ms
+15.8%
Raw BVH ray, 1080, leaf 8, LBVH
90.9 ms
90.9 ms
approximately 0%
The four read-only loads in the mesh leaf loop are one primitive-index load followed by three triangle vertex-index loads. On sm_110, those changes increase the exact mesh-ray kernel from 62 to 69 registers per thread while leaving the stack frame at 320 bytes. With 256 threads per block, residency falls from four blocks per SM to three, or from 1,024 to 768 active threads per SM.
The same source compiled for L40 (sm_89) moves from 64 to 63 registers, which is consistent with the 4% to 6% mesh-ray improvement originally measured there. CUDA 13.0 cross-compilation shows how the result varies across targets:
Target
Parent registers
Commit registers
Register-limited 256-thread blocks per SM
sm_89
64
63
4 to 4
sm_100
62
69
4 to 3
sm_103
62
69
4 to 3
sm_110
62
69
4 to 3
sm_120
63
61
4 to 4
sm_121
63
61
4 to 4
Thor is the only Blackwell target physically measured in this follow-up. The other Blackwell rows are compiler-resource results. They indicate that B200 and B300 have the same register cliff, while sm_120 and sm_121 do not.
The Thor clocks were not locked, so the absolute timings should be treated accordingly. The regression ratios repeated, and the register and occupancy changes are deterministic.
Test coverage
No test files changed in this commit. Existing BVH and mesh query tests cover functional correctness, while asv/benchmarks/spatial_query.py covers the relevant performance families.
The available performance coverage did not catch a compiler-resource cliff outside the L40 target. Resource summaries for each architecture would make this type of regression visible before runtime benchmarking.
Behavioral verification
A controlled test changed only the four loads in the primary mesh_query_ray() leaf loop back to ordinary array loads:
Variant
Thor time
Registers
Unmodified commit, block size 256
40.99 ms
69
Unmodified commit, block size 128
41.16 ms
69
Four ordinary loads, block size 256
35.60 ms
62
A fresh-cache rerun of the unmodified commit measured 40.96 ms. The four-load change therefore recovered 13.2% and returned the kernel to the parentâs 62-register, four-block residency regime. Reducing the block size did not help.
Follow-up to consider
For 1.18.0rc2, change only these four loads in the primary mesh_query_ray() leaf loop back to ordinary loads:
mesh.bvh.primitive_indices[pc]
mesh.indices[primitive_index * 3 + 0]
mesh.indices[primitive_index * 3 + 1]
mesh.indices[primitive_index * 3 + 2]
Keep the rest of fcc86dd, including the AABB-specific eager and read-only loads. A path-level exception is preferable to an sm_110 condition because sm_100 and sm_103 cross the same register boundary.
The any-hit, ordered, count-intersections, and closest-sign ray variants should be measured separately before changing their load policy. The RC2 validation matrix should include Thor, L40, B200 or B300, and one sm_120 or sm_121 system.
Progress update: follow-up testing of fcc86dd now covers the remaining mesh-ray variants on Jetson AGX Thor.
The comparison used the exact parent and commit wheels with the ASV replicated-bunny scene, 1080 by 1080 camera rays, LBVH and CuBQL, and leaf sizes 0 and 8. Each configuration was run twice with 15 samples per run. Positive values below are regressions.
Variant
LBVH default
LBVH leaf 8
CuBQL default
CuBQL leaf 8
Closest-hit
+9.23%
+15.13%
-0.20%
+16.11%
Any-hit
+5.89%
+11.86%
-2.58%
+12.60%
Ordered
+6.05%
+9.94%
+0.07%
+10.64%
Count-intersections
-0.53%
+4.25%
-9.91%
+6.17%
Closest-sign
-0.18%
-0.23%
-0.08%
-0.04%
The compiler results explain part of the split:
Variant
Registers, parent to commit
Stack/thread
Register-limited blocks/SM
Closest-hit
62 to 69
320 B unchanged
4 to 3
Any-hit
56 to 63
320 B unchanged
4 to 4
Ordered
56 to 63
320 B unchanged
4 to 4
Count-intersections
64 to 63
192 B unchanged
4 to 4
Closest-sign
48 to 48
128 B unchanged
5 to 5
Closest-hit still has the clear occupancy cliff. Any-hit and ordered stay in the same four-block residency regime, but the extra seven registers and the explicit LDG.E.CONSTANT load path cost 6% to 12% for LBVH and packed leaves. The penalty grows at leaf size 8, where each visited leaf executes more primitive and triangle-index loads.
Count-intersections is a real tradeoff. Read-only loads improve CuBQL with default leaves by 9.91%, while packed leaves regress by 4.25% on LBVH and 6.17% on CuBQL. Closest-sign remains neutral across the matrix.
Thor clocks were not locked, but the parent repeat medians differed by at most 0.61% and the commit repeats by at most 0.84%. All output checksums matched.
Test coverage
The existing ASV ray benchmark measures only closest-hit mesh_query_ray(). It has no direct performance cases for any-hit, ordered, or count-intersections. Closest-sign is exercised indirectly by the signed closest-point benchmark, where its cost is mixed with the surrounding point query.
No test files changed in the original commit. The targeted probe covers the missing performance paths, but it is not yet checked into ASV.
Behavioral verification
A causal build changed only the four primitive and triangle-index loads in each ray path back to ordinary loads. Every kernel returned to its parent register and stack counts, and all 20 timing cells returned to within 0.74% of the parent. This isolates the load policy as the cause of both the gains and regressions.
Follow-up to consider
For 1.18.0rc2, the load policy should be adjusted by path:
Use ordinary loads for closest-hit on the affected Blackwell targets. Thor physically regresses, and the earlier sm_100 and sm_103 compiler results show the same 62-to-69-register residency cliff.
Use ordinary loads for any-hit and ordered on sm_110. Other targets should be measured before extending that exception.
Use ordinary loads for count-intersections on sm_110 if the RC priority is to avoid regressions relative to the parent. This removes the packed-leaf losses but gives up the 9.91% CuBQL/default-leaf gain.
Keep the read-only loads for closest-sign.
The AABB-specific eager and read-only loads should remain in place because their packed-leaf improvement still reproduces on Thor. A split-load experiment for count-intersections can follow after 1.18 to see whether the CuBQL/default-leaf gain can be kept without the packed-leaf loss.
Activity
Point changelog fragment at tracking issue NVIDIAGH-1840
shi-eric commentedon Aug 25, 2026
This is addressed by fcc86dd.
The change routes primitive indices and AABB candidate bounds through CUDA read-only loads and eagerly materializes packed-leaf candidate bounds before overlap testing. The eager-load optimization is compile-time-gated to AABB queries, so sphere and capsule paths avoid the reported register-pressure regression.
Public API behavior
Python scope
wp.Bvh(..., leaf_size=...)andwp.Mesh(..., bvh_leaf_size=...)objects benefit without caller changes.Kernel scope
wp.bvh_query_aabb()andwp.mesh_query_aabb()queries are faster for packed leaves while preserving their returned hit sets.Test coverage
warp/tests/geometry/test_bvh.py::test_bvh_query_aabbandwarp/tests/geometry/test_mesh_query_aabb.py::test_mesh_query_aabb_count_overlap_with_checksumcover packed-leaf correctness, andasv/benchmarks/spatial_query.py::BvhAABBQuerycovers leaf-size 8 performance for LBVH and cuBQL.Behavioral verification
A CUDA probe with
leaf_size=8compared complete BVH and mesh hit matrices against brute-force AABB overlap for LBVH, SAH, and cuBQL; all six cases matched exactly.Follow-up to consider
BvhAABBQueryâs GPU constructor matrix to include SAH so the existing ASV benchmark covers all three constructors named in this issue.Use read-only loads in BVH and mesh query hot paths [NVIDIAGH-1840]
shi-eric commentedon Sep 20, 2026
Additional testing of fcc86dd on Jetson AGX Thor found an architecture-dependent mesh-ray regression in the read-only load conversion. The packed-leaf AABB improvement described in this issue still reproduces, so a full revert of the commit is not recommended.
The isolated AABB results on Thor vary with leaf size:
Packed leaves benefit from the eager and read-only loads because the fixed work is spread across several primitives. Some single-primitive leaves pay that cost without enough reuse.
The larger regression is in the primary closest-hit
mesh_query_ray()path:The four read-only loads in the mesh leaf loop are one primitive-index load followed by three triangle vertex-index loads. On
sm_110, those changes increase the exact mesh-ray kernel from 62 to 69 registers per thread while leaving the stack frame at 320 bytes. With 256 threads per block, residency falls from four blocks per SM to three, or from 1,024 to 768 active threads per SM.The same source compiled for L40 (
sm_89) moves from 64 to 63 registers, which is consistent with the 4% to 6% mesh-ray improvement originally measured there. CUDA 13.0 cross-compilation shows how the result varies across targets:sm_89sm_100sm_103sm_110sm_120sm_121Thor is the only Blackwell target physically measured in this follow-up. The other Blackwell rows are compiler-resource results. They indicate that B200 and B300 have the same register cliff, while
sm_120andsm_121do not.The Thor clocks were not locked, so the absolute timings should be treated accordingly. The regression ratios repeated, and the register and occupancy changes are deterministic.
Test coverage
asv/benchmarks/spatial_query.pycovers the relevant performance families.Behavioral verification
A controlled test changed only the four loads in the primary
mesh_query_ray()leaf loop back to ordinary array loads:A fresh-cache rerun of the unmodified commit measured 40.96 ms. The four-load change therefore recovered 13.2% and returned the kernel to the parentâs 62-register, four-block residency regime. Reducing the block size did not help.
Follow-up to consider
For 1.18.0rc2, change only these four loads in the primary
mesh_query_ray()leaf loop back to ordinary loads:mesh.bvh.primitive_indices[pc]mesh.indices[primitive_index * 3 + 0]mesh.indices[primitive_index * 3 + 1]mesh.indices[primitive_index * 3 + 2]Keep the rest of fcc86dd, including the AABB-specific eager and read-only loads. A path-level exception is preferable to an
sm_110condition becausesm_100andsm_103cross the same register boundary.The any-hit, ordered, count-intersections, and closest-sign ray variants should be measured separately before changing their load policy. The RC2 validation matrix should include Thor, L40, B200 or B300, and one
sm_120orsm_121system.shi-eric commentedon Sep 20, 2026
Progress update: follow-up testing of fcc86dd now covers the remaining mesh-ray variants on Jetson AGX Thor.
The comparison used the exact parent and commit wheels with the ASV replicated-bunny scene, 1080 by 1080 camera rays, LBVH and CuBQL, and leaf sizes 0 and 8. Each configuration was run twice with 15 samples per run. Positive values below are regressions.
The compiler results explain part of the split:
Closest-hit still has the clear occupancy cliff. Any-hit and ordered stay in the same four-block residency regime, but the extra seven registers and the explicit
LDG.E.CONSTANTload path cost 6% to 12% for LBVH and packed leaves. The penalty grows at leaf size 8, where each visited leaf executes more primitive and triangle-index loads.Count-intersections is a real tradeoff. Read-only loads improve CuBQL with default leaves by 9.91%, while packed leaves regress by 4.25% on LBVH and 6.17% on CuBQL. Closest-sign remains neutral across the matrix.
Thor clocks were not locked, but the parent repeat medians differed by at most 0.61% and the commit repeats by at most 0.84%. All output checksums matched.
Test coverage
mesh_query_ray(). It has no direct performance cases for any-hit, ordered, or count-intersections. Closest-sign is exercised indirectly by the signed closest-point benchmark, where its cost is mixed with the surrounding point query.Behavioral verification
A causal build changed only the four primitive and triangle-index loads in each ray path back to ordinary loads. Every kernel returned to its parent register and stack counts, and all 20 timing cells returned to within 0.74% of the parent. This isolates the load policy as the cause of both the gains and regressions.
Follow-up to consider
For 1.18.0rc2, the load policy should be adjusted by path:
sm_100andsm_103compiler results show the same 62-to-69-register residency cliff.sm_110. Other targets should be measured before extending that exception.sm_110if the RC priority is to avoid regressions relative to the parent. This removes the packed-leaf losses but gives up the 9.91% CuBQL/default-leaf gain.The AABB-specific eager and read-only loads should remain in place because their packed-leaf improvement still reproduces on Thor. A split-load experiment for count-intersections can follow after 1.18 to see whether the CuBQL/default-leaf gain can be kept without the packed-leaf loss.
Restore mesh ray-query occupancy [NVIDIAGH-1840]
Scope mesh ray load policy by target (NVIDIAGH-1840)
Record query regression evidence (NVIDIAGH-1840, NVIDIAGH-1844)
Audit query occupancy after reverts (NVIDIAGH-1844)
Record rebase validation (NVIDIAGH-1840, NVIDIAGH-1844)
Use read-only loads in BVH and mesh query hot paths [NVIDIAGH-1840]