Skip to content

Split out direct, SHAC, profiling, and accuracy extras - #61

Open
vmoens wants to merge 55 commits into
mainfrom
feat/rl-extras-direct-shac
Open

Split out direct, SHAC, profiling, and accuracy extras#61
vmoens wants to merge 55 commits into
mainfrom
feat/rl-extras-direct-shac

Conversation

@vmoens

@vmoens vmoens commented Apr 8, 2026

Copy link
Copy Markdown
Owner

Summary

Test Plan

  • not run

vmoens and others added 30 commits March 18, 2026 16:09
The bracket swap conditions were incorrect, causing the humanoid solver
to diverge from MuJoCo C with default iterations=1. Three fixes:

1. Replace ad-hoc swap conditions with in_bracket (matching C's
   updateBracket / MJX), which only accepts candidates that narrow
   toward zero derivative.

2. Add one-sided phase emulation: when both bracket endpoints have
   same-sign derivatives (not yet bracketed), also accept candidates
   closer to zero, matching C's one-sided Newton phase.

3. Add early convergence check after initial Newton step, and add
   cross-bracket candidate swaps (6 candidates like MJX).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Raise _INLINE_CHOLESKY_MAX_SIZE from 16 to 32 so humanoid (nv=28)
  uses the inline path instead of torch.linalg.cholesky
- Add clamp_min(s, 1e-12) to inline diagonal pivots to prevent NaN
  from numerically non-SPD mass matrices (degenerate physics states)
- Add eps*I regularisation to the fallback path (nv > 32) as safety net
- All changes are torch.compile friendly (no control flow/error handling)

Fixes Cholesky crash in SAC humanoid training when envs reach extreme
joint configurations that make the mass matrix non-positive-definite.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace data-dependent tensor shape checks (d.efc_J.numel() == 0) with
static Python int checks from constraint_sizes(m) so torch._dynamo
traces once without placing guards on tensor shapes. Also ensure
make_constraint always uses the static max nefc for the output tensor,
matching the MJX approach of fixed-size constraint arrays.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The benchmark shows 200K+ steps/sec at B=2048 but SAC was getting 4K.
Root cause: compile wrapped the inner step_fn, then vmap() was called
on it every iteration. This prevented compile from fusing the batched
kernels. Now we pre-vmap the function and compile the vmapped version,
matching the benchmark's approach.

Also caches the vmapped function instead of recreating it each call.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Frame skip loop (5 iterations) is now inside the compiled function,
  so torch.compile can fuse all 5 physics steps into one kernel launch
- Use update_(ctrl=ctrl) instead of replace(ctrl=ctrl) to avoid
  shallow-copying the 80-field MjData dict every step
- Cache vmap function instead of recreating per call

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace Python loop of n individual clone() + torch.stack with a single
expand(n).clone() (one broadcast copy kernel). Use in-place add_ for noise.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Humanoid zoo env wrapping MuJoCo humanoid model
- Direct backprop training script (humanoid-specific + generic)
- Gradient accuracy tests for humanoid physics

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When auto_reset=True, done envs are reset inside _step before building
observations. This eliminates the separate maybe_reset → _reset path
and allows the reset to be part of the same execution flow as physics.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
With _skip_maybe_reset=True, InitTracker never set is_init on episode
boundaries, causing SACLoss value estimation to produce NaN. Instead,
keep maybe_reset running but make _reset a no-op when auto_reset is
enabled (since _step already handled the reset).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The obs returned by _step must match the state that produced the reward.
Previously, we reset done envs before building obs, so the replay buffer
stored (terminal_reward, reset_obs) — causing Q-value divergence → NaN.

Now: build obs from terminal state first, then reset. maybe_reset calls
_reset (no-op) which returns obs from the already-reset state for the
next step's input.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- PPO with GAE, compile support, 2-GPU split (env/training)
- SAC with auto-tuned alpha, compile support, 2-GPU split
- Both support humanoid and humanoid_rich environments

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Collector profiling script with torch.profiler and record_function markers
- Humanoid numerical accuracy test (mujoco-torch vs MuJoCo C)
- Bug report: physics blowup from degenerate states
- TODO: fix humanoid contact filtering (spurious constraints)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- HumanoidRichEnv with 336-dim observations (dm_control-style)
- SmoothHumanoidRichEnv with vmap for differentiable physics
- SHAC loss module: differentiable actor + value bootstrap + auto-tuned entropy
- Direct backprop and SHAC training scripts for humanoid_rich
- Launch script for parallel experiments

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove the auto_reset flag and its control-flow-based reset logic.
Instead, always compute the reset candidate (dx0 + noise) and use
torch.where to select between current and reset state based on done
mask. This is fully compilable with no graph breaks.

The old auto_reset had two problems:
1. if/else broke the torch.compile graph
2. TorchRL's maybe_reset would overwrite terminal obs with post-reset
   obs, corrupting replay buffers for off-policy algorithms (SAC)

The branchless approach works with TorchRL's standard reset flow:
_step returns terminal obs, then maybe_reset calls _reset which does
the actual per-env reset through the normal path.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…fix/torch-compile', 'perf/make-batch-benchmark', 'feat/humanoid-direct-training', 'feat/humanoid-rich-shac', 'feat/ppo-sac-training' and 'feat/profiling-testing' into combined/humanoid-training-pipeline
Data fields have different numbers of dims (qpos is 2D, xpos is 3D,
xmat is 4D). Reshape done_mask to (B, 1, 1, ...) matching each
field's ndim so torch.where broadcasts correctly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Data fields have different numbers of dims (qpos is 2D, xpos is 3D,
xmat is 4D). Reshape done_mask to (B, 1, 1, ...) matching each
field's ndim so torch.where broadcasts correctly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Humanoid envs have large observation ranges that cause issues with ReLU
activations. Uses TorchRL's ObservationNorm with standard_normal=True,
initialized from 1000 random steps. Eval env shares the same stats.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
SAC training loop now checks at 5 stages:
1. Collected batch: obs, action, reward for NaN/Inf/extreme (>1e10)
2. Replay sample: all fields
3. Loss values: NaN/Inf/extreme + full context dump (network outputs,
   obs/action/reward ranges, alpha state)
4. Gradients: param and grad NaN/Inf after backward
5. Parameters: NaN/Inf after optimizer step

Each stage saves a .pt diagnostic file and crashes with a detailed
message showing exactly where the bad values originated.

base.py _step now checks:
- qpos/qvel after physics step for blow-up (>1e10, NaN, Inf)
- reward after _compute_reward for extreme values
Both dump the full env state (qpos, qvel, action, ctrl) for the
offending environment.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
vmoens and others added 25 commits March 25, 2026 21:05
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
rollout() returns [num_envs, T, obs_dim]. With reduce_dim=0 (default),
stats end up with shape [T, obs_dim] instead of [obs_dim]. Use
reduce_dim=(0, 1) to reduce over both batch and time dimensions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
load_state_dict fails because eval env's ObservationNorm has empty [0]
buffers before init. Set loc/scale directly instead. Also print timing
and range info during init_stats for visibility.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1000 rollouts with compile warmup takes >5 min. 50 rollouts with
8192 envs still gives 50*8192*3 = 1.2M samples — plenty for good
mean/std estimates.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove obs_norm.initialized = True (setting loc/scale already marks
  it initialized; the property has no setter)
- Create vmap and frame_skip loop once in constructor instead of
  per-call in _step, matching the combined branch approach

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…pipeline

Brings ObservationNorm initialized fix to combined branch.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Allows passing torch.compile kwargs (mode, backend, fullgraph, etc.)
from the CLI via --compile_mode through to the physics step compilation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
PixelRenderTransform triggers a reset during TransformedEnv construction
to discover observation spec, which runs through ObservationNorm. Move
loc/scale initialization before TransformedEnv creation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@vmoens
vmoens force-pushed the feat/ppo-sac-training branch from fd714b0 to 5aa8be8 Compare April 8, 2026 16:09
Base automatically changed from feat/ppo-sac-training to main April 11, 2026 19:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant