diff --git a/vllm_spyre_next/pyproject.toml b/vllm_spyre_next/pyproject.toml index 7eb841297..ca8c8ffad 100644 --- a/vllm_spyre_next/pyproject.toml +++ b/vllm_spyre_next/pyproject.toml @@ -25,6 +25,12 @@ dynamic = ["version"] [project.entry-points."vllm.platform_plugins"] spyre_next = "vllm_spyre_next:register" +[project.entry-points."vllm.general_plugins"] +spyre_next_ops = "vllm_spyre_next:register_ops" + +[project.entry-points."pytest11"] +spyre_next_test = "vllm_spyre_next.testing.pytest_plugin" + [tool.setuptools.packages.find] where = ["."] # list of folders that contain the packages (["."] by default) include = ["vllm_spyre_next*"] # package names should match these glob patterns (["*"] by default) @@ -88,10 +94,9 @@ extra-build-variables = { vllm = { VLLM_TARGET_DEVICE = "cpu" } } # vLLM and torch-spyre must be pulled from github to be built from source # NB: torch-spyre can only be compiled where `sendnn` is available [tool.uv.sources] -# This is the unreleased v0.16.0 tag with 2.10 support -vllm = { git = "https://github.com/vllm-project/vllm", rev = "2d5be1dd5ce2e44dfea53ea03ff61143da5137eb" } +vllm = { git = "https://github.com/vllm-project/vllm", rev = "v0.17.1" } # see https://github.com/torch-spyre/torch-spyre/pull/746 -torch-spyre = { git = "https://github.com/torch-spyre/torch-spyre", rev = "bd7502baa9123d292e56dbd4928147969ecdf63c" } +torch-spyre = { git = "https://github.com/torch-spyre/torch-spyre", rev = "c1f67bb4701630179d53dbfc060dde62fc2d7d18" } torch = [ { index = "pytorch-cpu" }, ] @@ -190,7 +195,6 @@ asyncio_default_fixture_loop_scope = "function" filterwarnings = [ "ignore::_pytest.warning_types.PytestUnknownMarkWarning" ] -addopts = "-m 'spyre or upstream_passing'" # --8<-- [start:test-markers-definition] markers = [ @@ -222,7 +226,8 @@ plugins.md007.indent = 4 [dependency-groups] dev = [ - "pytest" + "pytest", + "pyyaml", ] upstream-tests = [ "albumentations>=1.4.6", diff --git a/vllm_spyre_next/tests/conftest.py b/vllm_spyre_next/tests/conftest.py deleted file mode 100644 index 38916b016..000000000 --- a/vllm_spyre_next/tests/conftest.py +++ /dev/null @@ -1,299 +0,0 @@ -""" -Tests for vllm-spyre-next including upstream vLLM tests - -This conftest.py provides automatic integration of upstream vLLM tests into the -vllm-spyre-next test suite. It clones the vLLM repository at a specific commit -and dynamically injects those tests into the pytest collection. - -For usage instructions and configuration options, see: -docs/vllm_spyre_next/contributing/README.md#testing -""" - -import os -import re -import subprocess -import sys -import tempfile -import time -from pathlib import Path - -import pytest - - -# Global logger for pytest terminal output -_terminal_reporter = None - - -def _log(msg: str): - """Log message to pytest terminal reporter if available. - This allows logs to be printed from this file even when pytest is capturing output. - """ - if _terminal_reporter: - _terminal_reporter.write_line(msg) - else: - # Fallback to stderr when terminal reporter not available - print(msg, file=sys.stderr) - - -def _cache_root() -> Path: - """ - Cache directory for cloned tests (sticky between runs) - """ - # Respect XDG if present, fallback to ~/.cache - xdg = os.environ.get("XDG_CACHE_HOME") - base = Path(xdg) if xdg else Path.home() / ".cache" - return base / "vllm-upstream-tests" - - -def _extract_vllm_commit_from_pyproject() -> str | None: - """ - Extract the vLLM git commit SHA from pyproject.toml [tool.uv.sources] section. - Returns None if not found or parseable. - """ - pyproject_path = Path(__file__).parent.parent / "pyproject.toml" - if not pyproject_path.exists(): - return None - - try: - content = pyproject_path.read_text() - # Look for vllm source with git and rev - # Pattern: vllm = { git = "...", rev = "commit_sha" } - match = re.search( - r'vllm\s*=\s*\{\s*git\s*=\s*"[^"]+"\s*,\s*rev\s*=\s*"([0-9a-f]{7,40})"\s*\}', content - ) - if match: - return match.group(1) - except Exception: - pass - - return None - - -def _resolve_vllm_commit() -> str: - """ - Resolve the vLLM commit SHA to use for cloning upstream tests. - Priority: VLLM_COMMIT env var > pyproject.toml > error - """ - # Allow env var override for testing/CI - env_commit = os.environ.get("VLLM_COMMIT", "").strip() - if env_commit: - if not re.match(r"^[0-9a-f]{7,40}$", env_commit): - raise ValueError(f"Invalid VLLM_COMMIT format: {env_commit}") - return env_commit - - # Extract from pyproject.toml - sha = _extract_vllm_commit_from_pyproject() - if sha: - return sha - - # Fail with clear instructions - raise RuntimeError( - "Could not resolve vLLM commit. Either:\n" - " 1. Set VLLM_COMMIT= environment variable, or\n" - " 2. Ensure vllm is specified with 'rev' in pyproject.toml [tool.uv.sources]" - ) - - -def _run(cmd: list[str], cwd: Path | None = None, max_retries: int = 3) -> None: - """Run command with optional retries for network operations.""" - for attempt in range(max_retries): - try: - subprocess.run(cmd, cwd=str(cwd) if cwd else None, check=True) - return - except subprocess.CalledProcessError: - if attempt < max_retries - 1: - time.sleep(2**attempt) # Exponential backoff: 1s, 2s, 4s - else: - raise - - -def _ensure_repo_at_commit(repo_dir: Path, url: str, commit: str, sparse_paths: list[str]) -> Path: - """ - Ensure repo cloned at 'repo_dir/commit' with sparse checkout of 'sparse_paths'. - Returns the path to the working tree at that commit. - """ - # We create a separate worktree per commit to allow co-existence of different commits - base_dir = repo_dir - base_dir.mkdir(parents=True, exist_ok=True) - git_dir = base_dir / "repo.git" - - if not git_dir.exists(): - _run(["git", "init", "--bare", str(git_dir)]) - - # Prepare a worktree dir per commit - wt_dir = base_dir / f"worktree-{commit[:12]}" - if wt_dir.exists(): - # Already prepared; assume valid - _log(f"[vllm-upstream] Using cached worktree at {wt_dir}") - return wt_dir - - # Create temp dir to set up the sparse worktree then move into place atomically - with tempfile.TemporaryDirectory(dir=str(base_dir)) as td: - td_path = Path(td) - _run(["git", "--git-dir", str(git_dir), "remote", "add", "origin", url]) - _log(f"[vllm-upstream] Fetching commit {commit[:12]} from {url}") - _run(["git", "--git-dir", str(git_dir), "fetch", "--depth=1", "origin", commit]) - - # Create a new worktree at temp - _run( - ["git", "--git-dir", str(git_dir), "worktree", "add", "--detach", str(td_path), commit] - ) - - # Enable sparse checkout at the worktree - _run(["git", "sparse-checkout", "init", "--cone"], cwd=td_path) - _run(["git", "sparse-checkout", "set", *sparse_paths], cwd=td_path) - - # Ensure we're exactly at the commit (detached HEAD) - _run(["git", "checkout", "--detach", commit], cwd=td_path) - - # Atomically move into place - td_path.rename(wt_dir) - - return wt_dir - - -def _prepare_upstream_tests_dir() -> Path: - commit = _resolve_vllm_commit() - cache_root = _cache_root() - wt_dir = _ensure_repo_at_commit( - repo_dir=cache_root, - url=os.environ.get("VLLM_REPO_URL", "https://github.com/vllm-project/vllm"), - commit=commit, - sparse_paths=["tests"], - ) - tests_dir = wt_dir / "tests" - if not tests_dir.is_dir(): - raise RuntimeError(f"Upstream tests directory not found at {tests_dir}") - return tests_dir - - -# ------------------------------- -# Pytest hooks -# ------------------------------- - - -@pytest.hookimpl(trylast=True) -def pytest_configure(config): - """ - Clone vLLM and inject upstream tests into the test session. - This runs early in pytest initialization. - - Configure via environment variables: - - SKIP_UPSTREAM_TESTS: Set to "1" or "true" to skip cloning and running upstream tests - - UPSTREAM_TESTS_PATHS: Comma-separated paths (default: "models/language/generation") - """ - global _terminal_reporter - _terminal_reporter = config.pluginmanager.get_plugin("terminalreporter") - - # Check if upstream tests should be skipped entirely - skip_upstream = os.environ.get("SKIP_UPSTREAM_TESTS", "").lower() in ("1", "true", "yes") - if skip_upstream: - _log("[vllm-upstream] SKIP_UPSTREAM_TESTS is set, skipping upstream test collection") - config._upstream_tests_base = None - return - - try: - # Comma separated list of upstream paths - DEFAULT_UPSTREAM_TESTS_PATHS = "models/language/generation" - - # Ensure VLLM_PLUGINS is set to spyre_next for all tests - os.environ["VLLM_PLUGINS"] = "spyre_next" - - # Get list of paths to include from upstream tests - paths_env = os.environ.get("UPSTREAM_TESTS_PATHS", DEFAULT_UPSTREAM_TESTS_PATHS).strip() - upstream_paths = [p.strip() for p in paths_env.split(",") if p.strip()] - - if not upstream_paths: - _log("[vllm-upstream] No upstream test paths specified, skipping") - config._upstream_tests_base = None - return - - upstream_tests_base = _prepare_upstream_tests_dir() - - # Add each configured path to test collection - for rel_path in upstream_paths: - upstream_tests_dir = upstream_tests_base / rel_path - if not upstream_tests_dir.exists(): - _log(f"[vllm-upstream] Warning: Path not found: {upstream_tests_dir}") - continue - - _log(f"[vllm-upstream] Including tests from: {rel_path}") - config.args.append(str(upstream_tests_dir)) - - # Store upstream test base path for use in pytest_collection_modifyitems - config._upstream_tests_base = upstream_tests_base - - except Exception as e: - # Fail early with a readable message - raise SystemExit(f"[vllm-upstream] Failed to prepare upstream tests: {e}") from e - - -def pytest_collection_modifyitems(config, items): - """ - Mark all upstream tests with 'upstream' marker. - Mark subset of tests matching regex patterns with 'upstream_passing' marker. - - Can configure passing patterns via UPSTREAM_PASSING_PATTERNS env var with - comma-separated regex patterns - Example: "test_basic.*,test_simple_generation" - """ - upstream_tests_base = getattr(config, "_upstream_tests_base", None) - if not upstream_tests_base: - return - - # Get passing test patterns from environment and compile as regex - DEFAULT_UPSTREAM_PASSING_PATTERN = "facebook" - patterns_env = os.environ.get( - "UPSTREAM_PASSING_PATTERNS", DEFAULT_UPSTREAM_PASSING_PATTERN - ).strip() - passing_patterns = [] - if patterns_env: - for pattern_str in patterns_env.split(","): - pattern_str = pattern_str.strip() - if pattern_str: - try: - passing_patterns.append(re.compile(pattern_str)) - except re.error as e: - _log(f"[vllm-upstream] Warning: Invalid regex pattern '{pattern_str}': {e}") - - upstream_marker = pytest.mark.upstream - passing_marker = pytest.mark.upstream_passing - - marked_count = 0 - passing_count = 0 - - for item in items: - # Check if test is from upstream directory - test_path = Path(item.fspath) - if test_path.is_relative_to(upstream_tests_base): - # Mark as upstream - item.add_marker(upstream_marker) - marked_count += 1 - - # Check if test matches any passing pattern (regex) - test_nodeid = item.nodeid - if passing_patterns and any( - pattern.search(test_nodeid) for pattern in passing_patterns - ): - item.add_marker(passing_marker) - passing_count += 1 - - # Update node ID to include vLLM path prefix - rel_path = test_path.relative_to(upstream_tests_base) - vllm_prefix = f"VLLM_UPSTREAM/tests/{rel_path}" - # Replace the file path portion of nodeid with the prefixed version - original_nodeid = item.nodeid - # Extract the test name part (after ::) - if "::" in original_nodeid: - _, test_part = original_nodeid.split("::", 1) - item._nodeid = f"{vllm_prefix}::{test_part}" - else: - item._nodeid = vllm_prefix - - if marked_count > 0: - _log(f"[vllm-upstream] Marked {marked_count} tests as 'upstream'") - if passing_count > 0: - _log(f"[vllm-upstream] Marked {passing_count} tests as 'upstream_passing'") - - -# Made with Bob diff --git a/vllm_spyre_next/tests/test_rms_norm.py b/vllm_spyre_next/tests/test_rms_norm.py new file mode 100644 index 000000000..60560b7ab --- /dev/null +++ b/vllm_spyre_next/tests/test_rms_norm.py @@ -0,0 +1,123 @@ +""" +Test SpyreRMSNorm custom op correctness against a reference implementation. +""" + +import pytest +import torch + + +def reference_rms_norm( + x: torch.Tensor, + weight: torch.Tensor | None, + eps: float, + residual: torch.Tensor | None = None, +) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]: + """Golden reference: standard RMSNorm in PyTorch.""" + if residual is not None: + x = x + residual + x_float = x.float() + variance = x_float.pow(2).mean(dim=-1, keepdim=True) + x_normed = x_float * torch.rsqrt(variance + eps) + if weight is not None: + x_normed = x_normed * weight.float() + if residual is not None: + return x_normed, x.float() + return x_normed + + +@pytest.mark.spyre +@pytest.mark.rmsnorm +@pytest.mark.parametrize("batch_size", [1]) +# Hidden sizes that aren't multiples of 64 currently fail on CI with size errors +# @pytest.mark.parametrize("hidden_size", [63, 64, 65, 127, 128, 129, 256, 512]) +@pytest.mark.parametrize("hidden_size", [64, 128, 256, 512]) +@pytest.mark.parametrize("use_residual", [False, True]) +def test_spyre_rmsnorm_matches_reference( + default_vllm_config, batch_size, hidden_size, use_residual +): + """SpyreRMSNorm output matches golden reference. + + Tests both paths: + - forward(): custom op dispatch (no-compile path via torch.ops.vllm.spyre_rmsnorm) + - forward_native(): direct Spyre device execution + """ + from vllm_spyre_next.custom_ops.rms_norm import SpyreRMSNorm + + eps = 1e-6 + torch.manual_seed(42) + + x = torch.randn(batch_size, hidden_size, dtype=torch.float16) + layer = SpyreRMSNorm(hidden_size, eps=eps) + residual = torch.randn(batch_size, hidden_size, dtype=torch.float32) if use_residual else None + + expected = reference_rms_norm(x, layer.weight.data, eps, residual) + actual = layer.forward_native(x, residual) + + if use_residual: + expected_norm, expected_resid = expected + actual_norm, actual_resid = actual + torch.testing.assert_close(actual_norm.float(), expected_norm.float(), atol=1e-2, rtol=1e-2) + torch.testing.assert_close( + actual_resid.float(), expected_resid.float(), atol=1e-2, rtol=1e-2 + ) + else: + torch.testing.assert_close(actual.float(), expected.float(), atol=1e-2, rtol=1e-2) + + actual_forward = layer.forward(x, residual) + if use_residual: + actual_fwd_norm, actual_fwd_resid = actual_forward + torch.testing.assert_close( + actual_fwd_norm.float(), expected_norm.float(), atol=1e-2, rtol=1e-2 + ) + torch.testing.assert_close( + actual_fwd_resid.float(), expected_resid.float(), atol=1e-2, rtol=1e-2 + ) + else: + torch.testing.assert_close(actual_forward.float(), expected.float(), atol=1e-2, rtol=1e-2) + + +@pytest.fixture +def dummy_tensor(): + return torch.randn(4, 128, dtype=torch.float32) + + +def mock_forward_native_no_residual(x, residual=None): + """Mock: return x + 1 (no residual path).""" + return x + 1 + + +def mock_forward_native_with_residual(x, residual=None): + """Mock: return (2 * x, 2 * residual) (residual path).""" + return 2 * x, 2 * residual + + +@pytest.mark.spyre +@pytest.mark.rmsnorm +@pytest.mark.parametrize("use_residual", [False, True]) +def test_rmsnorm_oot_dispatch(default_vllm_config, monkeypatch, dummy_tensor, use_residual): + """Verify RMSNorm OOT registration: class swap and forward_oot routing.""" + from vllm.model_executor.layers.layernorm import RMSNorm + from vllm_spyre_next.custom_ops.rms_norm import SpyreRMSNorm + + layer = RMSNorm(128, eps=1e-6) + + # OOT class swap: RMSNorm.__new__ should produce SpyreRMSNorm + assert isinstance(layer, SpyreRMSNorm) + + # dispatch_forward should have selected forward_oot + assert layer._forward_method == layer.forward_oot + + residual = torch.randn(4, 128, dtype=torch.float32) if use_residual else None + + # Mock forward_native (called by forward_oot) with a known transform + if residual is not None: + monkeypatch.setattr(layer, "forward_native", mock_forward_native_with_residual) + out_x, out_residual = layer.forward_oot(dummy_tensor, residual) + + assert torch.allclose(out_x, 2 * dummy_tensor) + assert torch.allclose(out_residual, 2 * residual) + else: + monkeypatch.setattr(layer, "forward_native", mock_forward_native_no_residual) + out_x = layer.forward_oot(dummy_tensor, residual) + + assert torch.allclose(out_x, dummy_tensor + 1) diff --git a/vllm_spyre_next/tests/test_vllm_spyre_next.py b/vllm_spyre_next/tests/test_vllm_spyre_next.py index 884bb74b6..00dda936a 100644 --- a/vllm_spyre_next/tests/test_vllm_spyre_next.py +++ b/vllm_spyre_next/tests/test_vllm_spyre_next.py @@ -4,6 +4,7 @@ @pytest.mark.spyre +@pytest.mark.uses_subprocess def test_basic_model_load(): model = LLM("ibm-ai-platform/micro-g3.3-8b-instruct-1b", max_model_len=128, max_num_seqs=2) diff --git a/vllm_spyre_next/uv.lock b/vllm_spyre_next/uv.lock index e54a953c6..7b0fc1959 100644 --- a/vllm_spyre_next/uv.lock +++ b/vllm_spyre_next/uv.lock @@ -53,7 +53,7 @@ overrides = [ { name = "setuptools", specifier = ">=82" }, { name = "torch", marker = "platform_machine not in 's390x, ppc64le'", specifier = "==2.10.0", index = "https://download.pytorch.org/whl/cpu" }, { name = "triton", marker = "sys_platform == 'never'" }, - { name = "vllm", marker = "platform_machine not in 's390x, ppc64le'", git = "https://github.com/vllm-project/vllm?rev=2d5be1dd5ce2e44dfea53ea03ff61143da5137eb" }, + { name = "vllm", marker = "platform_machine not in 's390x, ppc64le'", git = "https://github.com/vllm-project/vllm?rev=v0.17.1" }, ] build-constraints = [{ name = "torch", specifier = "==2.10.0", index = "https://download.pytorch.org/whl/cpu" }] @@ -3263,6 +3263,37 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" }, ] +[[package]] +name = "kaldi-native-fbank" +version = "1.22.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/2c/84076b352107ce12d56f28c313f1aca1be332d953dd96aec7b84976e6d53/kaldi-native-fbank-1.22.3.tar.gz", hash = "sha256:387bf87225c6b83c93ae652eeaef1b4d531994b6e398e7a77189de340674f9af", size = 71013, upload-time = "2025-10-09T02:31:21.487Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/d0/07ab65d7c8389f56f8c772a55f8846a81c24d973abecfc0275c2c833f63e/kaldi_native_fbank-1.22.3-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:6b9ef5b6302ee45628a51a4484cb4f41006af02141508939c09ce36899fb3f41", size = 245879, upload-time = "2025-10-09T02:28:04.7Z" }, + { url = "https://files.pythonhosted.org/packages/64/2b/3132083b930fa6411f14469f36c465b7d2fba29a8a3e121d8fd6baffc8ea/kaldi_native_fbank-1.22.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:29452f2900e771086e9022dde17a92d191217ab3e34ca7dc361bd9be53e94fb4", size = 229180, upload-time = "2025-10-09T02:29:35.356Z" }, + { url = "https://files.pythonhosted.org/packages/e3/53/720ffbe8b30de203570f397866334eb4c6364c9214699010f2086de911ff/kaldi_native_fbank-1.22.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48e5dd8e897bf4509be2c6eeb4bbab728eaaef1f214ae0510c96219c4253d17", size = 299054, upload-time = "2025-10-09T02:28:42.011Z" }, + { url = "https://files.pythonhosted.org/packages/52/3f/beb161e4fdf6710938ccf18418c147d87ba8f102903d6c6e4eda25588e22/kaldi_native_fbank-1.22.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ce84c65779c9eed6ec02699797a4ba1859451977537a993be3ea8167a210ec3e", size = 321921, upload-time = "2025-10-09T02:31:21.646Z" }, + { url = "https://files.pythonhosted.org/packages/3b/bb/ee42418b77dbfc5ff619857b8eb372af98a88d47c8ca8b9a2d3ca2936c96/kaldi_native_fbank-1.22.3-cp311-cp311-win32.whl", hash = "sha256:516bce595eb5e5899a91dfec1142bea56a2fa232e53425e9966785aee8cd024e", size = 273018, upload-time = "2025-10-09T02:30:31.979Z" }, + { url = "https://files.pythonhosted.org/packages/40/68/da630b035cd343311168e5fe02c39fe7b192638717e3202de92ccf8ae18e/kaldi_native_fbank-1.22.3-cp311-cp311-win_amd64.whl", hash = "sha256:bd225d0624d45b533c1780094b3c59666276a6e9f20222943441212cdf301c9e", size = 303342, upload-time = "2025-10-09T02:28:17.429Z" }, + { url = "https://files.pythonhosted.org/packages/c2/de/fbdbfcc75fad9d9a6f9a250bc986f1002902581eaa47a5948f53a7f11851/kaldi_native_fbank-1.22.3-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:7f636ccdea28bd187f93b06a1e4b9275e42e43af9405b0684fc739e829299c4b", size = 249003, upload-time = "2025-10-09T02:29:48.509Z" }, + { url = "https://files.pythonhosted.org/packages/77/64/e57ce185dda028b7b9af72cdfb16825bfa52183653945681e7cb8e7c2dfa/kaldi_native_fbank-1.22.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:abd31a8bfe1db62a7ddb0beee84f3a5de9bb559fcdd2b96ca0fb729c551b9412", size = 228933, upload-time = "2025-10-09T02:31:35.8Z" }, + { url = "https://files.pythonhosted.org/packages/43/28/6f4fd8953c0b3f30de4526fd024095032abcdc25b6736c77a891687c604e/kaldi_native_fbank-1.22.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5a44b4a83cf9bf13d3f77858928068b06d3ec2238c27ff2e39393fbf7749c9f", size = 298887, upload-time = "2025-10-09T02:30:53.739Z" }, + { url = "https://files.pythonhosted.org/packages/84/90/01ef7331c52b1eaf9916f3f7a535155aac2e9e2ddad12a141613d92758c7/kaldi_native_fbank-1.22.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f16e74372fe9e20abb4183f98a8e2288d5ee4c48d04d94b6160311170e007661", size = 322002, upload-time = "2025-10-09T02:30:13.04Z" }, + { url = "https://files.pythonhosted.org/packages/66/1c/fce142bd3aeadb1292360a90ceb91f923c8e12081c21576fe69917243c5f/kaldi_native_fbank-1.22.3-cp312-cp312-win32.whl", hash = "sha256:a90f51377569575fc0d1a66ef7e89a36102bfb6dcd1d15d6c4afb930ce726672", size = 273308, upload-time = "2025-10-09T02:29:59.931Z" }, + { url = "https://files.pythonhosted.org/packages/cb/8d/c0b0b6280edabad85d7e15093fad612c027e175fe4e0b960ce2f36485143/kaldi_native_fbank-1.22.3-cp312-cp312-win_amd64.whl", hash = "sha256:cbbeea19fe6d584c54e93fe6615a7185b10e0d78fdb6471f9e44596018437c38", size = 308023, upload-time = "2025-10-09T02:28:43.909Z" }, + { url = "https://files.pythonhosted.org/packages/0d/df/4110f685067946c8b2e59ed76cebdf51c979ae999d90f65208a9d1966cba/kaldi_native_fbank-1.22.3-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:78ca163686a4aa1693194d098aa79b517845d851aa6fd27d5b162c05e1012361", size = 249056, upload-time = "2025-10-09T02:28:02.202Z" }, + { url = "https://files.pythonhosted.org/packages/d8/74/ef21aabdd2f32539735e2ed4d3ea072112d4e3d30dfc2d17695f6d9df072/kaldi_native_fbank-1.22.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3768ea99933aa25080cb820f93f7b612968633b9a4fa23bc8a7337e2137f3fbb", size = 229011, upload-time = "2025-10-09T02:31:50.593Z" }, + { url = "https://files.pythonhosted.org/packages/9a/72/adb11d27c545aca1db442da744ee430a6aae377a33574bfd2ec159dcf673/kaldi_native_fbank-1.22.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f74b85948328ab4b4c88522f98a59f83dd5295443b08483e945c7de2c35e5dcc", size = 299276, upload-time = "2025-10-09T02:30:38.1Z" }, + { url = "https://files.pythonhosted.org/packages/bc/1e/496c7ae814b2a7f8f47d423dc33aae2cdfb1edf898e2faaf5c5b39b90363/kaldi_native_fbank-1.22.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e3f9c6551ff5b6ae785dd15f819c3b2b7432d77bfb79ea8806748e2c7d900b5d", size = 322714, upload-time = "2025-10-09T02:30:32.698Z" }, + { url = "https://files.pythonhosted.org/packages/75/47/3fcb52e0ef081efa40b4ca5c04f207509d31157f33f3ac314578d93794f9/kaldi_native_fbank-1.22.3-cp313-cp313-win32.whl", hash = "sha256:a63d5bd6b5bd5f7c0e0af886c12c3f686fbc62347f6b886fed2694ab2f0dbd14", size = 273293, upload-time = "2025-10-09T02:30:13.979Z" }, + { url = "https://files.pythonhosted.org/packages/63/48/20bfa3f8d88605e2ec2c274c343dec1f112077e687440d64d3caa4b9136c/kaldi_native_fbank-1.22.3-cp313-cp313-win_amd64.whl", hash = "sha256:4fb769337c7d482166ada8ba041003e4a9de3a778dc970b6b5802a382e581724", size = 308032, upload-time = "2025-10-09T02:29:03.278Z" }, + { url = "https://files.pythonhosted.org/packages/b9/7e/d47f64d5332b2527e6b65490888d99793eb3280bca735d0b69348eaeb6a3/kaldi_native_fbank-1.22.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2efa8368cdd46a32c37a28c4baaa508b0a294ab1ca2aefddd3e97f62cfebc27b", size = 249216, upload-time = "2025-10-09T02:28:22.008Z" }, + { url = "https://files.pythonhosted.org/packages/78/9f/f98f72ba5a90a39675e82f2175dc5ec99a85892a88b9ccdd25f2dc916c82/kaldi_native_fbank-1.22.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8f6086073ec658a23d22f8657b3ee8c6ba69d65be57324a7284209ac7424b5ac", size = 229289, upload-time = "2025-10-09T02:31:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/d6/4b/1f3f17a7b601124df88112a1d1fcb543c8d908d6674f752f7d3322991770/kaldi_native_fbank-1.22.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:41fb506fde155d97aeef95dd6ceccc38c2c5dd4401f9b8fded9bacaf1bafef36", size = 300037, upload-time = "2025-10-09T02:30:10.203Z" }, + { url = "https://files.pythonhosted.org/packages/2b/6a/374ec4e1cf13e672f5acd8272116c1885c2a7f84be491fc652415fc6e870/kaldi_native_fbank-1.22.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f1cc2b8eeec52a33868cf59bb95d40b335fa9cff7e15a6208e0e9b67b7fd7236", size = 322854, upload-time = "2025-10-09T02:31:26.003Z" }, + { url = "https://files.pythonhosted.org/packages/63/2a/edd85a2292d2af28af68214a3bdd029ab8ce2e6bc5aaac77255aa57ce964/kaldi_native_fbank-1.22.3-cp314-cp314-win32.whl", hash = "sha256:d6387ab52b56e2978524590e11b24cf03419d9e9361965bc8d6ff34ff9e867da", size = 279733, upload-time = "2025-10-09T02:29:41.855Z" }, +] + [[package]] name = "kaleido" version = "1.0.0" @@ -4609,6 +4640,132 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f2/35/0858e9e71b36948eafbc5e835874b63e515179dc3b742cbe3d76bc683439/opencv_python_headless-4.12.0.88-cp37-abi3-win_amd64.whl", hash = "sha256:86b413bdd6c6bf497832e346cd5371995de148e579b9774f8eba686dee3f5528", size = 38923559, upload-time = "2025-07-07T09:15:25.229Z" }, ] +[[package]] +name = "opentelemetry-api" +version = "1.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/1d/4049a9e8698361cc1a1aa03a6c59e4fa4c71e0c0f94a30f988a6876a2ae6/opentelemetry_api-1.40.0.tar.gz", hash = "sha256:159be641c0b04d11e9ecd576906462773eb97ae1b657730f0ecf64d32071569f", size = 70851, upload-time = "2026-03-04T14:17:21.555Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/bf/93795954016c522008da367da292adceed71cca6ee1717e1d64c83089099/opentelemetry_api-1.40.0-py3-none-any.whl", hash = "sha256:82dd69331ae74b06f6a874704be0cfaa49a1650e1537d4a813b86ecef7d0ecf9", size = 68676, upload-time = "2026-03-04T14:17:01.24Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp" +version = "1.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-exporter-otlp-proto-grpc" }, + { name = "opentelemetry-exporter-otlp-proto-http" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d0/37/b6708e0eff5c5fb9aba2e0ea09f7f3bcbfd12a592d2a780241b5f6014df7/opentelemetry_exporter_otlp-1.40.0.tar.gz", hash = "sha256:7caa0870b95e2fcb59d64e16e2b639ecffb07771b6cd0000b5d12e5e4fef765a", size = 6152, upload-time = "2026-03-04T14:17:23.235Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/fc/aea77c28d9f3ffef2fdafdc3f4a235aee4091d262ddabd25882f47ce5c5f/opentelemetry_exporter_otlp-1.40.0-py3-none-any.whl", hash = "sha256:48c87e539ec9afb30dc443775a1334cc5487de2f72a770a4c00b1610bf6c697d", size = 7023, upload-time = "2026-03-04T14:17:03.612Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-proto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/bc/1559d46557fe6eca0b46c88d4c2676285f1f3be2e8d06bb5d15fbffc814a/opentelemetry_exporter_otlp_proto_common-1.40.0.tar.gz", hash = "sha256:1cbee86a4064790b362a86601ee7934f368b81cd4cc2f2e163902a6e7818a0fa", size = 20416, upload-time = "2026-03-04T14:17:23.801Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/ca/8f122055c97a932311a3f640273f084e738008933503d0c2563cd5d591fc/opentelemetry_exporter_otlp_proto_common-1.40.0-py3-none-any.whl", hash = "sha256:7081ff453835a82417bf38dccf122c827c3cbc94f2079b03bba02a3165f25149", size = 18369, upload-time = "2026-03-04T14:17:04.796Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-grpc" +version = "1.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "grpcio" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8f/7f/b9e60435cfcc7590fa87436edad6822240dddbc184643a2a005301cc31f4/opentelemetry_exporter_otlp_proto_grpc-1.40.0.tar.gz", hash = "sha256:bd4015183e40b635b3dab8da528b27161ba83bf4ef545776b196f0fb4ec47740", size = 25759, upload-time = "2026-03-04T14:17:24.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/6f/7ee0980afcbdcd2d40362da16f7f9796bd083bf7f0b8e038abfbc0300f5d/opentelemetry_exporter_otlp_proto_grpc-1.40.0-py3-none-any.whl", hash = "sha256:2aa0ca53483fe0cf6405087a7491472b70335bc5c7944378a0a8e72e86995c52", size = 20304, upload-time = "2026-03-04T14:17:05.942Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-http" +version = "1.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/fa/73d50e2c15c56be4d000c98e24221d494674b0cc95524e2a8cb3856d95a4/opentelemetry_exporter_otlp_proto_http-1.40.0.tar.gz", hash = "sha256:db48f5e0f33217588bbc00274a31517ba830da576e59503507c839b38fa0869c", size = 17772, upload-time = "2026-03-04T14:17:25.324Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/3a/8865d6754e61c9fb170cdd530a124a53769ee5f740236064816eb0ca7301/opentelemetry_exporter_otlp_proto_http-1.40.0-py3-none-any.whl", hash = "sha256:a8d1dab28f504c5d96577d6509f80a8150e44e8f45f82cdbe0e34c99ab040069", size = 19960, upload-time = "2026-03-04T14:17:07.153Z" }, +] + +[[package]] +name = "opentelemetry-proto" +version = "1.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4c/77/dd38991db037fdfce45849491cb61de5ab000f49824a00230afb112a4392/opentelemetry_proto-1.40.0.tar.gz", hash = "sha256:03f639ca129ba513f5819810f5b1f42bcb371391405d99c168fe6937c62febcd", size = 45667, upload-time = "2026-03-04T14:17:31.194Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/b2/189b2577dde745b15625b3214302605b1353436219d42b7912e77fa8dc24/opentelemetry_proto-1.40.0-py3-none-any.whl", hash = "sha256:266c4385d88923a23d63e353e9761af0f47a6ed0d486979777fe4de59dc9b25f", size = 72073, upload-time = "2026-03-04T14:17:16.673Z" }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/fd/3c3125b20ba18ce2155ba9ea74acb0ae5d25f8cd39cfd37455601b7955cc/opentelemetry_sdk-1.40.0.tar.gz", hash = "sha256:18e9f5ec20d859d268c7cb3c5198c8d105d073714db3de50b593b8c1345a48f2", size = 184252, upload-time = "2026-03-04T14:17:31.87Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/c5/6a852903d8bfac758c6dc6e9a68b015d3c33f2f1be5e9591e0f4b69c7e0a/opentelemetry_sdk-1.40.0-py3-none-any.whl", hash = "sha256:787d2154a71f4b3d81f20524a8ce061b7db667d24e46753f32a7bc48f1c1f3f1", size = 141951, upload-time = "2026-03-04T14:17:17.961Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.61b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/c0/4ae7973f3c2cfd2b6e321f1675626f0dab0a97027cc7a297474c9c8f3d04/opentelemetry_semantic_conventions-0.61b0.tar.gz", hash = "sha256:072f65473c5d7c6dc0355b27d6c9d1a679d63b6d4b4b16a9773062cb7e31192a", size = 145755, upload-time = "2026-03-04T14:17:32.664Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/37/cc6a55e448deaa9b27377d087da8615a3416d8ad523d5960b78dbeadd02a/opentelemetry_semantic_conventions-0.61b0-py3-none-any.whl", hash = "sha256:fa530a96be229795f8cef353739b618148b0fe2b4b3f005e60e262926c4d38e2", size = 231621, upload-time = "2026-03-04T14:17:19.33Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions-ai" +version = "0.4.15" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-sdk" }, + { name = "opentelemetry-semantic-conventions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/75/455c15f8360b475dd31101a87eab316420388486f7941bf019cbf4e63d5b/opentelemetry_semantic_conventions_ai-0.4.15.tar.gz", hash = "sha256:12de172d1e11d21c6e82bbf578c7e8a713589a7fda76af9ed785632564a28b81", size = 18595, upload-time = "2026-03-02T15:36:50.254Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/49/819fb212386f77cfd93f81bd916d674f0e735f87c8ac2262ed14e3b852c2/opentelemetry_semantic_conventions_ai-0.4.15-py3-none-any.whl", hash = "sha256:011461f1fba30f27035c49ab3b8344367adc72da0a6c8d3c7428303c6779edc9", size = 5999, upload-time = "2026-03-02T15:36:51.44Z" }, +] + [[package]] name = "optuna" version = "3.6.1" @@ -8611,7 +8768,7 @@ wheels = [ [[package]] name = "torch-spyre" version = "0.0.1" -source = { git = "https://github.com/torch-spyre/torch-spyre?rev=bd7502baa9123d292e56dbd4928147969ecdf63c#bd7502baa9123d292e56dbd4928147969ecdf63c" } +source = { git = "https://github.com/torch-spyre/torch-spyre?rev=c1f67bb4701630179d53dbfc060dde62fc2d7d18#c1f67bb4701630179d53dbfc060dde62fc2d7d18" } dependencies = [ { name = "numpy" }, { name = "psutil" }, @@ -8791,12 +8948,12 @@ dependencies = [ { name = "torch", version = "2.10.0", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "platform_machine != 'aarch64' and platform_machine not in 's390x, ppc64le' and sys_platform == 'darwin'" }, ] wheels = [ - { url = "https://download.pytorch.org/whl/cpu/torchvision-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a76ce7b8d4fce291a25721ee2f921c783acc6dbd4fc32dc741ed2a1d5a8dde2f" }, - { url = "https://download.pytorch.org/whl/cpu/torchvision-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:724f212a58a0d0d758649ce288601056b5f46a01de545702f42bccc5b25cb0cc" }, - { url = "https://download.pytorch.org/whl/cpu/torchvision-0.25.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6c444119c6af8fa48b79c59f1529fc15a45a2bbf823cf85f851e7203d84f727f" }, - { url = "https://download.pytorch.org/whl/cpu/torchvision-0.25.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:80895a40faa5783ce19ed5a692a54062c7888a385490e866324355f8d59b2eb3" }, - { url = "https://download.pytorch.org/whl/cpu/torchvision-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a64ae543fe0a057a93954af94c239629723db196ed65090e6775f136726b22f8" }, - { url = "https://download.pytorch.org/whl/cpu/torchvision-0.25.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b7df71942e31b74a5161dea3452da35e5fb1d36c752aedf61f2dd3c7be35739a" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a76ce7b8d4fce291a25721ee2f921c783acc6dbd4fc32dc741ed2a1d5a8dde2f" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:724f212a58a0d0d758649ce288601056b5f46a01de545702f42bccc5b25cb0cc" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.25.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6c444119c6af8fa48b79c59f1529fc15a45a2bbf823cf85f851e7203d84f727f" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.25.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:80895a40faa5783ce19ed5a692a54062c7888a385490e866324355f8d59b2eb3" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a64ae543fe0a057a93954af94c239629723db196ed65090e6775f136726b22f8" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.25.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b7df71942e31b74a5161dea3452da35e5fb1d36c752aedf61f2dd3c7be35739a" }, ] [[package]] @@ -8843,24 +9000,24 @@ dependencies = [ { name = "torch", version = "2.10.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "(platform_machine == 'aarch64' and platform_machine not in 's390x, ppc64le') or (platform_machine not in 's390x, ppc64le' and sys_platform != 'darwin')" }, ] wheels = [ - { url = "https://download.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:59be99d1c470ef470b134468aa6afa6f968081a503acb4ee883d70332f822e35" }, - { url = "https://download.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:aa016ab73e06a886f72edc8929ed2ed4c85aaaa6e10500ecdef921b03129b19e" }, - { url = "https://download.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp311-cp311-win_amd64.whl", hash = "sha256:c7eb5f219fdfaf1f65e68c00eb81172ab4fa08a9874dae9dad2bca360da34d0f" }, - { url = "https://download.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:727334e9a721cfc1ac296ce0bf9e69d9486821bfa5b1e75a8feb6f78041db481" }, - { url = "https://download.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:c1be164e93c68b2dbf460fd58975377c892dbcf3358fb72941709c3857351bba" }, - { url = "https://download.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp312-cp312-win_amd64.whl", hash = "sha256:2d444009c0956669ada149f61ed78f257c1cc96d259efa6acf3929ca96ceb3f0" }, - { url = "https://download.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:fe54cbd5942cd0b26a90f1748f0d4421caf67be35c281c6c3b8573733a03d630" }, - { url = "https://download.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:90eec299e1f82cfaf080ccb789df3838cb9a54b57e2ebe33852cd392c692de5c" }, - { url = "https://download.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp313-cp313-win_amd64.whl", hash = "sha256:783c8fc580bbfc159bff52f4f72cdd538e42b32956e70dffa42b940db114e151" }, - { url = "https://download.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:e985e12a9a232618e5a43476de5689e4b14989f5da6b93909c57afa57ec27012" }, - { url = "https://download.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:813f0106eb3e268f3783da67b882458e544c6fb72f946e6ca64b5ed4e62c6a77" }, - { url = "https://download.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp313-cp313t-win_amd64.whl", hash = "sha256:9212210f417888e6261c040495180f053084812cf873dedba9fc51ff4b24b2d3" }, - { url = "https://download.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:0c2d0da9bc011a0fde1d125af396a8fbe94d99becf9d313764f24ca7657a3448" }, - { url = "https://download.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:4d72a57a8f0b5146e26dac1fbfa2c905280cd04f5fcb23b9c56253506b683aeb" }, - { url = "https://download.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp314-cp314-win_amd64.whl", hash = "sha256:499eae1e535766391b6ee2d1e6e841239c20e2e6d88203a15b8f9f8d60a1f8bd" }, - { url = "https://download.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:7d47d544899fabac52ebe0d4812975608fd7ab79a3d7fb6383275eb667e33f53" }, - { url = "https://download.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:9511339b3b5eb75229e0b5041202e8aed9bef3b1de3a715b9fb319c9e97688fd" }, - { url = "https://download.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp314-cp314t-win_amd64.whl", hash = "sha256:fb9f07f6a10f0ac24ac482ae68c6df99110b74a0d80a4c64fddc9753267d8815" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:59be99d1c470ef470b134468aa6afa6f968081a503acb4ee883d70332f822e35" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:aa016ab73e06a886f72edc8929ed2ed4c85aaaa6e10500ecdef921b03129b19e" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp311-cp311-win_amd64.whl", hash = "sha256:c7eb5f219fdfaf1f65e68c00eb81172ab4fa08a9874dae9dad2bca360da34d0f" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:727334e9a721cfc1ac296ce0bf9e69d9486821bfa5b1e75a8feb6f78041db481" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:c1be164e93c68b2dbf460fd58975377c892dbcf3358fb72941709c3857351bba" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp312-cp312-win_amd64.whl", hash = "sha256:2d444009c0956669ada149f61ed78f257c1cc96d259efa6acf3929ca96ceb3f0" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:fe54cbd5942cd0b26a90f1748f0d4421caf67be35c281c6c3b8573733a03d630" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:90eec299e1f82cfaf080ccb789df3838cb9a54b57e2ebe33852cd392c692de5c" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp313-cp313-win_amd64.whl", hash = "sha256:783c8fc580bbfc159bff52f4f72cdd538e42b32956e70dffa42b940db114e151" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:e985e12a9a232618e5a43476de5689e4b14989f5da6b93909c57afa57ec27012" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:813f0106eb3e268f3783da67b882458e544c6fb72f946e6ca64b5ed4e62c6a77" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp313-cp313t-win_amd64.whl", hash = "sha256:9212210f417888e6261c040495180f053084812cf873dedba9fc51ff4b24b2d3" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:0c2d0da9bc011a0fde1d125af396a8fbe94d99becf9d313764f24ca7657a3448" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:4d72a57a8f0b5146e26dac1fbfa2c905280cd04f5fcb23b9c56253506b683aeb" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp314-cp314-win_amd64.whl", hash = "sha256:499eae1e535766391b6ee2d1e6e841239c20e2e6d88203a15b8f9f8d60a1f8bd" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:7d47d544899fabac52ebe0d4812975608fd7ab79a3d7fb6383275eb667e33f53" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:9511339b3b5eb75229e0b5041202e8aed9bef3b1de3a715b9fb319c9e97688fd" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp314-cp314t-win_amd64.whl", hash = "sha256:fb9f07f6a10f0ac24ac482ae68c6df99110b74a0d80a4c64fddc9753267d8815" }, ] [[package]] @@ -9139,8 +9296,8 @@ wheels = [ [[package]] name = "vllm" -version = "0.16.0rc4.dev1+g2d5be1dd5.cpu" -source = { git = "https://github.com/vllm-project/vllm?rev=2d5be1dd5ce2e44dfea53ea03ff61143da5137eb#2d5be1dd5ce2e44dfea53ea03ff61143da5137eb" } +version = "0.17.1+cpu" +source = { git = "https://github.com/vllm-project/vllm?rev=v0.17.1#95c0f928cdeeaa21c4906e73cee6a156e1b3b995" } dependencies = [ { name = "aiohttp" }, { name = "anthropic" }, @@ -9159,6 +9316,7 @@ dependencies = [ { name = "grpcio-reflection" }, { name = "ijson" }, { name = "intel-openmp", marker = "platform_machine == 'x86_64'" }, + { name = "kaldi-native-fbank" }, { name = "lark" }, { name = "llguidance", marker = "platform_machine == 'aarch64' or platform_machine == 'arm64' or platform_machine == 'ppc64le' or platform_machine == 's390x' or platform_machine == 'x86_64'" }, { name = "lm-format-enforcer" }, @@ -9172,6 +9330,10 @@ dependencies = [ { name = "openai" }, { name = "openai-harmony" }, { name = "opencv-python-headless" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp" }, + { name = "opentelemetry-sdk" }, + { name = "opentelemetry-semantic-conventions-ai" }, { name = "outlines-core" }, { name = "partial-json-parser" }, { name = "pillow" }, @@ -9220,6 +9382,7 @@ dependencies = [ [package.dev-dependencies] dev = [ { name = "pytest" }, + { name = "pyyaml" }, ] upstream-tests = [ { name = "albumentations" }, @@ -9283,13 +9446,16 @@ upstream-tests = [ [package.metadata] requires-dist = [ { name = "torch", index = "https://download.pytorch.org/whl/cpu" }, - { name = "torch-spyre", git = "https://github.com/torch-spyre/torch-spyre?rev=bd7502baa9123d292e56dbd4928147969ecdf63c" }, + { name = "torch-spyre", git = "https://github.com/torch-spyre/torch-spyre?rev=c1f67bb4701630179d53dbfc060dde62fc2d7d18" }, { name = "torchvision", index = "https://download.pytorch.org/whl/cpu" }, - { name = "vllm", git = "https://github.com/vllm-project/vllm?rev=2d5be1dd5ce2e44dfea53ea03ff61143da5137eb" }, + { name = "vllm", git = "https://github.com/vllm-project/vllm?rev=v0.17.1" }, ] [package.metadata.requires-dev] -dev = [{ name = "pytest" }] +dev = [ + { name = "pytest" }, + { name = "pyyaml" }, +] upstream-tests = [ { name = "albumentations", specifier = ">=1.4.6" }, { name = "arctic-inference", specifier = "==0.1.1" }, diff --git a/vllm_spyre_next/vllm_spyre_next/__init__.py b/vllm_spyre_next/vllm_spyre_next/__init__.py index 32f1d9a1a..5fda4cc0a 100644 --- a/vllm_spyre_next/vllm_spyre_next/__init__.py +++ b/vllm_spyre_next/vllm_spyre_next/__init__.py @@ -14,6 +14,13 @@ def register(): return "vllm_spyre_next.platform.TorchSpyrePlatform" +def register_ops(): + """Register OOT custom ops for Spyre.""" + from vllm_spyre_next.custom_ops import register_all + + register_all() + + def _init_logging(): """Setup logging, extending from the vLLM logging config""" config = dict[str, Any]() diff --git a/vllm_spyre_next/vllm_spyre_next/custom_ops/rms_norm.py b/vllm_spyre_next/vllm_spyre_next/custom_ops/rms_norm.py index 7d30eb673..8ed04734f 100644 --- a/vllm_spyre_next/vllm_spyre_next/custom_ops/rms_norm.py +++ b/vllm_spyre_next/vllm_spyre_next/custom_ops/rms_norm.py @@ -42,6 +42,7 @@ from vllm.config import get_current_vllm_config from vllm.forward_context import get_forward_context from vllm.model_executor.layers.layernorm import RMSNorm +from functools import lru_cache from .utils import convert_for_spyre, convert_from_spyre @@ -66,7 +67,7 @@ def __init__(self, *args, **kwargs): logger.debug("Building custom RMS norm") - self._fwd_spyre = torch.compile(self.forward_static, dynamic=False) + self._fwd_spyre = torch.compile(self.forward_spyre, dynamic=False) logger.warning( "SpyreRMSNorm: no dtype promotion is performed, \ @@ -140,7 +141,7 @@ def forward_impl( output.copy_(result) @staticmethod - def forward_static( + def forward_spyre( x: torch.Tensor, variance_epsilon: float, hidden_size: int, @@ -183,7 +184,8 @@ def forward_static( x_var = x[:, :, :variance_size_override] - variance = x_var.pow(2).mean(dim=-1, keepdim=True) + # After transpose, hidden dim is now dim=0 + variance = x_var.pow(2).mean(dim=0, keepdim=True) x = x * torch.rsqrt(variance + variance_epsilon) x = x.transpose(-1, -2).contiguous() @@ -224,14 +226,15 @@ def forward_native( if self.variance_size_override is not None: raise NotImplementedError("TODO: variance_size_override not yet implemented") - batch_padding = x.shape[0] + orig_batch_size = x.shape[0] # Pad to minimum batch size of 64 (Spyre constraint) + # Pad at END so original data stays at indices [0:orig_batch_size] if x.shape[0] < 64: - batch_padding = 64 - x.shape[0] - x = torch.nn.functional.pad(x, (0, 0, batch_padding, 0)) + pad_amount = 64 - x.shape[0] + x = torch.nn.functional.pad(x, (0, 0, 0, pad_amount)) if residual is not None: - residual = torch.nn.functional.pad(residual, (0, 0, batch_padding, 0)) + residual = torch.nn.functional.pad(residual, (0, 0, 0, pad_amount)) # Execute compiled kernel on Spyre device # convert_for_spyre: CPU tensor -> Spyre device (float16) @@ -246,9 +249,9 @@ def forward_native( # Transfer back to CPU and restore original shape return pytree.tree_map( - lambda el: el[:batch_padding, :], + lambda el: el[:orig_batch_size, :], convert_from_spyre(outs, dtype=x_dtype, device=x_device), - )[0] + ) def forward_oot( self, @@ -299,6 +302,7 @@ def spyre_rmsnorm_fake( return +@lru_cache(maxsize=1) def register(): """Register the spyre_rmsnorm custom op with vLLM. diff --git a/vllm_spyre_next/vllm_spyre_next/custom_ops/silu_and_mul.py b/vllm_spyre_next/vllm_spyre_next/custom_ops/silu_and_mul.py index f88e3cab1..e366c3f82 100644 --- a/vllm_spyre_next/vllm_spyre_next/custom_ops/silu_and_mul.py +++ b/vllm_spyre_next/vllm_spyre_next/custom_ops/silu_and_mul.py @@ -38,6 +38,7 @@ from vllm.config import get_current_vllm_config from vllm.forward_context import get_forward_context from vllm.model_executor.layers.activation import SiluAndMul +from functools import lru_cache from .utils import convert_for_spyre, convert_from_spyre @@ -216,6 +217,7 @@ def spyre_siluandmul_fake( return +@lru_cache(maxsize=1) def register(): """Register the spyre_siluandmul custom op with vLLM. diff --git a/vllm_spyre_next/vllm_spyre_next/platform.py b/vllm_spyre_next/vllm_spyre_next/platform.py index 85046173b..024d225e7 100644 --- a/vllm_spyre_next/vllm_spyre_next/platform.py +++ b/vllm_spyre_next/vllm_spyre_next/platform.py @@ -2,6 +2,7 @@ from typing import TYPE_CHECKING from string import Template import multiprocessing +import importlib.metadata # When running this plugin on a Mac, we assume it's for local development # purposes. However, due to a compatibility issue with vLLM, which overrides @@ -66,11 +67,11 @@ def log_server_boot(cls, vllm_config: VllmConfig) -> None: message = logo_template.substitute(colors) - from vllm_spyre_next import _version + version = importlib.metadata.version("vllm_spyre_next") - model_name = vllm_config.model_config.model + model_name = vllm_config.model_config.model if vllm_config.model_config else "N/A" - logger.info(message, _version.version, model_name) + logger.info(message, version, model_name) @classmethod def check_and_update_config(cls, vllm_config: VllmConfig) -> None: diff --git a/vllm_spyre_next/vllm_spyre_next/testing/__init__.py b/vllm_spyre_next/vllm_spyre_next/testing/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/vllm_spyre_next/vllm_spyre_next/testing/models.py b/vllm_spyre_next/vllm_spyre_next/testing/models.py new file mode 100644 index 000000000..07581cb23 --- /dev/null +++ b/vllm_spyre_next/vllm_spyre_next/testing/models.py @@ -0,0 +1,91 @@ +"""Data models for the vllm-spyre-next test infrastructure.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +# --------------------------------------------------------------------------- +# Upstream YAML config model +# --------------------------------------------------------------------------- + + +@dataclass(frozen=True) +class ParamSkip: + """A parameter-level skip rule within an allow_list entry.""" + + param_name: str + values: frozenset[Any] + + +@dataclass(frozen=True) +class ParamAllow: + """A parameter-level allow rule within an allow_list entry. + + Only test cases with parameter values in this allowlist will run. + If specified, this takes precedence over param_skips for the same parameter. + """ + + param_name: str + values: frozenset[Any] + + +@dataclass(frozen=True) +class ParamOverride: + """Replace upstream parametrize values with Spyre-compatible ranges.""" + + param_name: str + values: tuple[Any, ...] + + +@dataclass(frozen=True) +class AllowEntry: + """An allow_list entry for an upstream test function. + + Attributes: + test: fnmatch glob matched against the test function name. + mode: mandatory_pass | xfail | xfail_strict. + tags: Free-form labels for traceability (no runtime effect). + param_skips: Parameter combinations to skip within this test. + param_allows: Parameter combinations to allow (whitelist). If specified, + only these parameter values will run. + param_overrides: Parameter values to replace upstream defaults with. + """ + + test: str + mode: str = "mandatory_pass" + tags: tuple[str, ...] = () + param_skips: tuple[ParamSkip, ...] = () + param_allows: tuple[ParamAllow, ...] = () + param_overrides: tuple[ParamOverride, ...] = () + + +@dataclass(frozen=True) +class BlockEntry: + """A block_list entry — test function to skip entirely.""" + + test: str + + +@dataclass(frozen=True) +class FileConfig: + """Filter configuration for a single upstream test file. + + Attributes: + rel_path: Path relative to upstream repo root + (e.g. "tests/kernels/core/test_layernorm.py"). + allow_list: Tests allowed to run from this file. + block_list: Tests blocked from running (takes precedence over allow_list). + """ + + rel_path: str + allow_list: tuple[AllowEntry, ...] = () + block_list: tuple[BlockEntry, ...] = () + + +@dataclass(frozen=True) +class UpstreamTestConfig: + """Top-level upstream test filter configuration loaded from YAML.""" + + files: tuple[FileConfig, ...] = () diff --git a/vllm_spyre_next/vllm_spyre_next/testing/pytest_plugin.py b/vllm_spyre_next/vllm_spyre_next/testing/pytest_plugin.py new file mode 100644 index 000000000..52c44bbf3 --- /dev/null +++ b/vllm_spyre_next/vllm_spyre_next/testing/pytest_plugin.py @@ -0,0 +1,612 @@ +""" +pytest11 plugin for vllm-spyre-next. + +This plugin integrates upstream vLLM tests with vLLM-spyre-next +and filtering via a declarative YAML config (upstream_tests.yaml). + +Hook Execution Order +--------------------- +1. pytest_configure (tryfirst) + - Loads Spyre plugins (custom ops, platform) + - Detects local vLLM repo OR clones to ~/.cache/vllm-upstream-tests/ + - Injects test paths into pytest collection + +2. pytest_generate_tests (tryfirst) + - Overrides test parameters from YAML (e.g., num_tokens: [1, 16]) + - Must run during collection before parametrization finalizes + +3. pytest_collection_modifyitems + - Applies skip/xfail markers based on YAML allow_list/block_list + - Applies tag markers for filtering (e.g., pytest -m rmsnorm) + +4. pytest_fixture_setup (tryfirst) + - Overrides default_vllm_config fixture with Spyre-specific config + +Environment Variables +--------------------- +SKIP_UPSTREAM_TESTS Set to 1/true/yes to skip upstream test cloning +UPSTREAM_TESTS_PATHS Comma-separated paths (default: auto from YAML) +VLLM_COMMIT Override vLLM commit (default: from pyproject.toml) +VLLM_REPO_URL Override vLLM repo URL +XDG_CACHE_HOME Base cache directory (default: ~/.cache) +""" + +from __future__ import annotations + +import fnmatch +import os +import re +import subprocess +import sys +import tempfile +import time +from pathlib import Path + +import pytest +import yaml + +from vllm_spyre_next.testing.models import ( + AllowEntry, + BlockEntry, + FileConfig, + ParamAllow, + ParamOverride, + ParamSkip, + UpstreamTestConfig, +) + +_YAML_FILENAME = "upstream_tests.yaml" +_YAML_PATH = Path(__file__).parent / _YAML_FILENAME + +# Global terminal reporter for pytest-aware logging +_terminal_reporter = None + + +# --------------------------------------------------------------------------- +# Logging +# --------------------------------------------------------------------------- + + +def _log(msg: str): + """Log message to pytest terminal reporter if available. + This allows logs to be printed from this file even when pytest is capturing output. + """ + if _terminal_reporter: + _terminal_reporter.write_line(msg) + else: + # Fallback to stderr when terminal reporter not available + print(msg, file=sys.stderr) + + +# --------------------------------------------------------------------------- +# YAML Config Loading +# --------------------------------------------------------------------------- + + +def _load_upstream_config() -> UpstreamTestConfig: + with open(_YAML_PATH) as f: + raw = yaml.safe_load(f) + if not raw or "tests" not in raw or "files" not in raw["tests"]: + raise RuntimeError( + f'Invalid YAML in {_YAML_PATH}: missing "tests" or "tests.files" sections' + ) + return _parse_config(raw["tests"]) + + +def _parse_config(raw_tests: dict) -> UpstreamTestConfig: + files: list[FileConfig] = [] + for file_entry in raw_tests.get("files", []): + allow_list: list[AllowEntry] = [] + for allow in file_entry.get("allow_list", []): + params_section = allow.get("params", {}) + param_skips = [ + ParamSkip(param_name=k, values=frozenset(v)) + for k, v in params_section.get("skip", {}).items() + ] + param_allows = [ + ParamAllow(param_name=k, values=frozenset(v)) + for k, v in params_section.get("allow", {}).items() + ] + param_overrides = [ + ParamOverride(param_name=k, values=tuple(v)) + for k, v in params_section.get("override", {}).items() + ] + allow_list.append( + AllowEntry( + test=allow["test"], + mode=allow.get("mode", "mandatory_pass"), + tags=tuple(allow.get("tags", [])), + param_skips=tuple(param_skips), + param_allows=tuple(param_allows), + param_overrides=tuple(param_overrides), + ) + ) + block_list = [BlockEntry(test=b["test"]) for b in file_entry.get("block_list", [])] + files.append( + FileConfig( + rel_path=file_entry["rel_path"], + allow_list=tuple(allow_list), + block_list=tuple(block_list), + ) + ) + return UpstreamTestConfig(files=tuple(files)) + + +_UPSTREAM_CONFIG: UpstreamTestConfig = _load_upstream_config() + + +def _get_paths_from_yaml() -> str: + """Extract test paths from upstream_tests.yaml rel_path entries. + + Uses exact rel_path (file or folder), e.g.: + "tests/kernels/core/test_layernorm.py" -> "kernels/core/test_layernorm.py" + "tests/models/" -> "models/" + """ + paths = [] + for fc in _UPSTREAM_CONFIG.files: + p = Path(fc.rel_path) + # Strip "tests/" prefix if present + if p.parts and p.parts[0] == "tests": + paths.append(str(Path(*p.parts[1:]))) + else: + paths.append(str(p)) + return ",".join(paths) + + +# --------------------------------------------------------------------------- +# vLLM Repository Cloning +# --------------------------------------------------------------------------- + + +def _cache_root() -> Path: + """ + Cache directory for cloned tests (sticky between runs) + """ + # Respect XDG if present, fallback to ~/.cache + xdg = os.environ.get("XDG_CACHE_HOME") + base = Path(xdg) if xdg else Path.home() / ".cache" + return base / "vllm-upstream-tests" + + +def _extract_vllm_commit_from_pyproject() -> str: + """ + Extract the vLLM git reference from pyproject.toml [tool.uv.sources] section. + Returns None if not found or parseable. + """ + pyproject_path = Path(__file__).parent.parent.parent / "pyproject.toml" + if not pyproject_path.exists(): + raise FileNotFoundError( + f"pyproject.toml not found in {Path(__file__).parent.parent.parent}" + ) + + content = pyproject_path.read_text() + # Look for vllm source with git and rev + # Pattern: vllm = { git = "...", rev = "commit_sha_or_semver_tag" } + match = re.search( + r'vllm\s*=\s*\{\s*git\s*=\s*"[^"]+"\s*,\s*rev\s*=\s*"([0-9a-f]{7,40}|v\d+\.\d+\.\d+(?:-[a-zA-Z0-9.]+)?)"\s*\}', + content, + ) + if match: + return match.group(1) + + raise KeyError("Ensure vllm is specified with 'rev' in pyproject.toml [tool.uv.sources]") + + +def _resolve_vllm_commit() -> str: + """ + Resolve the vLLM git reference to use for cloning upstream tests. + Priority: VLLM_COMMIT env var > pyproject.toml > error + """ + # Allow env var override for testing/CI + env_commit = os.environ.get("VLLM_COMMIT", "").strip() + if env_commit: + if not re.match(r"^(?:[0-9a-f]{7,40}|v\d+\.\d+\.\d+(?:-[a-zA-Z0-9.]+)?)$", env_commit): + raise ValueError(f"Invalid VLLM_COMMIT format: {env_commit}") + return env_commit + + # Extract from pyproject.toml + return _extract_vllm_commit_from_pyproject() + + +def _run(cmd: list[str], cwd: Path | None = None, max_retries: int = 3) -> None: + """Run command with optional retries for network operations.""" + for attempt in range(max_retries): + try: + subprocess.run(cmd, cwd=str(cwd) if cwd else None, check=True) + return + except subprocess.CalledProcessError: + if attempt < max_retries - 1: + time.sleep(2**attempt) # Exponential backoff: 1s, 2s, 4s + else: + raise + + +def _ensure_repo_at_commit(repo_dir: Path, url: str, commit: str, sparse_paths: list[str]) -> Path: + """ + Ensure repo cloned at 'repo_dir/commit' with sparse checkout of 'sparse_paths'. + Returns the path to the working tree at that commit. + """ + # We create a separate worktree per commit to allow co-existence of different commits + base_dir = repo_dir + base_dir.mkdir(parents=True, exist_ok=True) + git_dir = base_dir / "repo.git" + + if not git_dir.exists(): + _run(["git", "init", "--bare", str(git_dir)]) + + # Prepare a worktree dir per commit + wt_dir = base_dir / f"worktree-{commit[:12]}" + if wt_dir.exists(): + _log(f"[vllm-upstream] Using cached worktree at {wt_dir}") + return wt_dir + + # Create temp dir to set up the sparse worktree then move into place atomically + with tempfile.TemporaryDirectory(dir=str(base_dir)) as td: + td_path = Path(td) + + # Ensure origin remote exists and points to the correct URL + result = subprocess.run( + ["git", "--git-dir", str(git_dir), "remote", "get-url", "origin"], + capture_output=True, + text=True, + ) + if result.returncode != 0: + # Origin doesn't exist - add it + _run(["git", "--git-dir", str(git_dir), "remote", "add", "origin", url]) + elif result.stdout.strip() != url: + # Origin exists but points to different URL - update it + _log(f"[vllm-upstream] Updating origin URL: {result.stdout.strip()} -> {url}") + _run(["git", "--git-dir", str(git_dir), "remote", "set-url", "origin", url]) + + # Determine if commit is a tag (starts with 'v' and matches semver pattern) or a SHA + is_tag = re.match(r"^v\d+\.\d+\.\d+(?:-[a-zA-Z0-9.]+)?$", commit) + + if is_tag: + _log(f"[vllm-upstream] Fetching tag {commit} from {url}") + # For tags, fetch the tag reference + _run( + [ + "git", + "--git-dir", + str(git_dir), + "fetch", + "--depth=1", + "origin", + f"refs/tags/{commit}:refs/tags/{commit}", + ] + ) + else: + _log(f"[vllm-upstream] Fetching commit {commit[:12]} from {url}") + # For commit SHAs, fetch the commit directly + _run(["git", "--git-dir", str(git_dir), "fetch", "--depth=1", "origin", commit]) + + # Create a new worktree at temp + # For tags, use the full tag reference; for commits, use the commit SHA directly + worktree_ref = f"refs/tags/{commit}" if is_tag else commit + _run( + [ + "git", + "--git-dir", + str(git_dir), + "worktree", + "add", + "--detach", + str(td_path), + worktree_ref, + ] + ) + + # Enable sparse checkout at the worktree + _run(["git", "sparse-checkout", "init", "--cone"], cwd=td_path) + _run(["git", "sparse-checkout", "set", *sparse_paths], cwd=td_path) + + # Ensure we're exactly at the commit (detached HEAD) + _run(["git", "checkout", "--detach", commit], cwd=td_path) + + # Atomically move into place + td_path.rename(wt_dir) + + return wt_dir + + +def _prepare_upstream_tests_dir() -> Path: + """Clone vLLM to cache and return path to tests directory.""" + commit = _resolve_vllm_commit() + cache_root = _cache_root() + wt_dir = _ensure_repo_at_commit( + repo_dir=cache_root, + url=os.environ.get("VLLM_REPO_URL", "https://github.com/vllm-project/vllm"), + commit=commit, + sparse_paths=["tests"], + ) + tests_dir = wt_dir / "tests" + if not tests_dir.is_dir(): + raise RuntimeError(f"Upstream tests directory not found at {tests_dir}") + return tests_dir + + +# --------------------------------------------------------------------------- +# Pytest Hooks +# --------------------------------------------------------------------------- + + +@pytest.hookimpl(tryfirst=True) +def pytest_configure(config): + """Register Spyre plugins and detect/clone vLLM repo.""" + global _terminal_reporter + _terminal_reporter = config.pluginmanager.get_plugin("terminalreporter") + + # Set env vars BEFORE any vllm imports + os.environ["VLLM_PLUGINS"] = "spyre_next,spyre_next_ops" + os.environ["VLLM_USE_AOT_COMPILE"] = "0" + + # Load plugins early to register custom ops before test modules import RMSNorm + from vllm.plugins import load_general_plugins + + load_general_plugins() + + # Detect local vLLM repo or clone it + rootdir = Path(config.rootdir) + tests_dir = rootdir / "tests" + vllm_pkg = rootdir / "vllm" + + if tests_dir.is_dir() and vllm_pkg.is_dir(): + # Running from vLLM repo itself + config._upstream_tests_base = tests_dir + _log("[vllm-upstream] Using local vLLM tests") + else: + # Not in vLLM repo - check if we should clone + skip_upstream = os.environ.get("SKIP_UPSTREAM_TESTS", "").lower() in ("1", "true", "yes") + if skip_upstream: + _log("[vllm-upstream] SKIP_UPSTREAM_TESTS is set, skipping upstream test collection") + config._upstream_tests_base = None + return + + try: + # Clone vLLM to cache + upstream_tests_base = _prepare_upstream_tests_dir() + config._upstream_tests_base = upstream_tests_base + + # Determine which test paths to inject + paths_env = _get_paths_from_yaml() + _log(f"[vllm-upstream] Auto-derived test paths from YAML: {paths_env}") + + if not paths_env: + _log("[vllm-upstream] No test paths configured, skipping upstream tests") + config._upstream_tests_base = None + return + + # Inject test paths into pytest collection + for rel_path in paths_env.split(","): + rel_path = rel_path.strip() + if not rel_path: + continue + test_dir = upstream_tests_base / rel_path + if test_dir.exists(): + _log(f"[vllm-upstream] Including tests from: {rel_path}") + config.args.append(str(test_dir)) + else: + _log(f"[vllm-upstream] Warning: Path not found: {test_dir}") + + except Exception as e: + raise SystemExit(f"[vllm-upstream] Failed to prepare upstream tests: {e}") from e + + +# --------------------------------------------------------------------------- +# YAML Filtering Helpers +# --------------------------------------------------------------------------- + + +def _find_file_config(test_path: Path, file_configs: dict[Path, FileConfig]) -> FileConfig | None: + if test_path in file_configs: + return file_configs[test_path] + for config_path, fc in file_configs.items(): + if test_path.is_relative_to(config_path): + return fc + return None + + +def _matches_block_list(test_name: str, block_list: tuple[BlockEntry, ...]) -> bool: + return any(fnmatch.fnmatch(test_name, e.test) for e in block_list) + + +def _find_allow_entry(test_name: str, allow_list: tuple[AllowEntry, ...]) -> AllowEntry | None: + for entry in allow_list: + if fnmatch.fnmatch(test_name, entry.test): + return entry + return None + + +def _should_skip_params(item: pytest.Item, allow_entry: AllowEntry) -> bool: + """Check if test should be skipped based on param_skips or param_allows. + + If param_allows is specified for a parameter, only those values are allowed. + Otherwise, param_skips is used to exclude specific values. + """ + callspec = getattr(item, "callspec", None) + if not callspec: + return False + + # Check param_allows first (whitelist takes precedence) + for pa in allow_entry.param_allows: + if pa.param_name in callspec.params and callspec.params[pa.param_name] not in pa.values: + # If allowlist exists for this param, skip if value is NOT in allowlist + return True + + # Check param_skips (blacklist) + for ps in allow_entry.param_skips: + if ps.param_name in callspec.params and callspec.params[ps.param_name] in ps.values: + return True + + return False + + +# --------------------------------------------------------------------------- +# Collection Modification +# --------------------------------------------------------------------------- + + +def pytest_collection_modifyitems(config: pytest.Config, items: list[pytest.Item]) -> None: + """Apply YAML-based filtering to upstream tests and reorder tests.""" + upstream_tests_base = getattr(config, "_upstream_tests_base", None) + if not upstream_tests_base: + # Still reorder tests even if not running upstream tests + _reorder_tests_by_name(items) + return + + upstream_tests_base = Path(upstream_tests_base).resolve() + upstream_repo_root = upstream_tests_base.parent + file_configs = { + (upstream_repo_root / fc.rel_path).resolve(): fc for fc in _UPSTREAM_CONFIG.files + } + + upstream_marker = pytest.mark.upstream + + for item in items: + test_path = Path(item.fspath).resolve() + if not test_path.is_relative_to(upstream_tests_base): + continue + + item.add_marker(upstream_marker) + + fc = _find_file_config(test_path, file_configs) + if fc is None: + item.add_marker(pytest.mark.skip(reason=f"not in {_YAML_FILENAME}")) + continue + + test_name = item.originalname or item.name + if _matches_block_list(test_name, fc.block_list): + item.add_marker(pytest.mark.skip(reason=f"blocked by {_YAML_FILENAME}")) + continue + + allow_entry = _find_allow_entry(test_name, fc.allow_list) + + if allow_entry: + for tag in allow_entry.tags: + item.add_marker(getattr(pytest.mark, tag)) + + if allow_entry is None: + item.add_marker(pytest.mark.skip(reason="not in allow_list")) + continue + + if _should_skip_params(item, allow_entry): + item.add_marker(pytest.mark.skip(reason="param skipped")) + continue + + if allow_entry.mode == "xfail": + item.add_marker(pytest.mark.xfail(strict=False)) + elif allow_entry.mode == "xfail_strict": + item.add_marker(pytest.mark.xfail(strict=True)) + + # Reorder tests so that tests with "model" in the name run first + _reorder_tests_by_name(items) + + +def _reorder_tests_by_name(items: list[pytest.Item]) -> None: + """Reorder tests so that tests with 'uses_subprocess' marker run first. + + This modifies the items list in-place using a stable sort, so tests marked + with 'uses_subprocess' will run first while preserving the relative order + within each group. + """ + stable_map = {item: idx for idx, item in enumerate(items)} + + def sort_key(item: pytest.Item) -> tuple[int, int]: + # Check if the test has the 'uses_subprocess' marker + has_subprocess_marker = any( + marker.name == "uses_subprocess" for marker in item.iter_markers() + ) + + # Priority 0: tests with uses_subprocess marker run first + # Priority 1: all other tests + priority = 0 if has_subprocess_marker else 1 + + return (priority, stable_map[item]) + + items.sort(key=sort_key) + + +@pytest.hookimpl(tryfirst=True) +def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: + """Apply parameter overrides from YAML config.""" + upstream_tests_base = getattr(metafunc.config, "_upstream_tests_base", None) + if not upstream_tests_base: + return + + upstream_repo_root = Path(upstream_tests_base).resolve().parent + test_path = Path(metafunc.definition.fspath).resolve() + file_configs = { + (upstream_repo_root / fc.rel_path).resolve(): fc for fc in _UPSTREAM_CONFIG.files + } + + fc = _find_file_config(test_path, file_configs) + if not fc: + return + + test_name = metafunc.definition.originalname or metafunc.definition.name + allow_entry = _find_allow_entry(test_name, fc.allow_list) + if not allow_entry or not allow_entry.param_overrides: + return + + for po in allow_entry.param_overrides: + if po.param_name not in metafunc.fixturenames: + continue + for i, marker in enumerate(metafunc.definition.own_markers): + if marker.name == "parametrize" and marker.args[0] == po.param_name: + metafunc.definition.own_markers[i] = pytest.mark.parametrize( + po.param_name, list(po.values) + ).mark + break + + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + + +def _spyre_default_vllm_config(monkeypatch): + from vllm.config import DeviceConfig, VllmConfig, set_current_vllm_config + from vllm.config.compilation import CompilationConfig + from vllm.platforms import PlatformEnum, current_platform + from vllm.forward_context import set_forward_context + + monkeypatch.setattr(type(current_platform), "_enum", PlatformEnum.OOT) + + # Explicitly register custom ops + from vllm_spyre_next.custom_ops import register_all + + register_all() + + config = VllmConfig( + device_config=DeviceConfig(device="cpu"), + compilation_config=CompilationConfig(custom_ops=["all"]), + ) + with set_current_vllm_config(config), set_forward_context(None, config): + # Set forward context so custom ops can access no_compile_layers + yield + + +@pytest.fixture() +def default_vllm_config(monkeypatch): + yield from _spyre_default_vllm_config(monkeypatch) + + +@pytest.fixture() +def should_do_global_cleanup_after_test(): + """Skip global cleanup for Spyre - torch.accelerator.empty_cache() doesn't work yet.""" + return False + + +@pytest.hookimpl(tryfirst=True) +def pytest_fixture_setup(fixturedef, request): + """Override fixtures when running upstream vLLM tests.""" + upstream_tests_base = getattr(request.config, "_upstream_tests_base", None) + if not upstream_tests_base: + return + + if fixturedef.argname == "default_vllm_config": + fixturedef.func = _spyre_default_vllm_config + fixturedef.argnames = ("monkeypatch",) + elif fixturedef.argname == "should_do_global_cleanup_after_test": + fixturedef.func = lambda: False + fixturedef.argnames = () diff --git a/vllm_spyre_next/vllm_spyre_next/testing/upstream_tests.yaml b/vllm_spyre_next/vllm_spyre_next/testing/upstream_tests.yaml new file mode 100644 index 000000000..d273502c7 --- /dev/null +++ b/vllm_spyre_next/vllm_spyre_next/testing/upstream_tests.yaml @@ -0,0 +1,49 @@ +# Upstream test filter configuration for vllm-spyre-next. +# +# Only tests listed here will run from upstream vLLM. All other upstream +# tests are skipped by default (opt-in / whitelist model). +# +# block_list entries take precedence over allow_list entries. +# +# Fields: +# rel_path Path relative to upstream vLLM repo root +# allow_list[].test fnmatch glob matched against test function name +# allow_list[].mode mandatory_pass (default) | xfail | xfail_strict +# allow_list[].tags Labels applied as pytest markers (filterable via -m) +# allow_list[].params.skip +# Parameter name -> list of values to skip +# allow_list[].params.override +# Parameter name -> replacement values (replaces upstream defaults) +# block_list[].test fnmatch glob matched against test function name + +tests: + files: + # RMSNorm tests require either https://github.com/vllm-project/vllm/pull/36246 or vLLM IR + # changes to merge in order to pass + # - rel_path: tests/kernels/core/test_layernorm.py + # allow_list: + # - test: "test_rms_norm" + # mode: mandatory_pass + # tags: [rmsnorm, upstream] + # params: + # skip: + # strided_input: [true] + # override: + # num_tokens: [1, 16] + # hidden_size: [64] + # block_list: + # - test: "test_fused_rms_norm_quant" + - rel_path: tests/models/language/generation/test_common.py + allow_list: + - test: "test_models" + mode: mandatory_pass + # uses_subprocess marker is added to make these tests run first. + # This must be added for any sets of tests that use `vllm.LLM` + tags: [facebook, upstream, uses_subprocess] + params: + allow: # skip every model except facebook/opt-125m + model: + - facebook/opt-125m + block_list: + - test: "test_fused_rms_norm_quant" +