From 3e926d0b1f727a7ad49e1020bd42700d9bb7257a Mon Sep 17 00:00:00 2001 From: zilch <147668916@qq.com> Date: Tue, 22 Sep 2026 17:46:40 +0800 Subject: [PATCH 01/12] perf(console,fastsac,numba): env_step sub-stage timing + optional TBB layer Console timing panel now breaks env_step down into env-internal sub-stages (apply_action / physics / transition / reset / observation, with nested detail such as transition -> read and reset internals), and an optional TBB threading layer removes an OpenMP wakeup storm on many-core machines. - TorchEnv: opt-in Perf instrumentation on step, mirroring ArrayEnv scope names - MotrixSim sim_data.execute: active_perf_scope("read") nests under the caller's stage (transition/reset) without touching concrete environments - async collector: enable the wrapped env's Perf, report sub-stage means as dotted paths; worker rebuilds them into the nested panel tree (folding scalar totals into nodes so either arrival order works) - motrix-env-core: pinned tbb extra + preload_tbb() (full-path RTLD_GLOBAL preload of /lib/libtbb.so.12) called in ManagerEnv.__init__, so numba auto-selects the TBB layer without LD_LIBRARY_PATH; silent no-op otherwise - workspace root exposes the tbb extra Measured on g1-wbt-dance @4096 envs (192-core): evaluate 8.4ms -> 1.0-1.3ms (OMP thread-wakeup storm, ~30 effective threads -> ~180), end-to-end throughput 132.5k -> 163k env-steps/s (+23%). On 32-core machines the two layers are equivalent (1.49 vs 1.52ms). --- motrix_env_core/pyproject.toml | 7 +++ .../src/motrix_env_core/numba/manager/env.py | 7 +++ .../src/motrix_env_core/numba/threading.py | 49 +++++++++++++++++ motrix_env_core/tests/test_numba_threading.py | 10 ++++ .../src/motrix_env_motrixsim/sim_data.py | 7 ++- .../src/motrix_env_motrixsim/torch_env.py | 23 ++++++-- .../motrix_rl/fastsac/async_impl/collector.py | 23 ++++++++ .../motrix_rl/fastsac/async_impl/worker.py | 47 +++++++++++++--- motrix_rl/tests/test_console.py | 16 +++++- motrix_rl/tests/test_fastsac_collector.py | 55 +++++++++++++++++++ motrix_rl/tests/test_fastsac_learner.py | 22 ++++++++ pyproject.toml | 6 ++ uv.lock | 24 +++++++- 13 files changed, 280 insertions(+), 16 deletions(-) create mode 100644 motrix_env_core/src/motrix_env_core/numba/threading.py create mode 100644 motrix_env_core/tests/test_numba_threading.py diff --git a/motrix_env_core/pyproject.toml b/motrix_env_core/pyproject.toml index ecbb959a..8cf18212 100644 --- a/motrix_env_core/pyproject.toml +++ b/motrix_env_core/pyproject.toml @@ -21,3 +21,10 @@ dependencies = [ "omegaconf>=2.3,<2.4", "typing-extensions>=4.1", ] + +[project.optional-dependencies] +# TBB threading layer for numba parallel kernels: on many-core machines the +# default OpenMP layer pays a per-region thread-wakeup storm that dominates +# short kernels. The wheel drops libtbb.so.12 into /lib, so the runtime +# preloads it by full path (see motrix_env_core.numba.threading). +tbb = ["tbb==2021.13.0"] diff --git a/motrix_env_core/src/motrix_env_core/numba/manager/env.py b/motrix_env_core/src/motrix_env_core/numba/manager/env.py index 6ab8d216..480368ad 100644 --- a/motrix_env_core/src/motrix_env_core/numba/manager/env.py +++ b/motrix_env_core/src/motrix_env_core/numba/manager/env.py @@ -52,6 +52,7 @@ TerminationTermCfg, ) from motrix_env_core.numba.program import NumbaTaskProgram +from motrix_env_core.numba.threading import preload_tbb from motrix_env_core.sim import ( ModelQuery, PhysicsReadProgram, @@ -343,6 +344,12 @@ def __init__(self, cfg: EnvCfgType, num_envs: int = 1, backend: str | None = Non """Construct a manager environment using the selected simulator backend.""" if not isinstance(cfg, ManagerBasedEnvCfg): raise TypeError(f"{type(cfg).__name__} must inherit ManagerBasedEnvCfg.") + # Prefer the TBB threading layer for the parallel step kernels when the + # tbb extra is installed: the default OpenMP layer pays a thread-wakeup + # storm per kernel call that dominates short kernels on many-core + # machines. Must happen before the first parallel kernel executes. + if preload_tbb(): + logger.info("Manager env %r: numba TBB threading layer available", type(self).__name__) super().__init__(cfg, num_envs) self._rand_seed = 1 if seed is None else seed from motrix_env_core.sim.registry import create_sim_backend, default_sim_backend_name diff --git a/motrix_env_core/src/motrix_env_core/numba/threading.py b/motrix_env_core/src/motrix_env_core/numba/threading.py new file mode 100644 index 00000000..56d2ce0e --- /dev/null +++ b/motrix_env_core/src/motrix_env_core/numba/threading.py @@ -0,0 +1,49 @@ +# Copyright Motphys Technology Co., Ltd. 2025, 2026 +# SPDX-License-Identifier: Apache-2.0 + +"""Runtime threading-layer helpers for numba parallel kernels. + +On many-core machines the default OpenMP layer pays a per-region thread-wakeup +storm (``OMP_WAIT_POLICY=PASSIVE`` parks every worker between calls, and each +``prange`` region must wake them all before any work starts). For short kernels +called every environment step this overhead dominates the kernel itself. The +TBB layer keeps a resident work-stealing pool and does not have this problem. + +The PyPI ``tbb`` wheel installs ``libtbb.so.12`` into ``/lib`` instead of +``site-packages``, so numba's plain ``CDLL("libtbb.so.12")`` lookup cannot find +it without ``LD_LIBRARY_PATH``. Preloading the library by full path with +``RTLD_GLOBAL`` registers its SONAME in the process, and numba's later lookup by +the same SONAME reuses the loaded handle. +""" + +from __future__ import annotations + +import ctypes +import glob +import sys +from pathlib import Path + +_TBB_SONAME = "libtbb.so.12" + + +def preload_tbb() -> bool: + """Preload ``libtbb.so.12`` from the active environment, if present. + + Returns ``True`` when the library was loaded (or was already loaded), which + lets numba select the TBB threading layer for parallel kernels. Returns + ``False`` when the ``tbb`` extra is not installed — callers keep the numba + default layer in that case. + """ + if not sys.platform.startswith("linux"): + return False + candidates = sorted(glob.glob(str(Path(sys.prefix) / "lib" / f"{_TBB_SONAME}*")), reverse=True) + for path in candidates: + try: + ctypes.CDLL(path, mode=ctypes.RTLD_GLOBAL) + except OSError: + continue + return True + return False + + +__all__ = ["preload_tbb"] diff --git a/motrix_env_core/tests/test_numba_threading.py b/motrix_env_core/tests/test_numba_threading.py new file mode 100644 index 00000000..4c2cd30b --- /dev/null +++ b/motrix_env_core/tests/test_numba_threading.py @@ -0,0 +1,10 @@ +# Copyright Motphys Technology Co., Ltd. 2025, 2026 +# SPDX-License-Identifier: Apache-2.0 + +from motrix_env_core.numba.threading import preload_tbb + + +def test_preload_tbb_returns_bool_without_raising() -> None: + # Whether the library exists depends on the tbb extra; the contract is + # only that discovery is side-effect-safe and reports success as bool. + assert isinstance(preload_tbb(), bool) diff --git a/motrix_env_motrixsim/src/motrix_env_motrixsim/sim_data.py b/motrix_env_motrixsim/src/motrix_env_motrixsim/sim_data.py index e7c3b1ab..54e60b20 100644 --- a/motrix_env_motrixsim/src/motrix_env_motrixsim/sim_data.py +++ b/motrix_env_motrixsim/src/motrix_env_motrixsim/sim_data.py @@ -11,6 +11,7 @@ import numpy as np import numpy.typing as npt +from motrix_env_core.perf import active_perf_scope from motrix_env_core.sim import PhysicsReadProgram, SimDataQuery, SimDataQueryCompiler FloatArray: TypeAlias = npt.NDArray[np.float32] @@ -370,8 +371,10 @@ def execute(self, env_ids: npt.NDArray[np.int64] | None = None) -> None: if env_ids is not None and (env_ids.dtype != np.int64 or env_ids.ndim != 1): raise TypeError("Partial simulator read env_ids must be a one-dimensional int64 ndarray.") # One native call writes the authoritative arena — the full batch, or - # only the rows selected by env_ids. - self._read.program.execute(self._source, env_ids=env_ids) + # only the rows selected by env_ids. The perf scope nests under the + # caller's stage (e.g. transition/reset in the console timing tree). + with active_perf_scope("read"): + self._read.program.execute(self._source, env_ids=env_ids) def compile_read_program( diff --git a/motrix_env_motrixsim/src/motrix_env_motrixsim/torch_env.py b/motrix_env_motrixsim/src/motrix_env_motrixsim/torch_env.py index 29094a1e..2e05c504 100644 --- a/motrix_env_motrixsim/src/motrix_env_motrixsim/torch_env.py +++ b/motrix_env_motrixsim/src/motrix_env_motrixsim/torch_env.py @@ -12,6 +12,7 @@ from gymnasium import Space, spaces from motrix_env_core.base import ABEnv, EnvCfg, ObsSpace +from motrix_env_core.perf import Perf, perf_root from motrix_env_core.sim.backend import ( RenderConfig, SimRenderer, @@ -126,6 +127,7 @@ def __init__( raise ValueError("EnvCfg.scene must be configured") self._model = MotrixSimSceneCompiler().compile(cfg.scene, cfg.sim) self._render_spacing = cfg.render_spacing + self.perf = Perf() @property def model(self) -> mtx.SceneModel: @@ -298,18 +300,29 @@ def _prev_physics_step(self): state.terminated.zero_() state.truncated.zero_() + @perf_root("step") def step(self, actions: torch.Tensor) -> TorchEnvState: + """Advance one control step. + + Timing: ``apply_action`` -> ``physics`` -> ``transition`` (state update) + -> ``reset(env_ids)`` for done rows, mirroring ArrayEnv's scope names so + the console panel renders the same env_step sub-stage tree. + """ if self._state is None: self.init_state() assert self._state is not None self._prev_physics_step() - self._state = self.apply_action(actions, self._state) - assert self._state is not None, "apply_action must return a valid TorchEnvState" - self.physics_step() - self._state = self.update_state(self._state) + with self.perf.scope("apply_action"): + self._state = self.apply_action(actions, self._state) + assert self._state is not None, "apply_action must return a valid TorchEnvState" + with self.perf.scope("physics"): + self.physics_step() + with self.perf.scope("transition"): + self._state = self.update_state(self._state) self._state = self._state.replace(obs=_as_obs(self._state.obs)) self._state.episode_steps += 1 self._update_truncate() - self._reset_done_envs() + with self.perf.scope("reset"): + self._reset_done_envs() return self._state diff --git a/motrix_rl/src/motrix_rl/fastsac/async_impl/collector.py b/motrix_rl/src/motrix_rl/fastsac/async_impl/collector.py index 2529d448..a080677d 100644 --- a/motrix_rl/src/motrix_rl/fastsac/async_impl/collector.py +++ b/motrix_rl/src/motrix_rl/fastsac/async_impl/collector.py @@ -95,6 +95,12 @@ def __init__( self.weights = weights self.control = control self.is_resume = is_resume + # Env-internal step profiling (Perf on the wrapped env, when present) + # feeds the panel's env_step sub-stage tree. + inner_env = getattr(env, "env", env) + self._env_perf = getattr(inner_env, "perf", None) + if self._env_perf is not None: + self._env_perf.enable() self._learning_starts = acfg.learning_starts self.actor = Actor( @@ -338,6 +344,23 @@ def snapshot_stats(self) -> dict: "sync_actor_load": self._sync_actor_load_t * 1000.0 / max(self._collect_n, 1), }, } + env_perf = self._env_perf + if env_perf is not None: + # Per-call mean of each env-internal step sub-stage (ms). Keys are + # dotted paths (``env_step_[.]``) so nested stages stay + # unambiguous; the panel rebuilds the tree under env_step. + def emit(node, path: tuple[str, ...]) -> None: + for child in node.children: + stats["timing_ms"]["env_step_" + ".".join((*path, child.name))] = ( + child.total_ns / 1e6 / max(child.count, 1) + ) + emit(child, (*path, child.name)) + + for root in env_perf.snapshot(): + if root.name == "step": + emit(root, ()) + break + env_perf.reset() self.term_accum, self.term_count = {}, 0 self._collect_t = 0.0 self._sample_actions_t = 0.0 diff --git a/motrix_rl/src/motrix_rl/fastsac/async_impl/worker.py b/motrix_rl/src/motrix_rl/fastsac/async_impl/worker.py index 4fcbd120..10cf3496 100644 --- a/motrix_rl/src/motrix_rl/fastsac/async_impl/worker.py +++ b/motrix_rl/src/motrix_rl/fastsac/async_impl/worker.py @@ -58,6 +58,27 @@ def _timing_mean(values: list[float]) -> float: return sum(values) / len(values) +def _nest_timing_path(tree: dict[str, Any], parts: tuple[str, ...], value: float) -> None: + """Insert one dotted timing path into a nested mapping. + + A stage's scalar total and its sub-stage paths may arrive in either order + (the collector emits parents before children); when both exist the scalar + becomes the node's ``total`` alongside its children. + """ + head, rest = parts[0], parts[1:] + node = tree.get(head) + if not rest: + if isinstance(node, dict): + node["total"] = value + else: + tree[head] = value + else: + if not isinstance(node, dict): + node = {"total": node} if node is not None else {} + tree[head] = node + _nest_timing_path(node, rest, value) + + # ------------------------------------------------------------------ builders def set_seed(seed: int | None) -> None: if seed is None: @@ -503,17 +524,29 @@ def _drain_stats(): key: value for key, value in collector_timing_ms.items() if key != "collect" } # Panel tree is per-process; the headline collect/learn means - # live on TrainingPanelStats, sub-stages nest under "sync" / - # "update" branches. + # live on TrainingPanelStats, sub-stages nest under "env_step" / + # "sync" / "update" branches while keeping their flat position. + # env_step children arrive as dotted paths (``stage.sub``) and + # rebuild into a nested mapping (e.g. transition -> read). collector_items: dict[str, Any] = {} - sync_items: dict[str, float] = {} + env_step_children: dict[str, Any] = {} + for key, value in collector_timing_detail_ms.items(): + if key.startswith("env_step_"): + _nest_timing_path(env_step_children, tuple(key[len("env_step_") :].split(".")), value) + sync_children = { + key[len("sync_") :]: value + for key, value in collector_timing_detail_ms.items() + if key.startswith("sync_") + } for key, value in collector_timing_detail_ms.items(): - if key.startswith("sync_"): - sync_items[key[len("sync_") :]] = value + if key == "env_step": + collector_items["env_step"] = {"total": value, **env_step_children} + elif key == "sync": + collector_items["sync"] = {"total": value, **sync_children} + elif key.startswith("env_step_") or key.startswith("sync_"): + continue else: collector_items[key] = value - if sync_items: - collector_items["sync"] = {"total": collector_items.pop("sync", 0.0), **sync_items} timing_groups = {"collector": collector_items} learner_items: dict[str, Any] = {} drain_ms = _timing_mean(learner_drain_samples_ms) if learner_drain_samples_ms else 0.0 diff --git a/motrix_rl/tests/test_console.py b/motrix_rl/tests/test_console.py index aef5a6a5..8c75576b 100644 --- a/motrix_rl/tests/test_console.py +++ b/motrix_rl/tests/test_console.py @@ -199,7 +199,15 @@ def test_render_training_panel_overview_keeps_timing_tree_hidden() -> None: def test_render_training_panel_detail_view_shows_timing_tree_with_shares() -> None: stats = _panel_stats( timing_groups={ - "collector": {"env_step": 1.0, "sync": {"total": 0.5, "weights": 0.25}}, + "collector": { + "env_step": { + "total": 18.0, + "apply_action": 1.0, + "physics": {"total": 15.0, "read": 12.0}, + "reset": 2.0, + }, + "sync": {"total": 0.5, "weights": 0.25}, + }, "learner": {"update": {"total": 3.0, "critic": 2.0}}, }, timing_metrics={"queue_depth": 1.0}, @@ -216,6 +224,12 @@ def test_render_training_panel_detail_view_shows_timing_tree_with_shares() -> No assert "env_step" in detail assert "Timing detail" in detail assert "Diagnostics" in detail + # env_step sub-stages render as an indented subtree under their total, + # with second-level stages (physics -> read) nested one level deeper + assert "apply_action" in detail + assert "physics" in detail + assert "read" in detail + assert "reset" in detail # known group totals render a share column assert "%" in detail diff --git a/motrix_rl/tests/test_fastsac_collector.py b/motrix_rl/tests/test_fastsac_collector.py index 1c5a7b89..4192a264 100644 --- a/motrix_rl/tests/test_fastsac_collector.py +++ b/motrix_rl/tests/test_fastsac_collector.py @@ -8,6 +8,7 @@ import pytest import torch +from motrix_env_core.perf import Perf from motrix_rl.fastsac.async_impl.collector import Collector, resolve_collector_inference_device from motrix_rl.fastsac.async_impl.shm import Control, SharedTransitionRing from motrix_rl.fastsac.async_impl.shm.weight_channel import HostWeightReceiver, HostWeightSender, WeightChannelShared @@ -41,6 +42,23 @@ def step(self, actions): ) +class _PerfEnv(_CpuEnv): + """Env exposing the core Perf contract the collector's panel hooks into.""" + + def __init__(self): + super().__init__() + self.perf = Perf() + + def step(self, actions): + with self.perf.scope("step"): + with self.perf.scope("apply_action"): + pass + with self.perf.scope("physics"): + with self.perf.scope("read"): + result = super().step(actions) + return result + + def _cfg(device: str, *, compile: bool = False, amp: bool = False): return SimpleNamespace( agent=SimpleNamespace( @@ -191,6 +209,43 @@ def test_collector_explicit_cpu_placement_and_timing() -> None: assert collector.ring.critic_obs.device.type == "cpu" +def test_collector_reports_env_step_substage_timing() -> None: + env = _PerfEnv() + cfg = _cfg("cpu") + ring = SharedTransitionRing(2, _NUM_ENVS, _OBS_DIM, _CRITIC_OBS_DIM, _ACT_DIM) + source_actor, source_normalizer = _source_policy() + shared = WeightChannelShared(_OBS_DIM) + weight_tx = HostWeightSender(shared, sum(p.numel() for p in source_actor.parameters())) + weight_rx = HostWeightReceiver(shared, weight_tx.params) + weight_tx.publish(source_actor, source_normalizer) + collector = Collector( + env, + cfg, + _OBS_DIM, + _CRITIC_OBS_DIM, + _ACT_DIM, + source_actor.action_scale, + source_actor.action_bias, + ring, + weight_rx, + Control(), + ) + collector.reset() + collector.sync_weights() + + assert collector.step_once() + stats = collector.snapshot_stats() + + assert "env_step" in stats["timing_ms"] + assert "env_step_apply_action" in stats["timing_ms"] + assert "env_step_physics" in stats["timing_ms"] + # nested sub-stages arrive as dotted paths for the panel's tree rebuild + assert "env_step_physics.read" in stats["timing_ms"] + assert stats["timing_ms"]["env_step_apply_action"] >= 0.0 + # sub-stage aggregation is windowed like the other timings + assert env.perf.snapshot() == () + + def test_collector_cuda_request_fails_without_cuda(monkeypatch) -> None: monkeypatch.setattr(torch.cuda, "is_available", lambda: False) diff --git a/motrix_rl/tests/test_fastsac_learner.py b/motrix_rl/tests/test_fastsac_learner.py index be4e8b12..5155948c 100644 --- a/motrix_rl/tests/test_fastsac_learner.py +++ b/motrix_rl/tests/test_fastsac_learner.py @@ -71,3 +71,25 @@ def test_own_leaves_non_tensors_alone() -> None: from motrix_rl.fastsac.agent import _own assert _own((1, "a", None)) == (1, "a", None) + + +def test_nest_timing_path_merges_scalar_total_with_children_in_any_order() -> None: + from motrix_rl.fastsac.async_impl.worker import _nest_timing_path + + # the collector emits a stage's scalar before its dotted sub-stages; the + # rebuild must fold it into a "total" instead of nesting under a float + tree: dict = {} + _nest_timing_path(tree, ("physics",), 15.0) + _nest_timing_path(tree, ("physics", "read"), 12.0) + assert tree == {"physics": {"total": 15.0, "read": 12.0}} + + # reverse order must converge to the same tree + tree = {} + _nest_timing_path(tree, ("physics", "read"), 12.0) + _nest_timing_path(tree, ("physics",), 15.0) + assert tree == {"physics": {"total": 15.0, "read": 12.0}} + + # stages without sub-stages stay scalar + tree = {} + _nest_timing_path(tree, ("apply_action",), 1.5) + assert tree == {"apply_action": 1.5} diff --git a/pyproject.toml b/pyproject.toml index 8e7e0416..dcf598f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,12 @@ cuda = [ "torchvision==0.22.0; sys_platform == 'linux' or sys_platform == 'win32'", "torchaudio==2.7.0; sys_platform == 'linux' or sys_platform == 'win32'", ] +# TBB threading layer for numba parallel manager kernels on many-core +# machines (see motrix-env-core's tbb extra); optional, combines with any +# GPU profile. +tbb = [ + "motrix-env-core[tbb]", +] # ROCm wheels only exist for Linux x86_64; the markers keep other platforms # from resolving these requirements against the default (PyPI) index. rocm = [ diff --git a/uv.lock b/uv.lock index 0028fd9d..b8683223 100644 --- a/uv.lock +++ b/uv.lock @@ -997,6 +997,11 @@ dependencies = [ { name = "typing-extensions" }, ] +[package.optional-dependencies] +tbb = [ + { name = "tbb" }, +] + [package.metadata] requires-dist = [ { name = "array-api-compat", specifier = "==1.15.0" }, @@ -1007,8 +1012,10 @@ requires-dist = [ { name = "numpy", specifier = ">=1.26" }, { name = "omegaconf", specifier = ">=2.3,<2.4" }, { name = "scipy", specifier = "==1.15.3" }, + { name = "tbb", marker = "extra == 'tbb'", specifier = "==2021.13.0" }, { name = "typing-extensions", specifier = ">=4.1" }, ] +provides-extras = ["tbb"] [[package]] name = "motrix-env-motrixsim" @@ -1111,6 +1118,9 @@ rocm = [ { name = "torch", version = "2.11.0+rocm7.2", source = { registry = "https://download.pytorch.org/whl/rocm7.2" }, marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "triton-rocm", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, ] +tbb = [ + { name = "motrix-env-core", extra = ["tbb"] }, +] unitree = [ { name = "motrix-deploy-unitree" }, ] @@ -1139,6 +1149,7 @@ requires-dist = [ { name = "motrix-deploy-mujoco", editable = "motrix_deploy_mujoco" }, { name = "motrix-deploy-tasks", editable = "motrix_deploy_tasks" }, { name = "motrix-deploy-unitree", marker = "extra == 'unitree'", editable = "motrix_deploy_unitree" }, + { name = "motrix-env-core", extras = ["tbb"], marker = "extra == 'tbb'", editable = "motrix_env_core" }, { name = "motrix-env-motrixsim", editable = "motrix_env_motrixsim" }, { name = "motrix-env-mujoco", editable = "motrix_env_mujoco" }, { name = "motrix-envs", editable = "motrix_envs" }, @@ -1158,7 +1169,7 @@ requires-dist = [ { name = "torchvision", marker = "(sys_platform == 'linux' and extra == 'cuda') or (sys_platform == 'win32' and extra == 'cuda')", specifier = "==0.22.0", index = "https://download.pytorch.org/whl/cu128" }, { name = "triton-rocm", marker = "platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'rocm'", specifier = "==3.6.0", index = "https://download.pytorch.org/whl/rocm7.2" }, ] -provides-extras = ["unitree", "docs", "cuda", "rocm"] +provides-extras = ["unitree", "docs", "cuda", "tbb", "rocm"] [package.metadata.requires-dev] dev = [ @@ -2453,6 +2464,17 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, ] +[[package]] +name = "tbb" +version = "2021.13.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/32/9c88009a9feb81f0525bf1c4729a7b39a4523d626a40682c792c6a53e323/tbb-2021.13.0-py2.py3-none-manylinux1_i686.whl", hash = "sha256:a2567725329639519d46d92a2634cf61e76601dac2f777a05686fea546c4fe4f", size = 5205096, upload-time = "2024-06-20T15:34:29.818Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b8/994132d9f9493d7b220637f614f3f87727e405b851d96aaffc7d43c2b298/tbb-2021.13.0-py2.py3-none-manylinux1_x86_64.whl", hash = "sha256:aaf667e92849adb012b8874d6393282afc318aca4407fc62f912ee30a22da46a", size = 5386610, upload-time = "2024-06-20T15:35:11.596Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ce/1dd6f2988d06d850374be2c56350d17399d4faaed53b489048f12a3d3474/tbb-2021.13.0-py3-none-win32.whl", hash = "sha256:6669d26703e9943f6164c6407bd4a237a45007e79b8d3832fe6999576eaaa9ef", size = 248996, upload-time = "2024-06-20T15:38:31.467Z" }, + { url = "https://files.pythonhosted.org/packages/f1/24/500811330b3b070e5995c3275181dbcd00c06cef26c6ebfe6ee1ca9b6223/tbb-2021.13.0-py3-none-win_amd64.whl", hash = "sha256:3528a53e4bbe64b07a6112b4c5a00ff3c61924ee46c9c68e004a1ac7ad1f09c3", size = 286910, upload-time = "2024-06-20T15:38:18.376Z" }, +] + [[package]] name = "tensorboard" version = "2.20.0" From acff7c208f15cb0ba9e334b64296333a23083fde Mon Sep 17 00:00:00 2001 From: zilch <147668916@qq.com> Date: Tue, 22 Sep 2026 17:48:31 +0800 Subject: [PATCH 02/12] perf: add --tbb option to install.sh --- install.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/install.sh b/install.sh index 72efd0fb..6d7f6366 100755 --- a/install.sh +++ b/install.sh @@ -11,6 +11,8 @@ # # Training backends are enabled with --skrl-torch (default), --skrl-jax or # --rslrl, named after the extras that install them; multiple flags combine. +# --tbb installs Intel TBB so numba parallel kernels use its threading layer +# (avoids the OpenMP per-region thread-wakeup storm on many-core hosts). set -e @@ -25,6 +27,7 @@ usage() { echo " --skrl-torch Training backend: SKRL on PyTorch (default)" echo " --skrl-jax Training backend: SKRL on JAX (Linux only)" echo " --rslrl Training backend: RSL-RL on PyTorch" + echo " --tbb Install Intel TBB for numba parallel kernels (recommended on many-core hosts)" echo " -h, --help Show this help" } @@ -34,6 +37,7 @@ SKRL_TORCH="" SKRL_JAX="" RSLRL="" DOCS="" +TBB="" while [ $# -gt 0 ]; do case "$1" in @@ -57,6 +61,9 @@ while [ $# -gt 0 ]; do --rslrl) RSLRL=1 ;; + --tbb) + TBB=1 + ;; -h|--help) usage exit 0 @@ -162,6 +169,7 @@ else [ -n "$SKRL_JAX" ] && EXTRAS="$EXTRAS --extra skrl-jax" [ -n "$RSLRL" ] && EXTRAS="$EXTRAS --extra rslrl" [ -n "$DOCS" ] && EXTRAS="$EXTRAS --extra docs" + [ -n "$TBB" ] && EXTRAS="$EXTRAS --extra tbb" set -x uv sync --all-packages --no-default-groups --extra "$GPU"$EXTRAS fi From 87344d1e2dda7f6e8ba86a62b00c8ddcfb163360 Mon Sep 17 00:00:00 2001 From: zilch <147668916@qq.com> Date: Tue, 22 Sep 2026 18:02:09 +0800 Subject: [PATCH 03/12] refactor(fastsac): require the wrapper .env contract for the perf probe FastSacEnvWrap.env always exposes the original environment, so read it directly; only the optional Perf capability stays probed. The collector test fake now implements the .env contract instead of the production code silently tolerating its absence. --- motrix_rl/src/motrix_rl/fastsac/async_impl/collector.py | 6 +++--- motrix_rl/tests/test_fastsac_collector.py | 6 ++++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/motrix_rl/src/motrix_rl/fastsac/async_impl/collector.py b/motrix_rl/src/motrix_rl/fastsac/async_impl/collector.py index a080677d..c41e2f13 100644 --- a/motrix_rl/src/motrix_rl/fastsac/async_impl/collector.py +++ b/motrix_rl/src/motrix_rl/fastsac/async_impl/collector.py @@ -96,9 +96,9 @@ def __init__( self.control = control self.is_resume = is_resume # Env-internal step profiling (Perf on the wrapped env, when present) - # feeds the panel's env_step sub-stage tree. - inner_env = getattr(env, "env", env) - self._env_perf = getattr(inner_env, "perf", None) + # feeds the panel's env_step sub-stage tree. ``env.env`` is the + # FastSacEnvWrap contract for reaching the original environment. + self._env_perf = getattr(env.env, "perf", None) if self._env_perf is not None: self._env_perf.enable() self._learning_starts = acfg.learning_starts diff --git a/motrix_rl/tests/test_fastsac_collector.py b/motrix_rl/tests/test_fastsac_collector.py index 4192a264..817f82d1 100644 --- a/motrix_rl/tests/test_fastsac_collector.py +++ b/motrix_rl/tests/test_fastsac_collector.py @@ -22,11 +22,17 @@ class _CpuEnv: + """Wrapper stand-in: the collector reads the original env via ``.env``.""" + def __init__(self): self.num_envs = _NUM_ENVS self.last_info = {} self.last_actions = None + @property + def env(self): + return self + def reset(self): return torch.zeros(_NUM_ENVS, _OBS_DIM), torch.zeros(_NUM_ENVS, _CRITIC_OBS_DIM) From a1927ce916f9b0f8768659e43a20e73d814e9327 Mon Sep 17 00:00:00 2001 From: zilch <147668916@qq.com> Date: Tue, 22 Sep 2026 18:27:30 +0800 Subject: [PATCH 04/12] perf(console,perf): run-progress time display, windowed perf helper, honest elapsed anchor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - console: Run progress card now shows elapsed and an ETA estimate (from the cumulative iteration rate); the plain-text header gains the same eta - Perf.stage_mean_ms(root): per-call mean ms of every sub-scope under a root, keyed by dotted path — the tree-flattening moves next to the data structure and the collector only adds its env_step_ prefix - async learner: anchor elapsed/ETA (and the first rate window) at the first ingested collector batch instead of learner readiness — the collector's env build (scene compile, numba JIT) can outlast the learner build by tens of seconds, which previously showed up as a large elapsed on the first panel --- motrix_env_core/src/motrix_env_core/perf.py | 21 +++++++++ motrix_env_core/tests/test_perf.py | 26 +++++++++++ motrix_rl/src/motrix_rl/console.py | 44 ++++++++++++++++--- .../motrix_rl/fastsac/async_impl/collector.py | 18 ++------ .../motrix_rl/fastsac/async_impl/worker.py | 10 +++++ motrix_rl/tests/test_console.py | 3 ++ 6 files changed, 102 insertions(+), 20 deletions(-) diff --git a/motrix_env_core/src/motrix_env_core/perf.py b/motrix_env_core/src/motrix_env_core/perf.py index 610fd400..4f581ada 100644 --- a/motrix_env_core/src/motrix_env_core/perf.py +++ b/motrix_env_core/src/motrix_env_core/perf.py @@ -187,6 +187,27 @@ def snapshot(self) -> tuple[PerfNode, ...]: raise RuntimeError("Cannot snapshot Perf while a scope is active.") return tuple(root.snapshot() for root in self._roots.values()) + def stage_mean_ms(self, root: str) -> dict[str, float]: + """Per-call mean milliseconds of every sub-scope under ``root``. + + Keys are dotted paths relative to the root's children (``stage`` or + ``stage.sub``), so nested scope names — which may themselves contain + underscores — stay unambiguously separable. Returns ``{}`` when the + root has not run. + """ + for node in self._roots.values(): + if node.name == root: + means: dict[str, float] = {} + + def emit(subtree: _MutablePerfNode, path: tuple[str, ...]) -> None: + for child in subtree.children.values(): + means[".".join((*path, child.name))] = child.total_ns / 1e6 / max(child.count, 1) + emit(child, (*path, child.name)) + + emit(node, ()) + return means + return {} + def call(self, name: str, function: Callable[_P, _R], *args: _P.args, **kwargs: _P.kwargs) -> _R: """Call a function inside a named scope.""" if not self._enabled: diff --git a/motrix_env_core/tests/test_perf.py b/motrix_env_core/tests/test_perf.py index 16352db7..b647eb60 100644 --- a/motrix_env_core/tests/test_perf.py +++ b/motrix_env_core/tests/test_perf.py @@ -139,3 +139,29 @@ def test_active_perf_scope_contributes_to_activated_profiler() -> None: def test_active_perf_scope_is_disabled_without_an_activated_profiler() -> None: with active_perf_scope("backend"): pass + + +def test_stage_mean_ms_flattens_nested_scopes_to_dotted_paths() -> None: + # two step calls; each scope entry/exit consumes one clock tick (8 per + # iteration). physics runs 3ms per call, transition 9ms with a 2ms read. + base = (0, 2_000_000, 5_000_000, 6_000_000, 10_000_000, 12_000_000, 15_000_000, 26_000_000) + ticks = base + tuple(t + 26_000_000 for t in base) + perf = Perf(enabled=True, clock=_Clock(*ticks)) + for _ in range(2): + with perf.scope("step"): + with perf.scope("physics"): + pass + with perf.scope("transition"): + with perf.scope("read"): + pass + + means = perf.stage_mean_ms("step") + + assert means["physics"] == pytest.approx(3.0) + assert means["transition"] == pytest.approx(9.0) + assert means["transition.read"] == pytest.approx(2.0) + assert "step" not in means # the root itself is not part of the paths + + +def test_stage_mean_ms_returns_empty_for_unrun_root() -> None: + assert Perf(enabled=True).stage_mean_ms("step") == {} diff --git a/motrix_rl/src/motrix_rl/console.py b/motrix_rl/src/motrix_rl/console.py index 2355feb6..3957671e 100644 --- a/motrix_rl/src/motrix_rl/console.py +++ b/motrix_rl/src/motrix_rl/console.py @@ -169,13 +169,27 @@ def _format_metric_items(items: Mapping[str, Any], *, precision: int = 3, signed return [f"{k} {_format_value(v, precision=precision, signed=signed)}" for k, v in items.items()] +def _format_duration(seconds: float) -> str: + """Compact elapsed/remaining time: ``3h05m``, ``12m07s`` or ``45s``.""" + t = int(seconds) + h, m, sec = t // 3600, (t % 3600) // 60, t % 60 + return f"{h}h{m:02d}m" if h else f"{m}m{sec:02d}s" + + +def _eta_seconds(stats: TrainingPanelStats) -> float | None: + """Remaining-time estimate from the cumulative iteration rate.""" + if stats.iteration <= 0 or stats.elapsed_seconds <= 0.0: + return None + rate = stats.iteration / stats.elapsed_seconds + if rate <= 0.0: + return None + return (stats.total_iterations - stats.iteration) / rate + + def format_training_panel(stats: TrainingPanelStats, *, title: str = "rl") -> str: """Render a plain-text RL training panel from backend-provided scalar stats.""" - def hms(t: float) -> str: - t = int(t) - h, m, sec = t // 3600, (t % 3600) // 60, t % 60 - return f"{h}h{m:02d}m" if h else f"{m}m{sec:02d}s" + hms = _format_duration def si(n: float) -> str: for unit in ("", "k", "M"): @@ -186,9 +200,11 @@ def si(n: float) -> str: width = 84 pct = 100.0 * stats.iteration / max(stats.total_iterations, 1) + eta = _eta_seconds(stats) + eta_text = f" - eta ~{hms(eta)}" if eta is not None else "" header = ( f" {title} - iter {stats.iteration}/{stats.total_iterations} ({pct:.1f}%) - " - f"{stats.steps_per_second:.0f} env-steps/s - {hms(stats.elapsed_seconds)}" + f"{stats.steps_per_second:.0f} env-steps/s - {hms(stats.elapsed_seconds)}{eta_text}" ) lines = [ "-" * width, @@ -478,6 +494,18 @@ def _format_memory(memory: MemoryUsage | None) -> str: return f"{memory.used_bytes / gib:.1f}/{memory.total_bytes / gib:.1f} GiB" +def _run_progress_time_text(stats: TrainingPanelStats): + """One ``elapsed · ~remaining`` line for the Run progress card.""" + from rich.text import Text + + elapsed = _format_duration(stats.elapsed_seconds) + eta = _eta_seconds(stats) + line = Text(f"{elapsed} elapsed", style="dim") + if eta is not None: + line.append(f" · ~{_format_duration(eta)} left", style="cyan") + return line + + def render_training_panel(stats: TrainingPanelStats, *, title: str = "rl", detail: bool = False): if not _RICH: raise RuntimeError("rich is not available") @@ -537,7 +565,11 @@ def memory_style(memory: MemoryUsage | None) -> str: summary.add_row( card( f"Run progress ({progress * 100:.1f}%)", - Group(Text(f"{stats.iteration:,}/{stats.total_iterations:,} iters", style="white"), progress_row), + Group( + Text(f"{stats.iteration:,}/{stats.total_iterations:,} iters", style="white"), + progress_row, + _run_progress_time_text(stats), + ), ), card( "Episode stats", diff --git a/motrix_rl/src/motrix_rl/fastsac/async_impl/collector.py b/motrix_rl/src/motrix_rl/fastsac/async_impl/collector.py index c41e2f13..bba4a762 100644 --- a/motrix_rl/src/motrix_rl/fastsac/async_impl/collector.py +++ b/motrix_rl/src/motrix_rl/fastsac/async_impl/collector.py @@ -346,20 +346,10 @@ def snapshot_stats(self) -> dict: } env_perf = self._env_perf if env_perf is not None: - # Per-call mean of each env-internal step sub-stage (ms). Keys are - # dotted paths (``env_step_[.]``) so nested stages stay - # unambiguous; the panel rebuilds the tree under env_step. - def emit(node, path: tuple[str, ...]) -> None: - for child in node.children: - stats["timing_ms"]["env_step_" + ".".join((*path, child.name))] = ( - child.total_ns / 1e6 / max(child.count, 1) - ) - emit(child, (*path, child.name)) - - for root in env_perf.snapshot(): - if root.name == "step": - emit(root, ()) - break + # Env-internal step sub-stages as per-call means; dotted paths keep + # nested stages unambiguous and the panel rebuilds the tree from them. + for path, mean_ms in env_perf.stage_mean_ms("step").items(): + stats["timing_ms"][f"env_step_{path}"] = mean_ms env_perf.reset() self.term_accum, self.term_count = {}, 0 self._collect_t = 0.0 diff --git a/motrix_rl/src/motrix_rl/fastsac/async_impl/worker.py b/motrix_rl/src/motrix_rl/fastsac/async_impl/worker.py index 10cf3496..bffb01ef 100644 --- a/motrix_rl/src/motrix_rl/fastsac/async_impl/worker.py +++ b/motrix_rl/src/motrix_rl/fastsac/async_impl/worker.py @@ -429,7 +429,13 @@ def run_learner_process( learner = Learner(agent, cfg, ring, weight_tx, control) learner.publish_weights() # give the collector an initial policy before it warms up + # Elapsed/ETA anchor. The collector's env build (scene compile, numba + # JIT) runs concurrently with the learner build and can outlast it by + # tens of seconds; timing from this point would bill that startup wait + # to elapsed and the first window's rates. The anchor moves to the + # first ingested collector batch below. start_time = time.time() + start_anchored = False last_log_time = start_time resume_step = control.collector_steps last_log_step = resume_step @@ -470,6 +476,10 @@ def _drain_stats(): t_drain = time.perf_counter() ingested = learner.drain() if ingested: + if not start_anchored: + start_time = time.time() + last_log_time = start_time + start_anchored = True learner_drain_samples_ms.append((time.perf_counter() - t_drain) * 1000.0) t_l = time.perf_counter() metrics = learner.maybe_train(ingested) diff --git a/motrix_rl/tests/test_console.py b/motrix_rl/tests/test_console.py index 8c75576b..9ab33652 100644 --- a/motrix_rl/tests/test_console.py +++ b/motrix_rl/tests/test_console.py @@ -188,6 +188,9 @@ def test_render_training_panel_overview_keeps_timing_tree_hidden() -> None: assert "Run progress" in panel assert "Episode stats" in panel assert "Throughput" in panel + # run-progress card carries elapsed and remaining-time estimate + assert "elapsed" in panel + assert "left" in panel assert "System health" in panel assert "Training (" in panel assert "Environment metrics (" in panel From 943f503ceca8b87ffec6c5b6802fcd2430c67175 Mon Sep 17 00:00:00 2001 From: zilch <147668916@qq.com> Date: Tue, 22 Sep 2026 19:06:59 +0800 Subject: [PATCH 05/12] refactor(fastsac): unify nested timing keys as dotted paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sync_wait_writer & friends become sync.wait_writer, and env_step stages become env_step.physics.read — one protocol for all nesting. The worker now rebuilds the panel tree with a single rule (split on '.') and zero per-prefix special cases; the scalar-total folding already handles either arrival order. TensorBoard scalar names and the async-trainer design doc follow the same dotted form. --- bench/bench_learner_update.py | 87 +++++++++++++++++++ .../motrix_rl/fastsac/async_impl/collector.py | 13 +-- .../motrix_rl/fastsac/async_impl/worker.py | 26 ++---- motrix_rl/tests/test_fastsac_collector.py | 14 +-- .../fastsac-async-heterogeneous-trainer.md | 2 +- 5 files changed, 107 insertions(+), 35 deletions(-) create mode 100644 bench/bench_learner_update.py diff --git a/bench/bench_learner_update.py b/bench/bench_learner_update.py new file mode 100644 index 00000000..2692a19d --- /dev/null +++ b/bench/bench_learner_update.py @@ -0,0 +1,87 @@ +# Profile the FastSAC learner update: where does wall time go? +# Builds a real FastSacAgent, fills the replay buffer with random transitions, +# then times agent.update(n) and profiles it with torch.profiler to split +# wall time into CUDA kernel time vs host-side gaps. +import os +import sys +from types import SimpleNamespace + +os.environ.setdefault("OMP_WAIT_POLICY", "PASSIVE") +os.environ.setdefault("GOMP_SPINCOUNT", "0") + +import numpy as np +import torch +from omegaconf import OmegaConf + +import motrix_envs # noqa: F401 (unused; keeps import parity with training) +from motrix_env_core import registry +from motrix_rl.fastsac.async_impl.worker import build_agent +from motrix_rl.fastsac.config import FastSacAgentCfg + +num_envs = int(sys.argv[1]) if len(sys.argv) > 1 else 2048 +n_updates = int(sys.argv[2]) if len(sys.argv) > 2 else 4 +run_cfg = OmegaConf.load(sys.argv[3]) if len(sys.argv) > 3 else None +agent_cfg = FastSacAgentCfg(**OmegaConf.to_container(run_cfg.algo.agent)) if run_cfg else FastSacAgentCfg() + +env = registry.make("g1-wbt-dance", num_envs=num_envs) +obs_space = env.observation_space +obs_dim = obs_space.policy.shape[0] +critic_dim = obs_space.value_or_policy.shape[0] +act_dim = env.action_space.shape[0] +del env + +cfg = SimpleNamespace(agent=agent_cfg) + +device = torch.device("cuda") +agent = build_agent( + cfg, (obs_dim, critic_dim, act_dim), num_envs, device, + torch.ones(act_dim), torch.zeros(act_dim), +) +rng = np.random.default_rng(0) +cap = agent.rb.buffer_size +for _ in range(cap): + agent.rb.extend( + torch.randn(num_envs, obs_dim, device=device), + torch.randn(num_envs, critic_dim, device=device), + torch.rand(num_envs, act_dim, device=device) * 2 - 1, + torch.randn(num_envs, device=device), + torch.zeros(num_envs, dtype=torch.long, device=device), + torch.zeros(num_envs, dtype=torch.long, device=device), + ) + +# warmup (compile/cudagraph) then timed windows +for _ in range(3): + agent.update(n_updates) +torch.cuda.synchronize() + +import time + +for trial in range(3): + t0 = time.perf_counter() + agent.update(n_updates) + torch.cuda.synchronize() + wall_ms = (time.perf_counter() - t0) * 1e3 + print(f"update({n_updates}) wall: {wall_ms:8.2f} ms ({wall_ms/n_updates:.2f} ms/update)") + +from torch.profiler import ProfilerActivity, profile + +with profile(activities=[ProfilerActivity.CPU, ProfilerActivity.CUDA]) as prof: + agent.update(n_updates) + torch.cuda.synchronize() + +events = prof.key_averages() +total_cuda_ms = sum(getattr(e, "device_time_total", 0) for e in events) / 1e3 +total_cpu_ms = sum(getattr(e, "self_cpu_time_total", 0) for e in events) / 1e3 +print(f"\nprofiled update: cuda kernel time ~{total_cuda_ms:.2f} ms, host self time ~{total_cpu_ms:.2f} ms") +print(f"\ntop ops by CUDA time:") +events.sort(key=lambda e: getattr(e, "device_time_total", 0), reverse=True) +for e in events[:12]: + d = getattr(e, "device_time_total", 0) / 1e3 + c = e.count + if d > 0.01: + print(f" {e.key[:64]:<64} {d:8.2f} ms x{c}") +print(f"\ntop ops by host (self) time:") +events.sort(key=lambda e: e.self_cpu_time_total, reverse=True) +for e in events[:12]: + if e.self_cpu_time_total / 1e3 > 0.05: + print(f" {e.key[:64]:<64} {e.self_cpu_time_total/1e3:8.2f} ms x{e.count}") diff --git a/motrix_rl/src/motrix_rl/fastsac/async_impl/collector.py b/motrix_rl/src/motrix_rl/fastsac/async_impl/collector.py index bba4a762..96f8fdca 100644 --- a/motrix_rl/src/motrix_rl/fastsac/async_impl/collector.py +++ b/motrix_rl/src/motrix_rl/fastsac/async_impl/collector.py @@ -331,6 +331,9 @@ def snapshot_stats(self) -> dict: "env_metrics": dict(self.last_env_metrics), "policy_lag": self.policy_lag, # Average wall-clock milliseconds per successful env-step batch. + # Nested stages use dotted paths (``sync.wait_writer``, + # ``env_step.physics.read``): the panel rebuilds one tree from + # them with a single rule, so key prefixes never encode structure. "timing_ms": { "collect": self._collect_t * 1000.0 / max(self._collect_n, 1), "wait": self._wait_t * 1000.0 / max(self._collect_n, 1), @@ -339,17 +342,15 @@ def snapshot_stats(self) -> dict: "push": self._push_t * 1000.0 / max(self._collect_n, 1), "bookkeep": self._bookkeep_t * 1000.0 / max(self._collect_n, 1), "sync": self._sync_t * 1000.0 / max(self._collect_n, 1), - "sync_wait_writer": self._sync_wait_writer_t * 1000.0 / max(self._collect_n, 1), - "sync_host_snapshot": self._sync_host_snapshot_t * 1000.0 / max(self._collect_n, 1), - "sync_actor_load": self._sync_actor_load_t * 1000.0 / max(self._collect_n, 1), + "sync.wait_writer": self._sync_wait_writer_t * 1000.0 / max(self._collect_n, 1), + "sync.host_snapshot": self._sync_host_snapshot_t * 1000.0 / max(self._collect_n, 1), + "sync.actor_load": self._sync_actor_load_t * 1000.0 / max(self._collect_n, 1), }, } env_perf = self._env_perf if env_perf is not None: - # Env-internal step sub-stages as per-call means; dotted paths keep - # nested stages unambiguous and the panel rebuilds the tree from them. for path, mean_ms in env_perf.stage_mean_ms("step").items(): - stats["timing_ms"][f"env_step_{path}"] = mean_ms + stats["timing_ms"][f"env_step.{path}"] = mean_ms env_perf.reset() self.term_accum, self.term_count = {}, 0 self._collect_t = 0.0 diff --git a/motrix_rl/src/motrix_rl/fastsac/async_impl/worker.py b/motrix_rl/src/motrix_rl/fastsac/async_impl/worker.py index bffb01ef..1dbea998 100644 --- a/motrix_rl/src/motrix_rl/fastsac/async_impl/worker.py +++ b/motrix_rl/src/motrix_rl/fastsac/async_impl/worker.py @@ -534,29 +534,13 @@ def _drain_stats(): key: value for key, value in collector_timing_ms.items() if key != "collect" } # Panel tree is per-process; the headline collect/learn means - # live on TrainingPanelStats, sub-stages nest under "env_step" / - # "sync" / "update" branches while keeping their flat position. - # env_step children arrive as dotted paths (``stage.sub``) and - # rebuild into a nested mapping (e.g. transition -> read). + # live on TrainingPanelStats. Every timing key is either a flat + # stage name or a dotted path (``sync.wait_writer``, + # ``env_step.physics.read``); nesting is rebuilt with one rule, + # and a stage's own total folds into its node. collector_items: dict[str, Any] = {} - env_step_children: dict[str, Any] = {} for key, value in collector_timing_detail_ms.items(): - if key.startswith("env_step_"): - _nest_timing_path(env_step_children, tuple(key[len("env_step_") :].split(".")), value) - sync_children = { - key[len("sync_") :]: value - for key, value in collector_timing_detail_ms.items() - if key.startswith("sync_") - } - for key, value in collector_timing_detail_ms.items(): - if key == "env_step": - collector_items["env_step"] = {"total": value, **env_step_children} - elif key == "sync": - collector_items["sync"] = {"total": value, **sync_children} - elif key.startswith("env_step_") or key.startswith("sync_"): - continue - else: - collector_items[key] = value + _nest_timing_path(collector_items, tuple(key.split(".")), value) timing_groups = {"collector": collector_items} learner_items: dict[str, Any] = {} drain_ms = _timing_mean(learner_drain_samples_ms) if learner_drain_samples_ms else 0.0 diff --git a/motrix_rl/tests/test_fastsac_collector.py b/motrix_rl/tests/test_fastsac_collector.py index 817f82d1..c90a42b7 100644 --- a/motrix_rl/tests/test_fastsac_collector.py +++ b/motrix_rl/tests/test_fastsac_collector.py @@ -208,9 +208,9 @@ def test_collector_explicit_cpu_placement_and_timing() -> None: assert "collect" in stats["timing_ms"] assert "sample_actions" in stats["timing_ms"] assert "sync" in stats["timing_ms"] - assert "sync_wait_writer" in stats["timing_ms"] - assert "sync_host_snapshot" in stats["timing_ms"] - assert "sync_actor_load" in stats["timing_ms"] + assert "sync.wait_writer" in stats["timing_ms"] + assert "sync.host_snapshot" in stats["timing_ms"] + assert "sync.actor_load" in stats["timing_ms"] assert collector.ring.obs.device.type == "cpu" assert collector.ring.critic_obs.device.type == "cpu" @@ -243,11 +243,11 @@ def test_collector_reports_env_step_substage_timing() -> None: stats = collector.snapshot_stats() assert "env_step" in stats["timing_ms"] - assert "env_step_apply_action" in stats["timing_ms"] - assert "env_step_physics" in stats["timing_ms"] + assert "env_step.apply_action" in stats["timing_ms"] + assert "env_step.physics" in stats["timing_ms"] # nested sub-stages arrive as dotted paths for the panel's tree rebuild - assert "env_step_physics.read" in stats["timing_ms"] - assert stats["timing_ms"]["env_step_apply_action"] >= 0.0 + assert "env_step.physics.read" in stats["timing_ms"] + assert stats["timing_ms"]["env_step.apply_action"] >= 0.0 # sub-stage aggregation is windowed like the other timings assert env.perf.snapshot() == () diff --git a/wiki/design/fastsac-async-heterogeneous-trainer.md b/wiki/design/fastsac-async-heterogeneous-trainer.md index ecd80aee..991341ed 100644 --- a/wiki/design/fastsac-async-heterogeneous-trainer.md +++ b/wiki/design/fastsac-async-heterogeneous-trainer.md @@ -194,7 +194,7 @@ producer lifetime 和 compiled collector 固定参数地址,不能只把 H2D episode return / length、reward 分项、env metrics、collector timing 都发生在 collector(它才有 reward/done)。collector 按根级 `logging.interval` 把一份紧凑 `snapshot_stats()` 放进 `StatsQueue`(先清掉旧快照,保证 learner 总见最新);learner 在日志相位 drain 出来,喂给与同步版**完全复用**的 rich 训练面板。 -TensorBoard scalar 与同步版同名(`rollout/mean_return`、`rollout/mean_ep_len`、`perf/env_steps_per_s` 等),并新增异构专属:`async/policy_lag`、`async/ring_fill`、`async/weight_version`、`async/utd`,以及 collector 细分 timing `perf/collector_{sample_actions,env_step,push,bookkeep,sync,sync_wait_writer,sync_host_snapshot,sync_actor_load}_ms`、整体 `perf/collect_ms_per_batch`,learner 侧 `perf/learn_ms_per_update`、`perf/learn_pct`、`perf/updates_per_s`。 +TensorBoard scalar 与同步版同名(`rollout/mean_return`、`rollout/mean_ep_len`、`perf/env_steps_per_s` 等),并新增异构专属:`async/policy_lag`、`async/ring_fill`、`async/weight_version`、`async/utd`,以及 collector 细分 timing `perf/collector_{sample_actions,env_step,env_step.*,push,bookkeep,sync,sync.*}_ms(嵌套阶段为点分路径,如 `collector_env_step.physics.read_ms`)`、整体 `perf/collect_ms_per_batch`,learner 侧 `perf/learn_ms_per_update`、`perf/learn_pct`、`perf/updates_per_s`。 > 面板中 `collect_ms` / `learn_ms` / `learn_pct` 因两进程并发,**不像同步版那样相加为 100%**:`learn_pct` 表示 learner wall-clock 中真正用于更新(vs 空转/欠数据)的比例,≈100% 表示 GPU-bound,偏低表示 collector 喂不满 buffer。 From 8b37fb8238aae0f74805b8ef20388aef09f5cd0d Mon Sep 17 00:00:00 2001 From: zilch <147668916@qq.com> Date: Tue, 22 Sep 2026 19:09:04 +0800 Subject: [PATCH 06/12] fix(bench): lint the learner-update profiler script (E402/F541) --- bench/bench_learner_update.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/bench/bench_learner_update.py b/bench/bench_learner_update.py index 2692a19d..4f65f4dd 100644 --- a/bench/bench_learner_update.py +++ b/bench/bench_learner_update.py @@ -9,14 +9,14 @@ os.environ.setdefault("OMP_WAIT_POLICY", "PASSIVE") os.environ.setdefault("GOMP_SPINCOUNT", "0") -import numpy as np -import torch -from omegaconf import OmegaConf +import numpy as np # noqa: E402 (env vars above must be set first) +import torch # noqa: E402 +from omegaconf import OmegaConf # noqa: E402 import motrix_envs # noqa: F401 (unused; keeps import parity with training) -from motrix_env_core import registry -from motrix_rl.fastsac.async_impl.worker import build_agent -from motrix_rl.fastsac.config import FastSacAgentCfg +from motrix_env_core import registry # noqa: E402 +from motrix_rl.fastsac.async_impl.worker import build_agent # noqa: E402 +from motrix_rl.fastsac.config import FastSacAgentCfg # noqa: E402 num_envs = int(sys.argv[1]) if len(sys.argv) > 1 else 2048 n_updates = int(sys.argv[2]) if len(sys.argv) > 2 else 4 @@ -73,14 +73,14 @@ total_cuda_ms = sum(getattr(e, "device_time_total", 0) for e in events) / 1e3 total_cpu_ms = sum(getattr(e, "self_cpu_time_total", 0) for e in events) / 1e3 print(f"\nprofiled update: cuda kernel time ~{total_cuda_ms:.2f} ms, host self time ~{total_cpu_ms:.2f} ms") -print(f"\ntop ops by CUDA time:") +print("\ntop ops by CUDA time:") events.sort(key=lambda e: getattr(e, "device_time_total", 0), reverse=True) for e in events[:12]: d = getattr(e, "device_time_total", 0) / 1e3 c = e.count if d > 0.01: print(f" {e.key[:64]:<64} {d:8.2f} ms x{c}") -print(f"\ntop ops by host (self) time:") +print("\ntop ops by host (self) time:") events.sort(key=lambda e: e.self_cpu_time_total, reverse=True) for e in events[:12]: if e.self_cpu_time_total / 1e3 > 0.05: From 780cd964e370814495e87e250af72cbd8623bcf1 Mon Sep 17 00:00:00 2001 From: zilch <147668916@qq.com> Date: Tue, 22 Sep 2026 19:10:04 +0800 Subject: [PATCH 07/12] fix(bench): move profiler imports to the top of the learner bench --- bench/bench_learner_update.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/bench/bench_learner_update.py b/bench/bench_learner_update.py index 4f65f4dd..eb7947ca 100644 --- a/bench/bench_learner_update.py +++ b/bench/bench_learner_update.py @@ -4,6 +4,7 @@ # wall time into CUDA kernel time vs host-side gaps. import os import sys +import time from types import SimpleNamespace os.environ.setdefault("OMP_WAIT_POLICY", "PASSIVE") @@ -12,6 +13,7 @@ import numpy as np # noqa: E402 (env vars above must be set first) import torch # noqa: E402 from omegaconf import OmegaConf # noqa: E402 +from torch.profiler import ProfilerActivity, profile # noqa: E402 import motrix_envs # noqa: F401 (unused; keeps import parity with training) from motrix_env_core import registry # noqa: E402 @@ -34,8 +36,12 @@ device = torch.device("cuda") agent = build_agent( - cfg, (obs_dim, critic_dim, act_dim), num_envs, device, - torch.ones(act_dim), torch.zeros(act_dim), + cfg, + (obs_dim, critic_dim, act_dim), + num_envs, + device, + torch.ones(act_dim), + torch.zeros(act_dim), ) rng = np.random.default_rng(0) cap = agent.rb.buffer_size @@ -54,16 +60,12 @@ agent.update(n_updates) torch.cuda.synchronize() -import time - for trial in range(3): t0 = time.perf_counter() agent.update(n_updates) torch.cuda.synchronize() wall_ms = (time.perf_counter() - t0) * 1e3 - print(f"update({n_updates}) wall: {wall_ms:8.2f} ms ({wall_ms/n_updates:.2f} ms/update)") - -from torch.profiler import ProfilerActivity, profile + print(f"update({n_updates}) wall: {wall_ms:8.2f} ms ({wall_ms / n_updates:.2f} ms/update)") with profile(activities=[ProfilerActivity.CPU, ProfilerActivity.CUDA]) as prof: agent.update(n_updates) @@ -84,4 +86,4 @@ events.sort(key=lambda e: e.self_cpu_time_total, reverse=True) for e in events[:12]: if e.self_cpu_time_total / 1e3 > 0.05: - print(f" {e.key[:64]:<64} {e.self_cpu_time_total/1e3:8.2f} ms x{e.count}") + print(f" {e.key[:64]:<64} {e.self_cpu_time_total / 1e3:8.2f} ms x{e.count}") From a0c7eaeac60cb08b3277e1b50bfc52d27d5fd58c Mon Sep 17 00:00:00 2001 From: zilch <147668916@qq.com> Date: Tue, 22 Sep 2026 19:32:09 +0800 Subject: [PATCH 08/12] fix(bench): add license header to the learner-update bench --- bench/bench_learner_update.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bench/bench_learner_update.py b/bench/bench_learner_update.py index eb7947ca..8effa8d2 100644 --- a/bench/bench_learner_update.py +++ b/bench/bench_learner_update.py @@ -1,3 +1,6 @@ +# Copyright Motphys Technology Co., Ltd. 2025, 2026 +# SPDX-License-Identifier: Apache-2.0 + # Profile the FastSAC learner update: where does wall time go? # Builds a real FastSacAgent, fills the replay buffer with random transitions, # then times agent.update(n) and profiles it with torch.profiler to split From 5baf99266b1ec9431f5a54319bb99976377003f9 Mon Sep 17 00:00:00 2001 From: zilch <147668916@qq.com> Date: Tue, 22 Sep 2026 19:43:28 +0800 Subject: [PATCH 09/12] fix: address review feedback on PR #72 - _format_duration: clamp negative inputs and document the 0m45s form - preload_tbb: cache the probe result; ManagerEnv constructs per env and should not repeat the glob + CDLL work - wiki: fix nested backticks breaking markdown rendering --- .../src/motrix_env_core/numba/threading.py | 12 +++++++++++- motrix_rl/src/motrix_rl/console.py | 4 ++-- wiki/design/fastsac-async-heterogeneous-trainer.md | 2 +- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/motrix_env_core/src/motrix_env_core/numba/threading.py b/motrix_env_core/src/motrix_env_core/numba/threading.py index 56d2ce0e..611292d0 100644 --- a/motrix_env_core/src/motrix_env_core/numba/threading.py +++ b/motrix_env_core/src/motrix_env_core/numba/threading.py @@ -24,6 +24,7 @@ from pathlib import Path _TBB_SONAME = "libtbb.so.12" +_tbb_preload_result: bool | None = None def preload_tbb() -> bool: @@ -32,8 +33,17 @@ def preload_tbb() -> bool: Returns ``True`` when the library was loaded (or was already loaded), which lets numba select the TBB threading layer for parallel kernels. Returns ``False`` when the ``tbb`` extra is not installed — callers keep the numba - default layer in that case. + default layer in that case. The probe runs once and is cached: every + ``ManagerEnv`` construction calls this, and the glob + CDLL work should + not repeat per instance. """ + global _tbb_preload_result + if _tbb_preload_result is None: + _tbb_preload_result = _probe_tbb() + return _tbb_preload_result + + +def _probe_tbb() -> bool: if not sys.platform.startswith("linux"): return False candidates = sorted(glob.glob(str(Path(sys.prefix) / "lib" / f"{_TBB_SONAME}*")), reverse=True) diff --git a/motrix_rl/src/motrix_rl/console.py b/motrix_rl/src/motrix_rl/console.py index 3957671e..79cde678 100644 --- a/motrix_rl/src/motrix_rl/console.py +++ b/motrix_rl/src/motrix_rl/console.py @@ -170,8 +170,8 @@ def _format_metric_items(items: Mapping[str, Any], *, precision: int = 3, signed def _format_duration(seconds: float) -> str: - """Compact elapsed/remaining time: ``3h05m``, ``12m07s`` or ``45s``.""" - t = int(seconds) + """Compact elapsed/remaining time: ``3h05m``, ``12m07s`` or ``0m45s``.""" + t = max(0, int(seconds)) h, m, sec = t // 3600, (t % 3600) // 60, t % 60 return f"{h}h{m:02d}m" if h else f"{m}m{sec:02d}s" diff --git a/wiki/design/fastsac-async-heterogeneous-trainer.md b/wiki/design/fastsac-async-heterogeneous-trainer.md index 991341ed..0ed753eb 100644 --- a/wiki/design/fastsac-async-heterogeneous-trainer.md +++ b/wiki/design/fastsac-async-heterogeneous-trainer.md @@ -194,7 +194,7 @@ producer lifetime 和 compiled collector 固定参数地址,不能只把 H2D episode return / length、reward 分项、env metrics、collector timing 都发生在 collector(它才有 reward/done)。collector 按根级 `logging.interval` 把一份紧凑 `snapshot_stats()` 放进 `StatsQueue`(先清掉旧快照,保证 learner 总见最新);learner 在日志相位 drain 出来,喂给与同步版**完全复用**的 rich 训练面板。 -TensorBoard scalar 与同步版同名(`rollout/mean_return`、`rollout/mean_ep_len`、`perf/env_steps_per_s` 等),并新增异构专属:`async/policy_lag`、`async/ring_fill`、`async/weight_version`、`async/utd`,以及 collector 细分 timing `perf/collector_{sample_actions,env_step,env_step.*,push,bookkeep,sync,sync.*}_ms(嵌套阶段为点分路径,如 `collector_env_step.physics.read_ms`)`、整体 `perf/collect_ms_per_batch`,learner 侧 `perf/learn_ms_per_update`、`perf/learn_pct`、`perf/updates_per_s`。 +TensorBoard scalar 与同步版同名(`rollout/mean_return`、`rollout/mean_ep_len`、`perf/env_steps_per_s` 等),并新增异构专属:`async/policy_lag`、`async/ring_fill`、`async/weight_version`、`async/utd`,以及 collector 细分 timing `perf/collector_{sample_actions,env_step,env_step.*,push,bookkeep,sync,sync.*}_ms`(嵌套阶段为点分路径,如 `collector_env_step.physics.read_ms`)、整体 `perf/collect_ms_per_batch`,learner 侧 `perf/learn_ms_per_update`、`perf/learn_pct`、`perf/updates_per_s`。 > 面板中 `collect_ms` / `learn_ms` / `learn_pct` 因两进程并发,**不像同步版那样相加为 100%**:`learn_pct` 表示 learner wall-clock 中真正用于更新(vs 空转/欠数据)的比例,≈100% 表示 GPU-bound,偏低表示 collector 喂不满 buffer。 From d53540973e934a164c8a516c79295e78d309d8e3 Mon Sep 17 00:00:00 2001 From: zilch <147668916@qq.com> Date: Tue, 22 Sep 2026 19:43:52 +0800 Subject: [PATCH 10/12] chore(bench): remove the learner-update profiler script --- bench/bench_learner_update.py | 92 ----------------------------------- 1 file changed, 92 deletions(-) delete mode 100644 bench/bench_learner_update.py diff --git a/bench/bench_learner_update.py b/bench/bench_learner_update.py deleted file mode 100644 index 8effa8d2..00000000 --- a/bench/bench_learner_update.py +++ /dev/null @@ -1,92 +0,0 @@ -# Copyright Motphys Technology Co., Ltd. 2025, 2026 -# SPDX-License-Identifier: Apache-2.0 - -# Profile the FastSAC learner update: where does wall time go? -# Builds a real FastSacAgent, fills the replay buffer with random transitions, -# then times agent.update(n) and profiles it with torch.profiler to split -# wall time into CUDA kernel time vs host-side gaps. -import os -import sys -import time -from types import SimpleNamespace - -os.environ.setdefault("OMP_WAIT_POLICY", "PASSIVE") -os.environ.setdefault("GOMP_SPINCOUNT", "0") - -import numpy as np # noqa: E402 (env vars above must be set first) -import torch # noqa: E402 -from omegaconf import OmegaConf # noqa: E402 -from torch.profiler import ProfilerActivity, profile # noqa: E402 - -import motrix_envs # noqa: F401 (unused; keeps import parity with training) -from motrix_env_core import registry # noqa: E402 -from motrix_rl.fastsac.async_impl.worker import build_agent # noqa: E402 -from motrix_rl.fastsac.config import FastSacAgentCfg # noqa: E402 - -num_envs = int(sys.argv[1]) if len(sys.argv) > 1 else 2048 -n_updates = int(sys.argv[2]) if len(sys.argv) > 2 else 4 -run_cfg = OmegaConf.load(sys.argv[3]) if len(sys.argv) > 3 else None -agent_cfg = FastSacAgentCfg(**OmegaConf.to_container(run_cfg.algo.agent)) if run_cfg else FastSacAgentCfg() - -env = registry.make("g1-wbt-dance", num_envs=num_envs) -obs_space = env.observation_space -obs_dim = obs_space.policy.shape[0] -critic_dim = obs_space.value_or_policy.shape[0] -act_dim = env.action_space.shape[0] -del env - -cfg = SimpleNamespace(agent=agent_cfg) - -device = torch.device("cuda") -agent = build_agent( - cfg, - (obs_dim, critic_dim, act_dim), - num_envs, - device, - torch.ones(act_dim), - torch.zeros(act_dim), -) -rng = np.random.default_rng(0) -cap = agent.rb.buffer_size -for _ in range(cap): - agent.rb.extend( - torch.randn(num_envs, obs_dim, device=device), - torch.randn(num_envs, critic_dim, device=device), - torch.rand(num_envs, act_dim, device=device) * 2 - 1, - torch.randn(num_envs, device=device), - torch.zeros(num_envs, dtype=torch.long, device=device), - torch.zeros(num_envs, dtype=torch.long, device=device), - ) - -# warmup (compile/cudagraph) then timed windows -for _ in range(3): - agent.update(n_updates) -torch.cuda.synchronize() - -for trial in range(3): - t0 = time.perf_counter() - agent.update(n_updates) - torch.cuda.synchronize() - wall_ms = (time.perf_counter() - t0) * 1e3 - print(f"update({n_updates}) wall: {wall_ms:8.2f} ms ({wall_ms / n_updates:.2f} ms/update)") - -with profile(activities=[ProfilerActivity.CPU, ProfilerActivity.CUDA]) as prof: - agent.update(n_updates) - torch.cuda.synchronize() - -events = prof.key_averages() -total_cuda_ms = sum(getattr(e, "device_time_total", 0) for e in events) / 1e3 -total_cpu_ms = sum(getattr(e, "self_cpu_time_total", 0) for e in events) / 1e3 -print(f"\nprofiled update: cuda kernel time ~{total_cuda_ms:.2f} ms, host self time ~{total_cpu_ms:.2f} ms") -print("\ntop ops by CUDA time:") -events.sort(key=lambda e: getattr(e, "device_time_total", 0), reverse=True) -for e in events[:12]: - d = getattr(e, "device_time_total", 0) / 1e3 - c = e.count - if d > 0.01: - print(f" {e.key[:64]:<64} {d:8.2f} ms x{c}") -print("\ntop ops by host (self) time:") -events.sort(key=lambda e: e.self_cpu_time_total, reverse=True) -for e in events[:12]: - if e.self_cpu_time_total / 1e3 > 0.05: - print(f" {e.key[:64]:<64} {e.self_cpu_time_total / 1e3:8.2f} ms x{e.count}") From 8fcef0dc86e93f6c8dbd5061b3ef00fea085310b Mon Sep 17 00:00:00 2001 From: zilch <147668916@qq.com> Date: Tue, 22 Sep 2026 19:47:52 +0800 Subject: [PATCH 11/12] docs: document the --tbb install option Adds the option to the install reference table in both languages and a short section explaining when the TBB threading layer matters (many-core training servers) and that it auto-activates once installed. --- .../getting_started/installation.md | 20 +++++++++++++++++++ .../getting_started/installation.md | 17 ++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/docs/source/en/user_guide/getting_started/installation.md b/docs/source/en/user_guide/getting_started/installation.md index 71e87cc1..6a292b93 100644 --- a/docs/source/en/user_guide/getting_started/installation.md +++ b/docs/source/en/user_guide/getting_started/installation.md @@ -92,7 +92,27 @@ training backend extra on top of them, and each option only selects or appends t | `--gpu` | `cuda`
`rocm` | Select the torch wheel flavor explicitly, overriding auto-detection | | `--skrl-jax` | — | SKRL on JAX backend, Linux only | | `--rslrl` | — | RSL-RL on PyTorch backend | +| `--tbb` | — | Install Intel TBB for the numba parallel kernels; recommended when training on many-core servers (see below) | | `--docs` | — | Add the toolchain (sphinx) needed to build the documentation locally | | `-h`, `--help` | — | Show the help message | Run `sh install.sh --help` for the full option reference. + +## TBB Threading Layer for Many-Core Machines (Optional) + +When training on servers with many cores (64+), add `--tbb`: + +```bash +sh install.sh --tbb +``` + +The manager kernels inside each environment step run in parallel through numba. The default +OpenMP threading layer must wake every worker thread before each kernel call; the more cores +and the shorter the kernel, the more this scheduling overhead dominates (measured to slow the +evaluate stage several-fold on a 192-core machine). The TBB layer keeps a resident +work-stealing pool and does not have this problem. Once installed it is enabled automatically +by the framework (look for `numba TBB threading layer available` in the logs) — no extra +configuration is needed. + +On small development machines (up to ~32 cores) both layers perform equivalently, so the +extra is optional there. diff --git a/docs/source/zh_CN/user_guide/getting_started/installation.md b/docs/source/zh_CN/user_guide/getting_started/installation.md index d94759ca..78c722e7 100644 --- a/docs/source/zh_CN/user_guide/getting_started/installation.md +++ b/docs/source/zh_CN/user_guide/getting_started/installation.md @@ -114,7 +114,24 @@ sh install.sh --all | `--gpu` | `cuda`
`rocm` | 指定 torch wheel 来源,覆盖自动探测 | | `--skrl-jax` | — | SKRL(JAX)训练后端,仅 Linux | | `--rslrl` | — | RSL-RL(PyTorch)训练后端 | +| `--tbb` | — | 为 numba 并行 kernel 安装 Intel TBB 线程层;多核服务器训练推荐启用(详见下方说明) | | `--docs` | — | 追加本地构建文档所需的工具链(Sphinx) | | `-h`、`--help` | — | 显示帮助 | 完整参数说明见 `sh install.sh --help`。 + +## 多核机器的 TBB 线程层(可选) + +在大核数服务器(如 64 核以上)上训练时,建议追加 `--tbb`: + +```bash +sh install.sh --tbb +``` + +环境步进中的 manager kernel 由 numba 并行执行。默认的 OpenMP 线程层在每次 kernel +调用前需要唤醒所有工作线程,核数越多、单次 kernel 越短,这部分调度开销越明显 +(在 192 核机器上实测可将 evaluate 阶段拖慢数倍)。TBB 线程层使用常驻的 +work-stealing 线程池,没有此问题;安装后由框架自动启用(日志中会出现 +`numba TBB threading layer available`),无需任何额外配置。 + +小核数开发机(如 32 核以内)两种线程层性能相当,可不安装。 From 19e81d5125a064f902e9af3fd5386de292f7b29d Mon Sep 17 00:00:00 2001 From: zilch <147668916@qq.com> Date: Tue, 22 Sep 2026 19:51:45 +0800 Subject: [PATCH 12/12] docs: fold the TBB guidance into the install option row --- .../getting_started/installation.md | 21 +------------------ .../getting_started/installation.md | 18 +--------------- 2 files changed, 2 insertions(+), 37 deletions(-) diff --git a/docs/source/en/user_guide/getting_started/installation.md b/docs/source/en/user_guide/getting_started/installation.md index 6a292b93..3d8c03c8 100644 --- a/docs/source/en/user_guide/getting_started/installation.md +++ b/docs/source/en/user_guide/getting_started/installation.md @@ -92,27 +92,8 @@ training backend extra on top of them, and each option only selects or appends t | `--gpu` | `cuda`
`rocm` | Select the torch wheel flavor explicitly, overriding auto-detection | | `--skrl-jax` | — | SKRL on JAX backend, Linux only | | `--rslrl` | — | RSL-RL on PyTorch backend | -| `--tbb` | — | Install Intel TBB for the numba parallel kernels; recommended when training on many-core servers (see below) | +| `--tbb` | — | Install Intel TBB for the numba parallel kernels; recommended for training on many-core servers (64+ cores), auto-activates once installed | | `--docs` | — | Add the toolchain (sphinx) needed to build the documentation locally | | `-h`, `--help` | — | Show the help message | Run `sh install.sh --help` for the full option reference. - -## TBB Threading Layer for Many-Core Machines (Optional) - -When training on servers with many cores (64+), add `--tbb`: - -```bash -sh install.sh --tbb -``` - -The manager kernels inside each environment step run in parallel through numba. The default -OpenMP threading layer must wake every worker thread before each kernel call; the more cores -and the shorter the kernel, the more this scheduling overhead dominates (measured to slow the -evaluate stage several-fold on a 192-core machine). The TBB layer keeps a resident -work-stealing pool and does not have this problem. Once installed it is enabled automatically -by the framework (look for `numba TBB threading layer available` in the logs) — no extra -configuration is needed. - -On small development machines (up to ~32 cores) both layers perform equivalently, so the -extra is optional there. diff --git a/docs/source/zh_CN/user_guide/getting_started/installation.md b/docs/source/zh_CN/user_guide/getting_started/installation.md index 78c722e7..12d39a8e 100644 --- a/docs/source/zh_CN/user_guide/getting_started/installation.md +++ b/docs/source/zh_CN/user_guide/getting_started/installation.md @@ -114,24 +114,8 @@ sh install.sh --all | `--gpu` | `cuda`
`rocm` | 指定 torch wheel 来源,覆盖自动探测 | | `--skrl-jax` | — | SKRL(JAX)训练后端,仅 Linux | | `--rslrl` | — | RSL-RL(PyTorch)训练后端 | -| `--tbb` | — | 为 numba 并行 kernel 安装 Intel TBB 线程层;多核服务器训练推荐启用(详见下方说明) | +| `--tbb` | — | 为 numba 并行 kernel 安装 Intel TBB 线程层;多核服务器(64 核以上)训练推荐启用,安装后自动生效 | | `--docs` | — | 追加本地构建文档所需的工具链(Sphinx) | | `-h`、`--help` | — | 显示帮助 | 完整参数说明见 `sh install.sh --help`。 - -## 多核机器的 TBB 线程层(可选) - -在大核数服务器(如 64 核以上)上训练时,建议追加 `--tbb`: - -```bash -sh install.sh --tbb -``` - -环境步进中的 manager kernel 由 numba 并行执行。默认的 OpenMP 线程层在每次 kernel -调用前需要唤醒所有工作线程,核数越多、单次 kernel 越短,这部分调度开销越明显 -(在 192 核机器上实测可将 evaluate 阶段拖慢数倍)。TBB 线程层使用常驻的 -work-stealing 线程池,没有此问题;安装后由框架自动启用(日志中会出现 -`numba TBB threading layer available`),无需任何额外配置。 - -小核数开发机(如 32 核以内)两种线程层性能相当,可不安装。