NVIDIA Warp and MuJoCo Warp (MJWarp) Enable 2,048‑World GPU Simulation for Robotics
TL;DR
NVIDIA Warp and its MuJoCo‑compatible layer MJWarp let you batch thousands of robot simulation worlds on a single GPU, achieving orders‑of‑magnitude higher aggregate throughput than classic CPU‑based MuJoCo.
Stack Overview
| Layer | Role |
|---|---|
| NVIDIA Warp | Python kernel language that compiles to CUDA, provides autodiff, and interoperates with PyTorch/JAX |
| MJWarp | GPU implementation of MuJoCo physics that preserves the MJCF format and supports batched stepping |
| Your scene (SO‑101) | Standard Menagerie / Robot Studio assets and task geometry |
| Next (Newton / Isaac Lab) | Multi‑solver API, USD assets, sensors, managers, and training loops |
When to use each component
| Need | Use |
|---|---|
| Single‑robot MPC or teleoperation | Classic MuJoCo CPU |
| Maximum raw MuJoCo physics throughput | MJWarp (or mjlab) |
| JAX‑based training recipes | MuJoCo Playground / MJX (implementation = warp) |
| Multi‑solver + Isaac Lab integration | Newton (future post) |
NVIDIA Warp: A Minimal GPU Kernel
Warp lets you write statically‑typed Python kernels that JIT‑compile to native CUDA. The example below integrates point positions under gravity, scaling from two points to millions without changing the kernel logic.
import numpy as np, warp as wp
@wp.kernel
def integrate(positions: wp.array[wp.vec3],
velocities: wp.array[wp.vec3],
dt: float):
i = wp.tid()
velocities[i] += wp.vec3(0.0, 0.0, -9.81) * dt
positions[i] += velocities[i] * dt
wp.init()
device = "cuda:0" if wp.is_cuda_available() else "cpu"
start = np.array([[0.0, 0.0, 0.5], [0.2, 0.0, 0.5]], dtype=np.float32)
positions = wp.array(start, dtype=wp.vec3, device=device)
velocities = wp.zeros_like(positions)
wp.launch(integrate, dim=len(start), inputs=[positions, velocities, 0.01], device=device)
wp.synchronize_device(device)
print(positions.numpy())
Why this matters for robotics
- Explicit parallel work –
wp.tid()identifies the logical thread (point, contact, body, or world). - Explicit device arrays – Arrays live on the selected device;
.numpy()triggers a host copy, while DLPack adapters enable zero‑copy sharing with PyTorch/JAX. - Composable launches – Multiple kernels can be captured into a CUDA graph, reducing launch overhead.
Differentiability & Determinism (Warp capabilities)
- Differentiable kernels – A
wp.Taperecords forward launches and automatically computes adjoints onbackward(). This enables differentiable geometry, CFD, and custom physics pipelines. - Deterministic execution – Introduced in Warp 1.15; deterministic mode trades a small performance hit for reproducible atomic ordering, useful for regression testing. These features are optional and not required for the SO‑101 walkthrough.
What Is MuJoCo Warp (MJWarp)?
MJWarp is a GPU‑backed implementation of MuJoCo’s physics pipeline built on Warp. It accepts the same MJCF models, places the model and a batch of independent states on the GPU, and advances the entire batch with a single mjw.step call.
- Latency vs. Throughput – MJWarp does not guarantee a faster single‑world step; its advantage is aggregate throughput: the total number of world‑steps completed per second when many worlds run in parallel. This is the key metric for reinforcement‑learning and large‑scale sampling.
Migrating a MuJoCo Scene to MJWarp
The guide walks through three stages: establishing a CPU baseline, validating a one‑world GPU rollout, and scaling to 2,048 worlds.
1. Establish a MuJoCo CPU Baseline
- Load the SO‑101 pick‑and‑place MJCF (standard Menagerie assets) with
mujoco.MjModel.from_xml_path. - Use a 50 Hz control loop with 10 physics sub‑steps (
timestep = 0.002 s). - Verify task success by checking horizontal error ≤ 0.015 m and vertical separation between 0.035 m and 0.055 m after the cubes settle.
2. Validate One‑World MJWarp Parity
import warp as wp, mujoco_warp as mjw
wp.init()
device = wp.get_device()
# Upload model and allocate a single‑world batch
m = mjw.put_model(mjm)
d = mjw.make_data(mjm, nworld=1, nconmax=spec.nconmax, njmax=spec.njmax)
# Seed state from the CPU rollout
wp.copy(d.qpos, wp.array(mjd.qpos[None, :], dtype=wp.float32, device=device))
wp.copy(d.qvel, wp.array(mjd.qvel[None, :], dtype=wp.float32, device=device))
wp.copy(d.ctrl, wp.array(mjd.ctrl[None, :], dtype=wp.float32, device=device))
mjw.forward(m, d)
- The host loop mirrors the CPU workflow, copying controls to the device each sub‑step and pulling back
qpos/qvelfor downstream calculations. - This path is not a performance benchmark; it validates that the GPU simulation reproduces the CPU result.
3. Size Contact & Constraint Buffers
- MJWarp pre‑allocates contact (
nconmax) and constraint (njmax) buffers per world. Insufficient capacity triggers overflow warnings and may corrupt trajectories. - Use
mjwarp-testspeed --measure_allocto measure actual usage and raise the limits accordingly (e.g.,nconmax=128,njmax=300for the SO‑101 task).
4. Scale to 2,048 Worlds
nworld = 2_048
d = mjw.make_data(m, nworld=nworld, nconmax=spec.nconmax, njmax=spec.njmax)
# Replicate the initial state across the batch
wp.copy(d.qpos, wp.array(np.tile(mjd.qpos, (nworld, 1)), dtype=wp.float32, device=device))
wp.copy(d.qvel, wp.array(np.tile(mjd.qvel, (nworld, 1)), dtype=wp.float32, device=device))
wp.copy(d.ctrl, wp.array(np.tile(mjd.ctrl, (nworld, 1)), dtype=wp.float32, device=device))
mjw.forward(m, d)
# Capture the step into a CUDA graph for low‑overhead replay
with wp.ScopedCapture() as cap:
mjw.step(m, d)
step_graph = cap.graph
- The captured graph reuses the same buffers; only
d.ctrlneeds updating between replays.
5. Measure Aggregate Throughput
# Warm‑up (compilation, allocation)
for _ in range(10):
wp.capture_launch(step_graph)
wp.synchronize()
t0 = time.perf_counter()
for _ in range(200):
wp.capture_launch(step_graph)
wp.synchronize()
elapsed = time.perf_counter() - t0
world_steps = 200 * nworld
print(f"{world_steps/elapsed:,.0f} world‑steps/second")
- Report both world‑steps/second and ms per batched step alongside the batch size.
- Use the provided
scaling_study.pyscript to sweep batch sizes (1, 64, 1 024, 2 048, 8 192) and observe where throughput saturates.
Getting Started
- Warp –
pip install warp-lang(≥ 1.15 for deterministic mode). Explore examples withpython -m warp.examples.browse. - MJWarp –
pip install mujoco-warp; launch a viewer withmjwarp-viewer path/to/scene.xml. - SO‑101 resources – NVIDIA’s Sim‑to‑Real course and Menagerie assets.
- Training on top of MJWarp –
mjlab, MuJoCo Playground (MJXimpl='warp'), or Isaac Lab via the upcoming Newton integration.
What’s Next
The next post will embed MJWarp inside the Newton physics engine, adding multi‑format assets, swappable solvers, sensors, and an Isaac Lab training pipeline. It will reuse the same SO‑101 task (and optional reBot variant) to illustrate the additional integration steps.
References
- State of Simulation for Physical AI – first blog in the series.
- NVIDIA Warp GitHub and documentation (v1.15.0 adds GPU determinism).
- MuJoCo Warp repository and official docs.
- mjlab (arXiv:2601.22074) and MuJoCo Playground.
- NVIDIA SO‑101 sim‑to‑real learning path.
- Upcoming Newton repository.