Skip to content

Add locomotion RL tasks with downloadable robot assets - #646

Draft
acrlw wants to merge 2 commits into
DexForce:mainfrom
acrlw:feat/locomotion-rl-hf-assets
Draft

acrlw wants to merge 2 commits into
DexForce:mainfrom
acrlw:feat/locomotion-rl-hf-assets

Conversation

@acrlw

@acrlw acrlw commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

Description

Add six velocity-control tasks and Humanoid Run to the official task package, with downloadable robot assets and default/Newton environment and PPO configurations. This makes the local locomotion implementations available through EmbodiChain's standard task discovery, asset loading and RL entry points.

Merge prerequisite: #635 must be merged first. Keep this PR in draft until the target simulator dependency version is confirmed and asset loading, environment and policy checks pass on the updated main branch.

Tasks and assets

Environment ID Asset class
UnitreeG1FlatRL-v1 UnitreeG1Locomotion
UnitreeH1_2FlatRL-v1 UnitreeH1_2Locomotion
UnitreeGo1FlatRL-v1 UnitreeGo1Locomotion
UnitreeGo2FlatRL-v1 UnitreeGo2Locomotion
ANYmalCFlatRL-v1 ANYmalCLocomotion
MicroDuckFlatRL-v1 MicroDuckLocomotion
HumanoidRun-v1 HumanoidRun

The seven ZIPs are published in DexForceAI/embodichain_data. Add their descriptors in embodichain/data/assets/locomotion_assets.py and register them with the download CLI. Task configurations resolve models through get_data_path(). The CLI now returns a nonzero exit status if any requested download fails.

Register the tasks and manager functions, with separate actor/critic observations, robot-specific control settings and PPO configurations.

Contact collection

Reading contacts only after the final physics substep can miss brief contacts needed by rewards and termination checks. Add an _advance_physics() hook for task-level collection after each substep, cache actor metadata until its tables change, and expose dropped-contact counts.

Validation

Local validation uses main@6a419136 with #635 at c502be19:

  • Seven assets passed empty-cache CLI download, extraction and model-path resolution.
  • The 2026-09-16 runtime matrix passed all 14 cases: seven tasks × two backends, each with four environments and 32 steps. Checks cover construction, full/selective reset, finite outputs, policy forward, PPO construction and cleanup. Untouched reset rows were unchanged; query-level dropped contacts were zero. Training updates: 0. G1 Flat passed a two-backend recheck on 2026-09-17.
  • 270 targeted tests passed on 2026-09-17 across the task integration and shared interfaces. On the publication base main@3224ac1e, seven task registrations, 41 asset/documentation checks, Black and API documentation checks passed.

Runtime and assets

The checks above used a locally modified simulator build. MicroDuck currently requires a short cache path; its model files carry the upstream BY-SA-NC designation. Source and license notices are included in each asset archive.

Type of change

  • New feature (non-breaking change which adds functionality)

Checklist

  • Changed Python files are formatted with Black 26.3.1; the repository-wide check passes.
  • Usage documentation and project context are updated.
  • Public API documentation passes python docs/scripts/check_api_docs.py.
  • Tests cover the added tasks, asset loading and contact collection.
  • Duplicate changes and generic interface tests are excluded from the task diff.
  • Fix RL joint isolation and expose batched simulation controls #635 is merged and this branch is updated to main.
  • The target simulator dependency version is confirmed; asset loading, environment and policy checks pass on that version.

@acrlw acrlw added enhancement New feature or request task A task written in openai gym format for imitation learning or reinforcement learning robot Module related to robot rl Features related to reinforcement learning assets Related to simulation assets (robot, CAD, material, etc) labels Sep 17, 2026
@yuecideng

Copy link
Copy Markdown
Contributor

I recommend moving the reusable runtime mechanisms in this PR into standard EmbodiChain components, while keeping task semantics and robot-specific configuration in the locomotion task package.

EmbodiChainVelocityEnv currently owns contact aggregation, substep contact history, contact timing, random streams, command generation, and reward-state assembly. Some of these responsibilities already have natural owners in the sensor, action, event, and environment lifecycle layers.

Capability Recommended owner What should remain task-specific
Contact counterpart filtering, per-link force aggregation, substep history, first-contact detection, and air-time tracking ContactSensor or a dedicated contact-history component owned by the sensor layer Foot/link selection, failure conditions, and reward thresholds
Physics-substep sampling and action-update scheduling Standard environment stepping lifecycle Which components participate and their configuration
Default-pose action offsets, scaling, and clipping Standard ActionTerm implementations Joint selection, default pose, and scale values
Action-delay buffers Standard action component with substep-update and selective-reset integration Delay ranges
Root-velocity disturbances Standard event/randomization functor Target robot, disturbance ranges, and intervals
Seeding and selective reset of stateful components Environment/manager lifecycle contracts Task-specific state initialization
Velocity-command sampling, heading control, and curricula A shared locomotion-family component for now Command ranges, curriculum stages, and gait parameters
Observation layout, reward composition, and termination rules Task layer Task semantics

Contact handling should be the first boundary to address before merging. In _read_contacts() and _capture_contact_substep(), the task checks sensor capabilities and implements a separate fallback. The reduction branch requests ground-only foot contacts, whereas the fallback aggregates any contact involving a foot. The fallback also accumulates illegal-contact forces and self-collisions across substeps, but reads foot contact/force from the latest sensor sample. This leaves the task responsible for reconciling sensor semantics and allows behavior to differ between paths.

The standard contact component should expose a consistent contract for counterpart filtering and contact history, including selective reset. It should also distinguish current air time from the completed air time captured on landing: the current task code clears air time before the ANYmal landing reward consumes it.

Action components need explicit interfaces before being promoted. DefaultJointPositionTerm reads task-specific configuration, and its processing path accesses record_locomotion_action and encoder_bias on the environment. Replace those implicit task dependencies with explicit configuration or standard state interfaces. Moving the file alone would retain the coupling. Likewise, a reusable delay term needs a defined substep-update and reset lifecycle, not just an advance_physics_step() method.

The task RNG should participate in explicit reseeding. _locomotion_generator is seeded during construction, but the inherited reset(seed=...) does not rewind it. A standard lifecycle for component-owned random streams would avoid this issue recurring in other tasks.

Suggested order:

  1. Move contact filtering/history ownership to the sensor layer and connect it to the standard substep lifecycle.
  2. Integrate component seeding and selective reset with the environment/manager lifecycle.
  3. Promote the generic action mapping and root-velocity disturbance implementations after removing task-specific dependencies.
  4. Keep command generation and curricula shared within the locomotion family until another consumer establishes a broader abstraction requirement.

Validation should cover these ownership boundaries: foot self-contact must not count as ground contact; a foot contact occurring only during an early substep must remain observable; a landing reward must receive the completed air time; repeated explicit seeds must reproduce task random streams; and selective reset must preserve untouched rows.

These recommendations are independent of the declared #635 prerequisite. They would address the underlying ownership problems rather than leave each task adapter responsible for maintaining its own contact and lifecycle implementation.

@acrlw

acrlw commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator Author

Updated in this PR:

  • Moved contact filtering, force aggregation, substep history, and air-time tracking into the sensor layer. BaseEnv drives substep sampling, and landing rewards use completed air time.
  • Integrated component RNGs, contact history, action history, and disturbance timers with environment and manager reseeding and selective reset.
  • Moved joint-position mapping and root-velocity disturbances into standard components, removed implicit task dependencies, and deleted the unused action-delay implementation.
  • Kept commands and curricula shared within the locomotion family, with observations, rewards, and termination rules in the task layer.

All 207 focused tests passed, including the five regression cases identified in the review. All seven tasks passed smoke tests on both backends, and the existing CartPole check passed.

@yuecideng yuecideng left a comment

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.

I reviewed the shared environment and sensor abstractions introduced for locomotion. The separation is promising, but the inline comments below identify scalability, lifecycle, observability, and API-contract issues that should be addressed before these APIs are treated as reusable foundation components.

force = torch.where(valid.unsqueeze(-1), force, 0.0)
matches = []
for side in (0, 1):
selected = pair[:, :, side, None] == self.actor_ids[:, None, :]

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.

[P1] Avoid dense env × contact × body reductions in the substep hot path

This broadcast materializes an (E, C, B) selection tensor, and the force path below materializes (E, C, B, 3) before reducing it. With the default G1 PPO setup (E=4096, contact capacity C=192) and only B=20 tracked bodies, the float intermediate alone is roughly 180 MiB per physics substep, before masks, conversions, and history buffers; locomotion executes this four times per control step. The current small-environment validation will not expose this training-scale cost. Please use a fused sparse Warp/CUDA reduction over the contacts that actually exist (for example, with precomputed actor-to-history indices and device-side counts), and validate at least one 4096-environment rollout plus PPO update.

for sensor in sensors:
sensor.begin_control_step()
for _ in range(self.cfg.sim_steps_per_control):
self.sim.update(self.physics_dt, 1)

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] Preserve the SimulationManager.update control-step boundary

Replacing one sim.update(dt, decimation) call with decimation calls to sim.update(dt, 1) changes observable manager behavior, not just sensor timing. Each call now runs the manager's prepare/profiling boundary, and the Viser path treats every single-step call as the final step and can capture camera images at physics rate instead of control rate. Please keep one manager update per control step and expose an after-substep observer/callback inside SimulationManager.update for sensors that require substep sampling.

self._component_generators: dict[str, torch.Generator] = {}
if name not in self._component_generators:
generator = torch.Generator(device=self.device)
generator.manual_seed(self._component_seed(name, self.cfg.seed or 0))

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] Do not turn seed=None into a fixed deterministic seed

EnvCfg.seed=None currently means that the environment is not explicitly seeded, but self.cfg.seed or 0 gives every fresh environment the same component seed derived from zero. That silently changes randomization behavior for unseeded runs. Please call generator.seed() (or otherwise use nondeterministic entropy) when cfg.seed is None, and derive stable named seeds only when an explicit base seed is provided. A regression test should compare separately constructed unseeded environments as well as reproducibility under an explicit seed.

self._data_buffer["is_valid"].zero_()

contact_buffer = self._query.fetch()
self._query_dropped_count = contact_buffer.dropped_count

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] Accumulate dropped contacts across the full control interval

This overwrites _query_dropped_count after every substep. If an early substep overflows and the final one does not, the public diagnostic reports zero even though contacts used by the interval-level history/reward logic were lost. Clear an interval counter in begin_control_step() and accumulate every fetch result instead. Also record scatter overflow with a separate device-side counter: _num_contacts_per_env is capped/decremented when the per-environment buffer is full, so deriving overflow from that value cannot reliably reveal discarded contacts.

@property
def requires_substep_update(self) -> bool:
"""Whether this sensor participates in the physics-substep lifecycle."""
return self.cfg.track_substeps and bool(self._histories)

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] Do not let a successfully created history remain silently inert

create_history() succeeds regardless of track_substeps, but this predicate disables all lifecycle updates when the option retains its default value (False). Callers then receive a valid-looking history whose data stays zero, despite the history API promising that the sensor lifecycle updates it. Either make the existence of a history enable substep sampling automatically, or reject create_history() with a clear configuration error when sampling is disabled. Please cover the default configuration in a regression test.

track_substeps: bool = False
"""Sample registered contact histories after every physics substep."""

self_collision_force_threshold: float | None = None

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.

[P3] Keep the locomotion reward threshold out of the generic sensor config

The sensor itself does not consume this field; only the locomotion setup reads it and passes it to create_history(). Putting a reward/policy threshold on ContactSensorCfg makes reusable sensor and embodiment configuration task-specific and creates two owners for history semantics. Please move this value to the locomotion task/reward configuration and pass it explicitly as force_threshold when constructing the self-contact history.

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

Labels

assets Related to simulation assets (robot, CAD, material, etc) enhancement New feature or request rl Features related to reinforcement learning robot Module related to robot task A task written in openai gym format for imitation learning or reinforcement learning

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants