Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .github/workflows/python-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
4 changes: 2 additions & 2 deletions docs/citation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down
12 changes: 7 additions & 5 deletions docs/concepts/architecture.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/python.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down
2 changes: 1 addition & 1 deletion src/keep_gpu/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

__author__ = """Siyuan Wang"""
__email__ = "sywang0227@gmail.com"
__version__ = "0.5.1"
__version__ = "1.0.0"
21 changes: 21 additions & 0 deletions tests/test_ci_workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
[
Expand Down
20 changes: 19 additions & 1 deletion tests/test_package_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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())
Expand Down