.. _faq: FAQ & Troubleshooting ===================== This page collects common questions about **platform support**, **performance**, **training stability**, and **visualization**, along with practical debugging tips and links to further resources. Platform Support ---------------- Does it work on macOS? ~~~~~~~~~~~~~~~~~~~~~~ Yes, but only with limited performance. mjlab runs on macOS using **CPU-only** execution through MuJoCo Warp. - **Training is not recommended on macOS**, as it lacks GPU acceleration. - **Evaluation works**, but is significantly slower than on Linux with CUDA. For serious training workloads, we strongly recommend **Linux with an NVIDIA GPU**. Does it work on Windows? ~~~~~~~~~~~~~~~~~~~~~~~~ We have performed preliminary testing on **Windows** and **WSL**, but some workflows are not guaranteed to be stable. - Windows support may **lag behind** Linux. - Windows will be **tested less frequently**, since Linux is the primary development and deployment platform. - Community contributions that improve Windows support are very welcome. CUDA Compatibility ~~~~~~~~~~~~~~~~~~ Not all CUDA versions are supported by MuJoCo Warp. - See `mujoco_warp#101 `_ for details on CUDA compatibility. - **Recommended**: CUDA **12.4+** (for conditional execution support in CUDA graphs). How do I run on CPU without touching the GPU? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Passing ``device="cpu"`` puts all mjlab computation on the CPU, but it does **not** stop Warp from initializing the GPU. The first time Warp's runtime comes up, it eagerly enumerates and creates a CUDA context on **every** visible device, regardless of which device you requested. So on a machine with a visible GPU, a ``device="cpu"`` run still claims VRAM. This happens inside Warp and cannot be prevented from Python once the package is imported. To keep the process entirely off the GPU, hide the devices from CUDA before launching: .. code-block:: bash CUDA_VISIBLE_DEVICES="" uv run train.py ... With no visible CUDA devices, Warp initializes CPU-only and never allocates on the GPU. See `issue #949 `_ for background. Performance ----------- Is it faster than Isaac Lab? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Based on our experience over the last few months, mjlab is **on par or faster** than Isaac Lab. What GPU do you recommend? ~~~~~~~~~~~~~~~~~~~~~~~~~~ - **RTX 40-series GPUs** (or newer) - **L40s, H100** Does mjlab support multi-GPU training? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Yes, mjlab supports **multi-GPU distributed training** using `torchrunx `_. - Use ``--gpu-ids "[0, 1]"`` (or ``--gpu-ids all``) when running the ``train`` command. - See the :doc:`training/distributed_training` for configuration details and examples. Training & Debugging -------------------- My training crashes with NaN errors ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ A typical error when using ``rsl_rl`` looks like: .. code-block:: bash RuntimeError: normal expects all elements of std >= 0.0 This occurs when NaN/Inf values in the **physics state** propagate to the policy network, causing its output standard deviation to become negative or NaN. There are many possible causes, including potential bugs in **MuJoCo Warp** (which is still in beta). mjlab offers two complementary mechanisms to help you handle this: 1. **For training stability** - NaN termination Add a ``nan_detection`` termination to reset environments that hit NaN: .. code-block:: python from mjlab.envs.mdp import terminations as mdp_term from mjlab.managers.termination_manager import TerminationTermCfg # In your ManagerBasedRlEnvCfg subclass: terminations = { # Your other terminations... "nan_term": TerminationTermCfg(func=mdp_term.nan_detection), } This marks NaN environments as terminated so they can reset while training continues. Terminations are logged as ``Episode_Termination/nan_term`` in your metrics. .. warning:: This is a **band-aid solution**. If NaNs correlate with your task objective (for example, NaNs occur exactly when the agent tries to grasp an object), the policy will never learn to complete that part of the task. Always investigate the **root cause** using ``nan_guard`` in addition to this termination. 2. **For debugging** - NaN guard Enable ``nan_guard`` to capture the simulation state when NaNs occur: .. code-block:: bash uv run train.py --enable-nan-guard True See the :doc:`NaN Guard documentation ` for details. The ``nan_guard`` tool makes it easier to: - Inspect the simulation state at the moment NaNs appear. - Build a minimal reproducible example (MRE). - Report potential framework bugs to the `MuJoCo Warp team `_. Reporting well-isolated issues helps improve the framework for everyone. How can I inspect the generated scene XML? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Use the ``export-scene`` script to write the full scene (XML and mesh assets) to a directory: .. code-block:: bash uv run export-scene g1 --output-dir /tmp/g1 The exported ``scene.xml`` can be loaded directly in MuJoCo for visual inspection or diffing. This is useful for verifying that task configuration and physics are set up correctly, and for creating minimal reproducible examples to share with mjlab or MuJoCo Warp developers. The script accepts task IDs, entity aliases (``g1``, ``go1``, ``yam``), or arbitrary import paths. See :doc:`debugging/export_scene` for full details. My contact sensor misses collisions when using decimation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ With ``decimation > 1`` the physics runs multiple substeps per policy step. A brief contact (e.g. a self collision or an illegal ground touch) can appear and disappear within the substep loop, so by the time the sensor is read, ``found`` is zero and the event is invisible to rewards and terminations. Set ``history_length`` on the ``ContactSensorCfg`` equal to your decimation value. The sensor then stores force, torque, and distance for the last *N* substeps. Your reward or termination function can inspect the history to detect contacts that would otherwise be missed: .. code-block:: python ContactSensorCfg( name="self_collision", ..., fields=("found", "force"), history_length=4, # matches decimation=4 ) # In the reward/termination function: force_mag = torch.norm(sensor.data.force_history, dim=-1) # [B, N, H] had_contact = (force_mag > 10.0).any(dim=1).any(dim=-1) # [B] See :ref:`contact-sensor-history` for full details. .. note:: Feet ground sensors with ``track_air_time=True`` already accumulate contact state across substeps, so they do not need history. .. _faq-sim-forward: When do I need to call ``sim.forward()``? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Short answer: you almost certainly don't. ``sim.forward()`` wraps MuJoCo's ``mj_forward``, which runs the full forward dynamics pipeline (kinematics, contacts, forces, constraint solving, sensors) but skips integration, leaving ``qpos``/``qvel`` unchanged. It brings all derived quantities in ``mjData`` (``xpos``, ``xquat``, ``site_xpos``, ``cvel``, ``sensordata``, etc.) into a consistent state with the current ``qpos``/``qvel``. The environment's ``step()`` method calls it once per step, right before observation computation, so observations and commands always see fresh derived quantities. Termination, reward, and step/interval events run *before* this call and therefore see derived quantities that are stale by one physics substep, a deliberate tradeoff that avoids a second ``forward()`` call while keeping the MDP well-defined (the staleness is consistent across all envs and all steps). Because events run before the call, any state they write (e.g. a velocity push) is refreshed by it and visible to the same step's observations. The one case where this matters is if you write an event or command that both writes state and reads derived quantities in the same function. For example, if Event A calls ``entity.write_root_velocity_to_sim()`` (which modifies ``qvel``) and then immediately reads ``entity.data.root_link_vel_w`` (which comes from ``cvel``), the read will see stale values from before the write. .. warning:: Write methods (``write_root_state_to_sim``, ``write_joint_state_to_sim``, etc.) modify ``qpos``/``qvel`` directly. Read properties (``root_link_pose_w``, ``body_link_vel_w``, etc.) return derived quantities that are only current as of the last ``sim.forward()`` call. If you need to write then read in the same function, call ``env.sim.forward()`` between them. For a deeper explanation, see `Discussion #289 `_. Why aren't my training runs reproducible even with a fixed seed? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ MuJoCo Warp does not yet guarantee determinism, so running the same simulation with identical inputs may produce slightly different outputs. This is a known limitation being tracked in `mujoco_warp#562 `_. Until determinism is implemented upstream, mjlab training runs will not be perfectly reproducible even when setting a seed. My XML ``