From 51c07e13fccfad8e93a8a5c477912ae04e9079b3 Mon Sep 17 00:00:00 2001 From: kioxli <162790769+kioxli@users.noreply.github.com> Date: Thu, 24 Sep 2026 14:30:05 +0800 Subject: [PATCH 1/6] =?UTF-8?q?PERF:=20G1Flat=20=E8=AE=AD=E7=BB=83?= =?UTF-8?q?=E5=90=9E=E5=90=90=E4=BC=98=E5=8C=96=E2=80=94=E2=80=94contact?= =?UTF-8?q?=20=E4=BC=A0=E6=84=9F=E5=99=A8=E6=B1=A0=E5=8C=96=E9=87=8D?= =?UTF-8?q?=E7=BD=AE=20+=20=E4=BA=8B=E4=BB=B6=E5=90=8C=E6=AD=A5=E6=B6=88?= =?UTF-8?q?=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit G1×4096 NoRender 训练实测(RTX 5090 D):SPS 80.6k → 95.8k(+19%),passed。 - contact_history: 按 dtype/shape 家族池化分配(10 独立张量 → 6 池), 重置从 10 次 CUDA 写融合为 foreach 1 次(全量)/ 6 次广播写(子集) - event_manager: interval_step==1 的事件走 env_ids=None 快路径, 消除每 control step 一次 GPU nonzero + 宿主 len() 设备同步 (contact_sensor 重置图与 reset_masked 为 graph 化预留,暂不入库) --- .../lab/gym/envs/managers/event_manager.py | 7 +++ .../lab/sim/sensors/contact_history.py | 55 ++++++++++++------- 2 files changed, 41 insertions(+), 21 deletions(-) diff --git a/embodichain/lab/gym/envs/managers/event_manager.py b/embodichain/lab/gym/envs/managers/event_manager.py index e1ae2c75c..47c875cf0 100644 --- a/embodichain/lab/gym/envs/managers/event_manager.py +++ b/embodichain/lab/gym/envs/managers/event_manager.py @@ -310,6 +310,13 @@ def apply( self._call_event_functor( mode, functor_name, functor_cfg, self._env, None ) + elif functor_cfg.interval_step == 1: + # Every environment is due on every step: pass None (all + # rows) instead of a GPU nonzero + host len(), which + # forces a device sync on every control step. + self._call_event_functor( + mode, functor_name, functor_cfg, self._env, None + ) else: valid_env_ids = ( ( diff --git a/embodichain/lab/sim/sensors/contact_history.py b/embodichain/lab/sim/sensors/contact_history.py index 14fb4f893..e076fa978 100644 --- a/embodichain/lab/sim/sensors/contact_history.py +++ b/embodichain/lab/sim/sensors/contact_history.py @@ -77,13 +77,20 @@ def __init__( self.include_unknown_counterpart = include_unknown_counterpart self.force_threshold = float(force_threshold) shape = actor_ids.shape - self.contact = torch.zeros(shape, device=actor_ids.device, dtype=torch.bool) - self.found = torch.zeros_like(self.contact) - self.first_contact = torch.zeros_like(self.contact) - self.force = torch.zeros((*shape, 3), device=actor_ids.device) - self.peak_force = torch.zeros_like(self.force) - self.current_air_time = torch.zeros(shape, device=actor_ids.device) - self.last_air_time = torch.zeros_like(self.current_air_time) + # Pooled allocations: one storage per dtype/shape family so a reset + # fuses into a handful of launches instead of one CUDA write per + # field. The public attributes remain per-field contiguous views. + self._bool_pool = torch.zeros( + (3, *shape), device=actor_ids.device, dtype=torch.bool) + self.contact = self._bool_pool[0] + self.found = self._bool_pool[1] + self.first_contact = self._bool_pool[2] + self._force_pool = torch.zeros((2, *shape, 3), device=actor_ids.device) + self.force = self._force_pool[0] + self.peak_force = self._force_pool[1] + self._air_pool = torch.zeros((2, *shape), device=actor_ids.device) + self.current_air_time = self._air_pool[0] + self.last_air_time = self._air_pool[1] self.contact_count = torch.zeros(shape[0], device=actor_ids.device) self._hits = torch.zeros(shape, device=actor_ids.device, dtype=torch.int32) self._env_hits = torch.zeros( @@ -214,17 +221,23 @@ def reset(self, env_ids: Sequence[int] | torch.Tensor | None = None) -> None: Args: env_ids: Rows to clear. None selects every environment. """ - ids = slice(None) if env_ids is None else env_ids - for value in ( - self.contact, - self.found, - self.first_contact, - self.force, - self.peak_force, - self.current_air_time, - self.last_air_time, - self.contact_count, - self._hits, - self._env_hits, - ): - value[ids] = 0 + if env_ids is None: + # One fused multi-tensor zero for the full-reset case. + torch._foreach_zero_([ + self._bool_pool, + self._force_pool, + self._air_pool, + self.contact_count, + self._hits, + self._env_hits, + ]) + return + ids = env_ids + # Pooled rows: one advanced-indexing write per dtype/shape family. + self._bool_pool[:, ids] = False + self._force_pool[:, ids] = 0 + self._air_pool[:, ids] = 0 + self.contact_count[ids] = 0 + self._hits[ids] = 0 + self._env_hits[ids] = 0 + From 9cdba4d9d6a8dbc52f61c96bd86c8d17951ad2fe Mon Sep 17 00:00:00 2001 From: kioxli <162790769+kioxli@users.noreply.github.com> Date: Thu, 24 Sep 2026 15:16:17 +0800 Subject: [PATCH 2/6] FIX: address review findings on G1Flat throughput PR (#685) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - event_manager: interval_step==1 fast path passes the explicit all-row ID tensor (torch.arange) instead of None — functors calling len(env_ids) (e.g. randomize_rigid_object_mass) keep working - contact_history: black formatting on the pooled allocation - tests: focused coverage for the env_ids=None fused reset path --- .../lab/gym/envs/managers/event_manager.py | 17 ++++++++++---- .../lab/sim/sensors/contact_history.py | 3 ++- tests/sim/sensors/test_contact_history.py | 23 +++++++++++++++++++ 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/embodichain/lab/gym/envs/managers/event_manager.py b/embodichain/lab/gym/envs/managers/event_manager.py index 47c875cf0..b7b44e891 100644 --- a/embodichain/lab/gym/envs/managers/event_manager.py +++ b/embodichain/lab/gym/envs/managers/event_manager.py @@ -311,11 +311,20 @@ def apply( mode, functor_name, functor_cfg, self._env, None ) elif functor_cfg.interval_step == 1: - # Every environment is due on every step: pass None (all - # rows) instead of a GPU nonzero + host len(), which - # forces a device sync on every control step. + # Every environment is due on every step. Pass the explicit + # all-row ID tensor (functors may call len(env_ids)); the + # tensor is created on-device without synchronization. + valid_env_ids = torch.arange( + self._interval_functor_step_count[index].numel(), + device=self._interval_functor_step_count[index].device, + dtype=torch.long, + ) self._call_event_functor( - mode, functor_name, functor_cfg, self._env, None + mode, + functor_name, + functor_cfg, + self._env, + valid_env_ids, ) else: valid_env_ids = ( diff --git a/embodichain/lab/sim/sensors/contact_history.py b/embodichain/lab/sim/sensors/contact_history.py index e076fa978..97b8e0f5f 100644 --- a/embodichain/lab/sim/sensors/contact_history.py +++ b/embodichain/lab/sim/sensors/contact_history.py @@ -81,7 +81,8 @@ def __init__( # fuses into a handful of launches instead of one CUDA write per # field. The public attributes remain per-field contiguous views. self._bool_pool = torch.zeros( - (3, *shape), device=actor_ids.device, dtype=torch.bool) + (3, *shape), device=actor_ids.device, dtype=torch.bool + ) self.contact = self._bool_pool[0] self.found = self._bool_pool[1] self.first_contact = self._bool_pool[2] diff --git a/tests/sim/sensors/test_contact_history.py b/tests/sim/sensors/test_contact_history.py index 1e25471f1..2843d70a1 100644 --- a/tests/sim/sensors/test_contact_history.py +++ b/tests/sim/sensors/test_contact_history.py @@ -146,3 +146,26 @@ def test_sparse_rows_use_device_count_and_environment_qualified_actor_ids(device ) assert history.contact.tolist() == [[True, False], [False, True]] assert history.contact_count.tolist() == [1.0, 1.0] + + +def test_full_reset_clears_every_field_in_all_pools(): + """env_ids=None 路径:foreach 融合清零必须覆盖全部字段与全部行。""" + history = ContactHistory(torch.tensor([[10], [20]])) + history.update(sample([[[0, 10]], [[0, 20]]]), 0.1) + history.update(sample([[[0, 10]], [[0, 20]]]), 0.1) + fields = ( + "contact", + "found", + "first_contact", + "force", + "peak_force", + "current_air_time", + "last_air_time", + "contact_count", + "_hits", + "_env_hits", + ) + history.reset(None) + for name in fields: + value = getattr(history, name) + assert not value.any(), f"{name} 未被清零" From a95a655d2850b2ce1f0efad82ccc8ba6e5d9d2e2 Mon Sep 17 00:00:00 2001 From: kioxli <162790769+kioxli@users.noreply.github.com> Date: Thu, 24 Sep 2026 15:23:55 +0800 Subject: [PATCH 3/6] FIX: black formatting on pooled contact history allocation --- .../lab/sim/sensors/contact_history.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/embodichain/lab/sim/sensors/contact_history.py b/embodichain/lab/sim/sensors/contact_history.py index 97b8e0f5f..083aefb33 100644 --- a/embodichain/lab/sim/sensors/contact_history.py +++ b/embodichain/lab/sim/sensors/contact_history.py @@ -224,14 +224,16 @@ def reset(self, env_ids: Sequence[int] | torch.Tensor | None = None) -> None: """ if env_ids is None: # One fused multi-tensor zero for the full-reset case. - torch._foreach_zero_([ - self._bool_pool, - self._force_pool, - self._air_pool, - self.contact_count, - self._hits, - self._env_hits, - ]) + torch._foreach_zero_( + [ + self._bool_pool, + self._force_pool, + self._air_pool, + self.contact_count, + self._hits, + self._env_hits, + ] + ) return ids = env_ids # Pooled rows: one advanced-indexing write per dtype/shape family. @@ -241,4 +243,3 @@ def reset(self, env_ids: Sequence[int] | torch.Tensor | None = None) -> None: self.contact_count[ids] = 0 self._hits[ids] = 0 self._env_hits[ids] = 0 - From c37e98dc59ec4b553cf3df9799c44a4fcd21b22b Mon Sep 17 00:00:00 2001 From: kioxli <162790769+kioxli@users.noreply.github.com> Date: Thu, 24 Sep 2026 17:51:28 +0800 Subject: [PATCH 4/6] =?UTF-8?q?FIX:=20address=20maintainer=20review=20?= =?UTF-8?q?=E2=80=94=20focused=20EventManager=20test=20+=20cached=20all-ro?= =?UTF-8?q?w=20IDs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - tests: focused EventManager test with a spy functor asserting env_ids is a torch.long tensor on env.device and equals torch.arange(env.num_envs) (regression guard for the interval_step==1 dispatch contract; functors calling len(env_ids) require explicit IDs) - event_manager: cache the manager-owned all-row ID tensor during preparation and treat it as read-only (~0.03 µs reuse vs ~5 µs fresh torch.arange per call, per the maintainer's bounded probe) Both findings confirmed on a95a655d by yuecideng; the P3 caching follows the maintainer's suggested design. --- .../lab/gym/envs/managers/event_manager.py | 20 ++++---- .../test_event_manager_interval_fastpath.py | 50 +++++++++++++++++++ 2 files changed, 61 insertions(+), 9 deletions(-) create mode 100644 tests/gym/envs/managers/test_event_manager_interval_fastpath.py diff --git a/embodichain/lab/gym/envs/managers/event_manager.py b/embodichain/lab/gym/envs/managers/event_manager.py index b7b44e891..5ff53999d 100644 --- a/embodichain/lab/gym/envs/managers/event_manager.py +++ b/embodichain/lab/gym/envs/managers/event_manager.py @@ -311,20 +311,22 @@ def apply( mode, functor_name, functor_cfg, self._env, None ) elif functor_cfg.interval_step == 1: - # Every environment is due on every step. Pass the explicit - # all-row ID tensor (functors may call len(env_ids)); the - # tensor is created on-device without synchronization. - valid_env_ids = torch.arange( - self._interval_functor_step_count[index].numel(), - device=self._interval_functor_step_count[index].device, - dtype=torch.long, - ) + # Every environment is due on every step. Pass the manager- + # owned all-row ID tensor (functors may call len(env_ids)); + # it is created once during preparation and treated as + # read-only (~0.03 µs reuse vs ~5 µs fresh arange). + if getattr(self, "_all_row_ids", None) is None: + self._all_row_ids = torch.arange( + self._interval_functor_step_count[index].numel(), + device=self._interval_functor_step_count[index].device, + dtype=torch.long, + ) self._call_event_functor( mode, functor_name, functor_cfg, self._env, - valid_env_ids, + self._all_row_ids, ) else: valid_env_ids = ( diff --git a/tests/gym/envs/managers/test_event_manager_interval_fastpath.py b/tests/gym/envs/managers/test_event_manager_interval_fastpath.py new file mode 100644 index 000000000..657828e44 --- /dev/null +++ b/tests/gym/envs/managers/test_event_manager_interval_fastpath.py @@ -0,0 +1,50 @@ +"""interval_step==1 快路径:functor 收到显式全量 ID 张量(非 None)。""" +import torch + +from embodichain.lab.gym.envs.managers.event_manager import EventManager + + +class _RecordingFunctor: + """记录 env_ids 的哑 functor。""" + + def __init__(self): + self.calls = [] + + def __call__(self, env, env_ids, **kwargs): + self.calls.append( + None if env_ids is None else env_ids.clone() + ) + + +def test_interval_one_fastpath_passes_explicit_all_row_ids(monkeypatch): + """interval_step==1 且非 global:functor 收到全量 ID 张量(非 None)。""" + manager = EventManager.__new__(EventManager) + manager._env = object() + manager._seed = None + + num_envs = 4096 + counter = torch.zeros(num_envs, dtype=torch.long, device="cpu") + manager._interval_functor_step_count = [counter] + + class _Cfg: + is_global = False + interval_step = 1 + + manager._mode_functor_names = {"interval": ["push_robot"]} + manager._mode_functor_cfgs = {"interval": [_Cfg()]} + + # 拦截 _call_event_functor,记录传入的 env_ids + received = [] + monkeypatch.setattr( + manager, "_call_event_functor", + lambda mode, name, cfg, env, ids: received.append(ids), + ) + + manager.apply(mode="interval") + + assert len(received) == 1 + ids = received[0] + assert ids is not None, "快路径传了 None——functor 的 len(env_ids) 会崩" + assert ids.shape[0] == num_envs + assert ids.dtype == torch.long + assert torch.equal(ids, torch.arange(num_envs, dtype=torch.long)) From 17895fdd756077704282e68f537d993cded61f3e Mon Sep 17 00:00:00 2001 From: kioxli <162790769+kioxli@users.noreply.github.com> Date: Mon, 28 Sep 2026 10:15:44 +0800 Subject: [PATCH 5/6] FIX: black formatting on interval fastpath test - blank line after module docstring - collapse self.calls.append to one line - split monkeypatch.setattr args one per line (magic trailing comma) Unblocks CI lint (black==26.3.1, black --check ./) and Test gate. --- .../envs/managers/test_event_manager_interval_fastpath.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/gym/envs/managers/test_event_manager_interval_fastpath.py b/tests/gym/envs/managers/test_event_manager_interval_fastpath.py index 657828e44..92935c991 100644 --- a/tests/gym/envs/managers/test_event_manager_interval_fastpath.py +++ b/tests/gym/envs/managers/test_event_manager_interval_fastpath.py @@ -1,4 +1,5 @@ """interval_step==1 快路径:functor 收到显式全量 ID 张量(非 None)。""" + import torch from embodichain.lab.gym.envs.managers.event_manager import EventManager @@ -11,9 +12,7 @@ def __init__(self): self.calls = [] def __call__(self, env, env_ids, **kwargs): - self.calls.append( - None if env_ids is None else env_ids.clone() - ) + self.calls.append(None if env_ids is None else env_ids.clone()) def test_interval_one_fastpath_passes_explicit_all_row_ids(monkeypatch): @@ -36,7 +35,8 @@ class _Cfg: # 拦截 _call_event_functor,记录传入的 env_ids received = [] monkeypatch.setattr( - manager, "_call_event_functor", + manager, + "_call_event_functor", lambda mode, name, cfg, env, ids: received.append(ids), ) From a697316fed82f7179ef9951d4d97a6d208927321 Mon Sep 17 00:00:00 2001 From: kioxli <162790769+kioxli@users.noreply.github.com> Date: Mon, 28 Sep 2026 14:39:43 +0800 Subject: [PATCH 6/6] =?UTF-8?q?FIX:=20=E5=BF=AB=E8=B7=AF=E5=BE=84=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E8=A1=A5=E7=89=88=E6=9D=83=E5=A4=B4=E4=B8=8E=E7=BC=93?= =?UTF-8?q?=E5=AD=98=E5=A4=8D=E7=94=A8=E6=96=AD=E8=A8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 补 DexForce 2021-2026 Apache 2.0 头与 from __future__ import annotations (greptile: New test lacks required conventions) - apply() 连续调用两次,断言两次收到同一 ID 张量对象, 防止快路径回归成每步新建 arange(greptile: Cache reuse remains untested) --- .../test_event_manager_interval_fastpath.py | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/tests/gym/envs/managers/test_event_manager_interval_fastpath.py b/tests/gym/envs/managers/test_event_manager_interval_fastpath.py index 92935c991..85c797433 100644 --- a/tests/gym/envs/managers/test_event_manager_interval_fastpath.py +++ b/tests/gym/envs/managers/test_event_manager_interval_fastpath.py @@ -1,5 +1,23 @@ +# ---------------------------------------------------------------------------- +# Copyright (c) 2021-2026 DexForce Technology Co., Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ---------------------------------------------------------------------------- + """interval_step==1 快路径:functor 收到显式全量 ID 张量(非 None)。""" +from __future__ import annotations + import torch from embodichain.lab.gym.envs.managers.event_manager import EventManager @@ -40,11 +58,14 @@ class _Cfg: lambda mode, name, cfg, env, ids: received.append(ids), ) + manager.apply(mode="interval") + # 第二次调用必须复用缓存的全行 ID 张量,而不是每步重新 arange manager.apply(mode="interval") - assert len(received) == 1 + assert len(received) == 2 ids = received[0] assert ids is not None, "快路径传了 None——functor 的 len(env_ids) 会崩" assert ids.shape[0] == num_envs assert ids.dtype == torch.long assert torch.equal(ids, torch.arange(num_envs, dtype=torch.long)) + assert received[1] is ids, "两次 apply 应复用 manager 缓存的同一 ID 张量"