Skip to content

Commit 2fa55dd

Browse files
Add MuJoCo Warp sleeping support (#3731)
Co-authored-by: Eric Heiden <eric-heiden@outlook.com>
1 parent 47beb1b commit 2fa55dd

13 files changed

Lines changed: 1503 additions & 124 deletions

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
- Add opt-in filtering of static-static, static-kinematic, and kinematic-kinematic contacts during broad-phase collision detection. Set `CollisionPipeline(include_static_kinematic_pairs=False)` to enable filtering; the default preserves existing contact generation. `Model.shape_contact_pairs` remains an unfiltered superset for direct consumers such as `SolverKamino` and hydroelastic SDF setup.
3636
- Add opt-in `body_frame_origin="com"` to `ModelBuilder.add_rod()` and `ModelBuilder.add_rod_graph()` for COM-centered cable capsule body frames.
3737
- Add `sign_method` argument to `Mesh.build_sdf` and `SDF.create_from_mesh` support for a `"normal"` (angle-weighted pseudo-normal) sign strategy, for selecting the inside/outside sign of the baked SDF (`"auto"`, `"parity"`, `"winding"`, or `"normal"`).
38+
- Add opt-in MuJoCo Warp sleeping support to `SolverMuJoCo` with MJCF sleep configuration, initial tree policies, `sleep_tolerance`, compact `nvmax` storage, and a launchable `mujoco_sleeping` example. (#3725)
3839
- Add `forward_depth_image` output support to `SensorTiledCamera.update()` and `SensorTiledCamera.utils.create_forward_depth_image_output()` for native forward-depth rendering without post-processing `depth_image`.
3940
- Add the `basic_conveyor_forces` example: a multi-belt conveyor circuit that transports rigid boxes with per-belt velocity fields, applying Coulomb-limited tangential body forces from reported per-contact normal forces across `SolverXPBD`, `SolverVBD`, and `SolverMuJoCo`.
4041
- Add optional `shear_stiffness`/`shear_damping` and `twist_stiffness`/`twist_damping` controls to `ModelBuilder.add_joint_cable()`, `ModelBuilder.add_rod()`, and `ModelBuilder.add_rod_graph()`; omitted shear defaults to stretch and omitted twist defaults to bend for compatibility.
@@ -55,6 +56,7 @@
5556
- Compile tiled camera render kernels with CUDA fast math by default for faster rendering; set `SensorTiledCamera.render_config.enable_fast_math = False` for bit-exact, IEEE-precise output.
5657
- Disable `HydroelasticSDF.Config.pre_prune_contacts` when `CollisionPipeline(deterministic=True)` (implied by any `contact_matching` mode other than `"disabled"`). Pre-pruning ranks faces per thread, so it cannot be made reproducible. The face-contact buffer doubles because `contact_buffer_fraction` no longer applies, and the generated contact set differs from the non-deterministic default. Deterministic hydroelastic contacts also cap the buffer at 2^20 faces; lower `buffer_mult_contact` or `buffer_fraction` if construction now raises.
5758
- Make `CollisionPipeline` the sole owner of rigid-contact geometry for `SolverVBD`: `"latest"` supplies fresh geometry and `"sticky"` supplies replayed geometry. `SolverVBD(rigid_contact_history=True)` uses either mode's match indices only to warm-start its numeric lambda/penalty state.
59+
- Upgrade `mujoco` and `mujoco-warp` to 3.11.0. (#3725)
5860
- Optimize raycast/raytrace queries by restructuring ray-shape intersection into local-space primitives and compile specialized depth/shadow variants that skip unused surface-normal work (mesh shadows also use any-hit queries).
5961
- Change experimental `SolverVBD` cable constraint slots from `[STRETCH=0, BEND=1]` to `[STRETCH=0, SHEAR=1, BEND=2, TWIST=3]`, allowing each stiffness and constraint mode to be configured independently. Existing cable calls using raw `slot=1` or `JointSlot.ANGULAR` now select shear; use `JointSlot.BEND` (now slot 2) to select bending.
6062
- Map `shape_material_kf` to per-contact MuJoCo `solreffriction` in `SolverMuJoCo` (elliptic friction cones with Newton contacts); resolve `kf` with priority/`solmix`, treat a resolved `kf = 0` as frictionless, and use native MuJoCo contacts or a pyramidal cone to preserve the previous solref-inherited friction.

asv.conf.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"build_command": ["python -m build --wheel -o {build_cache_dir} {build_dir}"],
1717
"install_command": [
1818
"in-dir={env_dir} python -m pip install {wheel_file}[dev] --extra-index-url=https://pypi.nvidia.com/",
19-
"in-dir={env_dir} python -m pip install {wheel_file}[sim] warp-lang==1.16.0.dev20260716 mujoco==3.10.0 mujoco-warp==3.10.0.3 --extra-index-url=https://pypi.nvidia.com/",
19+
"in-dir={env_dir} python -m pip install warp-lang==1.16.0.dev20260716 mujoco==3.11.0 mujoco-warp==3.11.0 --extra-index-url=https://pypi.nvidia.com/",
2020
"python -m pip install torch==2.10.0+cu130 --index-url https://download.pytorch.org/whl/cu130"
2121
]
2222
}

docs/solvers/mujoco.rst

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,89 @@ See MuJoCo's `solver documentation
592592
what each parameter does and when to tune it.
593593

594594

595+
Sleeping
596+
--------
597+
598+
MuJoCo Warp can exclude stationary constraint islands from collision
599+
detection and the compact Newton solver. Newton keeps this optimization
600+
disabled by default. Enable it when simulating many passive objects that
601+
spend substantial time at rest. Sleeping adds island bookkeeping and uses the
602+
compact-solver path even while everything remains awake, so scenes in which
603+
most degrees of freedom remain awake can be slower than with sleeping disabled.
604+
Benchmark representative workloads before enabling it::
605+
606+
solver = SolverMuJoCo(
607+
model,
608+
enable_sleeping=True,
609+
sleep_tolerance=0.01,
610+
)
611+
612+
Sleeping is only supported by Newton's MuJoCo Warp GPU path. It requires
613+
``solver="newton"`` and ``use_mujoco_contacts=True`` so MuJoCo Warp's
614+
collision pipeline can wake sleeping bodies, and it does not support the RK4
615+
integrator. Unsupported combinations raise ``ValueError`` during solver
616+
construction. When an imported MJCF contains
617+
``<option><flag sleep="enable"/></option>``, leaving ``enable_sleeping`` as
618+
``None`` honors that setting. An explicit constructor value takes precedence.
619+
620+
``sleep_tolerance`` is the scaled generalized-velocity threshold [m/s] below
621+
which an island becomes eligible to sleep. It follows the normal
622+
`Solver options`_ resolution order, including per-world
623+
``model.mujoco.sleep_tolerance`` values. The default is ``0.001``.
624+
625+
MuJoCo's per-tree ``body/sleep`` policy is available as the body-frequency
626+
``model.mujoco.sleep_policy`` custom attribute and is imported from MJCF.
627+
The supported values are :attr:`~newton.solvers.SolverMuJoCo.SleepPolicy.AUTO`,
628+
:attr:`~newton.solvers.SolverMuJoCo.SleepPolicy.NEVER`,
629+
:attr:`~newton.solvers.SolverMuJoCo.SleepPolicy.ALLOWED`, and
630+
:attr:`~newton.solvers.SolverMuJoCo.SleepPolicy.INIT`. The policy must be
631+
assigned to the moving root body of a MuJoCo kinematic tree. For a
632+
programmatically built model, register the MuJoCo custom attributes before
633+
adding the body::
634+
635+
builder = newton.ModelBuilder()
636+
SolverMuJoCo.register_custom_attributes(builder)
637+
root = builder.add_link(
638+
custom_attributes={
639+
"mujoco:sleep_policy": SolverMuJoCo.SleepPolicy.INIT,
640+
},
641+
)
642+
643+
``INIT`` makes the tree start asleep and is the intended way to construct a
644+
solver with compact ``nvmax`` storage. The setting applies to the shared model,
645+
so replicated worlds receive the same initial tree policies. Their default
646+
joint velocities must also match because MuJoCo Warp uses one shared initial
647+
sleep state; solver construction rejects differing per-world defaults.
648+
649+
``nvmax`` sizes the compact solver's active-DOF workspace per world. If it is
650+
``None``, MuJoCo Warp uses the model's full ``nv``. This is the safe starting
651+
point but does not reduce compact-workspace memory. Tune a smaller value to
652+
the maximum number of DOFs expected to be awake simultaneously. The capacity
653+
must include every DOF that is awake in the initial MuJoCo state; Newton rejects
654+
smaller values before allocating solver data. ``nvmax`` is rejected when
655+
sleeping is disabled because MuJoCo Warp would otherwise allocate unused
656+
compact-solver workspace. If more than ``nvmax`` DOFs become active later,
657+
MuJoCo Warp sets the ``NVMAX`` bit in
658+
``solver.mjw_data.overflow``; increase ``nvmax`` and recreate the solver.
659+
660+
MuJoCo Warp automatically wakes islands for applied forces and contacts.
661+
Actuated trees are not allowed to sleep by default. If their sleep policy is
662+
explicitly changed to allow sleeping, changing actuator controls alone does not
663+
wake them; apply a force or set a nonzero velocity first. Newton-side
664+
joint-position edits wake only the affected sleeping trees.
665+
:meth:`~newton.solvers.SolverMuJoCo.reset` restores the initial sleep state in
666+
selected worlds, while
667+
:meth:`~newton.solvers.SolverMuJoCo.notify_model_changed` wakes all worlds
668+
after model-property updates. The sleeping path supports whole-step CUDA graph
669+
capture.
670+
671+
See MuJoCo's `sleeping-islands documentation
672+
<https://mujoco.readthedocs.io/en/stable/programming/simulation.html#sleeping-islands>`_
673+
and `MuJoCo Warp compact-solver guide
674+
<https://mujoco.readthedocs.io/en/latest/mjwarp/#compact-solver>`_ for the
675+
underlying sleep policy, wake triggers, and ``nvmax`` tuning guidance.
676+
677+
595678
.. _mujoco-custom-attributes:
596679

597680
MuJoCo-specific parameters in USD and MJCF

newton/_src/solvers/mujoco/kernels.py

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1953,6 +1953,7 @@ def update_solver_options_kernel(
19531953
newton_tolerance: wp.array[float],
19541954
newton_ls_tolerance: wp.array[float],
19551955
newton_ccd_tolerance: wp.array[float],
1956+
newton_sleep_tolerance: wp.array[float],
19561957
newton_density: wp.array[float],
19571958
newton_viscosity: wp.array[float],
19581959
newton_wind: wp.array[wp.vec3],
@@ -1962,6 +1963,7 @@ def update_solver_options_kernel(
19621963
opt_tolerance: wp.array[float],
19631964
opt_ls_tolerance: wp.array[float],
19641965
opt_ccd_tolerance: wp.array[float],
1966+
opt_sleep_tolerance: wp.array[float],
19651967
opt_density: wp.array[float],
19661968
opt_viscosity: wp.array[float],
19671969
opt_wind: wp.array[wp.vec3],
@@ -1974,6 +1976,7 @@ def update_solver_options_kernel(
19741976
newton_tolerance: Per-world tolerance values (None if overridden)
19751977
newton_ls_tolerance: Per-world line search tolerance values (None if overridden)
19761978
newton_ccd_tolerance: Per-world CCD tolerance values (None if overridden)
1979+
newton_sleep_tolerance: Per-world sleep tolerance values (None if overridden)
19771980
newton_density: Per-world medium density values (None if overridden)
19781981
newton_viscosity: Per-world medium viscosity values (None if overridden)
19791982
newton_wind: Per-world wind velocity vectors (None if overridden)
@@ -1982,6 +1985,7 @@ def update_solver_options_kernel(
19821985
opt_tolerance: MuJoCo Warp opt.tolerance array (shape: nworld)
19831986
opt_ls_tolerance: MuJoCo Warp opt.ls_tolerance array (shape: nworld)
19841987
opt_ccd_tolerance: MuJoCo Warp opt.ccd_tolerance array (shape: nworld)
1988+
opt_sleep_tolerance: MuJoCo Warp opt.sleep_tolerance array (shape: nworld)
19851989
opt_density: MuJoCo Warp opt.density array (shape: nworld)
19861990
opt_viscosity: MuJoCo Warp opt.viscosity array (shape: nworld)
19871991
opt_wind: MuJoCo Warp opt.wind array (shape: nworld)
@@ -2009,6 +2013,9 @@ def update_solver_options_kernel(
20092013
if newton_ccd_tolerance:
20102014
opt_ccd_tolerance[worldid] = newton_ccd_tolerance[worldid]
20112015

2016+
if newton_sleep_tolerance:
2017+
opt_sleep_tolerance[worldid] = newton_sleep_tolerance[worldid]
2018+
20122019
if newton_density:
20132020
opt_density[worldid] = newton_density[worldid]
20142021

@@ -3183,6 +3190,159 @@ def reset_world_buffers_kernel(
31833190
xfrc_applied[worldid, i] = wp.spatial_vector(0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
31843191

31853192

3193+
@wp.kernel(enable_backward=False)
3194+
def reset_sleeping_state_kernel(
3195+
world_mask: wp.array[wp.bool],
3196+
clear_overflow: int,
3197+
nv: int,
3198+
nbody: int,
3199+
ntree: int,
3200+
awake_value: int,
3201+
sleep_state_static: int,
3202+
sleep_state_awake: int,
3203+
body_rootid: wp.array[wp.int32],
3204+
body_mocapid: wp.array[wp.int32],
3205+
body_treeid: wp.array[wp.int32],
3206+
tree_asleep: wp.array2d[wp.int32],
3207+
tree_awake: wp.array2d[wp.int32],
3208+
body_awake: wp.array2d[wp.int32],
3209+
body_awake_ind: wp.array2d[wp.int32],
3210+
dof_awake_ind: wp.array2d[wp.int32],
3211+
ntree_awake: wp.array[wp.int32],
3212+
nbody_awake: wp.array[wp.int32],
3213+
nv_awake: wp.array[wp.int32],
3214+
overflow: wp.array[wp.int32],
3215+
):
3216+
"""Wake every tree and rebuild sleep bookkeeping in selected worlds."""
3217+
worldid, elemid = wp.tid()
3218+
if world_mask and not world_mask[worldid]:
3219+
return
3220+
3221+
if elemid < ntree:
3222+
tree_asleep[worldid, elemid] = awake_value
3223+
tree_awake[worldid, elemid] = 1
3224+
3225+
if elemid < nbody:
3226+
if body_treeid[elemid] < 0:
3227+
rootid = body_rootid[elemid]
3228+
if body_mocapid[rootid] < 0:
3229+
body_awake[worldid, elemid] = sleep_state_static
3230+
else:
3231+
body_awake[worldid, elemid] = sleep_state_awake
3232+
else:
3233+
body_awake[worldid, elemid] = sleep_state_awake
3234+
body_awake_ind[worldid, elemid] = elemid
3235+
3236+
if elemid < nv:
3237+
dof_awake_ind[worldid, elemid] = elemid
3238+
3239+
if elemid == 0:
3240+
ntree_awake[worldid] = ntree
3241+
nbody_awake[worldid] = nbody
3242+
nv_awake[worldid] = nv
3243+
if clear_overflow:
3244+
overflow[worldid] = 0
3245+
3246+
3247+
@wp.kernel(enable_backward=False)
3248+
def restore_sleeping_state_kernel(
3249+
world_mask: wp.array[wp.bool],
3250+
clear_overflow: int,
3251+
nv: int,
3252+
nbody: int,
3253+
ntree: int,
3254+
initial_ntree_awake: int,
3255+
initial_nbody_awake: int,
3256+
initial_nv_awake: int,
3257+
initial_tree_asleep: wp.array[wp.int32],
3258+
initial_tree_awake: wp.array[wp.int32],
3259+
initial_body_awake: wp.array[wp.int32],
3260+
initial_body_awake_ind: wp.array[wp.int32],
3261+
initial_dof_awake_ind: wp.array[wp.int32],
3262+
tree_asleep: wp.array2d[wp.int32],
3263+
tree_awake: wp.array2d[wp.int32],
3264+
body_awake: wp.array2d[wp.int32],
3265+
body_awake_ind: wp.array2d[wp.int32],
3266+
dof_awake_ind: wp.array2d[wp.int32],
3267+
ntree_awake: wp.array[wp.int32],
3268+
nbody_awake: wp.array[wp.int32],
3269+
nv_awake: wp.array[wp.int32],
3270+
overflow: wp.array[wp.int32],
3271+
):
3272+
"""Restore the initial sleep bookkeeping in selected worlds."""
3273+
worldid, elemid = wp.tid()
3274+
if world_mask and not world_mask[worldid]:
3275+
return
3276+
3277+
if elemid < ntree:
3278+
tree_asleep[worldid, elemid] = initial_tree_asleep[elemid]
3279+
tree_awake[worldid, elemid] = initial_tree_awake[elemid]
3280+
3281+
if elemid < nbody:
3282+
body_awake[worldid, elemid] = initial_body_awake[elemid]
3283+
body_awake_ind[worldid, elemid] = initial_body_awake_ind[elemid]
3284+
3285+
if elemid < nv:
3286+
dof_awake_ind[worldid, elemid] = initial_dof_awake_ind[elemid]
3287+
3288+
if elemid == 0:
3289+
ntree_awake[worldid] = initial_ntree_awake
3290+
nbody_awake[worldid] = initial_nbody_awake
3291+
nv_awake[worldid] = initial_nv_awake
3292+
if clear_overflow:
3293+
overflow[worldid] = 0
3294+
3295+
3296+
@wp.kernel(enable_backward=False)
3297+
def copy_qpos_and_detect_tree_change_kernel(
3298+
qpos_new: wp.array2d[wp.float32],
3299+
tolerance: float,
3300+
qpos_treeid: wp.array[wp.int32],
3301+
qpos: wp.array2d[wp.float32],
3302+
tree_changed: wp.array2d[wp.int32],
3303+
):
3304+
"""Copy converted coordinates and flag trees with external pose edits."""
3305+
worldid, i = wp.tid()
3306+
value = qpos_new[worldid, i]
3307+
if wp.abs(value - qpos[worldid, i]) > tolerance:
3308+
treeid = qpos_treeid[i]
3309+
if treeid >= 0:
3310+
wp.atomic_max(tree_changed, worldid, treeid, 1)
3311+
qpos[worldid, i] = value
3312+
3313+
3314+
@wp.kernel(enable_backward=False)
3315+
def wake_changed_trees_kernel(
3316+
tree_changed: wp.array2d[wp.int32],
3317+
ntree: int,
3318+
awake_value: int,
3319+
tree_asleep: wp.array2d[wp.int32],
3320+
):
3321+
"""Wake edited trees and every tree in their sleeping-island cycles."""
3322+
# Negative values mean awake; non-negative values link the next tree in a
3323+
# sleeping-island cycle. The O(ntree) scan intentionally uses one walker
3324+
# per world because parallel walkers for overlapping edits could overwrite
3325+
# a link before a peer reads it.
3326+
worldid = wp.tid()
3327+
for treeid in range(ntree):
3328+
if tree_changed[worldid, treeid] == 0:
3329+
continue
3330+
3331+
asleep_value = tree_asleep[worldid, treeid]
3332+
if asleep_value < 0:
3333+
if awake_value < asleep_value:
3334+
tree_asleep[worldid, treeid] = awake_value
3335+
continue
3336+
3337+
current = treeid
3338+
for _step in range(ntree + 1):
3339+
next_tree = tree_asleep[worldid, current]
3340+
tree_asleep[worldid, current] = awake_value
3341+
current = next_tree
3342+
if current == treeid:
3343+
break
3344+
3345+
31863346
@wp.kernel(enable_backward=False)
31873347
def reset_joint_state_kernel(
31883348
world_mask: wp.array[wp.bool],

0 commit comments

Comments
 (0)