Internal change.

PiperOrigin-RevId: 842636101
Change-Id: I5fc8e1f5e4caee9d909963ebe396572b13b455b7
This commit is contained in:
Google DeepMind
2025-12-10 02:57:37 -08:00
committed by Copybara-Service
parent 9c1b8d8cfb
commit 11f0997edf
7 changed files with 215 additions and 7 deletions
+2
View File
@@ -103,6 +103,8 @@ def fwd_velocity(m: Model, d: Data) -> Data:
@named_scope @named_scope
def fwd_actuation(m: Model, d: Data) -> Data: def fwd_actuation(m: Model, d: Data) -> Data:
"""Actuation-dependent computations.""" """Actuation-dependent computations."""
if not isinstance(d._impl, DataJAX):
raise ValueError('fwd_actuation requires JAX backend implementation.')
if not m.nu or m.opt.disableflags & DisableBit.ACTUATION: if not m.nu or m.opt.disableflags & DisableBit.ACTUATION:
return d.replace( return d.replace(
act_dot=jp.zeros((m.na,)), act_dot=jp.zeros((m.na,)),
+2
View File
@@ -25,6 +25,7 @@ from mujoco.mjx._src import support
from mujoco.mjx._src.types import Data from mujoco.mjx._src.types import Data
from mujoco.mjx._src.types import DisableBit from mujoco.mjx._src.types import DisableBit
from mujoco.mjx._src.types import EnableBit from mujoco.mjx._src.types import EnableBit
from mujoco.mjx._src.types import Impl
from mujoco.mjx._src.types import IntegratorType from mujoco.mjx._src.types import IntegratorType
from mujoco.mjx._src.types import Model from mujoco.mjx._src.types import Model
@@ -83,6 +84,7 @@ def inv_constraint(m: Model, d: Data) -> Data:
def inverse(m: Model, d: Data) -> Data: def inverse(m: Model, d: Data) -> Data:
"""Inverse dynamics.""" """Inverse dynamics."""
d = forward.fwd_position(m, d) d = forward.fwd_position(m, d)
d = sensor.sensor_pos(m, d) d = sensor.sensor_pos(m, d)
d = forward.fwd_velocity(m, d) d = forward.fwd_velocity(m, d)
+102 -3
View File
@@ -21,6 +21,7 @@ from typing import Any, Dict, List, Optional, Tuple, Union
import warnings import warnings
import jax import jax
import jax.experimental
from jax import numpy as jp from jax import numpy as jp
from jax.extend import backend from jax.extend import backend
import mujoco import mujoco
@@ -82,7 +83,7 @@ def _resolve_device(
logging.debug('Picking default device: %s.', device_0) logging.debug('Picking default device: %s.', device_0)
return device_0 return device_0
if impl == types.Impl.C: if impl == types.Impl.C or impl == types.Impl.CPP:
cpu_0 = jax.devices('cpu')[0] cpu_0 = jax.devices('cpu')[0]
logging.debug('Picking default device: %s', cpu_0) logging.debug('Picking default device: %s', cpu_0)
return cpu_0 return cpu_0
@@ -124,7 +125,7 @@ def _check_impl_device_compatibility(
) )
is_cpu_device = device.platform == 'cpu' is_cpu_device = device.platform == 'cpu'
if impl == types.Impl.C: if impl == types.Impl.C or impl == types.Impl.CPP:
if not is_cpu_device: if not is_cpu_device:
raise AssertionError( raise AssertionError(
f'C implementation requires a CPU device, got {device}.' f'C implementation requires a CPU device, got {device}.'
@@ -489,6 +490,39 @@ def _put_model_warp(
return _strip_weak_type(model) return _strip_weak_type(model)
def _put_model_cpp(
m: mujoco.MjModel,
device: Optional[jax.Device] = None,
) -> types.Model:
"""Puts mujoco.MjModel onto a device, resulting in mjx.Model."""
mj_field_names = {f.name for f in types.Model.fields() if f.name != '_impl'}
fields = {f: getattr(m, f) for f in mj_field_names}
fields['cam_mat0'] = fields['cam_mat0'].reshape((-1, 3, 3))
fields['opt'] = _put_option(m.opt, impl=types.Impl.C)
fields['stat'] = _put_statistic(m.stat, impl=types.Impl.C)
# get the pointer address
# we use a 0-d array
addr = m._address # pytype: disable=attribute-error
# To ensure that we retain the full pointer even if jax.config.enable_x64 is
# set to True, we store the pointer as two 32-bit values. In the FFI call,
# we combine the two values into a single pointer value.
pointer_lo = jp.array(addr & 0xFFFFFFFF, dtype=jp.uint32)
pointer_hi = jp.array(addr >> 32, dtype=jp.uint32)
c_pointers_impl = types.ModelCPP(
pointer_lo=pointer_lo,
pointer_hi=pointer_hi,
_model=m,
)
model = types.Model(
**{k: copy.copy(v) for k, v in fields.items()}, _impl=c_pointers_impl
)
model = jax.device_put(model, device=device)
return _strip_weak_type(model)
def put_model( def put_model(
m: mujoco.MjModel, m: mujoco.MjModel,
device: Optional[jax.Device] = None, device: Optional[jax.Device] = None,
@@ -515,6 +549,8 @@ def put_model(
return _put_model_c(m, device) return _put_model_c(m, device)
elif impl == types.Impl.WARP: elif impl == types.Impl.WARP:
return _put_model_warp(m, device) return _put_model_warp(m, device)
elif impl == types.Impl.CPP:
return _put_model_cpp(m, device)
else: else:
raise ValueError(f'Unsupported implementation: {impl}') raise ValueError(f'Unsupported implementation: {impl}')
@@ -612,7 +648,7 @@ def _make_data_jax(
efc_address = constraint.make_efc_address(m, dim, efc_type) efc_address = constraint.make_efc_address(m, dim, efc_type)
float_ = jp.zeros(1, float).dtype float_ = jp.zeros(1, float).dtype
int_ = jp.zeros(1, int).dtype int_ = np.int32
contact = _make_data_contact_jax(dim, efc_address) contact = _make_data_contact_jax(dim, efc_address)
if m.opt.cone == types.ConeType.ELLIPTIC and np.any(contact.dim == 1): if m.opt.cone == types.ConeType.ELLIPTIC and np.any(contact.dim == 1):
@@ -1216,6 +1252,62 @@ def _put_data_c(
return _strip_weak_type(data) return _strip_weak_type(data)
def _put_data_cpp(
m: mujoco.MjModel,
d: mujoco.MjData,
device: Optional[jax.Device] = None,
dummy_arg_for_batching: Optional[jax.Array] = None,
) -> types.Data:
"""Puts mujoco.MjData onto a device, resulting in mjx.Data."""
data_list = []
def _copy_and_get_addr(unused_jax_array):
# We use the input to the callback as a dummy dependency to ensure
# io_callback runs for each element in the batch.
try:
new_d = mujoco.MjData(m)
except mujoco.FatalError as e:
raise ValueError('Failed to create new MjData') from e
mujoco.mj_copyState(m, d, new_d, int(mujoco.mjtState.mjSTATE_FULLPHYSICS))
mujoco.mj_forward(m, new_d)
data_list.append(new_d)
addr = new_d._address
# To ensure that we retain the full pointer even if jax.config.enable_x64 is
# set to True, we store the pointer as two 32-bit values. In the FFI call,
# we combine the two values into a single pointer value.
return (
np.array(addr & 0xFFFFFFFF, dtype=np.uint32),
np.array(addr >> 32, dtype=np.uint32),
)
# Pass a dummy dependency to ensure io_callback runs across the batch.
pointer_lo, pointer_hi = jax.experimental.io_callback(
_copy_and_get_addr,
(
jax.ShapeDtypeStruct((), jp.uint32),
jax.ShapeDtypeStruct((), jp.uint32),
),
dummy_arg_for_batching,
)
new_d = data_list[0]
fields = _put_data_public_fields(new_d)
c_pointers_impl = types.DataCPP(
pointer_lo=pointer_lo,
pointer_hi=pointer_hi,
_data=data_list,
)
data = types.Data(
_impl=c_pointers_impl,
**fields,
)
data = jax.device_put(data, device=device)
return _strip_weak_type(data)
def put_data( def put_data(
m: mujoco.MjModel, m: mujoco.MjModel,
d: mujoco.MjData, d: mujoco.MjData,
@@ -1224,6 +1316,7 @@ def put_data(
nconmax: Optional[int] = None, nconmax: Optional[int] = None,
naconmax: Optional[int] = None, naconmax: Optional[int] = None,
njmax: Optional[int] = None, njmax: Optional[int] = None,
dummy_arg_for_batching: Optional[jax.Array] = None,
) -> types.Data: ) -> types.Data:
"""Puts mujoco.MjData onto a device, resulting in mjx.Data. """Puts mujoco.MjData onto a device, resulting in mjx.Data.
@@ -1238,6 +1331,8 @@ def put_data(
`naconmax` argument to set the upper bound for the number of contacts `naconmax` argument to set the upper bound for the number of contacts
across all worlds, rather than the `nconmax` argument from MuJoCo Warp. across all worlds, rather than the `nconmax` argument from MuJoCo Warp.
njmax: maximum number of constraints to allocate for warp njmax: maximum number of constraints to allocate for warp
dummy_arg_for_batching: dummy argument to use for batching in cpp
implementation
Returns: Returns:
an mjx.Data placed on device an mjx.Data placed on device
@@ -1256,6 +1351,10 @@ def put_data(
return _put_data_jax(m, d, device) return _put_data_jax(m, d, device)
elif impl == types.Impl.C: elif impl == types.Impl.C:
return _put_data_c(m, d, device) return _put_data_c(m, d, device)
elif impl == types.Impl.CPP:
return _put_data_cpp(
m, d, device, dummy_arg_for_batching=dummy_arg_for_batching
)
# TODO(robotics-team): implement put_data_warp # TODO(robotics-team): implement put_data_warp
+78 -1
View File
@@ -136,7 +136,7 @@ class ModelIOTest(parameterized.TestCase):
@parameterized.product( @parameterized.product(
xml=(_MULTIPLE_CONVEX_OBJECTS, _MULTIPLE_CONSTRAINTS), xml=(_MULTIPLE_CONVEX_OBJECTS, _MULTIPLE_CONSTRAINTS),
impl=('jax', 'c', 'warp'), impl=('jax', 'c', 'warp', 'cpp'),
) )
@mock.patch.dict(os.environ, {'MJX_GPU_DEFAULT_WARP': 'true'}) @mock.patch.dict(os.environ, {'MJX_GPU_DEFAULT_WARP': 'true'})
def test_put_model(self, xml, impl): def test_put_model(self, xml, impl):
@@ -181,6 +181,9 @@ class ModelIOTest(parameterized.TestCase):
self.assertTrue(hasattr(mx.opt._impl, 'ls_parallel')) self.assertTrue(hasattr(mx.opt._impl, 'ls_parallel'))
# Fields private to Warp backend impl are populated. # Fields private to Warp backend impl are populated.
self.assertTrue(hasattr(mx._impl, 'nxn_geom_pair')) self.assertTrue(hasattr(mx._impl, 'nxn_geom_pair'))
elif impl == 'cpp':
self.assertTrue(hasattr(mx._impl, 'pointer_lo'))
self.assertTrue(hasattr(mx._impl, 'pointer_hi'))
np.testing.assert_allclose(mx.body_parentid, m.body_parentid) np.testing.assert_allclose(mx.body_parentid, m.body_parentid)
np.testing.assert_allclose(mx.geom_type, m.geom_type) np.testing.assert_allclose(mx.geom_type, m.geom_type)
@@ -410,6 +413,10 @@ class DataIOTest(parameterized.TestCase):
self.assertEqual(d._impl.actuator_moment.shape, (1, nv)) self.assertEqual(d._impl.actuator_moment.shape, (1, nv))
elif impl == 'c': elif impl == 'c':
self.assertEqual(d._impl.actuator_moment.shape, (m.nJmom,)) self.assertEqual(d._impl.actuator_moment.shape, (m.nJmom,))
elif impl == 'cpp':
self.assertTrue(hasattr(d._impl, 'pointer_lo'))
self.assertTrue(hasattr(d._impl, 'pointer_hi'))
return # cpp does not populate other fields in _impl
self.assertEqual(d._impl.contact.dist.shape, (ncon,)) self.assertEqual(d._impl.contact.dist.shape, (ncon,))
self.assertEqual(d._impl.contact.pos.shape, (ncon, 3)) self.assertEqual(d._impl.contact.pos.shape, (ncon, 3))
self.assertEqual(d._impl.contact.frame.shape, (ncon, 3, 3)) self.assertEqual(d._impl.contact.frame.shape, (ncon, 3, 3))
@@ -497,6 +504,10 @@ class DataIOTest(parameterized.TestCase):
np.testing.assert_allclose(dx._impl.qM, d.qM) np.testing.assert_allclose(dx._impl.qM, d.qM)
np.testing.assert_allclose(dx._impl.qLD, d.qLD) np.testing.assert_allclose(dx._impl.qLD, d.qLD)
np.testing.assert_allclose(dx._impl.qLDiagInv, d.qLDiagInv) np.testing.assert_allclose(dx._impl.qLDiagInv, d.qLDiagInv)
elif impl == 'cpp':
self.assertTrue(hasattr(dx._impl, 'pointer_lo'))
self.assertTrue(hasattr(dx._impl, 'pointer_hi'))
return # cpp does not populate other fields in _impl
# 4 contacts, 2 for each capsule against the plane # 4 contacts, 2 for each capsule against the plane
self.assertEqual(dx._impl.contact.dist.shape, (4,)) self.assertEqual(dx._impl.contact.dist.shape, (4,))
@@ -816,6 +827,39 @@ class DataIOTest(parameterized.TestCase):
self.assertEqual(dx._impl.contact__dist.shape, (dx._impl.naconmax,)) self.assertEqual(dx._impl.contact__dist.shape, (dx._impl.naconmax,))
self.assertEqual(dx[0]._impl.contact__dist.shape, (dx._impl.naconmax,)) self.assertEqual(dx[0]._impl.contact__dist.shape, (dx._impl.naconmax,))
def test_put_data_cpp(self):
m = mujoco.MjModel.from_xml_string("""
<mujoco>
<worldbody>
<body name="body1" pos="0 0 1">
<joint type="free"/>
<geom type="sphere" size="0.1"/>
</body>
</worldbody>
</mujoco>
""")
d = mujoco.MjData(m)
d.qpos[0] = 1.0
unused_mjx_data = mjx_io.put_data(m, d, impl='cpp')
def put_data(dummy_arg_for_batching):
dx = mjx_io.put_data(
m, d, impl='cpp', dummy_arg_for_batching=dummy_arg_for_batching
)
return dx
vmjx_data = jax.vmap(put_data, in_axes=0, out_axes=0)(
jp.zeros(2, dtype=jp.uint32)
)
self.assertEqual(vmjx_data.qpos.shape, (2, m.nq))
self.assertEqual(len(vmjx_data._impl._data), 2)
# check that the data pointers in fact point to different datas
self.assertNotEqual(
vmjx_data._impl._data[0]._address,
vmjx_data._impl._data[1]._address,
)
# Test cases for `_resolve_impl_and_device` where the device is # Test cases for `_resolve_impl_and_device` where the device is
# specified by the user and the device is available. # specified by the user and the device is available.
@@ -1117,6 +1161,39 @@ class StateIOTest(parameterized.TestCase):
mx = mjx.put_model(m) mx = mjx.put_model(m)
self.assertEqual(mjx.state_size(mx, spec), mujoco.mj_stateSize(m, spec)) self.assertEqual(mjx.state_size(mx, spec), mujoco.mj_stateSize(m, spec))
def test_put_data_cpp(self):
m = mujoco.MjModel.from_xml_string("""
<mujoco>
<worldbody>
<body name="body1" pos="0 0 1">
<joint type="free"/>
<geom type="sphere" size="0.1"/>
</body>
</worldbody>
</mujoco>
""")
d = mujoco.MjData(m)
d.qpos[0] = 1.0
unused_mjx_data = mjx_io.put_data(m, d, impl='cpp')
def put_data(dummy_arg_for_batching):
dx = mjx_io.put_data(
m, d, impl='cpp', dummy_arg_for_batching=dummy_arg_for_batching
)
return dx
vmjx_data = jax.vmap(put_data, in_axes=0, out_axes=0)(
jp.zeros(2, dtype=jp.uint32)
)
self.assertEqual(vmjx_data.qpos.shape, (2, m.nq))
self.assertEqual(len(vmjx_data._impl._data), 2)
# check that the data pointers in fact point to different datas
self.assertNotEqual(
vmjx_data._impl._data[0]._address,
vmjx_data._impl._data[1]._address,
)
def test_get_set_state(self): def test_get_set_state(self):
m = mujoco.MjModel.from_xml_string(_MULTIPLE_CONSTRAINTS) m = mujoco.MjModel.from_xml_string(_MULTIPLE_CONSTRAINTS)
d = mujoco.MjData(m) d = mujoco.MjData(m)
+3
View File
@@ -34,6 +34,9 @@ from mujoco.mjx._src.types import OptionJAX
def _spring_damper(m: Model, d: Data) -> jax.Array: def _spring_damper(m: Model, d: Data) -> jax.Array:
"""Applies joint level spring and damping forces.""" """Applies joint level spring and damping forces."""
if not isinstance(d._impl, DataJAX) and not isinstance(m._impl, ModelJAX):
raise ValueError('_spring_damper requires JAX backend implementation.')
assert isinstance(d._impl, DataJAX) and isinstance(m._impl, ModelJAX)
def fn(jnt_typs, stiffness, qpos_spring, qpos): def fn(jnt_typs, stiffness, qpos_spring, qpos):
qpos_i = 0 qpos_i = 0
+2 -1
View File
@@ -44,7 +44,6 @@ def kinematics(m: Model, d: Data) -> Data:
from mujoco.mjx.warp import smooth as mjxw_smooth # pylint: disable=g-import-not-at-top # pytype: disable=import-error from mujoco.mjx.warp import smooth as mjxw_smooth # pylint: disable=g-import-not-at-top # pytype: disable=import-error
return mjxw_smooth.kinematics(m, d) return mjxw_smooth.kinematics(m, d)
def fn(carry, jnt_typs, jnt_pos, jnt_axis, qpos, qpos0, pos, quat): def fn(carry, jnt_typs, jnt_pos, jnt_axis, qpos, qpos0, pos, quat):
# calculate joint anchors, axes, body pos and quat in global frame # calculate joint anchors, axes, body pos and quat in global frame
# also normalize qpos while we're at it # also normalize qpos while we're at it
@@ -140,6 +139,7 @@ def kinematics(m: Model, d: Data) -> Data:
def com_pos(m: Model, d: Data) -> Data: def com_pos(m: Model, d: Data) -> Data:
"""Maps inertias and motion dofs to global frame centered at subtree-CoM.""" """Maps inertias and motion dofs to global frame centered at subtree-CoM."""
if not isinstance(m._impl, ModelJAX) or not isinstance(d._impl, DataJAX): if not isinstance(m._impl, ModelJAX) or not isinstance(d._impl, DataJAX):
raise ValueError('com_pos requires JAX backend implementation.') raise ValueError('com_pos requires JAX backend implementation.')
@@ -412,6 +412,7 @@ def solve_m(m: Model, d: Data, x: jax.Array) -> jax.Array:
def com_vel(m: Model, d: Data) -> Data: def com_vel(m: Model, d: Data) -> Data:
"""Computes cvel, cdof_dot.""" """Computes cvel, cdof_dot."""
if not isinstance(m._impl, ModelJAX) or not isinstance(d._impl, DataJAX): if not isinstance(m._impl, ModelJAX) or not isinstance(d._impl, DataJAX):
raise ValueError('com_vel requires JAX backend implementation.') raise ValueError('com_vel requires JAX backend implementation.')
+26 -2
View File
@@ -14,8 +14,9 @@
# ============================================================================== # ==============================================================================
"""Base types used in MJX.""" """Base types used in MJX."""
import dataclasses
import enum import enum
from typing import Tuple, Union from typing import Any, Tuple, Union
import warnings import warnings
import jax import jax
@@ -29,6 +30,7 @@ class Impl(enum.Enum):
"""Implementation to use.""" """Implementation to use."""
C = 'c' C = 'c'
CPP = 'cpp'
JAX = 'jax' JAX = 'jax'
WARP = 'warp' WARP = 'warp'
@@ -529,6 +531,26 @@ class Option(PyTreeNode):
_impl: Union[OptionJAX, OptionC, mjxw_types.OptionWarp] _impl: Union[OptionJAX, OptionC, mjxw_types.OptionWarp]
class ModelCPP(PyTreeNode):
"""Minimal Model implementation holding only the pointer."""
# To ensure that we retain the full pointer even if jax.config.enable_x64 is
# set to True, we store the pointer as two 32-bit values. In the FFI call,
# we combine the two values into a single pointer value.
pointer_lo: jax.Array
pointer_hi: jax.Array
_model: mujoco.MjModel
class DataCPP(PyTreeNode):
"""Minimal Data implementation holding only the pointer."""
# To ensure that we retain the full pointer even if jax.config.enable_x64 is
# set to True, we store the pointer as two 32-bit values. In the FFI call,
# we combine the two values into a single pointer value.
pointer_lo: jax.Array
pointer_hi: jax.Array
_data: list[Any] = dataclasses.field(default_factory=list, repr=False)
class ModelC(PyTreeNode): class ModelC(PyTreeNode):
"""CPU-specific model data.""" """CPU-specific model data."""
@@ -943,6 +965,7 @@ class Model(PyTreeNode):
def impl(self) -> Impl: def impl(self) -> Impl:
return { return {
ModelC: Impl.C, ModelC: Impl.C,
ModelCPP: Impl.CPP,
ModelJAX: Impl.JAX, ModelJAX: Impl.JAX,
mjxw_types.ModelWarp: Impl.WARP, mjxw_types.ModelWarp: Impl.WARP,
}[type(self._impl)] }[type(self._impl)]
@@ -1180,12 +1203,13 @@ class Data(PyTreeNode):
qacc_smooth: jax.Array qacc_smooth: jax.Array
qfrc_constraint: jax.Array qfrc_constraint: jax.Array
qfrc_inverse: jax.Array qfrc_inverse: jax.Array
_impl: Union[DataC, DataJAX, mjxw_types.DataWarp] _impl: Union[DataC, DataCPP, DataJAX, mjxw_types.DataWarp]
@property @property
def impl(self) -> Impl: def impl(self) -> Impl:
return { return {
DataC: Impl.C, DataC: Impl.C,
DataCPP: Impl.CPP,
DataJAX: Impl.JAX, DataJAX: Impl.JAX,
mjxw_types.DataWarp: Impl.WARP, mjxw_types.DataWarp: Impl.WARP,
}[type(self._impl)] }[type(self._impl)]