diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index d5188ce..d0d8bf3 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -25,6 +25,25 @@ jobs: python -m pip install --upgrade pip pip install build + - name: Verify release tag matches package version + run: | + package_version="$(python - <<'PY' + import sys + sys.path.insert(0, "src") + import keep_gpu + print(keep_gpu.__version__) + PY + )" + expected_tag="v${package_version}" + if [ "${GITHUB_REF_TYPE}" != "tag" ]; then + echo "PyPI publish must run from a release tag, got ${GITHUB_REF_TYPE:-unset}." >&2 + exit 1 + fi + if [ "${GITHUB_REF_NAME}" != "${expected_tag}" ]; then + echo "Release tag ${GITHUB_REF_NAME:-unset} does not match package version ${package_version}; expected ${expected_tag}." >&2 + exit 1 + fi + - name: Build distributions run: python -m build diff --git a/AGENTS.md b/AGENTS.md index 4c705d0..c4e623b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -82,6 +82,10 @@ This file defines how coding agents should work in this repository. publishing with `secrets.PYPI_API_TOKEN`, keep `id-token: write` out of the workflow permissions; only add OIDC permissions when the workflow is switched fully to PyPI Trusted Publishing. +- PyPI publish workflows must verify the GitHub release tag matches the dynamic + package version before building or publishing. A release tagged `vX.Y.Z` must + build `keep_gpu` version `X.Y.Z`, and mismatches must fail before + `python -m build`. - Keep Python test CI dependency installs explicit. Do not reintroduce a root `requirements.txt` fallback; runtime and test dependencies belong in `pyproject.toml`, while docs dependencies belong in `docs/requirements.txt`. diff --git a/docs/citation.md b/docs/citation.md index c12304b..9218aee 100644 --- a/docs/citation.md +++ b/docs/citation.md @@ -4,9 +4,9 @@ If this polite GPU keeper helps your research or operations, cite the archived software record: ```bibtex -@software{Wangmerlyn_KeepGPU_v0_5_1, +@software{Wangmerlyn_KeepGPU_v1_0_0, author = {{Wang Siyuan} and {shiyaorui} and {Liu Yida} and {ChitandaErumanga}}, - title = {Wangmerlyn/KeepGPU: KeepGPU v0.5.1}, + title = {Wangmerlyn/KeepGPU: KeepGPU v1.0.0}, year = {2026}, publisher = {Zenodo}, doi = {10.5281/zenodo.17129114}, diff --git a/docs/concepts/architecture.md b/docs/concepts/architecture.md index 0bab993..9e3a6bb 100644 --- a/docs/concepts/architecture.md +++ b/docs/concepts/architecture.md @@ -1,9 +1,10 @@ # How KeepGPU Works -At runtime, KeepGPU spins up one lightweight worker per GPU. Each worker keeps a -tensor allocated and runs a short backend-specific keepalive burst, then sleeps. -This convinces most schedulers that the GPU is still busy, without burning a -full training workload. +At runtime, KeepGPU spins up one lightweight worker per GPU. Each worker checks +telemetry, allocates the requested tensor, and runs a short backend-specific +keepalive burst only when utilization backoff permits, then sleeps. If the GPU +is busy, or telemetry is unavailable under the default threshold, the loop backs +off instead of allocating work. ## Components @@ -96,7 +97,8 @@ CLI args ──▶ GlobalGPUController ──▶ [backend controller rank=0] Elementwise keep-alive batches: -- Allocate continuous VRAM quickly, which is what schedulers monitor. +- Allocate the requested VRAM signal when utilization backoff permits, which is + what schedulers can observe. - Exercise compute units enough to show non-zero utilization spikes. - Are deterministic and easy to tune with interval and positive integer iteration settings, trading power draw for stronger "busy" signals. diff --git a/docs/getting-started.md b/docs/getting-started.md index 92a5d92..df8cf57 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -153,7 +153,7 @@ Prefer code-level control? Import the controllers directly (full recipes in from keep_gpu.single_gpu_controller.cuda_gpu_controller import CudaGPUController with CudaGPUController(rank=0, interval=0.5, vram_to_keep="1GiB"): - preprocess_dataset() # Runs while GPU is pinned + preprocess_dataset() # Keepalive session stays active train_model() # GPU freed upon exiting the context ``` diff --git a/docs/guides/python.md b/docs/guides/python.md index 53b7657..36a8d59 100644 --- a/docs/guides/python.md +++ b/docs/guides/python.md @@ -17,7 +17,7 @@ def preprocess_shards(): ... with CudaGPUController(rank=0, interval=0.5, vram_to_keep="1.5GiB"): - preprocess_shards() # GPU 0 is marked “busy” the whole time + preprocess_shards() # Session stays active; work follows backoff train_model() # GPU memory is released automatically ``` diff --git a/pyproject.toml b/pyproject.toml index 6496973..fd08a21 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,7 +79,7 @@ version = {attr = "keep_gpu.__version__"} # ----------------------------- # bump-my-version configuration [tool.bumpversion] -current_version = "0.5.1" +current_version = "1.0.0" commit = true tag = true tag_name = "v{new_version}" diff --git a/src/keep_gpu/__init__.py b/src/keep_gpu/__init__.py index 3794d48..efc10e6 100644 --- a/src/keep_gpu/__init__.py +++ b/src/keep_gpu/__init__.py @@ -2,4 +2,4 @@ __author__ = """Siyuan Wang""" __email__ = "sywang0227@gmail.com" -__version__ = "0.5.1" +__version__ = "1.0.0" diff --git a/tests/test_ci_workflows.py b/tests/test_ci_workflows.py index 1aa37b7..ca914f6 100644 --- a/tests/test_ci_workflows.py +++ b/tests/test_ci_workflows.py @@ -288,6 +288,27 @@ def test_publish_workflow_does_not_mix_pypi_token_secret_with_oidc_permission(): ) +def test_publish_workflow_checks_release_tag_matches_package_version_before_build(): + workflow = (PROJECT_ROOT / ".github/workflows/python-publish.yml").read_text( + encoding="utf-8" + ) + active_workflow = _active_workflow_text(workflow) + build_index = active_workflow.find("python -m build") + publish_index = active_workflow.find("pypa/gh-action-pypi-publish@release/v1") + version_index = active_workflow.find("keep_gpu.__version__") + + assert build_index != -1 + assert publish_index != -1 + assert version_index != -1 + assert version_index < build_index < publish_index + assert active_workflow.find("GITHUB_REF_TYPE", 0, build_index) != -1 + assert active_workflow.find("GITHUB_REF_NAME", 0, build_index) != -1 + assert re.search( + r"expected_tag\s*=\s*['\"]v\$\{package_version\}['\"]", + active_workflow[:build_index], + ) + + @pytest.mark.parametrize( "password_line", [ diff --git a/tests/test_package_metadata.py b/tests/test_package_metadata.py index 7b09683..fb37a0c 100644 --- a/tests/test_package_metadata.py +++ b/tests/test_package_metadata.py @@ -173,7 +173,7 @@ def test_citation_page_matches_current_zenodo_concept_doi_metadata(): citation = (PROJECT_ROOT / "docs/citation.md").read_text(encoding="utf-8") assert "10.5281/zenodo.17129114" in citation - assert "Wangmerlyn/KeepGPU: KeepGPU v0.5.1" in citation + assert "Wangmerlyn/KeepGPU: KeepGPU v1.0.0" in citation assert "year = {2026}" in citation assert "Wang Siyuan" in citation assert "shiyaorui" in citation @@ -227,6 +227,24 @@ def test_public_docs_do_not_regress_to_cuda_only_or_experimental_mcp(): assert "start keepalive sessions" in public_docs["getting_started"].lower() +def test_public_docs_do_not_imply_unconditional_keepalive_work(): + public_docs = "\n".join( + path.read_text(encoding="utf-8") + for path in [ + PROJECT_ROOT / "docs/concepts/architecture.md", + PROJECT_ROOT / "docs/getting-started.md", + PROJECT_ROOT / "docs/guides/python.md", + ] + ).lower() + + assert "keeps a tensor allocated" not in public_docs + assert "marked “busy” the whole time" not in public_docs + assert 'marked "busy" the whole time' not in public_docs + assert "runs while gpu is pinned" not in public_docs + assert "gpu is pinned" not in public_docs + assert "allocate continuous vram quickly" not in public_docs + + def test_index_overview_describes_eco_safe_backoff_without_unconditional_claims(): index = (PROJECT_ROOT / "docs/index.md").read_text(encoding="utf-8") normalized_index = re.sub(r"\s+", " ", index.lower())