人无远虑,必有近忧。——孔子

一个名字就是一句定义:MuJoCo 与它那个「不分配内存」的秘密

先把名字拆开

MuJoCo 是缩写:Multi-Joint dynamics with Contact。多关节动力学,带接触。

名字本身就是定义。前半段说它模拟什么——由多个关节连起来的结构;后半段说它最难的地方——接触。

物理引擎里,自由落体是初中题,弹簧是高中题,而两个物体表面碰上那一刻的力,是研究生课题。因为接触是离散的、突变的、不光滑的:碰之前没有力,碰之后突然有;而且到底哪几个点接触、法线朝哪、摩擦锥多大,全都取决于当下的几何。

一个物理引擎能不能用,很大程度上取决于它怎么处理这个「碰」。

项目地址:https://github.com/google-deepmind/mujoco

它的描述很朴素:多关节动力学,带接触,一个通用物理模拟器。

文档展开了一点:一个通用物理引擎,目标是为机器人学、生物力学、图形与动画、机器学习,以及其他需要快速且准确地模拟「关节结构与环境交互」的领域,提供研发基础。

注意「快速且准确」这两个词被绑在一起。物理模拟里这两者经常对立——快就得近似,准就得慢。而 MuJoCo 的整个设计,几乎都在试图让它们不对立。

那个关键词:预分配

文档里有一句话,技术含量极高,但很容易被当成普通描述读过去:

运行时模拟模块经过调优以最大化性能,运行在内置 XML 编译器预先分配的低层数据结构上。

这句话值得拆成三段来看。

第一段,XML 编译器。 模型不是运行时解析的,是先用一个编译器把 MJCF 模型文件编译成一个内部表示。

第二段,预先分配。 所有需要的内存,在编译阶段就按这个模型的实际规模分配好了。

第三段,低层数据结构。 运行时模块直接在这些结构上算,不做对象创建,不做动态扩容,不做垃圾回收。

三段合起来,是一个很硬的决定:运行时零分配。

零分配意味着什么?意味着模拟一步的耗时可预测。没有「正好赶上一次内存分配」的抖动,没有「数组满了要扩容」的尖峰。对一个要跑强化学习几百万步的引擎来说,这种可预测性比平均速度更重要——因为训练的瓶颈往往不是平均耗时,是那些偶发的慢步。

这也是为什么它有 C API,并且明确说是给研究者和开发者用的。它不是给你点几下就出结果的软件,它是一块可以嵌进别人程序里的计算核心。

一个库,两个世界

文档里紧接着说:库包含原生桌面窗口和浏览器里的交互式可视化,还暴露了大量用于计算物理相关量的工具函数。

这句话透露了它的双重身份。

一方面,它是一个可视化应用:你可以打开一个窗口,加载一个机器人模型,用鼠标拖它、推它、看它摔倒。这对调试极其重要——数字看不出「这条腿的关节限位设错了」,但眼睛一秒就能看出来。

另一方面,它是一个函数库:质量矩阵、雅可比、逆动力学、接触力……这些量都直接暴露出来。做控制的人要的正是这些,因为他们的控制器就是建立在这些量上的。

一个引擎同时做好这两件事,说明它的分层是干净的:核心算物理,外面套可视化和工具。

而 Python 绑定和 Unity 插件,则是把这两个世界分别接到了更广的人群里。

三种上手方式,三种成本

文档给的入门路径很实在,按「安装成本」从零到一排序。

第一种,什么都不装。 打开 mujoco.org,桌面或手机上直接玩;或者去 live.mujoco.org,在 MuJoCo Studio 的 WebAssembly 版本里拖放自己的模型。

它自己补了一句很诚实的话:比原生 C 慢,因为跑在 WebAssembly 里。

这句话不重要,重要的是它说了。一个引擎愿意承认自己的网页版慢,说明它没打算用 demo 骗人。

第二种,装到本机。 下载预编译版本,跑 MuJoCo Studio 应用,或者那个经典的 simulate 查看器。

第三种,Python 用户直接上手。 一条命令:

1
pip install mujoco

原生绑定里已经打包了一份 MuJoCo,装完就能跑。要求 Python 3.11 以上,Linux 的预编译轮子针对 manylinux2014。

三种路径对应三种人:想先看一眼的、想认真调模型的、想写代码的。

七个笔记本,一条从入门到科研的路

Colab 笔记本那一节,是整份文档里最能说明「这个引擎被用来干什么」的部分。

入门:教基础。

模型编辑:用 mjspec 程序化地创建和编辑模型。

rollout:用多线程的 rollout 模块批量跑。

LQR:合成一个线性二次型控制器,让一个人形机器人单腿站立。

最小二乘:用 Python 版的非线性最小二乘求解器。

MJX:MuJoCo XLA 的用法示例——那是用 JAX 写的一个分支。

可微物理:用从物理步进中自动推导出的解析梯度,训练 locomotion 策略。

七个笔记本,前五个是「怎么用」,后两个是「它正在往哪走」。

MJX 的存在说明它不只满足于做一个快的模拟器,还要做一个能在加速器上批量跑的模拟器。而可微物理那一条,则把「模拟」和「训练」接在了一起:如果物理步进本身可导,你就可以直接对它求梯度,而不是靠采样估计。

这两件事加起来,是物理引擎在机器学习时代的新角色:它不再只是验证控制器的地方,它本身成了训练的一部分。

单腿站立,为什么是个好例子

七个笔记本里,LQR 那个最值得多看一眼:它要合成的控制器,是让一个人形机器人单腿站着。

这个任务听起来像杂技,其实是控制理论的标准考题。

因为单腿站立是一个不稳的平衡点。人形机器人站在一条腿上时,重心几乎贴着支撑点,任何微小的扰动都会把它推向倒下;而一旦开始倒,倒下去的速度会越来越快。

这不是「扶住」能解决的,是「提前用力」才能解决的——你必须在它还没歪的时候就开始纠正,而纠正本身又会带来新的动量。

LQR 干的事,就是算出一个最优的反馈增益:给定当前的状态偏差,应该施加多大的力矩。它需要一个线性化的模型,而这个模型从哪来?从物理引擎暴露出来的那些物理量来。

所以这个笔记本表面上在教控制,实际上在展示引擎的另一个用途:它不只是让你看机器人摔倒,它还提供让机器人不摔倒所需要的那些量。

快是有代价的,代价写在「编译」两个字里

预分配这套设计当然不是白拿的。

模型一旦编译,规模就定了。你想加一个关节、换一个几何体、改一个执行器,就得重新编译一次模型。运行时的数据结构不会自己长大。

这条约束在很多场景下是优点——它逼你把模型想清楚;但在另一些场景下是麻烦——比如你想在训练过程中动态改变机器人的结构。

而它对这个问题的回答,藏在另一个地方:mjspec 那个笔记本讲的是程序化地创建和编辑模型。

也就是说,静态的编译期分配,配上一个可以在运行前任意拼装模型的规格层,两者并不冲突。你先用代码把模型搭出来,再让它编译成一份固定布局的数据,然后跑。

这个分工很清晰:建模是动态的,运算是静态的。

一个月一版,和一套改过的语义化版本

版本节奏写得很清楚:目标是每月第一周发一版。

而且从 3.5.0 起,改用修改过的语义化版本。

「修改过的」这四个字很关键。物理引擎的版本号不能简单套用「不兼容就升大版本」,因为数值行为的细微变化,对用户来说可能比 API 改名更要命——同一个模型在 3.5 和 3.6 上跑出来的轨迹不一样,这算不算破坏性变更?

专门写一份 VERSIONING.md 来解释这件事,是一个成熟项目才会做的事。

生态:五种语言的绑定,四种格式的转换

相关软件那一节列了两类东西。

绑定:Python 是一等公民,还带 dm_control 和 PyMJCF;JavaScript 与 WebAssembly 有一等支持,社区项目 mjswan 在上面加了实时策略控制和交互施力;C# 有 Unity 插件。

第三方则有 MATLAB Simulink、Swift、Java、Julia、Rust 五种。

转换器:OpenSim 的模型能转成 MJCF;SDFormat 能双向转换;OBJ 能批量转成可加载的模型;Onshape 的 CAD 装配体能直接转成机器人。

后面这四个,才是它真正进入工程流程的方式。现实里的机器人模型不在 MJCF 里,它们散落在 CAD、在仿真标准格式、在生物力学软件里。一个引擎能不能被用起来,往往取决于「别人家的模型能不能进来」。

许可,和那句很不像 Google 的话

源码是 Apache 2.0。doc 目录里的文档、图片、视频走 CC BY 4.0。碰撞检测里有一段盒体碰撞代码,版权属于 2016 年的另一位作者。

然后是那句:这不是一个官方支持的 Google 产品。

这句话出现在几乎所有 DeepMind 开源项目的末尾,但它每次出现都值得读一遍。它划清了一件事:这个仓库由 Google DeepMind 维护,但它不是一个有 SLA、有客服、有商业支持承诺的产品。

对研究者来说这是常识,对企业来说这是必须知道的边界。

最后

回到那个「碰」字。

物理引擎的浪漫之处在于,它把一个我们每天都在经历、却几乎从不思考的现象——两个东西撞在一起——变成了一组可以求解的方程。而它的工程难点在于,这件事要在毫秒内做完几百万次。

MuJoCo 的答案很朴素,甚至有点反直觉:不要在运行时做决定。

模型在编译期就被拆解、量化、分配好;运行时只管算,不管要。所有的不确定性都被推到了前面,剩下的是一条可预测的快车道。

这套思路不新,操作系统和数据库早就这么干了。但把它用在物理模拟上,并且做得足够准,是另一回事。

至于那七个笔记本里的最后一个——用自动推导的梯度训练一个会走路的策略——那大概是这个引擎最想被用在的地方:不只是让人看见物理,而是让机器学会物理。