Skip to content

feat(physics): 为 SuperDex 增加 C++ 常驻场景并行与批量状态接口 - #1

Merged
TATP-233 merged 4 commits into
mainfrom
dev/issue-1533-cpp-batch-executor
Sep 8, 2026
Merged

TATP-233 merged 4 commits into
mainfrom
dev/issue-1533-cpp-batch-executor

Conversation

@TATP-233

@TATP-233 TATP-233 commented Sep 8, 2026 •

Copy link
Copy Markdown

问题与设计动机

原先 UniSim 为每个环境创建独立 SuperDex scene,但在 Python 中逐环境写入力、step、读取状态/接触,GIL 和大量 binding 调用限制了 CPU 利用率。仅将 scene.step 放在线程池中不能消除串行读写开销。

本 PR 在 SuperDex 自身 pybind 扩展增加 SceneBatchExecutor,把独立 scene 的力写入、求解与结果回写统一放进常驻 C++ worker,使用一次调用及完成屏障向下游提供一致的批次结果。没有引入 subprocess IPC。

实现与新增功能

  • 构造时绑定唯一 scene、articulated actor、link actor 列表以及 contact source/other、类型和距离;校验所属 scene、维度与重复对象。
  • 常驻 std::thread workers 使用任务队列推进独立 scene。每个线程绑定/解绑引擎 context;SDK 必须 initialize(num_worker_threads=0),防止两层线程池叠加。
  • step 接收 dt、连续 generalized-force (N,nv),回写 qpos/qvel (N,nv)、link state (N,L,16)、contact (N,C,3)、diverged (N,) uint8。
  • link state 提供位置、wxyz 四元数、COM、线速度、角速度;contact 支持 found(含 actor pair 和距离过滤)、世界系力及力矩。
  • solver divergence 在原生 worker 中读取并批量回传;临时 link transform vector 用 thread_local 复用。
  • 等待 C++ 屏障期间释放 GIL,Python array 生命周期保持在持有 GIL 的范围内;异常完成批次等待后向调用者传播。
  • close/context manager/shutdown 负责 join workers;scene lease 防止活跃 executor 使用的 scene 被 destroy。拒绝 leased scene 上新注册的 Python step callback。
  • 只提供完整 batch buffer API,无旧 qpos/qvel-only 重载或串行兼容路径。

改动文件:mochi_physics_pybind.cpp 注册绑定和生命周期钩子;新增 scene_batch_executor.cpp 与 test_scene_batch_executor.py。

上下游责任

project_superdex C++/pybind → UniSim SuperDexBackend → UniLab task/env;独立 UniRL 负责训练 runner 和 rank CPU 分配,通过环境工厂注入,不依赖 UniLab/UniSim。

SuperDex 只提供 native scene batch 能力;物理核心默认 worker 数、状态坐标系转换、reset、资产物化及 MuJoCo 离线回放由 UniSim 管理。UniLab 提供 Go2/FR3 owner、奖励、观测、资产 hub 和 PPO 配置。统一 resolve_collector_cpu_ids 由 UniRL 管理拓扑发现与 rank 分配。下游需使用与该 fork 匹配的源码构建,依赖通过本地 editable/build 链接,不发布 wheel。

原生 SuperDex renderer 不在本 PR 范围;实际视频由下游 MuJoCo renderer 读取 SuperDex 状态完成,MuJoCo 不推进物理。

验证与待审核项

  • 当前本地 build 的 native executor pytest:2 passed;包含 step 输出与 shutdown/scene 生命周期。
  • UniSim 扩展预加载后全量 pytest:167 passed, 26 skipped;SuperDex 定向测试:26 passed, 1 skipped。
  • UniLab SuperDex/FR3 spawn 集成:18 passed, 1 skipped。
  • 已有 build-cpu-pool 为 Release,Clang 18;此前构建成功。本次重新运行 uv run --no-project cmake --build build-cpu-pool --parallel 16 被 CMake 缓存引用的已失效 Ninja 路径阻断,不能宣称本次干净重建通过。
  • 待审查输出 buffer 类型/生命周期、场景并发使用约束与跨平台原生构建。macOS 仅有下游 topology mock 测试,未实机验证 C++ executor。

下游验证:原版基线与最终实现

本节的 env-step/s 有且只有一个定义:

num_envs * measured_complete_env_steps / elapsed_wall_seconds

这里的完整 env.step() 是一次控制步,包含动作处理、SuperDex physics、状态/传感器刷新、观测、reward 和可能发生的 reset。它不是单个 Scene::Step,不是 physics substep,也不是 PPO learner FPS。所有数据均为 Linux x86_64、CPU headless、Go2、128 个环境、测量 100 次完整 env.step()。32/64 worker 补测与当前测量均为 20 次预热;较早的原版历史记录对预热次数曾出现 20 与 100 两种记载,因此原版行只保留为历史基线,不能作为严格 A/B 的一部分。

原版基线与最终 native batch path 的历史观察如下:

实现 配置 完整 env-step/s 含义
原版 Python adapter 1 worker,原始 UniSim/SuperDex 集成 1011.85 原始基线
最终 C++ batch path 32 native workers,ec4a5b0 测量环境 10929.82 历史轮次中的最终实现峰值观察

两项历史数据说明原版逐环境 Python read/write 和最终 C++ batch path 的量级差异;因完整提交、依赖和运行状态未冻结为同一实验,这不是严格单变量 A/B,不能据此宣称精确倍率。

32/64 worker 补测 与“当前物理核心默认策略”使用上述同一物理量,却是两次独立实验,不能拼成一条缩放曲线或彼此计算增减。区别如下:

独立实验 运行条件 结果 应如何解读
32/64 worker 补测 SuperDex ec4a5b0、UniSim e95e97b、32 logical CPU affinity、20 次预热、同一 zero action 16/32/64 workers = 10114.65 / 10929.82 / 10432.68 该轮中 32 是峰值;64 已超过可用 affinity,吞吐下降
当前物理核心默认策略 最终代码、UniSim 4d0bedd 之后的 vectorized hot path、更新后的 UniRL/Torch、20 次预热 1/16/32/auto(16) = 2096.68 / 18280.64 / 20997.90 / 17707.48 auto 是产品默认值;32 logical worker 仅为同轮性能观察

这两轮差异接近两倍的首要已确认原因是热路径实现不同,而不是物理核心默认策略。e95e97b 已使用 C++ 回写 state/contact buffer,但 _refresh 仍按环境执行 Python 循环,逐项转换 qpos/qvel 并散射 link state;4d0bedd 将 qpos/qvel 四元数转换、link-state cache scatter 和 actuator force 写入改为整批 NumPy 操作,同时在没有外力时跳过逐环境 wrench 扫描。相同的显式 16 workers 从 10114.65 到 18280.64、显式 32 workers 从 10929.82 到 20997.90,说明 worker 默认值不是主因。

不过第二轮各 case 连续推进固定随机数生成器,case 间动作序列并不完全相同,且动作从 zero action 改为固定随机 action;Python/Torch、CPU affinity 和进程状态也不同。因此现有数据不能将增益精确归因于上述向量化,也不能直接比较 10929.82、20997.90 和 17707.48。当前默认策略选择 16 physical workers 是为避免默认占用 SMT sibling;它不等同于该主机上的吞吐最大点。

当前硬件为 AMD Ryzen 9 9950X3D2(16 cores / 32 threads,单 NUMA),约 60.5 GiB RAM;SuperDex 使用 CPU FP32,主机另有 NVIDIA GeForce RTX 4090,但本 PR 不实现 GPU physics。

UniLab 的端到端 PPO 复核使用 1024 Go2 环境、24-step horizon、151 iterations、seed 1、CPU learner:自动 16 native workers,累计 3,710,976 环境步,训练吞吐 10,980 env-steps/s,最终平均 reward 26.90,TensorBoard 曲线峰值 28.26,平均 episode length 1000。该实验还由下游 MuJoCo 离线 renderer 生成 4 秒回放视频;SuperDex 仍负责全部物理推进。训练成功不等于已证明与 MuJoCo 的动力学或行走质量等价。

这些数据支持本 PR 的边界:SuperDex 上游只需接纳通用、可复用的 C++ batch executor 与明确的生命周期/并发契约;MuJoCo renderer、物理核心选择、rank CPU 分片、UniLab owner 和训练指标仍由下游仓库维护。若该方向符合 SuperDex 的公开 API 规划,后续改动希望直接在 facebookresearch/project_superdex 的 issue/PR 中讨论和提交,当前 fork PR 仅作为可运行的设计与性能证据,不要求上游接受 UniLab 的全部适配层。

复现指南

  1. 构建本 PR 分支的 native extension:

    cd project_superdex
    uv run --no-project cmake --build build-cpu-pool --parallel 16
  2. 在 UniSim/UniLab 使用同一源码扩展和本地 editable checkout。必须预加载 mochi_physics,否则 SuperDex facade 可能优先找到已安装 wheel:

    export PYTHONPATH=/absolute/project_superdex/build-cpu-pool/bin:/absolute/UniLab/src:/absolute/unisim/src
    export SUPERDEX_ASSETS_PATH=/absolute/project_superdex/assets
    export UV_NO_SYNC=1
  3. 性能复现必须冻结实验条件:记录 SuperDex、UniSim、UniLab、UniRL 的完整 git SHA,Python/Torch 版本,CPU 型号、物理核心/逻辑线程数、NUMA、CPU affinity、环境变量、num_envs、warmup 和测量次数。每个 worker case 在独立新进程中运行至少 3 次,并报告 median、min/max。

  4. 为使各 worker case 使用相同轨迹,先以 np.random.default_rng(1533) 一次性生成形状为 (120, 128, action_dim) 的 action 数组;每个 case 从该数组第一个 action 开始,以前 20 个动作预热,再以相同的后 100 个动作测量。用 time.perf_counter() 包围恰好 100 个完整 env.step(),计算 128 * 100 / elapsed_seconds。每次结果同时输出 native worker 数、SDK worker 数、mean reward、当前 affinity 和实际物理核心数。不要让不同 case 共用持续推进的 RNG,也不要复用已运行过其他 case 的进程。

  5. 对严格的原版/最终 A/B,除被比较的实现外,必须固定同一机器、同一 affinity、同一 action 数组、同一任务配置和同一依赖锁定版本。未满足这些条件的结果只能作为独立性能观察,不能计算或报告跨轮次倍率。

  6. Go2 端到端训练复现:

    cd UniLab
    PYTHONPATH=/absolute/project_superdex/build-cpu-pool/bin:/absolute/UniLab/src:/absolute/unisim/src \
    SUPERDEX_ASSETS_PATH=/absolute/project_superdex/assets UV_NO_SYNC=1 \
    uv run --project . train --algo ppo --task go2_joystick_flat --sim superdex \
      algo.num_envs=1024 algo.num_steps_per_env=24 algo.max_iterations=151 \
      algo.seed=1 +env.seed=1 env.superdex_num_workers=0 \
      training.device=cpu training.log_root=/absolute/output/superdex-go2

    该配置会使用物理核心默认 worker,并按 owner 的 play_render_mode=record 在训练后调用 MuJoCo 离线 renderer;SuperDex 仍负责所有 physics step。记录 run_config.json、run_summary.json、TensorBoard event、实际 worker 数和生成的 play_video.mp4。

  7. 复现 rank CPU 分片:设置 training.devices=[0,1],不设置 training.dp_collector_cpu_ids;每个 rank 自动获得一半物理核心组及其 SMT siblings。若要固定实验,使用 training.dp_collector_cpu_ids=[[...],[...]] 显式给出每 rank 的 CPU id 列表。

关联路线图:Motphys/UniLab#1533。

关联 PR 与审核顺序

  1. SuperDex native executor:feat(physics): 为 SuperDex 增加 C++ 常驻场景并行与批量状态接口 #1
  2. UniRL CPU 分配:feat(ipc): 统一物理核心发现与多 rank collector CPU 分配 unilab_rl#20
  3. UniSim backend:feat(superdex): 集成原生批处理后端、物理核心默认配置与 MuJoCo 回放 unisim#47
  4. UniLab owner 集成:feat(superdex): 完成 #1533 任务集成、rank CPU 分片与训练回放 Motphys/UniLab#1542

建议先审核原生 API 与 rank 分配,再审核适配器和任务集成;依赖通过本地源码链接验证。

用户已确认接受本轮实验结果,申请合入 main 并等待逐仓库人工审核。未向 facebookresearch 原仓库提交 PR;不发布 PyPI、不打 release tag,不主动调整版本号。开发分支合并的版本变化来自上游 main 已有发布。

集成实验

Linux x86_64,AMD Ryzen 9 9950X3D2(16 物理核/32 逻辑线程,单 NUMA),约 60.5 GiB RAM,CPython 3.12.3,SuperDex CPU FP32。主机报告 NVIDIA GeForce RTX 4090;物理和 PPO learner 均使用 CPU,EGL 渲染可使用 GPU。

Go2JoystickFlat PPO:1024 环境、每轮 24 步、151 轮(索引 0–150)、seed=1、自动 16 native worker、SDK worker=0。累计 3,710,976 环境步,训练 338.66 秒,总流程含回放 350.35 秒;训练吞吐 10,980 env-steps/s,末轮 11,294。平均回报从 0.0275 到 26.90,TensorBoard 平均回报曲线峰值 28.26;最终平均 episode length=1000。生成 200 帧、4 秒、562330 字节的 MuJoCo 离线回放视频。成功完成训练不能单独证明充分的速度跟踪或跨后端动力学等价。

实验目录:
/home/user/ws/unilabsim2/experiments/superdex-go2-physical-render-rerun/Go2JoystickFlat/2026-09-08_21-58-30_superdex

目录包含 run_config.json、run_summary.json、TensorBoard events、model_150.pt、play_video.mp4。产物位于本地,未作为源码提交。run_summary 的 best_mean_reward 是末尾 episode buffer 的单次最大值,报告曲线峰值使用 TensorBoard 的 Train/mean_reward。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant