From 9711e36dd02cdb68ffd84949c15afdd5b15faf32 Mon Sep 17 00:00:00 2001 From: zhouaihui Date: Thu, 12 Nov 2026 16:39:58 +0800 Subject: [PATCH] ci: discover CPU contracts by directory --- .github/workflows/ci.yml | 38 ++----------------- CONTRIBUTING.md | 13 ++++++- tests/{ => cpu}/test_capacity_handling.py | 0 tests/{ => cpu}/test_cudnn_gqa_mode.py | 0 tests/{ => cpu}/test_dependency_audit.py | 0 tests/{ => cpu}/test_dependency_extras.py | 2 +- tests/{ => cpu}/test_dmtp.py | 2 +- tests/{ => cpu}/test_draft_dsa.py | 4 +- tests/{ => cpu}/test_draft_model_config.py | 0 .../{ => cpu}/test_dspark_teacher_contract.py | 0 tests/{ => cpu}/test_eagle4.py | 2 +- tests/{ => cpu}/test_eagle4_moe.py | 0 tests/{ => cpu}/test_eagle4_validation.py | 0 tests/{ => cpu}/test_environment_scripts.py | 2 +- tests/{ => cpu}/test_evaluation_contract.py | 0 tests/{ => cpu}/test_fa4_import_compat.py | 0 .../{ => cpu}/test_flex_attention_backend.py | 0 tests/{ => cpu}/test_node_local_files.py | 0 tests/{ => cpu}/test_onboarding.py | 2 +- .../{ => cpu}/test_open_source_entrypoints.py | 6 +-- tests/{ => cpu}/test_owner_fence.py | 0 tests/{ => cpu}/test_planning.py | 2 +- tests/{ => cpu}/test_recipe_capacity.py | 0 tests/{ => cpu}/test_recipe_node_loss.py | 0 tests/{ => cpu}/test_release_audit.py | 0 tests/{ => cpu}/test_release_recipes.py | 0 tests/{ => cpu}/test_renderer_registry.py | 0 tests/{ => cpu}/test_reporting.py | 0 tests/{ => cpu}/test_sglang_patch_contract.py | 0 .../{ => cpu}/test_shipped_config_defaults.py | 2 +- tests/{ => cpu}/test_stage_recipe_inputs.py | 2 +- tests/{ => cpu}/test_target_lm_head.py | 0 .../test_training_config_validation.py | 0 tests/{ => cpu}/test_training_resume_rng.py | 0 tests/{ => cpu}/test_wheel_assets.py | 0 .../test_source_quality_contracts.py | 9 +++-- tools/recipes.py | 20 +++++----- 37 files changed, 43 insertions(+), 63 deletions(-) rename tests/{ => cpu}/test_capacity_handling.py (100%) rename tests/{ => cpu}/test_cudnn_gqa_mode.py (100%) rename tests/{ => cpu}/test_dependency_audit.py (100%) rename tests/{ => cpu}/test_dependency_extras.py (98%) rename tests/{ => cpu}/test_dmtp.py (99%) rename tests/{ => cpu}/test_draft_dsa.py (99%) rename tests/{ => cpu}/test_draft_model_config.py (100%) rename tests/{ => cpu}/test_dspark_teacher_contract.py (100%) rename tests/{ => cpu}/test_eagle4.py (99%) rename tests/{ => cpu}/test_eagle4_moe.py (100%) rename tests/{ => cpu}/test_eagle4_validation.py (100%) rename tests/{ => cpu}/test_environment_scripts.py (98%) rename tests/{ => cpu}/test_evaluation_contract.py (100%) rename tests/{ => cpu}/test_fa4_import_compat.py (100%) rename tests/{ => cpu}/test_flex_attention_backend.py (100%) rename tests/{ => cpu}/test_node_local_files.py (100%) rename tests/{ => cpu}/test_onboarding.py (99%) rename tests/{ => cpu}/test_open_source_entrypoints.py (97%) rename tests/{ => cpu}/test_owner_fence.py (100%) rename tests/{ => cpu}/test_planning.py (99%) rename tests/{ => cpu}/test_recipe_capacity.py (100%) rename tests/{ => cpu}/test_recipe_node_loss.py (100%) rename tests/{ => cpu}/test_release_audit.py (100%) rename tests/{ => cpu}/test_release_recipes.py (100%) rename tests/{ => cpu}/test_renderer_registry.py (100%) rename tests/{ => cpu}/test_reporting.py (100%) rename tests/{ => cpu}/test_sglang_patch_contract.py (100%) rename tests/{ => cpu}/test_shipped_config_defaults.py (98%) rename tests/{ => cpu}/test_stage_recipe_inputs.py (97%) rename tests/{ => cpu}/test_target_lm_head.py (100%) rename tests/{ => cpu}/test_training_config_validation.py (100%) rename tests/{ => cpu}/test_training_resume_rng.py (100%) rename tests/{ => cpu}/test_wheel_assets.py (100%) rename tests/{ => source}/test_source_quality_contracts.py (97%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c3373a8..0b3248f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,42 +51,10 @@ jobs: python -m pip install -r requirements/ci-cpu.txt python -m pip install --no-deps -e . - name: Run dependency-light source contracts - run: python -m pytest -q tests/test_source_quality_contracts.py + run: python -m pytest -q tests/source - name: Run offline CPU contracts env: + CUDA_VISIBLE_DEVICES: "" HF_HUB_OFFLINE: "1" TRANSFORMERS_OFFLINE: "1" - run: | - python -m pytest -q \ - tests/test_capacity_handling.py \ - tests/test_environment_scripts.py \ - tests/test_onboarding.py \ - tests/test_dependency_extras.py \ - tests/test_cudnn_gqa_mode.py \ - tests/test_dependency_audit.py \ - tests/test_draft_model_config.py \ - tests/test_draft_dsa.py \ - tests/test_eagle4.py \ - tests/test_eagle4_moe.py \ - tests/test_eagle4_validation.py \ - tests/test_fa4_import_compat.py \ - tests/test_flex_attention_backend.py \ - tests/test_dmtp.py \ - tests/test_training_resume_rng.py \ - tests/test_evaluation_contract.py \ - tests/test_open_source_entrypoints.py \ - tests/test_release_audit.py \ - tests/test_shipped_config_defaults.py \ - tests/test_release_recipes.py \ - tests/test_recipe_node_loss.py \ - tests/test_owner_fence.py \ - tests/test_recipe_capacity.py \ - tests/test_node_local_files.py \ - tests/test_stage_recipe_inputs.py \ - tests/test_target_lm_head.py \ - tests/test_dspark_teacher_contract.py \ - tests/test_planning.py \ - tests/test_renderer_registry.py \ - tests/test_reporting.py \ - tests/test_training_config_validation.py - python -m pytest -q tests/test_wheel_assets.py tests/test_sglang_patch_contract.py + run: python -m pytest -q tests/cpu diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b7a2c14..5edd219 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -31,7 +31,18 @@ python -m tools.build_release --outdir /absolute/path/to/new-release-dist Default tests are fast/offline. GPU, tokenizer, training and cuDNN gates need explicit switches declared in their test files. A skip is not GPU acceptance. Public CI checks lint, local doc links, compilation, plus a curated CPU/offline contract suite installed from `requirements/ci-cpu.txt` -on pull requests and configured branch pushes. The separate [release build](.github/workflows/build-release.yml) +on pull requests and configured branch pushes. CI uses pytest's directory discovery: put new +dependency-light repository checks in `tests/source/test_*.py` and offline CPU contracts supported by +the CI runtime in `tests/cpu/test_*.py`. New tests in either directory run automatically without +editing the workflow. Keep tests requiring GPUs, model downloads or the full pinned runtime outside +these directories. Run the same suites locally with: + +```bash +python -m pytest -q tests/source +CUDA_VISIBLE_DEVICES="" HF_HUB_OFFLINE=1 TRANSFORMERS_OFFLINE=1 python -m pytest -q tests/cpu +``` + +The separate [release build](.github/workflows/build-release.yml) workflow builds and audits sdist/wheel artifacts and checks wheel installation only when a `v*` tag is pushed; see [release instructions](docs/releasing.md). Neither workflow certifies GPU health or replaces the complete pinned-runtime suite above. Equivalent remote checks are not proof that hosted diff --git a/tests/test_capacity_handling.py b/tests/cpu/test_capacity_handling.py similarity index 100% rename from tests/test_capacity_handling.py rename to tests/cpu/test_capacity_handling.py diff --git a/tests/test_cudnn_gqa_mode.py b/tests/cpu/test_cudnn_gqa_mode.py similarity index 100% rename from tests/test_cudnn_gqa_mode.py rename to tests/cpu/test_cudnn_gqa_mode.py diff --git a/tests/test_dependency_audit.py b/tests/cpu/test_dependency_audit.py similarity index 100% rename from tests/test_dependency_audit.py rename to tests/cpu/test_dependency_audit.py diff --git a/tests/test_dependency_extras.py b/tests/cpu/test_dependency_extras.py similarity index 98% rename from tests/test_dependency_extras.py rename to tests/cpu/test_dependency_extras.py index a22928b..36601ee 100644 --- a/tests/test_dependency_extras.py +++ b/tests/cpu/test_dependency_extras.py @@ -17,7 +17,7 @@ import tomllib from pathlib import Path -ROOT = Path(__file__).resolve().parents[1] +ROOT = Path(__file__).resolve().parents[2] SHARED_CUTE_PACKAGES = ("nvidia-cutlass-dsl", "quack-kernels") diff --git a/tests/test_dmtp.py b/tests/cpu/test_dmtp.py similarity index 99% rename from tests/test_dmtp.py rename to tests/cpu/test_dmtp.py index d06247e..76cc9aa 100644 --- a/tests/test_dmtp.py +++ b/tests/cpu/test_dmtp.py @@ -496,7 +496,7 @@ def test_conditioned_plan_rejects_unimplemented_combinations(key, value, monkeyp monkeypatch.setenv("ASPEC_TRAIN_DATA", "examples/data/sample_conversations.jsonl") monkeypatch.setenv("ASPEC_EVAL_DATA", "examples/data/sample_conversations.jsonl") - path = Path(__file__).parents[1] / "configs/recipes/qwen3_5_9b_dmtp.yaml" + path = Path(__file__).parents[2] / "configs/recipes/qwen3_5_9b_dmtp.yaml" config = load_config(str(path), save_snapshot=False) plan = compile_execution_plan(config) assert plan.workload.objective["prediction_start"] == 1 diff --git a/tests/test_draft_dsa.py b/tests/cpu/test_draft_dsa.py similarity index 99% rename from tests/test_draft_dsa.py rename to tests/cpu/test_draft_dsa.py index d4cf956..1626074 100644 --- a/tests/test_draft_dsa.py +++ b/tests/cpu/test_draft_dsa.py @@ -355,7 +355,7 @@ def test_serving_export_does_not_silently_erase_dsa(): def test_dsa_example_configs_keep_same_identity_across_phase_boundaries(): - root = Path(__file__).parents[1] + root = Path(__file__).parents[2] configs = [] for stage, name in (("dense", "dense"), ("indexer_warmup", "indexer"), ("sparse", "sparse")): path = root / f"aspec/config/dspark_qwen3_8b_dsa_{name}_draft_config.json" @@ -372,7 +372,7 @@ def test_plan_preserves_dsa_and_packing_without_compression_alignment(monkeypatc from aspec.config.train_config import load_config from aspec.planning import compile_execution_plan - root = Path(__file__).parents[1] + root = Path(__file__).parents[2] monkeypatch.chdir(root) recipe = root / "configs/examples/qwen3_8b_dsa_dspark.yaml" run = load_config(str(recipe), save_snapshot=False) diff --git a/tests/test_draft_model_config.py b/tests/cpu/test_draft_model_config.py similarity index 100% rename from tests/test_draft_model_config.py rename to tests/cpu/test_draft_model_config.py diff --git a/tests/test_dspark_teacher_contract.py b/tests/cpu/test_dspark_teacher_contract.py similarity index 100% rename from tests/test_dspark_teacher_contract.py rename to tests/cpu/test_dspark_teacher_contract.py diff --git a/tests/test_eagle4.py b/tests/cpu/test_eagle4.py similarity index 99% rename from tests/test_eagle4.py rename to tests/cpu/test_eagle4.py index 9935fa9..1b7d266 100644 --- a/tests/test_eagle4.py +++ b/tests/cpu/test_eagle4.py @@ -31,7 +31,7 @@ from aspec.training.checkpoint import load_initial_draft_weights from tools import convert_to_hf -ROOT = Path(__file__).parents[1] +ROOT = Path(__file__).parents[2] def raw_config(stage="draft", **changes): diff --git a/tests/test_eagle4_moe.py b/tests/cpu/test_eagle4_moe.py similarity index 100% rename from tests/test_eagle4_moe.py rename to tests/cpu/test_eagle4_moe.py diff --git a/tests/test_eagle4_validation.py b/tests/cpu/test_eagle4_validation.py similarity index 100% rename from tests/test_eagle4_validation.py rename to tests/cpu/test_eagle4_validation.py diff --git a/tests/test_environment_scripts.py b/tests/cpu/test_environment_scripts.py similarity index 98% rename from tests/test_environment_scripts.py rename to tests/cpu/test_environment_scripts.py index 1ced720..aa27eb6 100644 --- a/tests/test_environment_scripts.py +++ b/tests/cpu/test_environment_scripts.py @@ -21,7 +21,7 @@ import pytest -ROOT = Path(__file__).resolve().parents[1] +ROOT = Path(__file__).resolve().parents[2] WRAPPER = ROOT / "tools/run_in_env.sh" diff --git a/tests/test_evaluation_contract.py b/tests/cpu/test_evaluation_contract.py similarity index 100% rename from tests/test_evaluation_contract.py rename to tests/cpu/test_evaluation_contract.py diff --git a/tests/test_fa4_import_compat.py b/tests/cpu/test_fa4_import_compat.py similarity index 100% rename from tests/test_fa4_import_compat.py rename to tests/cpu/test_fa4_import_compat.py diff --git a/tests/test_flex_attention_backend.py b/tests/cpu/test_flex_attention_backend.py similarity index 100% rename from tests/test_flex_attention_backend.py rename to tests/cpu/test_flex_attention_backend.py diff --git a/tests/test_node_local_files.py b/tests/cpu/test_node_local_files.py similarity index 100% rename from tests/test_node_local_files.py rename to tests/cpu/test_node_local_files.py diff --git a/tests/test_onboarding.py b/tests/cpu/test_onboarding.py similarity index 99% rename from tests/test_onboarding.py rename to tests/cpu/test_onboarding.py index 5789fe1..2b3df82 100644 --- a/tests/test_onboarding.py +++ b/tests/cpu/test_onboarding.py @@ -25,7 +25,7 @@ from aspec.planning import compile_execution_plan from tools.quickstart import MODELS, build_profile -ROOT = Path(__file__).resolve().parents[1] +ROOT = Path(__file__).resolve().parents[2] @pytest.mark.parametrize("name", MODELS) diff --git a/tests/test_open_source_entrypoints.py b/tests/cpu/test_open_source_entrypoints.py similarity index 97% rename from tests/test_open_source_entrypoints.py rename to tests/cpu/test_open_source_entrypoints.py index f8d5843..bff80e9 100644 --- a/tests/test_open_source_entrypoints.py +++ b/tests/cpu/test_open_source_entrypoints.py @@ -111,13 +111,13 @@ def test_document_links_ignore_snapshots_and_fenced_examples(tmp_path): def test_maintained_documentation_has_no_missing_local_targets(): - assert not check_links(Path(__file__).resolve().parents[1]) + assert not check_links(Path(__file__).resolve().parents[2]) def test_quickstart_has_two_gpu_roles_and_fixed_resume_horizon(): from aspec.config.train_config import load_config - root = Path(__file__).resolve().parents[1] + root = Path(__file__).resolve().parents[2] config = load_config(str(root / "configs/recipes/quickstart_qwen3_8b.yaml")) assert config.training.training_num_gpus_per_node == 1 assert config.inference.inference_num_gpus == 1 @@ -128,7 +128,7 @@ def test_quickstart_has_two_gpu_roles_and_fixed_resume_horizon(): def test_sdist_includes_runtime_install_requirements(): - root = Path(__file__).resolve().parents[1] + root = Path(__file__).resolve().parents[2] assert "recursive-include requirements *.txt" in (root / "MANIFEST.in").read_text() diff --git a/tests/test_owner_fence.py b/tests/cpu/test_owner_fence.py similarity index 100% rename from tests/test_owner_fence.py rename to tests/cpu/test_owner_fence.py diff --git a/tests/test_planning.py b/tests/cpu/test_planning.py similarity index 99% rename from tests/test_planning.py rename to tests/cpu/test_planning.py index 79612b7..4e97c9c 100644 --- a/tests/test_planning.py +++ b/tests/cpu/test_planning.py @@ -28,7 +28,7 @@ from aspec.planning import PlanCompileError, compile_execution_plan from aspec.topology import current_capability_bundle -_REPO_ROOT = Path(__file__).parents[1] +_REPO_ROOT = Path(__file__).parents[2] _REFERENCE_CONFIG = _REPO_ROOT / "configs" / "examples" / "sglang_qwen3_8b_dspark.yaml" _DRAFT_CONFIG = _REPO_ROOT / "aspec" / "config" / "dspark_draft_config.json" diff --git a/tests/test_recipe_capacity.py b/tests/cpu/test_recipe_capacity.py similarity index 100% rename from tests/test_recipe_capacity.py rename to tests/cpu/test_recipe_capacity.py diff --git a/tests/test_recipe_node_loss.py b/tests/cpu/test_recipe_node_loss.py similarity index 100% rename from tests/test_recipe_node_loss.py rename to tests/cpu/test_recipe_node_loss.py diff --git a/tests/test_release_audit.py b/tests/cpu/test_release_audit.py similarity index 100% rename from tests/test_release_audit.py rename to tests/cpu/test_release_audit.py diff --git a/tests/test_release_recipes.py b/tests/cpu/test_release_recipes.py similarity index 100% rename from tests/test_release_recipes.py rename to tests/cpu/test_release_recipes.py diff --git a/tests/test_renderer_registry.py b/tests/cpu/test_renderer_registry.py similarity index 100% rename from tests/test_renderer_registry.py rename to tests/cpu/test_renderer_registry.py diff --git a/tests/test_reporting.py b/tests/cpu/test_reporting.py similarity index 100% rename from tests/test_reporting.py rename to tests/cpu/test_reporting.py diff --git a/tests/test_sglang_patch_contract.py b/tests/cpu/test_sglang_patch_contract.py similarity index 100% rename from tests/test_sglang_patch_contract.py rename to tests/cpu/test_sglang_patch_contract.py diff --git a/tests/test_shipped_config_defaults.py b/tests/cpu/test_shipped_config_defaults.py similarity index 98% rename from tests/test_shipped_config_defaults.py rename to tests/cpu/test_shipped_config_defaults.py index b7d7439..5b50236 100644 --- a/tests/test_shipped_config_defaults.py +++ b/tests/cpu/test_shipped_config_defaults.py @@ -18,7 +18,7 @@ def _repo_root() -> Path: - return Path(__file__).parents[1] + return Path(__file__).parents[2] def test_shipped_small_draft_configs_use_replicate(): diff --git a/tests/test_stage_recipe_inputs.py b/tests/cpu/test_stage_recipe_inputs.py similarity index 97% rename from tests/test_stage_recipe_inputs.py rename to tests/cpu/test_stage_recipe_inputs.py index a9066ec..4ae3880 100644 --- a/tests/test_stage_recipe_inputs.py +++ b/tests/cpu/test_stage_recipe_inputs.py @@ -44,7 +44,7 @@ def test_stage_and_verify_with_local_ssh_double(tmp_path): "--host", "fake-node", ] - root = Path(__file__).resolve().parents[1] + root = Path(__file__).resolve().parents[2] subprocess.run(cmd, env=env, cwd=root, check=True, capture_output=True) assert checksum_lines(source) == checksum_lines(destination) assert subprocess.run(cmd, env=env, cwd=root, capture_output=True).returncode != 0 diff --git a/tests/test_target_lm_head.py b/tests/cpu/test_target_lm_head.py similarity index 100% rename from tests/test_target_lm_head.py rename to tests/cpu/test_target_lm_head.py diff --git a/tests/test_training_config_validation.py b/tests/cpu/test_training_config_validation.py similarity index 100% rename from tests/test_training_config_validation.py rename to tests/cpu/test_training_config_validation.py diff --git a/tests/test_training_resume_rng.py b/tests/cpu/test_training_resume_rng.py similarity index 100% rename from tests/test_training_resume_rng.py rename to tests/cpu/test_training_resume_rng.py diff --git a/tests/test_wheel_assets.py b/tests/cpu/test_wheel_assets.py similarity index 100% rename from tests/test_wheel_assets.py rename to tests/cpu/test_wheel_assets.py diff --git a/tests/test_source_quality_contracts.py b/tests/source/test_source_quality_contracts.py similarity index 97% rename from tests/test_source_quality_contracts.py rename to tests/source/test_source_quality_contracts.py index a16d496..fb155c2 100644 --- a/tests/test_source_quality_contracts.py +++ b/tests/source/test_source_quality_contracts.py @@ -17,7 +17,7 @@ import ast from pathlib import Path -ROOT = Path(__file__).parents[1] +ROOT = Path(__file__).parents[2] PUBLIC_TREES = ( ".github", "configs", @@ -60,8 +60,8 @@ ".yml", } HYGIENE_FIXTURES = { - ROOT / "tests/test_open_source_entrypoints.py", - ROOT / "tests/test_release_audit.py", + ROOT / "tests/cpu/test_open_source_entrypoints.py", + ROOT / "tests/cpu/test_release_audit.py", Path(__file__), } @@ -228,5 +228,6 @@ def test_eagle4_validation_gates_do_not_use_optimizable_asserts(): def test_public_ci_includes_eagle4_cpu_contracts(): workflow = (ROOT / ".github/workflows/ci.yml").read_text(encoding="utf-8") + assert "python -m pytest -q tests/cpu" in workflow for name in ("test_eagle4.py", "test_eagle4_moe.py", "test_eagle4_validation.py"): - assert f"tests/{name}" in workflow + assert (ROOT / "tests/cpu" / name).is_file() diff --git a/tools/recipes.py b/tools/recipes.py index 787114a..f34fbc2 100644 --- a/tools/recipes.py +++ b/tools/recipes.py @@ -192,21 +192,21 @@ def main(): "-m", "pytest", "-q", - "tests/test_release_recipes.py", - "tests/test_node_local_files.py", - "tests/test_recipe_node_loss.py", - "tests/test_owner_fence.py", - "tests/test_recipe_capacity.py", + "tests/cpu/test_release_recipes.py", + "tests/cpu/test_node_local_files.py", + "tests/cpu/test_recipe_node_loss.py", + "tests/cpu/test_owner_fence.py", + "tests/cpu/test_recipe_capacity.py", "tests/test_loop_cleanup.py", - "tests/test_stage_recipe_inputs.py", + "tests/cpu/test_stage_recipe_inputs.py", "tests/test_packing.py", "tests/test_direct_cp.py", "tests/test_eagle3_trainer.py", - "tests/test_eagle4.py", + "tests/cpu/test_eagle4.py", "tests/test_native_mtp.py", - "tests/test_dmtp.py", - "tests/test_training_resume_rng.py", - "tests/test_evaluation_contract.py", + "tests/cpu/test_dmtp.py", + "tests/cpu/test_training_resume_rng.py", + "tests/cpu/test_evaluation_contract.py", "tests/test_ling_moe.py", "tests/test_multi_draft_plan.py", "tests/test_multinode_tcp_reference.py",