Skip to content

Fix quadratic startup scaling in NewtonSiteFrameView for body-mounted frames#6423

Open
r-schmitt wants to merge 12 commits into
isaac-sim:developfrom
r-schmitt:dev/rschmitt/warp_init_spike
Open

Fix quadratic startup scaling in NewtonSiteFrameView for body-mounted frames#6423
r-schmitt wants to merge 12 commits into
isaac-sim:developfrom
r-schmitt:dev/rschmitt/warp_init_spike

Conversation

@r-schmitt

@r-schmitt r-schmitt commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Description

Summary
Camera-heavy Newton scenes exhibited a severe environment-initialization time spike that scaled quadratically with num_envs. On an RTX PRO 6000 (Blackwell), the Isaac-Lift-KukaAllegro-Camera benchmark at 8192 envs spent ~29 minutes in "Simulation Start" alone.

The root cause is an O(num_envs²) regex loop in NewtonSiteFrameView._initialize_from_specs: for a frame that resolves to a per-environment body path (e.g. a body-mounted camera), the view issued one resolve_matching_names call per environment, and each call regex-scans the entire Newton body_label list (which itself grows with num_envs).

This PR fast-paths literal body paths through an exact dict lookup built once, keeping the common per-environment case linear while preserving the regex fallback for genuine patterns (e.g. the cloned .* pattern).

How the slow path is reached
The spike only appears when a frame/sensor is anchored to a physics body that is replicated per environment. The duo_camera preset for Isaac-Lift-KukaAllegro-Camera includes exactly such a camera, mounted on the robot hand:

  • /World/envs/env_.*/Camera — anchored at the env root (a static frame)

  • /World/envs/env_.*/Robot/ee_link/palm_link/Camera — anchored to the palm_link rigid body

Call chain during env creation:

gym.make(...)
└─ sim.reset() # Timer: "simulation_start"
└─ NewtonManager.start_simulation()
└─ dispatch_event(PhysicsEvent.PHYSICS_READY)
└─ Camera._initialize_impl() # one per camera sensor
└─ FrameView(cfg.prim_path) # == NewtonSiteFrameView
└─ _initialize_from_specs(model)
└─ for each of num_envs specs:
resolve_matching_names(, body_labels) # scans ALL labels

The env-root camera resolves to body_patterns is None and takes the cheap branch (~174 ms at 2048 envs).

The body-mounted camera expands to one concrete body path per environment. Each is regex-matched against all body labels:

At 2048 envs: resolve_matching_names called 2048 times, each scanning 65,536 body labels → 70,136 ms.

Because both the number of calls and the label-list length grow with num_envs, cost is O(num_envs²).

Fixes

In _initialize_from_specs:

  • Build label_to_index = {label: idx for idx, label in enumerate(body_labels)} once (O(num_bodies)).

  • For each body pattern, if it contains no regex metacharacters, resolve it with an O(1) dict lookup; otherwise fall back to resolve_matching_names.

This is behavior-preserving: literal paths previously matched exactly one label via regex full-match, which the dict lookup reproduces; patterns with metacharacters (.*+?|^$) still use the regex scan.

using benchmark_non_rl, Isaac-Lift-KukaAllegro-Camera, preset cube,duo_camera,newton,newton_renderer,rgb64 (matching the benchmark runs that identified the issue) the fix produced the following results:

Simulation Start Time: before 1729 seconds, after 19 seconds
Task Creation and Start Time: before 1763 seconds, after 59 seconds

note the cost is CPU-bound (python regex) so it is not GPU specific

Type of change

  • Bug fix (non-breaking change which fixes an issue)

Checklist

  • I have read and understood the contribution guidelines
  • I have run the pre-commit checks with ./isaaclab.sh --format
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • I have updated the changelog and the corresponding version in the extension's config/extension.toml file
  • I have added my name to the CONTRIBUTORS.md or my name already exists there

@github-actions github-actions Bot added the isaac-lab Related to Isaac Lab team label Jul 8, 2026
@greptile-apps

greptile-apps Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes an O(num_envs²) startup-time bottleneck in NewtonSiteFrameView._initialize_from_specs. When a sensor is anchored to a per-environment physics body (e.g. a hand-mounted camera), the original code called resolve_matching_names once per environment, each call doing a regex scan across all body labels, making both the call count and the scan length grow with num_envs.

  • Introduces a label_to_index dict built once from all Newton body labels; literal body patterns (no regex metacharacters) are resolved in O(1) via dict lookup, while genuine regex patterns (containing ., *, [, etc.) still fall back to the existing resolve_matching_names path.
  • Adds a module-level _REGEX_TOKENS frozenset and _has_regex_tokens helper to distinguish literal USD paths from regex expressions.
  • Adds two changelog fragments: one for the Newton frame-view fix and one for a CameraCfg default-renderer change whose code already exists in the repository (not introduced by this PR's diff).

Confidence Score: 4/5

The Newton frame-view change is safe to merge; the logic is correct and behavior-preserving for all pattern types.

The core fix is well-reasoned and correctly implemented: the is not None guard handles index 0, the fallback to resolve_matching_names fires whenever the dict lookup yields nothing, and genuine regex patterns are routed to the existing scan unchanged. The only open question is the rschmitt_default_cameracfg_renderer.rst changelog entry, which documents code not present in this PR's diff.

source/isaaclab/changelog.d/rschmitt_default_cameracfg_renderer.rst — documents CameraCfg/renderer changes already in the codebase, not introduced by this PR.

Important Files Changed

Filename Overview
source/isaaclab_newton/isaaclab_newton/sim/views/newton_site_frame_view.py Core performance fix: builds a label→index dict once and uses O(1) exact lookup for literal body patterns, falling back to the existing regex scan only when needed. Logic is correct, index-0 is handled safely via is not None, and non-literal patterns fall back properly.
source/isaaclab_newton/changelog.d/rschmitt-frameview-site-init-scaling.rst Changelog entry accurately describes the O(num_envs²) → O(num_envs) fix for the Newton site frame view.
source/isaaclab/changelog.d/rschmitt_default_cameracfg_renderer.rst Changelog entry for CameraCfg/renderer changes not present in this PR's code diff — the documented API already exists in the codebase. May be a missing-changelog backfill or accidental leftover.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["_initialize_from_specs(model)"] --> B["Build label_to_index dict once O(num_bodies)"]
    B --> C["For each body_pattern"]
    C --> D{"_has_regex_tokens?"}
    D -- No --> E["label_to_index.get(body_pattern)"]
    E -- Found --> F["matched_indices = [exact_index]"]
    E -- None --> G["resolve_matching_names() O(num_bodies)"]
    D -- Yes --> G
    F --> H["Append body_idx, xform, scale"]
    G --> H
    H --> I{"matched_indices empty?"}
    I -- Yes --> J["raise ValueError"]
    I -- No --> C
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A["_initialize_from_specs(model)"] --> B["Build label_to_index dict once O(num_bodies)"]
    B --> C["For each body_pattern"]
    C --> D{"_has_regex_tokens?"}
    D -- No --> E["label_to_index.get(body_pattern)"]
    E -- Found --> F["matched_indices = [exact_index]"]
    E -- None --> G["resolve_matching_names() O(num_bodies)"]
    D -- Yes --> G
    F --> H["Append body_idx, xform, scale"]
    G --> H
    H --> I{"matched_indices empty?"}
    I -- Yes --> J["raise ValueError"]
    I -- No --> C
Loading

Reviews (1): Last reviewed commit: "fix initialize from spec scaling issues" | Re-trigger Greptile

Comment thread source/isaaclab/changelog.d/rschmitt_default_cameracfg_renderer.rst Outdated
@r-schmitt r-schmitt self-assigned this Jul 8, 2026
@kellyguo11 kellyguo11 moved this to In review in Isaac Lab Jul 9, 2026
ooctipus added 2 commits July 8, 2026 22:41
NewtonSiteFrameView's post-finalize path expanded a body-mounted frame
into one destination path per environment and resolved each against the
fully cloned Newton body-label list. The exact-lookup fast path removed
the quadratic regex cost, but still reverse-mapped through renamed label
strings information the cloner had structurally and discarded.

Record per-source body offsets while replicate_builder_mapping appends
source builders (mirroring how injected-site indices are computed) and
retain them on NewtonManager. Body-anchored frames now locate the source
body once in its source builder and derive per-environment global
indices as offset + local index, keeping pattern matching confined to
source builders like the rest of the cloner design. Frames without
replication metadata (no clone plan, flat stages) keep the exact
label-lookup fallback.
Camera sensors built their FrameView during PHYSICS_READY, after the
Newton model was finalized, so the view resolved its anchoring bodies
against the finalized model instead of going through the injected-site
path that every other site-based sensor (IMU, FrameTransformer,
RayCaster, PVA) uses from its constructor.

Add FrameView.register_frame so sensors can pre-register frame prims
with the active physics backend before finalization; backends without
pre-registration ignore the call. The Newton backend resolves the site
specs once in source space, registers them for injection during
replication, and records the labels so a view constructed later for
the same expression initializes from the injected sites. The camera
now registers during construction and re-registers on invalidation,
mirroring the IMU. Post-finalize resolution remains only as the
fallback for unregistered frames.
@ooctipus ooctipus requested a review from pascal-roth as a code owner July 9, 2026 07:32
ooctipus added 3 commits July 9, 2026 01:33
Review feedback: NewtonManager is already global state; avoid growing
it for FrameView bookkeeping.

Replace the registered-frames map with a registration handle:
register_frame now returns the site labels and scales, the camera
stores the handle, and the view consumes it through a new registration
constructor parameter. Ownership of the bridge between registration
and view construction moves to the sensor that needs it.

Drop the per-source body-offset tracking from replication entirely.
With cameras pre-registering their frames, post-finalize resolution
only serves ad-hoc views, and the exact label lookup is sufficient
there. replicate_builder_mapping and NewtonManager return to their
original shapes.
Drop the re-registration on camera invalidate. Registrations feed a
pending queue that each model build drains, and site labels restart
per build, so a handle held across a stop could alias another sensor's
fresh site and bind silently wrong. Discard the handle on invalidation
instead; a rebuilt simulation resolves the frame from its specs.

register_frame now returns None once the model is finalized, keeping
handles valid only for the build they precede, and loses its unused
validate flag. Remove the _SiteSpec dataclass; specs return to the
original tuples now that nothing needs extra fields.
Site and extended-attribute registrations feed a pending queue that
each Newton model build drains, so sensors surviving a stop had to
re-register in their invalidate callbacks — a push pattern every
sensor duplicated and that silently broke when forgotten (RayCaster
never re-registered).

Add PhysicsEvent.PRE_MODEL_BUILD, dispatched once before each model
build at the injection entry points. SensorBase subscribes a
_pre_model_build_callback hook; IMU, PVA, FrameTransformer,
JointWrenchSensor, RayCaster, and Camera declare their requests
there, and NewtonSiteFrameView refreshes deferred views the same way.
All invalidate re-register blocks are gone; every build collects
requests from live consumers by construction. Backends without a
build phase never dispatch the event.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

isaac-lab Related to Isaac Lab team

Projects

Status: In review

Development

Successfully merging this pull request may close these issues.

3 participants