Skip to content

perf: 定位 Env overhead 并行化实测天花板 — Numba prange 融合 kernel 在 Xeon 8568Y+ 到 31x #665

Description

@TATP-233

Context

Follow-up to #663。跑了一组独立于 UniLab 的 standalone probe(benchmark/benchmark_env_overhead_parallel.py, 812 行,无 UniLab 依赖)来回答:把 NpEnv.step 里 obs/reward/termination 那段 update_state 并行化,实测天花板在哪。

Probe 设置对齐真实训练num_envs=8192/32768, act_dim=29, obs_dim=98/101, dtype=float32,15 个 active reward terms(对齐 motion-tracking scale),4 个 noise buffer,per-reward log mean,2% _reset_done_envs 拷贝。

三条路径对比:numpy 基线 / ThreadPoolExecutor + numpy shards / Numba prange 融合 kernel

结论

1. Numba prange 融合 kernel 在三台机器上都拿到显著加速

Xeon 8568Y+ (160T, 2-socket)

配置 numpy 基线 Numba 最佳 Speedup
8k envs, 双 socket (tbb) 4.55 ms 0.312 ms @ 32t 14.6x
8k envs, `numactl -N 0` 4.55 ms 0.260 ms @ 24t 18.0x
32k envs, 双 socket 19.4 ms 0.671 ms @ 48t 28.8x
32k envs, `numactl -N 0` 19.4 ms 0.664 ms @ 32t 31.4x

AMD Ryzen 9950X3D (16C/32T)

配置 numpy 基线 Numba 最佳 Speedup
8k envs (omp) 2.24 ms 0.139 ms @ 32t 16.14x
32k envs (omp) 8.84 ms 0.334 ms @ 16t 26.45x

Apple M-series (18C, workqueue)

配置 numpy 基线 Numba 最佳 Speedup
8k envs 1.78 ms 0.255 ms @ 16t 6.96x
32k envs 9.40 ms 0.567 ms @ 16t 16.56x

单核 fusion 贡献 2.3-3.8x(消掉每个 reward term 的中间 numpy array),并行再拿 ~5-10x。大 batch 一致地给出更高 speedup——每个 parallel section 摊到更多 env,barrier / launch cost 摊平。

2. False sharing 是并行的真正天花板

kernel 里 15 个 reward log 累加不能写成 shared scalar 的 `log_partial_sum[k] += ...`——所有 thread `+=` 同一 cache line 会造成持续 ping-pong,Xeon 上 `nthreads=32` 只到 5.5x。必须用 per-thread scratch `log_thread_sum[tid, k]`,kernel 结束后主线程 `axis=0` sum。同一份代码这个改动之后到 14.6x(8k)/ 31.4x(32k)。

3. update_state 只是 Env overhead 的一半,reset_done 是并列瓶颈

来自 #663b44ae656 之后的完整细分(Xeon 8568Y+ / 8192 envs):

Task Backend update_state reset_done 主瓶颈
g1_motion_tracking mujoco 35.1 ms 12.6 ms update_state 主导
g1_motion_tracking motrix 40.7 ms 16.4 ms update_state 主导
g1_walk_flat mujoco 8.4 ms 8.6 ms 两者相当
g1_walk_flat motrix 10.3 ms 12.4 ms reset_done 略大

`reset_done` 的开销分布:

  • Terminal-observation copy / scatter:sub-ms,可忽略
  • `reset call` 8-15.8 ms,主要在:
    • `g1_walk_flat`:几乎全在 backend `set_state`(7.2-11.7 ms)
    • `g1_motion_tracking`:reset plan + backend `set_state` + reset observation(其中 full-batch body-pose getter ~3.2-3.7 ms)

只上 Numba 优化 update_state 时:`g1_motion_tracking` 的 overhead 削减 71%(40.7→1.3ms + 16.4→16.4ms),`g1_walk_flat` 只削减 54%(10.3→0.3 + 12.4→12.4ms)。walk_flat 的 reset_done 会立即成为新瓶颈。

4. ThreadPool + numpy shards 是死路

Xeon 上 K=2 只 1.7x,K=4 已经打平,K≥8 全崩(GIL 争用 + Python log dict + 双路 NUMA)。Ryzen 稍好(K=4 @ 32k 到 2.95x),但仍远不及 Numba,也无法组合使用。

5. Distribute / aggregate 可忽略

`pool.submit` ~10 μs,aggregate(log 归并 + 2% final_observation 拷贝)~30-50 μs,总占比 <2%。不用为分片调度做优化。

6. RNG 是独立的天花板

Noise 生成(`rng.standard_normal` × 4)单线程占 5-6 ms,在 update_state 里占 55-80%(Xeon 上 80%)。Numba prange 无法帮它并行,需要独立方案(in-kernel numba RNG / 多 BitGenerator / 复用 buffer)。

推荐路径

主线 A(P0):Numba prange 融合 kernel — `update_state` 层,跨 task 通用

对 `g1_motion_tracking` 直接把 overhead 打掉 71%,对 `g1_walk_flat` 也能砍掉 update_state 全部时间。关键实现要点:

  • 融合 reward dispatch + obs concat + termination 到一个 `@njit(parallel=True)` prange loop
  • reward log 累加必须 per-thread scratch,`(nthreads, num_terms)` shape,最后 `axis=0` sum
  • noise buffer 从 kernel 外传入(RNG 独立处理)
  • kernel 内部通过 `numba.set_num_threads(24-32)` 控制并发,不要全进程 `numactl -N 0`(那会伤 Physics,Xeon 8568Y+ Physics 已经吃到 22-66ms/步)

主线 B(P1):reset_done 拆解优化 — 按 task 分头做,与 A 并行推进

  • `g1_motion_tracking`:row-wise body-pose getter,只对 done 的 env 拉数据(3.2-3.7 ms → sub-ms)
  • `g1_walk_flat`:sparse `backend.set_state` — 需要 backend 层增加 index-based 写接口(7-12 ms → 期望 sub-ms)

P2(可分阶段叠加)

  • RNG 并行化(独立 ~5ms 收益,对所有 task 有效)
  • Async pipeline(`step t+1 physics ‖ step t update_state`,乘性 ~2x)

不做

  • `ThreadPoolExecutor + numpy shards`(Xeon 无效,Mac/Ryzen 上限低,无法组合 Numba)
  • 对 distribute/aggregate 做优化(<2% 占比)
  • 全进程 `numactl -N 0` pin(伤 Physics)

预期收益(Xeon 8568Y+ / 8192 envs / motrix)

阶段 动作 motion_tracking overhead walk_flat overhead motion_tracking throughput
现状 60.3 ms 27.9 ms 84k env/s
P0 Numba prange `update_state` 17.7 ms (-71%) 12.7 ms (-54%) ~145k env/s (+72%)
P0+P1a motion_tracking row-wise body-pose ~15 ms ~155k env/s
P0+P1b walk_flat sparse `set_state` ~5-7 ms walk_flat +20-30%

CPU% 预期从当前 13-77% 提到 60-80% 区间(依 task 和 backend 不同)。P0 完成后 Physics 可能成为新瓶颈,届时再评估。

探针复现

  • 代码:benchmark/benchmark_env_overhead_parallel.py(branch: `bench/env-overhead-parallel-probe`,812 行,无 UniLab 依赖)
  • 运行:`uv run --with numpy --with numba python benchmark/benchmark_env_overhead_parallel.py`
  • 大 batch:`PROBE_NUM_ENVS=32768 uv run --with numpy --with numba python benchmark/benchmark_env_overhead_parallel.py`
  • Xeon 单 socket 测试:`PROBE_NUM_ENVS=32768 numactl -N 0 --membind 0 uv run --with numpy --with numba python benchmark/benchmark_env_overhead_parallel.py`

Refs: #663

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions