Add graph mode to MJX, in preparation of supporting staged graph capture.

PiperOrigin-RevId: 865380826
Change-Id: I840b48027e1142f87eb745d0d19b0cd3e87904b2
This commit is contained in:
Baruch Tabanpour
2026-02-04 06:37:21 -08:00
committed by Copybara-Service
parent b37caf6d0c
commit d07f39b4a0
8 changed files with 60 additions and 16 deletions
+1 -1
View File
@@ -84,7 +84,7 @@ General
MJX
^^^
- Added ``actuator_length``, ``cdof`` and ``cdof_dof`` fields to ``mjx.Data``.
- Add ``graph_mode`` argument to ``put_model`` to support multiple Warp graph capture modes.
Documentation
^^^^^^^^^^^^^
+18 -11
View File
@@ -50,6 +50,11 @@ def _is_cuda_gpu_device(device: jax.Device) -> bool:
return device in jax.devices('cuda')
def _check_warp_installed():
if not mjxw.WARP_INSTALLED:
raise RuntimeError('warp-lang is not installed. Cannot use WARP implementation of MJX.')
def _resolve_impl(
device: jax.Device,
) -> types.Impl:
@@ -119,10 +124,7 @@ def _check_impl_device_compatibility(
'Warp implementation requires a CUDA GPU device, got '
f'{device}.'
)
if not mjxw.WARP_INSTALLED:
raise RuntimeError(
'Warp is not installed. Cannot use Warp implementation of MJX.'
)
_check_warp_installed()
is_cpu_device = device.platform == 'cpu'
if impl == types.Impl.C or impl == types.Impl.CPP:
@@ -448,12 +450,10 @@ def _put_model_c(
def _put_model_warp(
m: mujoco.MjModel,
graph_mode: mjxw.types.GraphMode,
device: Optional[jax.Device] = None,
) -> types.Model:
"""Puts mujoco.MjModel onto a device, resulting in mjx.Model."""
if not mjxw.WARP_INSTALLED:
raise RuntimeError('Warp not installed.')
with wp.ScopedDevice('cpu'): # pylint: disable=undefined-variable
mw = mjwp.put_model(m) # pylint: disable=undefined-variable
@@ -464,7 +464,10 @@ def _put_model_warp(
option_keys = {f.name for f in mjxw.types.OptionWarp.fields()} - {
f.name for f in types.Option.fields()
}
# graph_mode is MJX-specific, not from mujoco.mjx.third_party.mujoco_warp.
option_keys = option_keys - {'graph_mode'}
private_options = {k: getattr(mw.opt, k) for k in option_keys}
private_options['graph_mode'] = graph_mode
fields['opt'] = _put_option(m.opt, types.Impl.WARP, private_options)
fields['stat'] = _put_statistic(m.stat, types.Impl.WARP)
@@ -527,6 +530,7 @@ def put_model(
m: mujoco.MjModel,
device: Optional[jax.Device] = None,
impl: Optional[Union[str, types.Impl]] = None,
graph_mode: Optional[mjxw.types.GraphMode] = None,
) -> types.Model:
"""Puts mujoco.MjModel onto a device, resulting in mjx.Model.
@@ -534,12 +538,15 @@ def put_model(
m: the model to put onto device
device: which device to use - if unspecified picks the default device
impl: implementation to use
graph_mode: CUDA graph capture mode (for Warp only). Use GraphMode enum from
warp._src.jax_experimental.ffi. GraphMode.WARP is the default mode.
Returns:
an mjx.Model placed on device
Raises:
ValueError: if impl is not supported
RuntimeError: if impl is WARP and warp-lang is not installed
"""
impl, device = _resolve_impl_and_device(impl, device)
@@ -548,7 +555,9 @@ def put_model(
elif impl == types.Impl.C:
return _put_model_c(m, device)
elif impl == types.Impl.WARP:
return _put_model_warp(m, device)
_check_warp_installed()
graph_mode = graph_mode or getattr(mjxw.types.GraphMode, 'WARP')
return _put_model_warp(m, graph_mode, device)
elif impl == types.Impl.CPP:
return _put_model_cpp(m, device)
else:
@@ -861,9 +870,6 @@ def _make_data_warp(
f' {type(m)}.'
)
if not mjxw.WARP_INSTALLED:
raise RuntimeError('Warp is not installed.')
with wp.ScopedDevice('cpu'): # pylint: disable=undefined-variable
dw = mjwp.make_data(m, nworld=1, naconmax=naconmax, njmax=njmax) # pylint: disable=undefined-variable
@@ -1003,6 +1009,7 @@ def make_data(
elif impl == types.Impl.CPP:
return _make_data_cpp(m, device)
elif impl == types.Impl.WARP:
_check_warp_installed()
naconmax = nconmax if naconmax is None else naconmax
return _make_data_warp(m, device, naconmax, njmax)
+19
View File
@@ -334,6 +334,25 @@ class ModelIOTest(parameterized.TestCase):
_ = jax.tree.map_with_path(check_ndim, mx)
@parameterized.parameters('JAX', 'WARP', None)
def test_put_model_warp_graph_mode(self, mode: str | None):
"""Tests that put_model accepts graph_mode parameter."""
if not mjxw.WARP_INSTALLED:
self.skipTest('Warp not installed.')
if not mjx_io.has_cuda_gpu_device():
self.skipTest('No CUDA GPU device available.')
if mode is None:
graph_mode = None
else:
graph_mode = getattr(mjxw_types.GraphMode, mode)
m = mujoco.MjModel.from_xml_string(_SIMPLE_BODY)
mx = mjx.put_model(m, impl='warp', graph_mode=graph_mode)
expected = graph_mode or mjxw_types.GraphMode.WARP
self.assertEqual(mx.opt._impl.graph_mode, expected)
@parameterized.parameters('c', 'jax')
def test_unsupported_contact_types(self, impl):
"""Tests that unsupported contact types raise an error."""
+1 -1
View File
@@ -42,7 +42,6 @@ _e = mjwarp.Constraint(
**{f.name: None for f in dataclasses.fields(mjwarp.Constraint) if f.init}
)
@ffi.format_args_for_warp
def _collision_shim(
# Model
@@ -279,6 +278,7 @@ def _collision_jax_impl(m: types.Model, d: types.Data):
'contact__type',
'contact__worldid',
},
graph_mode=m.opt._impl.graph_mode,
)
out = jf(
d.qpos.shape[0],
+2
View File
@@ -1220,6 +1220,7 @@ def _forward_jax_impl(m: types.Model, d: types.Data):
'efc__type',
'efc__vel',
},
graph_mode=m.opt._impl.graph_mode,
)
out = jf(
d.qpos.shape[0],
@@ -3018,6 +3019,7 @@ def _step_jax_impl(m: types.Model, d: types.Data):
'efc__type',
'efc__vel',
},
graph_mode=m.opt._impl.graph_mode,
)
out = jf(
d.qpos.shape[0],
+7 -2
View File
@@ -216,8 +216,11 @@ class StepTest(parameterized.TestCase):
'pendula.xml',
),
batch_size=(1, 7),
# NOTE: GraphMode.JAX is incompatible with MuJoCo Warp at the moment,
# even when setting graph_conditional=False.
graph_mode=('WARP',),
)
def test_step(self, xml: str, batch_size: int):
def test_step(self, xml: str, batch_size: int, graph_mode: str):
if not _FORCE_TEST:
if not mjxw.WARP_INSTALLED:
self.skipTest('Warp not installed.')
@@ -227,7 +230,9 @@ class StepTest(parameterized.TestCase):
m = test_util.load_test_file(xml)
m.opt.iterations = 10
m.opt.ls_iterations = 10
mx = mjx.put_model(m, impl='warp')
mx = mjx.put_model(
m, impl='warp', graph_mode=getattr(mjxw.types.GraphMode, graph_mode)
)
d = mujoco.MjData(m)
worldids = jp.arange(batch_size)
+2
View File
@@ -170,6 +170,7 @@ def _kinematics_jax_impl(m: types.Model, d: types.Data):
'xpos',
'xquat',
},
graph_mode=m.opt._impl.graph_mode,
)
out = jf(
d.qpos.shape[0],
@@ -379,6 +380,7 @@ def _tendon_jax_impl(m: types.Model, d: types.Data):
'wrap_obj',
'wrap_xpos',
},
graph_mode=m.opt._impl.graph_mode,
)
out = jf(
d.qpos.shape[0],
+10 -1
View File
@@ -16,12 +16,21 @@
DO NOT EDIT. This file is auto-generated.
"""
import dataclasses
import typing
from typing import Tuple
import jax
from jax import tree_util
from jax.interpreters import batching
from mujoco.mjx._src import dataclasses as mjx_dataclasses
import numpy as np
if typing.TYPE_CHECKING:
GraphMode = int
else:
try:
from warp._src.jax_experimental.ffi import GraphMode
except ImportError:
GraphMode = int
PyTreeNode = mjx_dataclasses.PyTreeNode
@dataclasses.dataclass(frozen=True)
@@ -35,7 +44,6 @@ class TileSet:
adr: address of each tile in the set
size: size of all the tiles in this set
"""
adr: np.ndarray
size: int
@@ -95,6 +103,7 @@ class OptionWarp(PyTreeNode):
ccd_tolerance: jax.Array
contact_sensor_maxmatch: int
graph_conditional: bool
graph_mode: GraphMode
has_fluid: bool
impratio_invsqrt: jax.Array
is_sparse: bool