From 5dfaebda24fcc3ad0f53a8e082b1cf8f7fe49922 Mon Sep 17 00:00:00 2001 From: yuecideng Date: Wed, 5 Aug 2026 17:05:02 +0000 Subject: [PATCH 1/2] perf(tests): reduce runtime and GPU memory --- .github/workflows/main.yml | 25 ++++-- embodichain/lab/sim/objects/articulation.py | 16 ++-- embodichain/lab/sim/sim_manager.py | 24 +++-- embodichain/lab/sim/solvers/srs_solver.py | 16 +++- pyproject.toml | 7 ++ tests/conftest.py | 88 ++++++++++++++++--- tests/data_pipeline/test_online_data.py | 5 +- tests/gym/envs/test_base_env.py | 2 + tests/gym/envs/test_embodied_env.py | 2 + tests/gym/envs/test_profiler_integration.py | 2 + tests/gym/envs/test_replay.py | 3 + tests/learning/test_newton_planar_reach.py | 1 + tests/learning/test_rl.py | 6 ++ tests/learning/test_rl_distributed.py | 2 + tests/learning/test_shared_rollout.py | 4 + tests/sim/objects/test_rigid_constraint.py | 2 + tests/sim/planners/test_curobo_planner.py | 14 +++ tests/sim/planners/test_toppra_batched.py | 10 ++- tests/sim/test_sim_manager.py | 54 ++++++++++-- tests/sim/test_sim_profiler.py | 3 + tests/toolkits/test_batch_convex_collision.py | 8 +- 21 files changed, 249 insertions(+), 45 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3ad17a9bc..0801d4fb4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -150,20 +150,35 @@ jobs: - name: Verify Scene Engine gensim installation run: | python -c "import matplotlib, numpy, open3d, requests, scipy, shapely, trimesh; from PIL import Image; import embodichain.gen_sim.scene_engine.pipeline.generate" - pytest tests/gen_sim/scene_engine -q - name: Run default tests run: | - echo "Default test suite (GPU-marked tests are skipped)" + echo "Documentation tests" export HF_ENDPOINT=https://hf-mirror.com pytest tests/docs -q --confcutdir=tests/docs - pytest tests --ignore=tests/docs - - name: Run GPU tests serially + echo "Pure-Python tests (GPU hidden, four workers)" + CUDA_VISIBLE_DEVICES="" pytest tests --ignore=tests/docs \ + -m "not requires_sim and not gpu" -n 4 --dist loadgroup + + - name: Run real-simulation tests serially + run: | + echo "Real-simulation tests (one process, GPU tests excluded)" + export HF_ENDPOINT=https://hf-mirror.com + pytest tests --ignore=tests/docs -m "requires_sim and not gpu" + + - name: Run distributed GPU tests in isolation + run: | + echo "Distributed GPU tests (isolated CUDA process tree)" + export HF_ENDPOINT=https://hf-mirror.com + pytest tests/learning/test_rl_distributed.py --run-gpu -m gpu + + - name: Run remaining GPU tests serially run: | echo "Dedicated GPU test suite" export HF_ENDPOINT=https://hf-mirror.com - pytest tests --run-gpu -m gpu + pytest tests --ignore=tests/docs \ + --ignore=tests/learning/test_rl_distributed.py --run-gpu -m gpu release-build: if: startsWith(github.ref, 'refs/tags/v') diff --git a/embodichain/lab/sim/objects/articulation.py b/embodichain/lab/sim/objects/articulation.py index a2ef24f2b..e90931aaa 100644 --- a/embodichain/lab/sim/objects/articulation.py +++ b/embodichain/lab/sim/objects/articulation.py @@ -1683,23 +1683,27 @@ def set_joint_drive( cache_env_ids = self._resolve_env_ids(env_ids) cache_joint_ids = self._resolve_joint_ids(joint_ids) + def _drive_arg(value: torch.Tensor, index: int) -> float | np.ndarray: + result = value[index].detach().cpu().numpy() + return result.item() if result.size == 1 else result + for i, env_idx in enumerate(local_env_ids): drive_args = { "drive_type": get_dexsim_drive_type(drive_type), "joint_ids": local_joint_ids, } if stiffness is not None: - drive_args["stiffness"] = stiffness[i].cpu().numpy() + drive_args["stiffness"] = _drive_arg(stiffness, i) if damping is not None: - drive_args["damping"] = damping[i].cpu().numpy() + drive_args["damping"] = _drive_arg(damping, i) if max_effort is not None: - drive_args["max_force"] = max_effort[i].cpu().numpy() + drive_args["max_force"] = _drive_arg(max_effort, i) if max_velocity is not None: - drive_args["max_velocity"] = max_velocity[i].cpu().numpy() + drive_args["max_velocity"] = _drive_arg(max_velocity, i) if friction is not None: - drive_args["joint_friction"] = friction[i].cpu().numpy() + drive_args["joint_friction"] = _drive_arg(friction, i) if armature is not None: - drive_args["armature"] = armature[i].cpu().numpy() + drive_args["armature"] = _drive_arg(armature, i) self._entities[env_idx].set_drive(**drive_args) if max_velocity is not None: diff --git a/embodichain/lab/sim/sim_manager.py b/embodichain/lab/sim/sim_manager.py index c6e8ce9e5..90e325243 100644 --- a/embodichain/lab/sim/sim_manager.py +++ b/embodichain/lab/sim/sim_manager.py @@ -3151,19 +3151,33 @@ def _sever_wrapper_refs(obj_registry): gc.collect() @staticmethod - def flush_cleanup_queue(): - """Dequeue executor and synchronization barrier provided for top-level main loop / Pytest Fixture calls""" + def flush_cleanup_queue() -> None: + """Run pending destruction tasks and wait for their scenes to disappear. + + An empty queue means that no manager requested destruction. In that + case, returning immediately is important: other managers may still own + live worlds, and waiting for the global world count to reach zero would + block until the timeout even though there is nothing to clean up. + """ import gc - while not SimulationManager._cleanup_queue.empty(): - task = SimulationManager._cleanup_queue.get_nowait() + drained_task = False + while True: + try: + task = SimulationManager._cleanup_queue.get_nowait() + except queue.Empty: + break + + drained_task = True try: task() except Exception as e: from embodichain.utils import logger logger.log_error(f"Error during delayed destruction: {e}") - pass + + if not drained_task: + return # After the queue is emptied, perform a top-level full GC to thoroughly reclaim dead objects that haven't released their RefPtrs yet gc.collect() diff --git a/embodichain/lab/sim/solvers/srs_solver.py b/embodichain/lab/sim/solvers/srs_solver.py index 967e8ec7e..23c742fab 100644 --- a/embodichain/lab/sim/solvers/srs_solver.py +++ b/embodichain/lab/sim/solvers/srs_solver.py @@ -14,6 +14,8 @@ # limitations under the License. # ---------------------------------------------------------------------------- +from __future__ import annotations + import torch import numpy as np import warp as wp @@ -36,7 +38,7 @@ from embodichain.lab.sim.robots.dexforce_w1.params import W1ArmKineParams -all = ["SRSSolver", "SRSSolverCfg"] +__all__ = ["SRSSolver", "SRSSolverCfg"] @configclass @@ -404,13 +406,16 @@ def _process_single_solution( return success_tensor, ik_qpos_tensor[:, :1, :] def _get_each_ik( - self, target_pose: np.ndarray, nsparam: float, config: np.ndarray + self, + target_pose: np.ndarray | torch.Tensor, + nsparam: float, + config: np.ndarray, ) -> tuple[bool, np.ndarray | None]: """ Computes the inverse kinematics for a given target pose, normalization parameter, and configuration. Args: - target_pose (np.ndarray): 4x4 target pose matrix. + target_pose (np.ndarray | torch.Tensor): 4x4 target pose matrix. nsparam (float): Normalization parameter (angle). config (np.ndarray): Configuration index. @@ -419,7 +424,10 @@ def _get_each_ik( np.ndarray: List of joint solutions (7) or None if no solution is found. """ # Validate the target pose matrix - target_pose = np.array(target_pose) + if isinstance(target_pose, torch.Tensor): + target_pose = target_pose.detach().cpu().numpy() + else: + target_pose = np.asarray(target_pose) if target_pose.ndim == 3 and target_pose.shape[0] == 1: target_pose = target_pose[0] # Extract the first matrix if target_pose.shape != (4, 4): diff --git a/pyproject.toml b/pyproject.toml index db51c3041..af9177192 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -106,9 +106,16 @@ include-package-data = false [tool.black] [tool.pytest.ini_options] +addopts = ["-m", "not slow"] +filterwarnings = [ + "ignore:`torch\\.jit\\.script(_method)?` is deprecated.*:DeprecationWarning:torch\\.jit\\._script", +] markers = [ "slow: marks tests as slow (deselect with '-m \"not slow\"')", "requires_sim: marks tests that require a real simulation backend", + "no_sim: marks mock-only tests that must not initialize a simulation backend", + "requires_tasks: marks tests that require installed task-package discovery", "gpu: marks tests that execute CUDA/GPU code (run serially)", "renderer: marks tests that exercise a rendering backend", + "xdist_group(name): keeps resource-sharing tests on one pytest-xdist worker", ] diff --git a/tests/conftest.py b/tests/conftest.py index b761b7697..c0f1fa47d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -16,14 +16,17 @@ from __future__ import annotations +from functools import lru_cache + import inspect import os +import re import pytest os.environ.setdefault("EMBODICHAIN_SIM_EXIT_PROCESS", "0") -@pytest.fixture(scope="session", autouse=True) +@pytest.fixture(scope="session") def _discover_task_packages(): """Discover all installed task packages once per test session. @@ -45,6 +48,14 @@ def _discover_task_packages(): execute_init_hooks() +@pytest.fixture(autouse=True) +def _discover_task_packages_for_marked_tests(request): + """Discover task packages only for tests that consume registered tasks.""" + if request.node.get_closest_marker("requires_tasks") is not None: + request.getfixturevalue("_discover_task_packages") + yield + + def pytest_addoption(parser): parser.addoption( "--renderer", @@ -68,34 +79,78 @@ def pytest_configure(config): f"Invalid renderer: {renderer}. Must be one of 'hybrid', 'fast-rt'" ) - # Override the global default renderer in the simulation config - from embodichain.lab.sim import cfg - - cfg.DEFAULT_RENDERER = renderer - # DexSim initialization is intentionally deferred to the first real-simulation # test. Most of the suite consists of pure-Python tests and should not acquire # a CUDA/Vulkan context merely because pytest has started. +_SIMULATION_MANAGER_CONSTRUCTOR = re.compile(r"\bSimulationManager\s*\(") + + +@lru_cache(maxsize=None) +def _source_constructs_real_sim(obj): + """Return whether an object's source directly constructs a simulation manager.""" + try: + return ( + _SIMULATION_MANAGER_CONSTRUCTOR.search(inspect.getsource(obj)) is not None + ) + except (OSError, TypeError): + return False + + +def _callable_constructs_real_sim(obj): + """Check a callable and the module-level helpers it directly references.""" + if _source_constructs_real_sim(obj): + return True + + code = getattr(obj, "__code__", None) + module = inspect.getmodule(obj) + if code is None or module is None: + return False + + for name in code.co_names: + helper = vars(module).get(name) + if inspect.isfunction(helper) and _source_constructs_real_sim(helper): + return True + return False + + def _requires_real_sim(item): - """Return whether a test module creates a real SimulationManager.""" + """Return whether a test item creates a real SimulationManager.""" if item.get_closest_marker("requires_sim") is not None: return True + if item.get_closest_marker("no_sim") is not None: + return False + module = getattr(item, "module", None) if module is not None and "SimulationManager" in vars(module): return True - # Some planner regression tests intentionally import the simulation manager - # inside the test body to keep their module import lightweight. - try: - return "SimulationManager" in inspect.getsource(item.obj) - except (OSError, TypeError): - return False + if _callable_constructs_real_sim(item.obj): + return True + + test_class = getattr(item, "cls", None) + if test_class is not None: + for cls in test_class.__mro__: + if _source_constructs_real_sim(cls): + return True + + fixture_info = getattr(item, "_fixtureinfo", None) + fixture_defs_by_name = getattr(fixture_info, "name2fixturedefs", {}) + for fixture_defs in fixture_defs_by_name.values(): + for fixture_def in fixture_defs: + if _callable_constructs_real_sim(fixture_def.func): + return True + + return False def _initialize_sim_engine(renderer): """Initialize DexSim once, immediately before the first real-sim test.""" + from embodichain.lab.sim import cfg + + cfg.DEFAULT_RENDERER = renderer + import dexsim import dexsim.types @@ -113,6 +168,7 @@ def _initialize_sim_engine(renderer): dexsim.init_sim_engine(sim_config) +@pytest.hookimpl(tryfirst=True) def pytest_collection_modifyitems(config, items): """Classify real-simulation tests for fast and resource-aware test selection.""" for item in items: @@ -120,6 +176,12 @@ def pytest_collection_modifyitems(config, items): requires_sim = _requires_real_sim(item) if requires_sim: item.add_marker(pytest.mark.requires_sim) + item.add_marker(pytest.mark.xdist_group(name="simulation")) + else: + module_name = getattr(item.module, "__name__", "unknown") + item.add_marker( + pytest.mark.xdist_group(name=f"module-{module_name.replace('.', '-')}") + ) if "cuda" in nodeid or "gpu" in nodeid: item.add_marker(pytest.mark.gpu) if requires_sim and ( diff --git a/tests/data_pipeline/test_online_data.py b/tests/data_pipeline/test_online_data.py index 4b931e8a6..5282cb4de 100644 --- a/tests/data_pipeline/test_online_data.py +++ b/tests/data_pipeline/test_online_data.py @@ -120,7 +120,10 @@ def _make_fake_engine( engine._close_signal = engine._mp_ctx.Event() engine._sample_count = engine._mp_ctx.Value("i", 0) - engine.start() + # Sampling tests intentionally bypass the simulation worker. Starting it + # here would exercise unrelated environment setup and make teardown wait + # for the subprocess timeout when the synthetic config cannot run a task. + engine._sim_process = None return engine diff --git a/tests/gym/envs/test_base_env.py b/tests/gym/envs/test_base_env.py index 40823bb84..b156c9ec5 100644 --- a/tests/gym/envs/test_base_env.py +++ b/tests/gym/envs/test_base_env.py @@ -37,6 +37,8 @@ NUM_ENVS = 10 +pytestmark = pytest.mark.requires_sim + @register_env("RandomReach-v1", max_episode_steps=100, override=True) class RandomReachEnv(BaseEnv): diff --git a/tests/gym/envs/test_embodied_env.py b/tests/gym/envs/test_embodied_env.py index 77b8334bf..2ecacc0a9 100644 --- a/tests/gym/envs/test_embodied_env.py +++ b/tests/gym/envs/test_embodied_env.py @@ -39,6 +39,8 @@ NUM_ENVS = 2 +pytestmark = pytest.mark.requires_sim + urdf_path = get_data_path("UniversalRobots/UR5/UR5.urdf") METADATA = { "id": "EmbodiedEnv-v1", diff --git a/tests/gym/envs/test_profiler_integration.py b/tests/gym/envs/test_profiler_integration.py index ef7fed55d..8b5b667d2 100644 --- a/tests/gym/envs/test_profiler_integration.py +++ b/tests/gym/envs/test_profiler_integration.py @@ -34,6 +34,8 @@ from embodichain.lab.sim.cfg import RobotCfg from embodichain.lab.sim.objects import Robot +pytestmark = pytest.mark.requires_sim + @register_env("ProfilerProbe-v1", max_episode_steps=100, override=True) class _ProfilerProbeEnv(BaseEnv): diff --git a/tests/gym/envs/test_replay.py b/tests/gym/envs/test_replay.py index c80e26391..be9441f92 100644 --- a/tests/gym/envs/test_replay.py +++ b/tests/gym/envs/test_replay.py @@ -21,6 +21,7 @@ from unittest.mock import MagicMock, patch import numpy as np +import pytest import torch from embodichain.data import get_data_path @@ -33,6 +34,8 @@ from embodichain.lab.gym.envs.managers.actions import DeltaQposTerm from embodichain.lab.gym.envs.managers.cfg import ActionTermCfg +pytestmark = [pytest.mark.requires_sim, pytest.mark.slow] + @register_env("ReplayTest-v1", max_episode_steps=100, override=True) class ReplayTestEnv(EmbodiedEnv): diff --git a/tests/learning/test_newton_planar_reach.py b/tests/learning/test_newton_planar_reach.py index d9067778b..f1d20ff54 100644 --- a/tests/learning/test_newton_planar_reach.py +++ b/tests/learning/test_newton_planar_reach.py @@ -351,6 +351,7 @@ def test_evaluation_uses_eval_mode_and_restores_policy_mode( assert policy.training is initial_training_mode +@pytest.mark.slow def test_apg_training_generalizes_to_held_out_reaches() -> None: result = train_planar_reach( NewtonPlanarReachTrainingCfg( diff --git a/tests/learning/test_rl.py b/tests/learning/test_rl.py index 22b916ccb..37583c66e 100644 --- a/tests/learning/test_rl.py +++ b/tests/learning/test_rl.py @@ -22,6 +22,12 @@ import tempfile from pathlib import Path +pytestmark = [ + pytest.mark.requires_sim, + pytest.mark.requires_tasks, + pytest.mark.slow, +] + class TestRLTraining: """Test suite for RL training pipeline.""" diff --git a/tests/learning/test_rl_distributed.py b/tests/learning/test_rl_distributed.py index 0fdaeb9b1..0f12027f7 100644 --- a/tests/learning/test_rl_distributed.py +++ b/tests/learning/test_rl_distributed.py @@ -62,6 +62,8 @@ def _create_minimal_distributed_config(): not torch.distributed.is_available(), reason="torch.distributed is not available", ) +@pytest.mark.gpu +@pytest.mark.slow def test_distributed_training_via_torchrun(): """Run distributed training via torchrun (subprocess) to exercise the distributed path. diff --git a/tests/learning/test_shared_rollout.py b/tests/learning/test_shared_rollout.py index 9443cceb0..bb0e5d3b5 100644 --- a/tests/learning/test_shared_rollout.py +++ b/tests/learning/test_shared_rollout.py @@ -18,6 +18,7 @@ from copy import deepcopy +import pytest import torch from tensordict import TensorDict @@ -117,6 +118,7 @@ def _make_obs(self, step: int) -> TensorDict: ) +@pytest.mark.no_sim def test_shared_rollout_collects_policy_and_env_fields(): device = torch.device("cpu") num_envs = 3 @@ -179,6 +181,8 @@ def test_shared_rollout_collects_policy_and_env_fields(): ) +@pytest.mark.requires_sim +@pytest.mark.requires_tasks def test_embodied_env_writes_next_fields_into_external_rollout(): gym_config = load_json( "embodichain_tasks/configs/agents/rl/basic/cart_pole/gym_config.json" diff --git a/tests/sim/objects/test_rigid_constraint.py b/tests/sim/objects/test_rigid_constraint.py index 3328b98e0..9911135d3 100644 --- a/tests/sim/objects/test_rigid_constraint.py +++ b/tests/sim/objects/test_rigid_constraint.py @@ -29,6 +29,8 @@ from embodichain.lab.sim.objects.constraint import RigidConstraint from embodichain.lab.sim.sim_manager import SimulationManager +pytestmark = pytest.mark.no_sim + def test_rigid_constraint_cfg_defaults(): """RigidConstraintCfg requires name + both object uids; frames default None.""" diff --git a/tests/sim/planners/test_curobo_planner.py b/tests/sim/planners/test_curobo_planner.py index 67dfc8836..f679809a7 100644 --- a/tests/sim/planners/test_curobo_planner.py +++ b/tests/sim/planners/test_curobo_planner.py @@ -99,6 +99,20 @@ """ +@pytest.fixture(scope="module", autouse=True) +def _restore_torch_precision_settings(): + """Keep cuRobo's process-wide TF32 changes local to this test module.""" + matmul_allow_tf32 = torch.backends.cuda.matmul.allow_tf32 + cudnn_allow_tf32 = torch.backends.cudnn.allow_tf32 + matmul_precision = torch.get_float32_matmul_precision() + + yield + + torch.set_float32_matmul_precision(matmul_precision) + torch.backends.cuda.matmul.allow_tf32 = matmul_allow_tf32 + torch.backends.cudnn.allow_tf32 = cudnn_allow_tf32 + + def _raise_module_not_found(*args, **kwargs): raise ModuleNotFoundError("curobo not installed") diff --git a/tests/sim/planners/test_toppra_batched.py b/tests/sim/planners/test_toppra_batched.py index d885c73bb..f9d331770 100644 --- a/tests/sim/planners/test_toppra_batched.py +++ b/tests/sim/planners/test_toppra_batched.py @@ -140,7 +140,7 @@ def _make_planner(self): from embodichain.lab.sim.robots import CobotMagicCfg sim = SimulationManager( - SimulationManagerCfg(headless=True, sim_device="cpu", num_envs=3) + SimulationManagerCfg(headless=True, sim_device="cpu", num_envs=2) ) robot = sim.add_robot( cfg=CobotMagicCfg.from_dict( @@ -156,7 +156,7 @@ def test_plan_batched_quantity_uniform_N(self): planner, sim = self._make_planner() try: - B, dofs = 3, 6 + B, dofs = 2, 6 wp = torch.zeros(B, dofs) wp[:, 0] = torch.linspace(0.0, 0.4, B) states = [ @@ -186,9 +186,9 @@ def test_plan_batched_time_tailpads(self): planner, sim = self._make_planner() try: - B, dofs = 3, 6 + B, dofs = 2, 6 wp = torch.zeros(B, dofs) - wp[:, 0] = torch.tensor([0.1, 0.4, 0.9]) # different durations + wp[:, 0] = torch.tensor([0.1, 0.9]) # different durations states = [ PlanState.from_qpos(torch.zeros(B, dofs)), PlanState.from_qpos(wp), @@ -228,6 +228,7 @@ def test_plan_batched_time_tailpads(self): om.SimulationManager.flush_cleanup_queue() + @pytest.mark.slow @pytest.mark.parametrize("mp_context", ["fork", "spawn"]) def test_plan_batched_pool_path(self, mp_context): # Exercise the real ProcessPoolExecutor branch (max_workers=2, B=3) @@ -279,6 +280,7 @@ def test_plan_batched_pool_path(self, mp_context): om.SimulationManager.flush_cleanup_queue() + @pytest.mark.slow @pytest.mark.parametrize("mp_context", ["fork", "spawn"]) def test_workers_reaped_on_gc(self, mp_context): # Regression: abandoning the planner must reap its worker processes, diff --git a/tests/sim/test_sim_manager.py b/tests/sim/test_sim_manager.py index 6a4fd5e4f..655c56ed3 100644 --- a/tests/sim/test_sim_manager.py +++ b/tests/sim/test_sim_manager.py @@ -16,6 +16,9 @@ from __future__ import annotations +import gc +import queue + from types import SimpleNamespace from unittest.mock import MagicMock @@ -43,6 +46,8 @@ (0.0, 0.0, 1.0), ) +pytestmark = pytest.mark.no_sim + class FakeCamera: """Simple camera stub for recorder unit tests.""" @@ -198,6 +203,45 @@ def _make_visualization_sim_manager() -> ( return sim, runtime +def test_flush_cleanup_queue_returns_immediately_when_no_destroy_is_pending( + monkeypatch: pytest.MonkeyPatch, +) -> None: + cleanup_queue: queue.Queue = queue.Queue() + collect = MagicMock() + wait_scene_destruction = MagicMock() + monkeypatch.setattr(SimulationManager, "_cleanup_queue", cleanup_queue) + monkeypatch.setattr(gc, "collect", collect) + monkeypatch.setattr( + SimulationManager, "wait_scene_destruction", wait_scene_destruction + ) + + SimulationManager.flush_cleanup_queue() + + collect.assert_not_called() + wait_scene_destruction.assert_not_called() + + +def test_flush_cleanup_queue_waits_after_running_pending_destroy( + monkeypatch: pytest.MonkeyPatch, +) -> None: + cleanup_queue: queue.Queue = queue.Queue() + destroy = MagicMock() + cleanup_queue.put(destroy) + collect = MagicMock() + wait_scene_destruction = MagicMock() + monkeypatch.setattr(SimulationManager, "_cleanup_queue", cleanup_queue) + monkeypatch.setattr(gc, "collect", collect) + monkeypatch.setattr( + SimulationManager, "wait_scene_destruction", wait_scene_destruction + ) + + SimulationManager.flush_cleanup_queue() + + destroy.assert_called_once_with() + collect.assert_called_once_with() + wait_scene_destruction.assert_called_once_with() + + def test_sim_update_refreshes_dirty_visualization_and_captures_current_state() -> None: sim, runtime = _make_visualization_sim_manager() @@ -308,7 +352,7 @@ def test_native_window_availability_depends_on_visualization_backend( runtime_active: bool, expected: bool, ) -> None: - sim = SimulationManager.__new__(SimulationManager) + sim = object.__new__(SimulationManager) sim.sim_config = SimpleNamespace( visualization=SimpleNamespace(backend=backend), ) @@ -318,7 +362,7 @@ def test_native_window_availability_depends_on_visualization_backend( def test_open_window_skips_viser_backend() -> None: - sim = SimulationManager.__new__(SimulationManager) + sim = object.__new__(SimulationManager) sim.sim_config = SimpleNamespace( visualization=SimpleNamespace(backend="viser"), ) @@ -332,7 +376,7 @@ def test_open_window_skips_viser_backend() -> None: def test_open_window_allows_native_backend() -> None: - sim = SimulationManager.__new__(SimulationManager) + sim = object.__new__(SimulationManager) sim.sim_config = SimpleNamespace( visualization=SimpleNamespace(backend="none"), ) @@ -352,7 +396,7 @@ def test_open_window_allows_native_backend() -> None: def test_open_window_is_idempotent() -> None: - sim = SimulationManager.__new__(SimulationManager) + sim = object.__new__(SimulationManager) sim.sim_config = SimpleNamespace( visualization=SimpleNamespace(backend="none"), ) @@ -367,7 +411,7 @@ def test_open_window_is_idempotent() -> None: def test_start_visualization_rejects_open_native_window() -> None: - sim = SimulationManager.__new__(SimulationManager) + sim = object.__new__(SimulationManager) sim.sim_config = SimpleNamespace( visualization=SimpleNamespace(backend="viser"), ) diff --git a/tests/sim/test_sim_profiler.py b/tests/sim/test_sim_profiler.py index ac398d678..bcc46a165 100644 --- a/tests/sim/test_sim_profiler.py +++ b/tests/sim/test_sim_profiler.py @@ -18,10 +18,13 @@ import types +import pytest import torch from embodichain.lab.sim import Profiler, ProfilerCfg, SimulationManager +pytestmark = pytest.mark.no_sim + class _WorldUpdateProbe: """Minimal world interface used by ``SimulationManager.update``.""" diff --git a/tests/toolkits/test_batch_convex_collision.py b/tests/toolkits/test_batch_convex_collision.py index 67116ff5f..5e6255f4a 100644 --- a/tests/toolkits/test_batch_convex_collision.py +++ b/tests/toolkits/test_batch_convex_collision.py @@ -16,15 +16,19 @@ from __future__ import annotations +import pytest import torch -from embodichain.data import get_data_path import trimesh +import warp as wp + +from embodichain.data import get_data_path from embodichain.toolkits.graspkit.pg_grasp.collision_checker import ( ConvexCollisionChecker, ConvexCollisionCheckerCfg, ) from embodichain.utils.math import transform_points_mat -import warp as wp + +pytestmark = pytest.mark.gpu def batch_convex_collision_query(device=torch.device("cuda")): From 7eef168aa29a15fd58237ff4cfc5f8b1193cb4d2 Mon Sep 17 00:00:00 2001 From: yuecideng Date: Thu, 6 Aug 2026 05:17:54 +0000 Subject: [PATCH 2/2] fix(ci): keep CUDA visible during test collection --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0801d4fb4..bc268c79b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -157,8 +157,8 @@ jobs: export HF_ENDPOINT=https://hf-mirror.com pytest tests/docs -q --confcutdir=tests/docs - echo "Pure-Python tests (GPU hidden, four workers)" - CUDA_VISIBLE_DEVICES="" pytest tests --ignore=tests/docs \ + echo "Non-simulation tests (four workers, CUDA available for native imports)" + pytest tests --ignore=tests/docs \ -m "not requires_sim and not gpu" -n 4 --dist loadgroup - name: Run real-simulation tests serially