Skip to content

[MISC] Reduce memory footprint of ray casting-based sensors via cross-env memory sharing.#2914

Merged
duburcqa merged 13 commits into
Genesis-Embodied-AI:mainfrom
Kashu7100:kashu/raycast-shared-static-bvh
Jul 21, 2026
Merged

[MISC] Reduce memory footprint of ray casting-based sensors via cross-env memory sharing.#2914
duburcqa merged 13 commits into
Genesis-Embodied-AI:mainfrom
Kashu7100:kashu/raycast-shared-static-bvh

Conversation

@Kashu7100

@Kashu7100 Kashu7100 commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator

Description

The raycast BVHs (Raycaster / DepthCamera / Lidar, also traversed by the tactile probes) allocate one tree per env: nodes, AABBs, morton codes and the radix-sort scratch are all (n_envs, ...). For a high-poly static terrain this replication dominates GPU memory and OOMs at a few thousand envs, even though the per-env trees are identical or fall into a handful of variants.

Building on the static/dynamic split of #3078, this PR makes every static BVH automatically allocate one tree per distinct per-env geometry instead of one per env: identical envs share a single tree, N heterogeneous variants get N trees. It applies to the static collision subset of a mixed robot-on-terrain solver, to fully static solvers, and to the static visual BVH.

Envs are grouped by exact equality of every per-env input of the tree build - active geom ranges and free-vert positions for collision, vgeom poses and custom vverts for visual, each restricted to the entry's own subset - so a wrong merge is impossible and the grouping is blind to the movable geometry diverging under physics. An is_env_uniform entry (#3078) collapses to one tree with the signature left unread. A per-env set_pos on fixed geometry regroups on rebuild (up to one tree per env) and an identical reset merges back; tree allocations are pooled per tree count so diverge/reset cycles reuse them. Two routing arrays drive the cast: env_bvh_idx (tree slot per env) and env_cast_order (envs sorted by tree, keeping the env-major thread mapping coalesced even when group membership interleaves). Tactile probes traverse both folded trees through the same routing.

Related Issue

None.

Motivation and Context

Massively parallel RL on static terrain: static-BVH memory drops by the ratio of env count to distinct-geometry count, on top of #3078's rebuild-cost win.

Measured on the #3078 benchmark scene (default Terrain, 41.7k static faces + Franka, 2.3k rays/env, distances-only), interleaved A/B/A/B against main:

  • CUDA, 1024 envs: total GPU memory 7755 -> 2051 MB (3.8x; the static subset collapses from 1024 trees to 1), stepping 11.0 -> 11.0 ms/step and per-step configuration teleports 16.0 -> 16.0 ms/step (parity), per-step scene.reset() 93.8 -> 29.5 ms (3.2x: one static tree rebuilt instead of 1024, and the identical-envs short-circuit replaces the per-env AABB comparison).
  • CPU, single env: parity within run noise on every arm (~6 ms/step).

On a fully static 4.6k-face terrain scene against per-env trees, 1284 -> 497 MB; per-env trees OOM at 4096 envs while the shared tree runs.

How Has This Been / Can This Be Tested?

  • tests/sensors/test_raycaster.py::test_shared_static_bvh_regroup (new, n_envs 0 and 4, mixed solver): collision static subset and kinematic visual BVH collapse to one tree each with analytic distances; a non-monotonic per-env set_pos splits the static subset to per-env trees with per-env distances; an interleaved per-env set_pos on the visual entity splits the visual BVH into two trees; a per-env divergence of the movable entity leaves the restored static subset on one shared tree; reset merges everything back.
  • tests/sensors/test_raycaster.py::test_static_dynamic_bvh_split_merge asserts the static subset holds a single tree while the dynamic subset stays per env, through every merge/rebuild phase of [MISC] Speed up raycast sensors by splitting the rigid collision BVH into static and dynamic subsets. #3078 (tactile fold included).
  • tests/sensors/test_raycaster.py::test_heterogeneous_object now runs 6 envs and asserts the collapse to 3 trees with per-variant distances.
  • tests/sensors/test_tactile.py::test_raycast_probe_on_fully_fixed_solver (new): tactile probes traverse the shared tree through the env routing.
  • Full tests/sensors/test_raycaster.py + test_tactile.py + test_api.py + tests/core/test_misc.py pass on CPU, CUDA and Metal backends.

Checklist:

  • I read the CONTRIBUTING document.
  • I followed the Submitting Code Changes section of CONTRIBUTING document.
  • I tagged the title correctly (including BUG FIX/FEATURE/MISC/BREAKING)
  • I updated the documentation accordingly or no change is needed.
  • I tested my changes and added instructions on how to test it for reviewers.
  • I have added tests to cover my changes.
  • All new and existing tests passed.

@Kashu7100
Kashu7100 force-pushed the kashu/raycast-shared-static-bvh branch from 27c3160 to 89a1d7e Compare June 8, 2026 10:23
@Kashu7100 Kashu7100 changed the title [PERF] Optional shared static-geometry raycast BVH (n_batches=1) [PERF] Share static-geometry raycast BVH across envs (N trees, not B) Jun 8, 2026
@Kashu7100
Kashu7100 force-pushed the kashu/raycast-shared-static-bvh branch 2 times, most recently from 9304fb8 to a530b77 Compare June 10, 2026 09:18
@Kashu7100
Kashu7100 marked this pull request as ready for review June 11, 2026 11:22

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a530b7776c

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread genesis/utils/raycast_qd.py
@github-actions

Copy link
Copy Markdown

⚠️ Abnormal Benchmark Result Detected ➡️ Report

Kashu7100 and others added 2 commits July 7, 2026 13:56
The raycast BVH (Raycaster / DepthCamera) is allocated per env
(n_batches=n_envs): nodes, AABBs, morton codes and radix-sort scratch are
all replicated across envs. For a high-poly static terrain this is the
dominant GPU-memory cost — it OOMs at a few thousand envs even though the
trees are identical (or fall into a handful of distinct variants) and the
cast already reads batch 0 when it detects the trees match (Genesis-Embodied-AI#2867).

Add RigidOptions.shared_static_raycast_bvh (default False). When True, the
static (fully-fixed) collision BVH is allocated per *distinct geometry*
rather than per env: envs that share geometry share one tree.
  - homogeneous scene            -> 1 shared tree
  - N variants across n_envs      -> N trees   (N << n_envs, e.g. a terrain
    curriculum: distinct geometries are read from the per-env active-geom
    ranges Genesis already stores for heterogeneous envs)

Mechanism: two small routing arrays unify all three layouts — env_bvh_idx
[n_env] (which tree each env casts against) and batch_repr_env[n_batches]
(which env's geometry builds each tree). The cast kernels index
env_bvh_idx; update_aabbs builds each tree slot from its representative env.
The per-env path is env_bvh_idx=arange (unchanged); the runtime
shared-across-envs detection (Genesis-Embodied-AI#2867) becomes env_bvh_idx=0.

Opt-in rather than auto-detected because env-identity is a runtime property
(a per-env set_pos on a fixed body diverges geometry after build); the flag
is a caller guarantee the static geometry stays grouped as built. Default
False keeps per-env trees and the runtime detection unchanged.

Benchmark — raycast DepthCamera over static eden_dwbp terrain (RTX 3080),
total GPU memory:
  single terrain, 64x36:   256 env  775 ->  154 MB (5.0x)
                          1024 env 2884 ->  366 MB (7.9x)
                          4096 env  OOM -> runs (was CUDA_ERROR_OUT_OF_MEMORY)
  4 terrains across 1024 env:     7300 ->  530 MB (13.8x)
Depth is bit-identical to the per-env path (homogeneous and N-variant);
cast speed is unchanged (it already read its tree; the build is one-time).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
kernel_update_verts_and_aabbs gained a required batch_repr_env parameter,
but the interactive viewer caller (RaycasterViewerPlugin -> Raycaster) was
left unchanged, so any scene that opens the viewer raycaster would fail with
a missing-argument error before the BVH could build.

The viewer BVH is always per-env (n_batches=n_envs), so pass the identity
mapping arange(n_envs) for batch_repr_env, reproducing the pre-change per-env
semantics (i_env == i_b) exactly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Milotrince
Milotrince force-pushed the kashu/raycast-shared-static-bvh branch from 4b1e870 to 1348ed8 Compare July 7, 2026 21:15
… opt-in flag)

Replace RigidOptions.shared_static_raycast_bvh (opt-in, caller-guaranteed) with automatic,
always-correct detection of env-identical static collision geometry.

Why: the flag had two failure modes. It silently did nothing on any solver with a movable link
(the common robot-on-terrain case), and with the flag on a legal per-env set_pos on a fixed body
silently produced wrong results -- the range-only grouping could not see per-env vertex divergence,
so every env read env 0's geometry.

How:
- Group envs by an exact per-env signature: the active geom ranges (when link info is batched) plus
  the per-env free-vert positions. fixed_verts_state is env-shared, so it is omitted. Two envs share
  a tree iff every input to update_aabbs matches -> a wrong merge is impossible; failure modes reduce
  to tree count, never correctness.
- Refresh the free verts (existing kernel_update_all_verts) before grouping, since they are B-sized
  and independent of tree count; then size the BVH to the group count and build. Split the AABB build
  out of the fused kernel via a new kernel_update_aabbs so the static path can group in between.
- Re-group on the existing rebuild trigger (the GEOMETRY subscriber fires on set_pos/set_quat/
  set_vverts; reset forces a rebuild), reallocating the AABB+LBVH pair only when the group count
  changes. A divergent set_pos now splits to per-env trees and stays correct; an identical reset
  merges back to one.
- Movable solvers and the visual BVH stay per-env; the visual path keeps the runtime all-identical
  shared-cast detection. Mixed static/dynamic scenes fall back to per-env automatically (documented;
  extending the win to the static portion needs a static/dynamic BVH split, left as future work).

Tests: shared (n_envs 0 and 4) -> 1 tree; N variants -> N trees with block routing; per-env set_pos
divergence -> per-env trees with correct per-env distances (guards the silent-corruption bug) and a
reset that merges back to one; mixed static/dynamic -> per-env fallback. Existing lidar_bvh and
heterogeneous_object tests updated for the automatic behavior and now assert the tree count.
@Milotrince
Milotrince force-pushed the kashu/raycast-shared-static-bvh branch from 1348ed8 to 7c343ed Compare July 7, 2026 21:52
@Milotrince

Copy link
Copy Markdown
Member

I removed the shared_static_raycast_bvh flag in favor of it being automatic!

@Milotrince Milotrince added the ready for review First pass review complete and ready for final review and merge. label Jul 7, 2026
duburcqa and others added 9 commits July 20, 2026 00:13
Port the automatic shared static collision BVH onto the per-component
rigid data repack (Genesis-Embodied-AI#3060) and the relocated sensor tests (Genesis-Embodied-AI#3037):

- Kernels take the DynState / DynInfo aggregates; the per-face AABB
  update is factored into a shared func so the new grouped build kernel
  (kernel_update_grouped_aabbs) reuses it, and the fused per-env kernel
  keeps its signature (the viewer plugin caller is untouched).
- The BVH traversal funcs now take a tree slot index for nodes / morton
  codes and the env index for leaf verts. Routing the single batch index
  through the group id read the wrong env's verts whenever the env ->
  group mapping was a non-monotonic permutation (covered by the
  non-monotonic set_pos phase of test_shared_static_bvh_regroup).
- Env grouping compares the active geom ranges and free-vert positions
  exactly, with a zero-copy identical-envs short-circuit and integer /
  float parts grouped separately (a mixed concatenation would promote
  the ranges to float). The representative env is derived with a stable
  argsort, portable across CUDA / CPU / Metal.
- Static collision BVH allocations are pooled per tree count: quadrants
  fields live for the whole process and template kernels specialize per
  field instance, so per-regroup reallocation would leak GPU memory and
  recompile the cast kernels on every diverge / reset cycle.
- Tests live in tests/sensors/test_raycaster.py; the sharing, regroup
  and merge-back scenarios are packed into one parametrized test, and
  test_heterogeneous_object now runs 6 envs so the 3-variants < n_envs
  collapse is observable.
…e mapping.

The tactile sensors (contact depth probe, kinematic taxel, elastomer
taxel) traverse the raycaster's collision BVH with the env index as the
tree batch index. Once a fully-fixed solver shares that BVH across envs,
the tree buffer holds one slot per distinct geometry, so the per-env
indexing reads out of bounds. The BVH traversal funcs now take the tree
slot for nodes / morton codes, resolved through the collision entry's
env_bvh_idx, while leaf triangles and per-env masks keep the env index.
…ulti-depth)

Follow-up to Genesis-Embodied-AI#2867. A scene with one moving robot on a large static terrain
still rebuilds a single combined collision BVH over every face each step,
because the rebuild-skip keys off "all links in the solver are fixed" -- false
as soon as the robot moves. The static terrain (the bulk of the faces) is
re-fit every step for nothing.

This decomposes the rigid solver's collision faces into two compacted BVHs by
owning-link fixedness (RaycastContext.activate):

  - static subset  (faces on fixed links: terrain / walls): maybe_static, built
    once, then skipped + shared across envs (the dominant per-step cost for one
    robot on a big static terrain).
  - dynamic subset (faces on movable links: the robot): rebuilt each step, but
    the rebuild + radix sort now scale with the robot's face count, not the
    whole scene.

The two are cast separately and merged via the existing is_merge path (closest
hit wins), so the result is identical to one combined BVH. This is the
"multi-depth" decomposition from RPL (arXiv:2602.03002): cast the dynamic robot
and static terrain meshes separately and reuse the static acceleration
structure across timesteps and environments.

Implementation
- The split lives in the shared RaycastContext (activate / update), so a
  Raycaster and a DepthCamera on the same solver share the one static + dynamic
  BVH pair. Each subset BVH is built over compacted AABB slots: a `face_ids`
  tensor maps a leaf slot to the global face index; bvh_ray_cast remaps after
  reading the morton-code primitive id and update_aabbs iterates the slots.
  `n_triangles` in bvh_ray_cast derives from the morton-codes shape (the BVH's
  own leaf count) instead of the solver-global face count.
- The remap is gated by a compile-time `is_remapped` flag: a BVH covering every
  face in order (pure-static / pure-dynamic solver, viewer pick, combined
  consumers below) skips both the map allocation and the per-leaf indirection,
  compiling to the exact pre-split kernels.
- An entry is static when every face it covers sits on a fixed link. This
  per-face criterion also upgrades the single-BVH case: a movable link without
  collision geoms no longer disables the static-rebuild skip.
- The existing maybe_static/needs_rebuild skip and the AABB-derived
  shared_across_envs test are already per-entry, so they apply to each subset
  unchanged: the static subset's GEOMETRY subscriber only fires on an explicit
  set_pos/set_quat (e.g. re-randomized terrain), never on physics-driven robot
  motion, so it stays skipped + shared while the robot subset rebuilds.
- Tactile sensors in contact_depth_query="raycast" mode walk one tree over
  every collision face (collision_bvh_context), which the split would tear
  apart. They declare require_combined_collision_bvh() at construction time
  (before any consumer activates the context), keeping today's single combined
  BVH in such scenes; collision_bvh_context now raises unless a consumer
  declared that need.

Merge correctness for no_hit_value < max_range
- Splitting a robot-on-terrain collision scene turns a former single-BVH cast
  into a two-BVH merge. The merge overwrites a slot only when it finds a closer
  hit, so a first-pass miss that stamped `no_hit_value` directly would beat a
  real hit from a later pass whenever no_hit_value < max_range (e.g. 0/-1) --
  previously guarded by a hard `no_hit_value >= max_range` check that would now
  newly reject those (valid, formerly single-BVH) configs.
- write_ray_hit instead seeds an intermediate miss with the `max_range`
  sentinel (a real hit is always strictly below it, so it loses every distance
  comparison) and stamps `no_hit_value` only on the final pass (is_last), over
  any slot still at the sentinel. A single-BVH cast is both first and last, so
  it writes no_hit_value directly -- unchanged. The `no_hit_value >= max_range`
  check is removed; this also lifts the same restriction from the pre-existing
  visual+collision merge. The debug-draw hit reconstruction for distances-only
  sensors now discriminates misses by equality with no_hit_value instead of an
  ordering test, which the lifted restriction would break.

Tests
- tests/sensors/test_raycaster.py::test_static_dynamic_bvh_split_merge
  (n_envs in {0, 2}): asserts the split structure (one static + one dynamic
  collision entry, static shared across envs), the three-pass merge (static +
  dynamic + visual) reporting the closest hit as boxes enter/leave a ray with
  no_hit_value=-1 < max_range, the rebuild-on-explicit-set_pos of the static
  subset, the settle-to-no_hit_value when every pass misses, and the static
  skip across physics-driven motion; a distances-only sensor shares the chain.
- tests/sensors/test_raycaster.py::
  test_tactile_raycast_consumer_forces_combined_collision_bvh: a tactile probe
  in raycast mode keeps the mixed scene on one combined collision BVH and both
  consumers stay functional.
- tests/sensors/test_api.py::test_shared_context updated to expect the
  static+dynamic split (2 entries).

Perf (perceptive depth camera, 64x36 rays, 18.5k-face terrain + G1, RTX 3080):
  1024 envs: 1037 -> 706 ms/step (1.47x) measured on the pre-refactor branch;
  win grows with env count x terrain faces.
Measured on this branch (A/B/A, default Terrain with 41.7k static faces +
Franka, 2.3k rays/env, distances-only): CUDA 1024 envs 76.8 -> 9.4 ms/step
(8.2x); CPU single env 8.5 -> 2.8 ms/step (3.1x).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…shared-static-bvh

Compose the static/dynamic collision BVH split (Genesis-Embodied-AI#2878) with the shared
static BVH grouping: the split's static face subset is now the groupable
entry, so its trees collapse across envs even when movable links share
the solver. The grouped build kernel and the traversal remap compose
through the shared per-face AABB func (face_ids / is_remapped), the
grouped allocation pool sizes slots to the subset, and the cast chain
carries is_merge / is_last / is_remapped / is_env_major together. The
split test asserts the static subset collapses to one tree while the
dynamic subset stays per env.
Re-express the shared static BVH grouping on the landed static/dynamic
split (Genesis-Embodied-AI#3078):

- The grouped build kernels adopt the landed subset architecture: they
  fit compacted AABB slots per tree and rely on the post-build leaf
  payload rewrite (kernel_remap_leaf_faces), so the traversals stay
  subset-agnostic and the cast kernels only gain the env routing
  (env_bvh_idx, env_cast_order, is_env_major replacing shared_bvh).
- An is_env_uniform entry collapses to a single tree with the grouping
  signature left unread; other static entries group by exact equality of
  their subset's build inputs, restricted by the signature masks so the
  movable subset's divergence under physics cannot split them.
- The tactile probes route each of the two folded trees through its
  entry's env_bvh_idx (the tree slot for nodes and morton codes, the env
  for leaf triangles and candidate masks).
- The link-filtered rebuild subscriptions and the refresh-once flow of
  the landed update() are kept; the standalone vert refresh serves the
  grouped static entries.
- Tests follow suit: tree-count assertions replace the retired
  shared_across_envs flag, the split test keeps its phases with the
  static subset asserted at one tree throughout, the heterogeneous test
  runs 6 envs to expose the 3-variants collapse, and the regroup test
  covers a mixed solver (static-subset masks) with a kinematic visual
  entity (grouped visual splits with interleaved membership). The
  monkeypatch-based spies are removed from tests/sensors: integration
  tests assert observable behavior.
@duburcqa duburcqa changed the title [PERF] Share static-geometry raycast BVH across envs (N trees, not B) [PERF] Reduce memory footprint of ray casting-based sensors by sharing memory between envs. Jul 20, 2026
@duburcqa duburcqa changed the title [PERF] Reduce memory footprint of ray casting-based sensors by sharing memory between envs. [PERF] Reduce memory footprint of ray casting-based sensors via cross-env memory sharing. Jul 20, 2026
@duburcqa duburcqa changed the title [PERF] Reduce memory footprint of ray casting-based sensors via cross-env memory sharing. [MISC] Reduce memory footprint of ray casting-based sensors via cross-env memory sharing. Jul 20, 2026
@duburcqa
duburcqa merged commit 32124fd into Genesis-Embodied-AI:main Jul 21, 2026
21 of 22 checks passed
@github-actions

Copy link
Copy Markdown

🔴 Benchmark Regression Detected ➡️ Report

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

Labels

ready for review First pass review complete and ready for final review and merge.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants