Spaces:
Running on L40S
Running on L40S
| # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | |
| # SPDX-License-Identifier: OpenMDW-1.1 | |
| from cosmos_framework.utils.lazy_config import lazy_call | |
| lazy_call._CONVERT_TARGET_TO_STRING = True | |
| import gc | |
| import os | |
| from functools import cache | |
| from pathlib import Path | |
| import pytest | |
| from cosmos_framework.inference.fixtures.args import ALL_LEVELS, ALL_NUM_GPUS, ALLOWED_GPUS_BY_LEVEL, Args, get_args, init_args | |
| def original_datadir(request: pytest.FixtureRequest) -> Path: | |
| root_dir = request.config.rootpath | |
| relative_path = request.path.with_suffix("").relative_to(root_dir) | |
| return root_dir / "tests/data" / relative_path | |
| def _get_available_gpus() -> int: | |
| import pynvml | |
| try: | |
| pynvml.nvmlInit() | |
| device_count = pynvml.nvmlDeviceGetCount() | |
| pynvml.nvmlShutdown() | |
| return device_count | |
| except pynvml.NVMLError as e: | |
| print(f"WARNING: Failed to get available GPUs: {e}") | |
| return 0 | |
| def pytest_addoption(parser: pytest.Parser): | |
| parser.addoption("--manual", action="store_true", default=False, help="Run manual tests") | |
| parser.addoption( | |
| "--num-gpus", | |
| default=None, | |
| type=int, | |
| choices=ALL_NUM_GPUS, | |
| help="Run tests with the specified number of GPUs", | |
| ) | |
| parser.addoption("--levels", default=None, help="Run tests with the specified levels (comma-separated list)") | |
| def pytest_xdist_auto_num_workers(config: pytest.Config) -> int | None: | |
| num_gpus: int | None = config.option.num_gpus | |
| if num_gpus is None: | |
| return 1 | |
| if num_gpus == 0: | |
| return None | |
| available_gpus = _get_available_gpus() | |
| if available_gpus < num_gpus: | |
| raise ValueError(f"Not enough GPUs available. Required: {num_gpus}, Available: {available_gpus}") | |
| return available_gpus // num_gpus | |
| def pytest_configure(config: pytest.Config): | |
| args = Args.from_config(config) | |
| init_args(args) | |
| if ( | |
| args.num_gpus is not None | |
| and args.levels is not None | |
| and all(args.num_gpus not in ALLOWED_GPUS_BY_LEVEL[level] for level in args.levels) | |
| ): | |
| pytest.exit(f"No tests for {args.num_gpus} GPUs and levels {args.levels}.", returncode=0) | |
| if args.worker_id == "master": | |
| return | |
| if args.worker_index > 1: | |
| if args.num_gpus is None: | |
| raise NotImplementedError(f"Running parallel tests requires --num-gpus to be set.") | |
| # Check if there are enough GPUs available. | |
| if args.num_gpus is not None and args.num_gpus > 0: | |
| required_gpus = args.num_gpus * (args.worker_index + 1) | |
| else: | |
| required_gpus = 1 | |
| available_gpus = _get_available_gpus() | |
| if available_gpus < required_gpus: | |
| raise ValueError(f"Not enough GPUs available. Required: {required_gpus}, Available: {available_gpus}") | |
| # Limit threading to reduce contention | |
| import torch | |
| torch.set_num_threads(1) | |
| torch.set_num_interop_threads(1) | |
| def _get_marker(item: pytest.Item, name: str) -> pytest.Mark | None: | |
| markers = list(item.iter_markers(name=name)) | |
| if not markers: | |
| return None | |
| marker = markers[0] | |
| for other_marker in markers[1:]: | |
| if other_marker != marker: | |
| raise ValueError(f"Multiple different markers found for {name}: {markers}") | |
| return marker | |
| def _parse_level_marker(mark: pytest.Mark) -> int: | |
| if len(mark.args) != 1: | |
| raise ValueError(f"Invalid arguments: {mark.args}") | |
| if mark.kwargs: | |
| raise ValueError(f"Invalid keyword arguments: {mark.kwargs}") | |
| level = mark.args[0] | |
| if level not in ALL_LEVELS: | |
| raise ValueError(f"Invalid level {level} not in {ALL_LEVELS}") | |
| return level | |
| def _parse_gpus_marker(mark: pytest.Mark) -> int: | |
| if len(mark.args) != 1: | |
| raise ValueError(f"Invalid arguments: {mark.args}") | |
| if mark.kwargs: | |
| raise ValueError(f"Invalid keyword arguments: {mark.kwargs}") | |
| required_gpus = int(mark.args[0]) | |
| if required_gpus not in ALL_NUM_GPUS: | |
| raise ValueError(f"Invalid number of GPUs {required_gpus} not in {ALL_NUM_GPUS}") | |
| return required_gpus | |
| def pytest_collection_modifyitems(config: pytest.Config, items: list[pytest.Item]): | |
| args = get_args() | |
| for item in items: | |
| manual_mark = _get_marker(item, "manual") | |
| level_mark = _get_marker(item, "level") | |
| gpus_mark = _get_marker(item, "gpus") | |
| try: | |
| level = _parse_level_marker(level_mark) if level_mark else 0 | |
| gpus = _parse_gpus_marker(gpus_mark) if gpus_mark else 0 | |
| except ValueError as e: | |
| pytest.fail(f"Invalid marker on test {item.name}: {e}") | |
| assert False, "unreachable" | |
| allowed_gpus = ALLOWED_GPUS_BY_LEVEL[level] | |
| if gpus not in allowed_gpus: | |
| pytest.fail(f"Level {level} tests must have {allowed_gpus} GPUs, but {item.name} has {gpus} GPUs") | |
| # Check if the test should be skipped | |
| if not args.enable_manual and manual_mark is not None: | |
| item.add_marker(pytest.mark.skip(reason="test requires --manual")) | |
| if args.levels is not None and level not in args.levels: | |
| item.add_marker(pytest.mark.skip(reason=f"test requires --levels={level}")) | |
| if args.num_gpus is not None and gpus != args.num_gpus: | |
| item.add_marker(pytest.mark.skip(reason=f"test requires --num-gpus={gpus}")) | |
| available_gpus = _get_available_gpus() | |
| if gpus > available_gpus: | |
| item.add_marker( | |
| pytest.mark.skip(reason=f"test requires {gpus} GPUs, but only {available_gpus} are available") | |
| ) | |
| # Exclude skipped tests | |
| selected_items = [] | |
| deselected_items = [] | |
| for item in items: | |
| if item.get_closest_marker("skip"): | |
| deselected_items.append(item) | |
| continue | |
| selected_items.append(item) | |
| items[:] = selected_items | |
| config.hook.pytest_deselected(items=deselected_items) | |
| def pytest_runtest_setup(item: pytest.Item): | |
| import torch | |
| args = get_args() | |
| # Distributed tests launched via torchrun manage their own per-rank device | |
| # (each rank calls torch.cuda.set_device(rank/LOCAL_RANK)). We must NOT pin | |
| # CUDA_VISIBLE_DEVICES here or every rank would see only GPU 0, and rank>0's | |
| # set_device(rank) crashes with "invalid device ordinal". torchrun sets RANK | |
| # in the environment, so use it to detect the distributed launch and leave | |
| # device selection to the test. | |
| if "RANK" in os.environ: | |
| return | |
| gpus_mark = item.get_closest_marker(name="gpus") | |
| try: | |
| gpus = _parse_gpus_marker(gpus_mark) if gpus_mark else 0 | |
| except ValueError as e: | |
| pytest.fail(f"Invalid marker on test {item.name}: {e}") | |
| assert False, "unreachable" | |
| # Limit the number of GPUs used by the test | |
| if gpus > 0: | |
| device_start = args.worker_index * gpus | |
| device_end = device_start + gpus | |
| os.environ["CUDA_VISIBLE_DEVICES"] = ",".join(map(str, range(device_start, device_end))) | |
| os.environ["NUM_GPUS"] = str(gpus) | |
| else: | |
| device = 0 | |
| os.environ["CUDA_VISIBLE_DEVICES"] = str(device) | |
| os.environ["NUM_GPUS"] = "1" | |
| test_max_processes = int(os.environ.get("TEST_MAX_PROCESSES", "8")) | |
| device_memory_fraction = 1 / max(args.worker_count, test_max_processes) | |
| os.environ["DEVICE_MEMORY_FRACTION"] = str(device_memory_fraction) | |
| # Guard the GPU-only call so CPU-only test runs (e.g. the cpu-tests CI | |
| # job on a GPU-less runner) don't crash in setup; a no-op when a GPU is | |
| # present, so GPU CI behavior is unchanged. | |
| if torch.cuda.is_available(): | |
| torch.cuda.set_per_process_memory_fraction(device_memory_fraction) | |
| def init_cosmos_test(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): | |
| from cosmos_framework.inference.common.init import _init_log_console, _init_log_files | |
| monkeypatch.setenv("IMAGINAIRE_OUTPUT_ROOT", str(tmp_path / "imaginaire4-output")) | |
| _init_log_console() | |
| _init_log_files(tmp_path) | |
| yield | |
| def init_torch_test(): | |
| import torch | |
| from cosmos_framework.inference.common.init import set_seed | |
| # Reproducibility | |
| set_seed(0) | |
| # Restore autograd in case a previously-imported/-run module left it | |
| # globally disabled (e.g. inference/ray/serve.py calls | |
| # torch.set_grad_enabled(False) at import time), which would otherwise break | |
| # later tests that need backward(). | |
| torch.set_grad_enabled(True) | |
| yield | |
| # Cleanup memory | |
| gc.collect() | |
| if torch.cuda.is_available(): | |
| torch.cuda.empty_cache() | |
| _WHITELIST_ENV_VARS = { | |
| "LD_LIBRARY_PATH", | |
| "QT_QPA_FONTDIR", | |
| "QT_QPA_PLATFORM_PLUGIN_PATH", | |
| "TORCHINDUCTOR_CACHE_DIR", | |
| "TRITON_CACHE_DIR", # set by Triton during flex-attention compilation | |
| "NVTE_CUDA_INCLUDE_DIR", # set by Transformer Engine during its CUDA extension setup | |
| } | |
| def detect_env_modifications(): | |
| original_env = dict(os.environ) | |
| yield | |
| new_env = dict(os.environ) | |
| for env in [original_env, new_env]: | |
| for k in list(env.keys()): | |
| if k.startswith("PYTEST_") or k in _WHITELIST_ENV_VARS: | |
| del env[k] | |
| if new_env != original_env: | |
| added, removed, modified = _compare_dict(new_env, original_env) | |
| os.environ.clear() | |
| os.environ.update(original_env) | |
| raise ValueError( | |
| f"Environment variables modified by test! Use 'monkeypatch.setenv' to temporarily modify environment variables. \n" | |
| f"Added: {added}\n" | |
| f"Removed: {removed}\n" | |
| f"Modified: {modified}" | |
| ) | |
| def _compare_dict(actual: dict[str, str], expected: dict[str, str]) -> tuple[set[str], set[str], set[str]]: | |
| added = set(actual) - set(expected) | |
| removed = set(expected) - set(actual) | |
| modified = {k for k in expected if k in actual and expected[k] != actual[k]} | |
| return added, removed, modified | |