mjbatch: running thousands of MuJoCo simulations in parallel on CPU
A Python library for running thousands of MuJoCo simulations in parallel on CPU
At a glance
- What is it?
- mjbatch is a Python library that batches MuJoCo physics across CPU threads, exposing qpos, ctrl and model parameters as shared arrays. It suits RL, MPC and system identification workloads; it is not a GPU simulator and not a drop-in replacement for mujoco.MjData.
- Who is it for?
- Adopt mjbatch if your workload is thousands of independent MuJoCo rollouts on CPU hardware and you want the state exposed as NumPy arrays rather than per-environment Python objects. Do not adopt it if you need GPU-resident simulation, a stable API, or a model whose per-simulation parameters cannot be expressed through expand and set_const.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 7 days ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 11, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What mjbatch is for
MuJoCo's own Python bindings give you one MjModel and one MjData per simulation. If you want 4096 rollouts, you allocate 4096 MjData objects and step them one at a time from Python. That works, and for a few dozen environments it is fine. It stops working when a controller needs the whole batch stepped per control tick, because the per-call Python overhead dominates the actual physics.
mjbatch targets exactly that gap. The README describes it as "a Python library for running thousands of MuJoCo simulations in parallel on CPU." The intended users are people doing reinforcement learning, model predictive control, system identification and hardware co-design, which are the four application areas the repository's examples directory covers. The topics list on the repository (mpc, mujoco, reinforcement-learning, simulation) matches that framing.
The important word is CPU. This is not a GPU simulator and does not pretend to be one. It buys throughput by saturating cores you already have, which matters if your machine has many of them and no useful accelerator, or if your policy is small enough that kernel launch overhead would eat the gain.
How the batch, bind and expand mechanism works
The design is a single MjModel shared across N simulations, with the per-simulation MjData laid out so that a field like qpos becomes one contiguous array of shape (num_sims, ...). The README's example makes this concrete: batch.bind("qpos") returns an array you can read and write directly, and the comment states that qpos updates in place after batch.step().
Execution is a C++ thread pool with the GIL released. That is the part that makes the library more than a convenience wrapper: while the threads are stepping physics, Python is not holding the interpreter lock, so the stepping is genuinely concurrent rather than interleaved. The README lists this as the first feature.
The second mechanism is per-simulation model parameters. A single shared MjModel would normally force every environment to share the same friction, mass or gain. batch.expand("geom_friction") returns a writable array with one entry per simulation, and the example assigns a random uniform value per environment. Because MuJoCo caches derived constants computed from model fields, changing a field is not enough on its own; set_const is the documented way to recompute those derived constants after a write. The README names both, and the pairing is the whole point: expand without set_const would silently leave the simulation using stale derived values.
Data flow per control tick is therefore: read bound state arrays, compute controls in NumPy or your framework of choice, write into the bound ctrl array, call batch.step(), then read the updated state arrays again. There is no copy in or out of per-environment objects, which is where the time savings come from.
Installing mjbatch and running a first batch
The package is published on PyPI as mjbatch, and pyproject.toml declares a single runtime dependency pinned to a specific MuJoCo version: mujoco==3.13.0, alongside numpy. Python 3.10 or newer is required. The build backend is scikit-build-core with nanobind, so installing from source compiles a C++ extension; a wheel install avoids that.
pip install mjbatchAfter installation you should be able to import Batch from mjbatch in a Python 3.10+ interpreter that also has mujoco 3.13.0 available. The repository's own development workflow uses uv (the Makefile runs pytest and pyright through uv run, and the examples README line says to run them with uv run examples/<file>.py), so a uv-based environment is the path the project exercises.
The smallest useful program follows the README example. Load a model, construct a batch, bind the fields you intend to read or write, then step in a loop.
import mujoco, numpy as np
from mjbatch import Batch
model = mujoco.MjModel.from_xml_path("scene.xml")
batch = Batch(model, num_sims=4096) # threads default to every logical CPU
qpos, ctrl = batch.bind("qpos"), batch.bind("ctrl")
batch.expand("geom_friction")[:, :, 0] = np.random.uniform(0.4, 1.2, (4096, 1))
for _ in range(1000):
ctrl[:] = policy(qpos) # your controller, all 4096 at once
batch.step() # step them in parallel; qpos updates in placeTwo things to note about that snippet. First, num_sims=4096 is the README's own figure and threads default to every logical CPU, so on a small machine the thread count and the batch size are independent knobs. Second, the loop body is where your controller lives; mjbatch does not provide a policy, only the batched stepping and the array views.
If you want a working reference before writing your own model, the repository ships examples/hello.py plus task examples for cart-pole swing-up, cart-pole MPC, a G1 backflip, a Go1 joystick controller, a throwing arm and Rizon inertia identification. Some of them need optional dependencies pulled in with uv sync --group examples, and the ones that open a window need a display; --headless runs the solver without one.
Where mjbatch stops being the right tool
The single shared MjModel is the sharpest constraint. expand gives you per-simulation values for model fields, but it is a per-field mechanism. If your problem requires structurally different models per simulation, for example different XML topologies, different numbers of bodies, or different contact geometries, one Batch cannot represent that. You would need one Batch per distinct model, and the parallelism then applies within each group rather than across the whole population.
The second constraint is the pinned MuJoCo version. pyproject.toml requires exactly mujoco==3.13.0. That is a hard pin, not a floor, so a project already locked to a different MuJoCo release has to reconcile the two before mjbatch is usable at all. Nothing in the README suggests a compatibility range.
The third is the maturity signal. The classifier in pyproject.toml is "Development Status :: 4 - Beta", the version is 0.1.1, and the first release listed is 0.1.0. The API surface shown in the README is small, which limits how much can break, but a 0.x version number is a statement about stability expectations.
Finally, CPU parallelism has a ceiling. Once every logical core is busy, adding simulations does not add throughput, it adds queueing. A workload whose bottleneck is per-environment compute rather than Python overhead will not benefit much, and a workload that would fit comfortably on a GPU simulator is the wrong shape for this library entirely.
mjbatch compared with MuJoCo's own bindings and GPU simulators
The honest comparison is against plain mujoco with a Python loop. That approach needs no extra dependency, works with any MuJoCo version your environment already has, and gives you one MjData per environment with all of MuJoCo's own API available per simulation. Its cost is Python-level per-step overhead multiplied by the number of environments, which is what mjbatch removes. If your batch is small, or your per-step physics is expensive relative to the loop overhead, plain mujoco is the simpler choice and mjbatch adds a build dependency for nothing.
The other comparison is against GPU-batched simulators. Those move the physics onto the accelerator and typically keep state in device memory, which changes the whole shape of the program: controllers must be written against the accelerator's array library and data movement becomes an explicit concern. mjbatch keeps everything in host NumPy arrays and uses threads, so a controller written with NumPy or a CPU framework works without modification. The trade is peak throughput on large batches, where a GPU simulator has more headroom, against ease of integration and the absence of host-to-device transfers. Which one wins depends on whether your machine has a usable GPU and whether your policy can run there.
A third option worth naming is writing your own C++ loop over MjData with a thread pool. That is what mjbatch is, essentially, plus the array binding layer and the expand/set_const bookkeeping. If you need a different field layout or a different scheduling policy, rolling it yourself is a real option rather than a hypothetical one.
Maintenance, licensing and upgrade cost
The repository is not archived, and the last push was on 2026-09-14, which is three days before this writing. Two releases are listed, v0.1.0 on 2026-09-10 and v0.1.1 on 2026-09-14. That is a young project with a short release history, and the release cadence so far reflects a single burst rather than a track record.
The licence is Apache-2.0, declared both in pyproject.toml (license = "Apache-2.0", with license-files = ["LICENSE"]) and in the README's License section. Apache-2.0 is permissive and includes an explicit patent grant, which matters for a library that links against MuJoCo. This is a description of what the repository states, not legal advice; if you are redistributing mjbatch inside a product, read the LICENSE file and MuJoCo's own terms yourself.
Upgrade cost is dominated by the MuJoCo pin. Every mjbatch release so far ties to mujoco==3.13.0, so a MuJoCo upgrade on your side is gated on a matching mjbatch release. The build also compiles a C++ extension through scikit-build-core and nanobind, which means source installs need a working compiler toolchain; wheel installs sidestep that. The repository's Makefile shows the maintenance surface the author works with: pytest for tests, ruff for formatting and linting, clang-format for the C++ sources, pyright for type checking, and a stubs target that regenerates MuJoCo type stubs and runs nanobind.stubgen. That is a normal amount of tooling for a project with a compiled core, and it also means contributions have to satisfy all of it.
Editorial conclusion
Adopt mjbatch if your workload is thousands of independent MuJoCo rollouts on CPU hardware and you want the state exposed as NumPy arrays rather than per-environment Python objects. Do not adopt it if you need GPU-resident simulation, a stable API, or a model whose per-simulation parameters cannot be expressed through expand and set_const. Before committing, check that mujoco==3.13.0 resolves in your environment, that your model's parameters are reachable through expand, and that a single Batch.step() call reproduces the same trajectories as a hand-written loop over mujoco.MjData.
Frequently asked questions
Does mjbatch run MuJoCo simulations on the GPU?
No. The README describes it as a library for running thousands of MuJoCo simulations in parallel on CPU, and the execution model is a C++ thread pool with the GIL released. It is aimed at saturating CPU cores, not at accelerator hardware.
Which MuJoCo version does mjbatch require?
pyproject.toml pins the runtime dependency to mujoco==3.13.0, which is an exact pin rather than a minimum. A project locked to a different MuJoCo release has to reconcile that before mjbatch can be installed.
How do I install mjbatch?
It is published on PyPI as mjbatch, so pip install mjbatch is the documented route, and Python 3.10 or newer is required. The repository's own examples are run with uv run examples/<file>.py.
How do I give each simulation in a batch different model parameters?
Use batch.expand on the model field you want to vary, which returns a writable array with one entry per simulation, then call set_const so MuJoCo recomputes the derived constants. The README shows expand("geom_friction") being assigned per-simulation values.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/kevinzakka-mjbatch)