Skip to content

Fix actuator gain snapshot corruption for multi-env floating-base articulations#6407

Open
AntoineRichard wants to merge 1 commit into
isaac-sim:developfrom
AntoineRichard:antoiner/fix-actuator-gain-env-stride
Open

Fix actuator gain snapshot corruption for multi-env floating-base articulations#6407
AntoineRichard wants to merge 1 commit into
isaac-sim:developfrom
AntoineRichard:antoiner/fix-actuator-gain-env-stride

Conversation

@AntoineRichard

Copy link
Copy Markdown
Collaborator

Description

The init-time actuator gain snapshot (scatter_gain_kernel, shipped with the actuators integration #5455) decodes environment/local joint slots from global model DOF indices using the articulation's joint count as the per-environment stride. Newton's global DOF space also contains free-root DOFs, so for a floating-base articulation with more than one environment the per-env stride is larger than the joint count (ANYmal: 18 vs 12), later environments' joints decode to the wrong or a nonexistent environment slot, and the kernel writes past the snapshot buffer.

Observed failure modes, all from the same OOB write:

  • CUDA, release: silent — randomize_actuator_gains baselines (actuator.stiffness/damping defaults) are corrupted (env-1 rows partially zero / misplaced) with no error.
  • CPU, warp ≥ 1.15.0.dev20260626 (the minimum required by the current newton pin): glibc corrupted size vs. prev_size → deterministic SIGABRT at articulation init. Older warp allocator layouts absorbed the write silently.
  • warp debug mode: device-side bounds asserts on both devices, naming this kernel.

Why no existing test caught it: the equivalence suites build multi-env floating-base ANYmal with Newton actuators — they execute the buggy scatter — but only compare joint trajectories; the corrupted snapshot never feeds the sim step. The gain-randomization test reads controller.kp/kd directly rather than the snapshot. Verified side-by-side: the DR test passes while the new regression test fails on the identical unfixed build.

Fix: thread the true per-environment DOF stride (adapter.num_joints, which by construction equals the model's per-env DOF count on both backends) into the kernel and decode env = global_dof // env_stride. The PhysX call site passes a stride identical to its joint count and is behaviorally unchanged (covered by its existing tests). patch_actuator_param_kernel shares the decode pattern but its only live caller guarantees stride == joint count; documented that invariant in its docstring. All other kernels in the module audited clean.

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 added tests that prove my fix is effective
  • I have added a changelog fragment under source/<pkg>/changelog.d/ for every touched package
  • My name exists in CONTRIBUTORS.md

Test Plan

  • New TestNewtonActuatorGainSnapshotEnvStride::test_snapshot_matches_config_for_all_envs (floating-base ANYmal, 2 envs, explicit PD gains) — FAILS without the fix (Mismatched elements: 6 / 24, env-1 rows zeroed), PASSES with it
  • ./isaaclab.sh -p -m pytest source/isaaclab_newton/test/assets/test_newton_actuators_newton.py → 72 passed
  • PhysX cross-check (TestRandomizeActuatorGainsViaEventsPhysx, TestIdealPDEquivalence) → 6 passed (signature threading is a no-op on PhysX)
  • ./isaaclab.sh -f → all hooks pass

The init-time gain snapshot kernel decoded environment and local joint
slots from global model DOF indices using the articulation's joint
count as the per-environment stride. Newton's global DOF space also
contains free-root DOFs, so for floating-base articulations with more
than one environment the decode misattributed later environments'
joints and wrote past the snapshot buffer: silently corrupted gain
randomization baselines on CUDA, and heap corruption aborts on CPU
under newer Warp allocator layouts. Thread the true per-environment
DOF stride from the adapter into the kernel; the PhysX call site
passes an identical stride and is behaviorally unchanged.

The existing suites missed this because the equivalence tests never
read the snapshot and the randomization test reads controller
parameters directly. The new regression test asserts the snapshot
values for every environment row of a floating-base multi-environment
articulation and fails deterministically without the fix.
@github-actions github-actions Bot added bug Something isn't working isaac-lab Related to Isaac Lab team labels Jul 8, 2026
@greptile-apps

greptile-apps Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes an out-of-bounds write in the scatter_gain_kernel that corrupted the per-articulation kp/kd snapshot (newton_default_stiffness/newton_default_damping) for multi-environment floating-base articulations on Newton. The root cause was that the kernel decoded the per-env slot by dividing the global DOF index by num_joints (the articulation-local joint count = 12) instead of the whole-model per-env DOF count (= 18 for ANYmal, because free-root DOFs inflate that stride). The fix threads env_stride — taken from adapter.num_joints which is the correct whole-model per-env DOF count — into the kernel as a new, separately-tracked parameter.

  • kernels.py: scatter_gain_kernel gains an env_stride parameter and now computes env = global_dof // env_stride and local_dof = global_dof - env * env_stride; the existing build_per_dof_env_mask_kernel is unaffected because it already receives adapter.num_joints (= the whole-model DOF stride) as its num_joints argument.
  • adapter.py / articulation.py (Newton & PhysX): Both call sites thread env_stride=adapter.num_joints through; for PhysX this is a no-op since there are no free-root DOFs.
  • test: A new regression test (TestNewtonActuatorGainSnapshotEnvStride) exercises 2-env floating-base ANYmal and asserts every cell of both env rows matches the configured gain, which fails on the unpatched build.

Confidence Score: 4/5

Safe to merge — the fix is a targeted, well-scoped change to a single kernel parameter and its two call sites, with a dedicated regression test and thorough docstring explaining the invariants.

The core logic is correct: env_stride (the whole-model per-env DOF count) is distinct from num_joints (the articulation-local destination-buffer stride), and the fix correctly separates them. The build_per_dof_env_mask_kernel is unaffected because NewtonActuatorAdapter.num_joints already carries the whole-model stride there. The only gap is that the regression test runs on cuda:0 only, leaving the deterministic CPU SIGABRT scenario without automated coverage.

No files require special attention beyond the test device coverage note.

Important Files Changed

Filename Overview
source/isaaclab_newton/isaaclab_newton/actuators/kernels.py Core fix: scatter_gain_kernel receives env_stride separately; env decode corrected to use env_stride instead of num_joints; patch_actuator_param_kernel gets a clear docstring warning against reuse on floating-base layouts.
source/isaaclab_newton/isaaclab_newton/actuators/adapter.py build_newton_actuator_defaults gains an env_stride parameter and correctly forwards it to both scatter_gain_kernel calls; docstring explains the distinction between num_joints (dst inner stride) and env_stride (global DOF layout stride).
source/isaaclab_newton/isaaclab_newton/assets/articulation/articulation.py Newton Articulation call site passes env_stride=adapter.num_joints (whole-model per-env DOF count) to build_newton_actuator_defaults; one-line addition that threads the correct stride.
source/isaaclab_physx/isaaclab_physx/assets/articulation/articulation.py PhysX call site updated to pass env_stride=adapter.num_joints; behaviorally a no-op since PhysX has no free-root DOFs and adapter.num_joints already equals num_joints there.
source/isaaclab_newton/test/assets/test_newton_actuators_newton.py New regression test TestNewtonActuatorGainSnapshotEnvStride checks that both env rows of the kp/kd snapshot match the configured gain for a floating-base ANYmal; test only runs on cuda:0 even though the CPU failure (SIGABRT) is a documented failure mode.
source/isaaclab_newton/changelog.d/fix-actuator-gain-env-stride.rst Accurate changelog entry describing the env-stride fix for floating-base Newton actuators.
source/isaaclab_physx/changelog.d/fix-actuator-gain-env-stride.skip Changelog skip marker for the PhysX package correctly notes the call-site adaptation is behaviorally transparent.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Art as Articulation._initialize_actuators
    participant Fn as build_newton_actuator_defaults
    participant K as scatter_gain_kernel (Warp)
    participant Buf as flat_stiffness/flat_damping

    Art->>Fn: "num_joints=12, env_stride=adapter.num_joints(18)"
    loop per actuator with kp/kd
        Fn->>K: "num_joints=12, env_stride=18, indices"
        Note over K: env = global_dof // env_stride
        Note over K: local_dof = global_dof - env*env_stride
        Note over K: dst[env*num_joints + local_dof] = src[i]
        K-->>Buf: correct (env, joint) slot
    end
    Fn-->>Art: stiffness, damping, joint_indices
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"}}}%%
sequenceDiagram
    participant Art as Articulation._initialize_actuators
    participant Fn as build_newton_actuator_defaults
    participant K as scatter_gain_kernel (Warp)
    participant Buf as flat_stiffness/flat_damping

    Art->>Fn: "num_joints=12, env_stride=adapter.num_joints(18)"
    loop per actuator with kp/kd
        Fn->>K: "num_joints=12, env_stride=18, indices"
        Note over K: env = global_dof // env_stride
        Note over K: local_dof = global_dof - env*env_stride
        Note over K: dst[env*num_joints + local_dof] = src[i]
        K-->>Buf: correct (env, joint) slot
    end
    Fn-->>Art: stiffness, damping, joint_indices
Loading

Reviews (1): Last reviewed commit: "Fix actuator gain snapshot env stride de..." | Re-trigger Greptile

gravity_enabled=True,
add_ground_plane=True,
sim_cfg=sim_cfg,
) as sim:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Regression test only exercises cuda:0

The PR description explicitly calls out the CPU failure mode (glibc corrupted size vs. prev_size → deterministic SIGABRT) as a separate, more visible symptom of the same OOB write. The new test is hardcoded to device="cuda:0", so a CPU-only CI runner would never exercise this path. If the test suite has a mechanism to parameterize over devices, adding a "cpu" variant here would directly cover the documented CPU crash scenario.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@AntoineRichard AntoineRichard requested a review from jvonmuralt July 9, 2026 13:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working isaac-lab Related to Isaac Lab team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants