Fix quadratic startup scaling in NewtonSiteFrameView for body-mounted frames#6423
Fix quadratic startup scaling in NewtonSiteFrameView for body-mounted frames#6423r-schmitt wants to merge 12 commits into
Conversation
Greptile SummaryThis PR fixes an O(num_envs²) startup-time bottleneck in
Confidence Score: 4/5The 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 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
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
%%{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
Reviews (1): Last reviewed commit: "fix initialize from spec scaling issues" | Re-trigger Greptile |
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.
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.
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
Checklist
pre-commitchecks with./isaaclab.sh --formatconfig/extension.tomlfileCONTRIBUTORS.mdor my name already exists there