pytest-gpu-proof 0.4.0__tar.gz → 0.4.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. pytest_gpu_proof-0.4.1/.github/workflows/docs.yml +69 -0
  2. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/.gitignore +3 -1
  3. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/CHANGELOG.md +51 -0
  4. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/PKG-INFO +29 -7
  5. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/README.md +26 -4
  6. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/docs/ci_gpu_mode.md +4 -1
  7. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/docs/index.md +3 -0
  8. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/docs/policy.md +2 -2
  9. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/docs/security_model.md +7 -3
  10. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/docs/sharding.md +18 -4
  11. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/mkdocs.yml +2 -0
  12. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/pyproject.toml +3 -3
  13. pytest_gpu_proof-0.4.1/src/pytest_gpu_proof/__init__.py +1 -0
  14. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/src/pytest_gpu_proof/cli.py +108 -0
  15. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/src/pytest_gpu_proof/config.py +9 -2
  16. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/src/pytest_gpu_proof/gitutils.py +9 -0
  17. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/src/pytest_gpu_proof/merge.py +130 -2
  18. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/src/pytest_gpu_proof/plugin.py +22 -0
  19. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/src/pytest_gpu_proof/receipt.py +40 -7
  20. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/src/pytest_gpu_proof/verify.py +55 -12
  21. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/tests/test_config.py +17 -0
  22. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/tests/test_merge.py +2 -2
  23. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/tests/test_receipt.py +41 -0
  24. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/tests/test_sharding.py +229 -0
  25. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/tests/test_verifier.py +91 -0
  26. pytest_gpu_proof-0.4.1/website/README.md +85 -0
  27. pytest_gpu_proof-0.4.1/website/build_site.py +82 -0
  28. pytest_gpu_proof-0.4.1/website/check_site.py +90 -0
  29. pytest_gpu_proof-0.4.1/website/favicon.svg +1 -0
  30. pytest_gpu_proof-0.4.1/website/index.html +127 -0
  31. pytest_gpu_proof-0.4.1/website/main.js +17 -0
  32. pytest_gpu_proof-0.4.1/website/overrides/main.html +4 -0
  33. pytest_gpu_proof-0.4.1/website/provenance.json +10 -0
  34. pytest_gpu_proof-0.4.1/website/requirements.txt +2 -0
  35. pytest_gpu_proof-0.4.1/website/style.css +67 -0
  36. pytest_gpu_proof-0.4.1/website/test_site.py +89 -0
  37. pytest_gpu_proof-0.4.1/website/workflow-mobile.svg +32 -0
  38. pytest_gpu_proof-0.4.1/website/workflow.svg +35 -0
  39. pytest_gpu_proof-0.4.0/.github/workflows/docs.yml +0 -29
  40. pytest_gpu_proof-0.4.0/src/pytest_gpu_proof/__init__.py +0 -1
  41. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/.github/workflows/ci.yml +0 -0
  42. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/.github/workflows/publish-testpypi.yml +0 -0
  43. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/.github/workflows/publish.yml +0 -0
  44. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/AGENTS.md +0 -0
  45. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/CLAUDE.md +0 -0
  46. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/CONTRIBUTING.md +0 -0
  47. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/LICENSE +0 -0
  48. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/RELEASING.md +0 -0
  49. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/ROADMAP.md +0 -0
  50. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/SECURITY.md +0 -0
  51. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/docs/architecture.md +0 -0
  52. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/docs/landscape.md +0 -0
  53. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/docs/local_mode.md +0 -0
  54. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/docs/quickstart.md +0 -0
  55. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/examples/cuda_ctypes_matmul/Makefile +0 -0
  56. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/examples/cuda_ctypes_matmul/README.md +0 -0
  57. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/examples/cuda_ctypes_matmul/cuda_ctypes_matmul.py +0 -0
  58. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/examples/cuda_ctypes_matmul/matmul_kernel.cu +0 -0
  59. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/examples/cuda_ctypes_matmul/test_cuda_ctypes_matmul.py +0 -0
  60. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/examples/cuda_nanobind_matmul/CMakeLists.txt +0 -0
  61. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/examples/cuda_nanobind_matmul/README.md +0 -0
  62. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/examples/cuda_nanobind_matmul/src/bindings.cpp +0 -0
  63. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/examples/cuda_nanobind_matmul/src/matmul_cuda.h +0 -0
  64. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/examples/cuda_nanobind_matmul/src/matmul_kernel.cu +0 -0
  65. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/examples/cuda_nanobind_matmul/test_cuda_nanobind_matmul.py +0 -0
  66. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/examples/cuda_pybind11_matmul/CMakeLists.txt +0 -0
  67. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/examples/cuda_pybind11_matmul/README.md +0 -0
  68. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/examples/cuda_pybind11_matmul/src/bindings.cpp +0 -0
  69. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/examples/cuda_pybind11_matmul/src/matmul_cuda.h +0 -0
  70. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/examples/cuda_pybind11_matmul/src/matmul_kernel.cu +0 -0
  71. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/examples/cuda_pybind11_matmul/test_cuda_pybind11_matmul.py +0 -0
  72. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/examples/github_gpu_runner/.github/workflows/gpu-test.yml +0 -0
  73. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/examples/jax_ffi_cuda_matmul/CMakeLists.txt +0 -0
  74. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/examples/jax_ffi_cuda_matmul/README.md +0 -0
  75. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/examples/jax_ffi_cuda_matmul/jax_ffi_matmul.py +0 -0
  76. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/examples/jax_ffi_cuda_matmul/src/matmul_ffi.cu +0 -0
  77. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/examples/jax_ffi_cuda_matmul/test_jax_ffi_cuda_matmul.py +0 -0
  78. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/examples/local_receipt_verify/.github/workflows/verify.yml +0 -0
  79. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/examples/minimal_python_only/conftest.py +0 -0
  80. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/examples/minimal_python_only/test_minimal.py +0 -0
  81. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/examples/wrapped_cuda_ctypes/fake_cuda_lib.py +0 -0
  82. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/examples/wrapped_cuda_ctypes/test_cuda_wrapper.py +0 -0
  83. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/install.sh +0 -0
  84. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/requirements-dev.txt +0 -0
  85. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/requirements.txt +0 -0
  86. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/src/pytest_gpu_proof/__main__.py +0 -0
  87. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/src/pytest_gpu_proof/compare.py +0 -0
  88. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/src/pytest_gpu_proof/fingerprint.py +0 -0
  89. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/src/pytest_gpu_proof/signers/__init__.py +0 -0
  90. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/src/pytest_gpu_proof/signers/base.py +0 -0
  91. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/src/pytest_gpu_proof/signers/ed25519.py +0 -0
  92. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/tests/conftest.py +0 -0
  93. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/tests/test_cli.py +0 -0
  94. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/tests/test_compare.py +0 -0
  95. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/tests/test_fingerprint.py +0 -0
  96. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/tests/test_gitutils.py +0 -0
  97. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/tests/test_plugin_capture.py +0 -0
  98. {pytest_gpu_proof-0.4.0 → pytest_gpu_proof-0.4.1}/tests/test_signing.py +0 -0
@@ -0,0 +1,69 @@
1
+ name: Docs
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ paths:
8
+ - 'docs/**'
9
+ - 'website/**'
10
+ - 'mkdocs.yml'
11
+ - '.github/workflows/docs.yml'
12
+
13
+ permissions:
14
+ contents: read
15
+
16
+ concurrency:
17
+ group: docs-${{ github.ref }}
18
+ cancel-in-progress: false
19
+
20
+ jobs:
21
+ build:
22
+ runs-on: ubuntu-latest
23
+ steps:
24
+ - uses: actions/checkout@v4
25
+
26
+ - uses: actions/setup-python@v5
27
+ with:
28
+ python-version: "3.12"
29
+
30
+ - name: Install website dependencies
31
+ run: pip install -r website/requirements.txt
32
+
33
+ - name: Build and check the complete website
34
+ run: python website/build_site.py
35
+
36
+ - name: Test website checks
37
+ run: python -m unittest discover -s website -p 'test_*.py'
38
+
39
+ - uses: actions/upload-artifact@v4
40
+ with:
41
+ name: website-preview
42
+ path: build/website
43
+ include-hidden-files: true
44
+
45
+ deploy:
46
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
47
+ needs: build
48
+ runs-on: ubuntu-latest
49
+ permissions:
50
+ contents: write
51
+ steps:
52
+ - uses: actions/checkout@v4
53
+ with:
54
+ fetch-depth: 0
55
+ - uses: actions/setup-python@v5
56
+ with:
57
+ python-version: "3.12"
58
+ - run: pip install -r website/requirements.txt
59
+ - uses: actions/download-artifact@v4
60
+ with:
61
+ name: website-preview
62
+ path: build/website
63
+ - name: Configure git author
64
+ run: |
65
+ git config user.name "github-actions[bot]"
66
+ git config user.email "github-actions[bot]@users.noreply.github.com"
67
+
68
+ - name: Deploy to gh-pages
69
+ run: ghp-import --no-jekyll --push --branch gh-pages build/website
@@ -299,4 +299,6 @@ Testing/
299
299
  *.fatbin
300
300
 
301
301
  # Specifics
302
- gpu-proof.json
302
+ gpu-proof.json
303
+ # Local working notes (never committed)
304
+ docs/open-tasks/
@@ -4,6 +4,57 @@ All notable changes to pytest-gpu-proof are documented here. The format is
4
4
  based on [Keep a Changelog](https://keepachangelog.com/); versions follow
5
5
  [SemVer](https://semver.org/) (pre-1.0: minor bumps may break).
6
6
 
7
+ ## [0.4.1] — 2026-10-12
8
+
9
+ ### Fixed
10
+
11
+ - Shard entries record `dirty` (the recording tree's state); merge ORs shard
12
+ flags into the merged `repo.dirty`, carry-forward refuses a dirty shard, and
13
+ verification refuses any dirty shard unless `allow_dirty` is set.
14
+ - `gpu-proof merge --carry-from` refuses a source receipt recorded with a dirty
15
+ working tree. The merged `repo.dirty` is OR-ed over the fresh inputs only, so a
16
+ carried shard from a dirty recording was laundered into a clean-looking merged
17
+ receipt (found by GRiD's split driver, 2026-10-10). Carried shards now record
18
+ `carried.original_dirty`, and verification refuses a carried shard recorded
19
+ dirty unless `allow_dirty` is set.
20
+
21
+ - The expected-skips mismatch names the delta (unexpected skips and stale
22
+ baseline entries) instead of only reporting that the sets differ.
23
+ - The receipt's `environment.gpu_info` falls back to the same torch probe the
24
+ `gpu_required` marker uses when nvidia-smi is absent or fails (`source`
25
+ records which probe answered), so a torch-only box no longer records
26
+ `gpu_info: null` and then fails a `require_gpu` policy.
27
+
28
+ - Verification refuses a receipt whose session started more than five
29
+ minutes before its commit existed (committer time): a cheap replay and
30
+ forgery guard that needs no nonce.
31
+
32
+ - `mode: ci-gpu` is now bound to the signing key: a ci-gpu receipt verifies
33
+ only when the policy lists `allowed_key_fingerprints` and the receipt was
34
+ signed by one of them (previously the mode was an unverified label).
35
+ - Without an explicit `carried_max_age_days`, a carried shard's original run
36
+ may not be older than the receipt's `max_age_days` (was a fixed 30 days).
37
+
38
+ ### Added
39
+
40
+ - `gpu-proof merge --verify-inputs`: authenticate every input shard against
41
+ its signer's current GitHub keys before merging (unsigned or tampered inputs
42
+ are refused).
43
+ - `gpu-proof status PATH...`: one-line summaries of receipt files or
44
+ directories (commit, dirty, signed, outcome, counts, shards, carried).
45
+ - `gpu-proof durations RECEIPT... --out map.json`: fold per-test durations
46
+ into a JSON map for shard bin-packing.
47
+ - Command-line equivalents for the toml-only options:
48
+ `--gpu-proof-no-gpu-uuid`, `--gpu-proof-require-gpu`,
49
+ `--gpu-proof-max-age-days N`.
50
+ - `gpu-proof plan --receipt OLD [--repo .] [--json]`: a dry run of the
51
+ carry-forward conditions against the current tree, reporting per shard
52
+ whether it could be carried or must be re-run (and why). Drivers that plan
53
+ partial re-runs no longer reimplement the checks.
54
+ - `record_gpu_uuid` (`[tool.gpu_proof]`, default `true`): set `false` to omit each GPU's
55
+ nvidia-smi UUID from the receipt environment while keeping name, driver, memory and
56
+ compute capability. Verification does not read the UUID.
57
+
7
58
  ## [0.4.0] — 2026-08-17
8
59
 
9
60
  ### Added
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytest-gpu-proof
3
- Version: 0.4.0
3
+ Version: 0.4.1
4
4
  Summary: pytest plugin for GPU equivalence testing with signed receipts verified via GitHub SSH keys
5
- Project-URL: Homepage, https://github.com/A2R-Lab/pytest-gpu-proof
6
- Project-URL: Documentation, https://a2r-lab.github.io/pytest-gpu-proof/
5
+ Project-URL: Homepage, https://a2r-lab.org/pytest-gpu-proof/
6
+ Project-URL: Documentation, https://a2r-lab.org/pytest-gpu-proof/docs/
7
7
  Project-URL: Repository, https://github.com/A2R-Lab/pytest-gpu-proof
8
8
  Project-URL: Issues, https://github.com/A2R-Lab/pytest-gpu-proof/issues
9
9
  Project-URL: Changelog, https://github.com/A2R-Lab/pytest-gpu-proof/blob/main/CHANGELOG.md
@@ -39,13 +39,17 @@ Description-Content-Type: text/markdown
39
39
  # pytest-gpu-proof
40
40
 
41
41
  [![CI](https://github.com/A2R-Lab/pytest-gpu-proof/actions/workflows/ci.yml/badge.svg)](https://github.com/A2R-Lab/pytest-gpu-proof/actions/workflows/ci.yml)
42
- [![Docs](https://github.com/A2R-Lab/pytest-gpu-proof/actions/workflows/docs.yml/badge.svg)](https://a2r-lab.github.io/pytest-gpu-proof/)
42
+ [![Docs](https://github.com/A2R-Lab/pytest-gpu-proof/actions/workflows/docs.yml/badge.svg)](https://a2r-lab.org/pytest-gpu-proof/docs/)
43
43
  [![PyPI](https://img.shields.io/pypi/v/pytest-gpu-proof.svg)](https://pypi.org/project/pytest-gpu-proof/)
44
44
  [![Python](https://img.shields.io/pypi/pyversions/pytest-gpu-proof.svg)](https://pypi.org/project/pytest-gpu-proof/)
45
45
  [![Coverage: 100%](https://img.shields.io/badge/coverage-100%25-brightgreen.svg)](https://github.com/A2R-Lab/pytest-gpu-proof/actions/workflows/ci.yml)
46
46
 
47
47
  Signed pytest receipts for local GPU runs, verified in CPU-only CI.
48
48
 
49
+ [Project overview](https://a2r-lab.org/pytest-gpu-proof/) ·
50
+ [Documentation](https://a2r-lab.org/pytest-gpu-proof/docs/) ·
51
+ [Website preview instructions](website/README.md)
52
+
49
53
  `pytest-gpu-proof` records which marked tests ran, their outcomes, the Git
50
54
  commit and source fingerprint, the environment, and the run time. It signs
51
55
  that payload with an existing SSH key. A normal CPU runner can then verify the
@@ -126,15 +130,23 @@ fingerprint scope, no selected tests, or write failure makes pytest fail and
126
130
  leaves no stale receipt behind. `--gpu-proof-best-effort` is an explicit
127
131
  development-only opt-out.
128
132
 
129
- ## Four interfaces
133
+ ## Five interfaces
130
134
 
131
- The project exposes four small interfaces:
135
+ The project exposes five small interfaces:
132
136
 
133
137
  1. **Markers** select receipt tests.
134
- 2. **`gpu_proof_check`** compares a reference callable with a candidate.
138
+ 2. **`gpu_proof_check`** compares a reference callable with a candidate
139
+ (optional; every current user selects tests with the marker alone).
135
140
  3. **`gpu-proof verify`** validates a receipt and repository policy.
136
141
  4. **`gpu-proof merge`** combines separately executed shards and optionally
137
142
  carries unchanged shards forward.
143
+ 5. **`gpu-proof plan`** reports, before a partial re-run, which shards of an
144
+ old receipt could still be carried and which must be re-run.
145
+
146
+ Two maintenance helpers round it out: `gpu-proof status PATH...` summarizes
147
+ receipt files (commit, dirty, signed, outcome, counts, shards), and
148
+ `gpu-proof durations RECEIPT... --out map.json` folds per-test durations into
149
+ a map that shard bin-packers can calibrate from.
138
150
 
139
151
  ### Markers
140
152
 
@@ -197,6 +209,7 @@ fingerprint_excluded_paths = ["gpu-proof.json"]
197
209
  required_marker = "gpu_proof"
198
210
  max_age_days = 30
199
211
  require_gpu = true
212
+ record_gpu_uuid = false # keep device name/driver/memory, omit the card's UUID
200
213
  ```
201
214
 
202
215
  By default, the fingerprint covers every tracked file in the repository,
@@ -304,6 +317,15 @@ runtime environments, duplicate tests, and duplicate shard names. It records
304
317
  input provenance but does not verify each input signature; the merger signs
305
318
  for the union. See [sharding and carry-forward](docs/sharding.md).
306
319
 
320
+ Carry-forward (`gpu-proof merge ... --carry-from last-green.json`) grafts an
321
+ absent old shard only when its receipt commit is an ancestor of the current
322
+ one, both the receipt and the shard were recorded with a clean tree, its
323
+ narrow fingerprint recomputes identically, and it introduces no duplicate
324
+ test. `gpu-proof plan --receipt last-green.json` prints the same verdicts
325
+ without merging. Generated inputs named through `fingerprint_extra_paths`
326
+ should be gitignored: an untracked generated file makes the recording tree
327
+ dirty, and dirty recordings are never carried.
328
+
307
329
  ## Receipt schema
308
330
 
309
331
  New receipts use schema `"3"`. Signer identity is inside the signed payload,
@@ -1,13 +1,17 @@
1
1
  # pytest-gpu-proof
2
2
 
3
3
  [![CI](https://github.com/A2R-Lab/pytest-gpu-proof/actions/workflows/ci.yml/badge.svg)](https://github.com/A2R-Lab/pytest-gpu-proof/actions/workflows/ci.yml)
4
- [![Docs](https://github.com/A2R-Lab/pytest-gpu-proof/actions/workflows/docs.yml/badge.svg)](https://a2r-lab.github.io/pytest-gpu-proof/)
4
+ [![Docs](https://github.com/A2R-Lab/pytest-gpu-proof/actions/workflows/docs.yml/badge.svg)](https://a2r-lab.org/pytest-gpu-proof/docs/)
5
5
  [![PyPI](https://img.shields.io/pypi/v/pytest-gpu-proof.svg)](https://pypi.org/project/pytest-gpu-proof/)
6
6
  [![Python](https://img.shields.io/pypi/pyversions/pytest-gpu-proof.svg)](https://pypi.org/project/pytest-gpu-proof/)
7
7
  [![Coverage: 100%](https://img.shields.io/badge/coverage-100%25-brightgreen.svg)](https://github.com/A2R-Lab/pytest-gpu-proof/actions/workflows/ci.yml)
8
8
 
9
9
  Signed pytest receipts for local GPU runs, verified in CPU-only CI.
10
10
 
11
+ [Project overview](https://a2r-lab.org/pytest-gpu-proof/) ·
12
+ [Documentation](https://a2r-lab.org/pytest-gpu-proof/docs/) ·
13
+ [Website preview instructions](website/README.md)
14
+
11
15
  `pytest-gpu-proof` records which marked tests ran, their outcomes, the Git
12
16
  commit and source fingerprint, the environment, and the run time. It signs
13
17
  that payload with an existing SSH key. A normal CPU runner can then verify the
@@ -88,15 +92,23 @@ fingerprint scope, no selected tests, or write failure makes pytest fail and
88
92
  leaves no stale receipt behind. `--gpu-proof-best-effort` is an explicit
89
93
  development-only opt-out.
90
94
 
91
- ## Four interfaces
95
+ ## Five interfaces
92
96
 
93
- The project exposes four small interfaces:
97
+ The project exposes five small interfaces:
94
98
 
95
99
  1. **Markers** select receipt tests.
96
- 2. **`gpu_proof_check`** compares a reference callable with a candidate.
100
+ 2. **`gpu_proof_check`** compares a reference callable with a candidate
101
+ (optional; every current user selects tests with the marker alone).
97
102
  3. **`gpu-proof verify`** validates a receipt and repository policy.
98
103
  4. **`gpu-proof merge`** combines separately executed shards and optionally
99
104
  carries unchanged shards forward.
105
+ 5. **`gpu-proof plan`** reports, before a partial re-run, which shards of an
106
+ old receipt could still be carried and which must be re-run.
107
+
108
+ Two maintenance helpers round it out: `gpu-proof status PATH...` summarizes
109
+ receipt files (commit, dirty, signed, outcome, counts, shards), and
110
+ `gpu-proof durations RECEIPT... --out map.json` folds per-test durations into
111
+ a map that shard bin-packers can calibrate from.
100
112
 
101
113
  ### Markers
102
114
 
@@ -159,6 +171,7 @@ fingerprint_excluded_paths = ["gpu-proof.json"]
159
171
  required_marker = "gpu_proof"
160
172
  max_age_days = 30
161
173
  require_gpu = true
174
+ record_gpu_uuid = false # keep device name/driver/memory, omit the card's UUID
162
175
  ```
163
176
 
164
177
  By default, the fingerprint covers every tracked file in the repository,
@@ -266,6 +279,15 @@ runtime environments, duplicate tests, and duplicate shard names. It records
266
279
  input provenance but does not verify each input signature; the merger signs
267
280
  for the union. See [sharding and carry-forward](docs/sharding.md).
268
281
 
282
+ Carry-forward (`gpu-proof merge ... --carry-from last-green.json`) grafts an
283
+ absent old shard only when its receipt commit is an ancestor of the current
284
+ one, both the receipt and the shard were recorded with a clean tree, its
285
+ narrow fingerprint recomputes identically, and it introduces no duplicate
286
+ test. `gpu-proof plan --receipt last-green.json` prints the same verdicts
287
+ without merging. Generated inputs named through `fingerprint_extra_paths`
288
+ should be gitignored: an untracked generated file makes the recording tree
289
+ dirty, and dirty recordings are never carried.
290
+
269
291
  ## Receipt schema
270
292
 
271
293
  New receipts use schema `"3"`. Signer identity is inside the signed payload,
@@ -2,7 +2,10 @@
2
2
 
3
3
  CI-GPU mode uses the same receipt and verifier but records `mode: ci-gpu`.
4
4
  Choose it when GPU tests must run on a controlled runner rather than a
5
- developer machine.
5
+ developer machine. The verifier binds the mode to the runner's signing key:
6
+ a `ci-gpu` receipt verifies only when the repository policy lists
7
+ `allowed_key_fingerprints` and the receipt was signed by one of them, so the
8
+ label cannot be claimed by an arbitrary developer key.
6
9
 
7
10
  ## Dedicated signing identity
8
11
 
@@ -1,5 +1,8 @@
1
1
  # pytest-gpu-proof
2
2
 
3
+ This is the code documentation. Use the project-overview link above for the
4
+ workflow figure, paper, and introduction to the tool.
5
+
3
6
  **Signed pytest receipts for local GPU runs, verified in CPU-only CI.**
4
7
 
5
8
  The plugin turns an ordinary marked pytest run into a signed, reviewable
@@ -53,7 +53,7 @@ schema 3. Repositories that have finished migrating should pin
53
53
  | `allowed_key_fingerprints` | SSH SHA-256 fingerprints accepted in restricted mode |
54
54
  | `min_schema` | Minimum acceptable receipt schema version (1, 2, or 3) |
55
55
  | `max_age_days` | Maximum age of the merged/session receipt |
56
- | `require_mode` | Required receipt mode: `local` or `ci-gpu` |
56
+ | `require_mode` | Required receipt mode: `local` or `ci-gpu` (a `ci-gpu` receipt additionally requires `allowed_key_fingerprints` and must be signed by a listed key) |
57
57
  | `allow_dirty` | Permit dirty recording or verification trees |
58
58
  | `required_fingerprint_paths` | Exact tracked path list |
59
59
  | `required_fingerprint_extra_paths` | Exact generated/ignored path list |
@@ -61,7 +61,7 @@ schema 3. Repositories that have finished migrating should pin
61
61
  | `required_test_manifest` | Repository-relative file containing exact node IDs |
62
62
  | `required_shard_fingerprints` | Exact paths/extras for every named shard |
63
63
  | `allow_carried` | Permit carry-forward shards |
64
- | `carried_max_age_days` | Maximum age of each carried shard's original run |
64
+ | `carried_max_age_days` | Maximum age of each carried shard's original run (default: `max_age_days`, capped at 30) |
65
65
 
66
66
  Example shard scope:
67
67
 
@@ -75,7 +75,9 @@ escaping, or unmerged inputs fail closed.
75
75
  Schema 3 rejects dirty recording and verification trees by default. A receipt
76
76
  may verify at its exact commit or a descendant only when the source manifest
77
77
  still matches. Freshness limits replay duration but no server-issued nonce is
78
- currently used.
78
+ currently used; a session that started more than five minutes before its
79
+ commit's committer time is refused, so a receipt cannot be back-dated onto a
80
+ commit that did not yet exist.
79
81
 
80
82
  `allow_dirty: true`, long age limits, `--allow-skipped`, and
81
83
  `--allow-unsigned` weaken guarantees. Unsigned mode authenticates no signer,
@@ -98,8 +100,10 @@ each input signature. The merger's signature attests to the union. A review
98
100
  workflow that needs independent shard authentication should verify every
99
101
  input before merging.
100
102
 
101
- Carry-forward means a test passed at an ancestor commit and its declared shard
102
- inputs are byte-identical now. It does not establish that omitted dependencies
103
+ Carry-forward means a test passed at an ancestor commit, with a clean working
104
+ tree, and its declared shard inputs are byte-identical now. A receipt recorded
105
+ dirty is never a carry source: the merged `repo.dirty` is computed from fresh
106
+ inputs only, so carrying from it would launder the dirty flag. It does not establish that omitted dependencies
103
107
  or the old environment remain equivalent. Carried shards are rejected unless
104
108
  policy opts in and bounds their original age.
105
109
 
@@ -38,8 +38,10 @@ session failed.
38
38
 
39
39
  The merged receipt records input source names, counts, times, and recorded
40
40
  signers, then signs the result with the merger's key. Input signatures are
41
- provenance, not independently verified during offline merge. The merger
42
- attests to the union.
41
+ provenance, not verified during an offline merge, unless `--verify-inputs` is
42
+ passed: then every input must verify against its signer's current GitHub keys
43
+ (unsigned inputs are refused) before the union is formed. The merger attests
44
+ to the union either way.
43
45
 
44
46
  ## Carry forward unchanged shards
45
47
 
@@ -55,10 +57,22 @@ An absent old shard is carried only when:
55
57
  1. its receipt commit is the current commit or an ancestor;
56
58
  2. its stored narrow manifest—including explicit extra paths—recomputes
57
59
  identically at the current tree;
58
- 3. it does not duplicate a fresh node ID;
59
- 4. every claimed node ID exists in the old tests list.
60
+ 3. its receipt was recorded with a clean working tree (`repo.dirty` false): a
61
+ dirty recording may have exercised uncommitted bytes outside the shard's
62
+ narrow manifest, and the merged `repo.dirty` covers fresh inputs only;
63
+ 4. it does not duplicate a fresh node ID;
64
+ 5. every claimed node ID exists in the old tests list.
65
+
66
+ Every shard entry records `dirty` (the recording tree's state), merge ORs
67
+ shard flags into the merged `repo.dirty`, carry-forward refuses a dirty shard,
68
+ each carried shard records `carried.original_dirty`, and verification refuses
69
+ any dirty shard unless `allow_dirty` is set.
60
70
 
61
71
  Freshly rerun shards always win. Changed or malformed scopes require a rerun.
72
+ `gpu-proof plan --receipt old.json --repo .` applies the same conditions as a
73
+ dry run and reports, per shard, `carryable` or `stale` with the reason (add
74
+ `--json` for drivers), so a partial re-run can be planned without reimplementing
75
+ the checks.
62
76
 
63
77
  Verification rejects carried shards by default:
64
78
 
@@ -1,4 +1,5 @@
1
1
  site_name: pytest-gpu-proof
2
+ site_url: https://a2r-lab.org/pytest-gpu-proof/docs/
2
3
  site_description: >-
3
4
  pytest plugin for GPU equivalence testing with signed receipts verified via
4
5
  GitHub SSH keys
@@ -7,6 +8,7 @@ repo_name: A2R-Lab/pytest-gpu-proof
7
8
 
8
9
  theme:
9
10
  name: material
11
+ custom_dir: website/overrides
10
12
  palette:
11
13
  - scheme: default
12
14
  primary: indigo
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pytest-gpu-proof"
7
- version = "0.4.0"
7
+ version = "0.4.1"
8
8
  description = "pytest plugin for GPU equivalence testing with signed receipts verified via GitHub SSH keys"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -29,8 +29,8 @@ dependencies = [
29
29
  ]
30
30
 
31
31
  [project.urls]
32
- Homepage = "https://github.com/A2R-Lab/pytest-gpu-proof"
33
- Documentation = "https://a2r-lab.github.io/pytest-gpu-proof/"
32
+ Homepage = "https://a2r-lab.org/pytest-gpu-proof/"
33
+ Documentation = "https://a2r-lab.org/pytest-gpu-proof/docs/"
34
34
  Repository = "https://github.com/A2R-Lab/pytest-gpu-proof"
35
35
  Issues = "https://github.com/A2R-Lab/pytest-gpu-proof/issues"
36
36
  Changelog = "https://github.com/A2R-Lab/pytest-gpu-proof/blob/main/CHANGELOG.md"
@@ -0,0 +1 @@
1
+ __version__ = "0.4.1"
@@ -106,9 +106,116 @@ def main():
106
106
  "allow_carried (default: rejected).")
107
107
  mp.add_argument("--repo", default=".", metavar="PATH",
108
108
  help="Repo root for carry-from fingerprint/ancestry checks")
109
+ mp.add_argument("--verify-inputs", action="store_true", default=False,
110
+ help="Authenticate every input shard's signature against the "
111
+ "signer's current GitHub keys before merging (unsigned inputs "
112
+ "are refused)")
113
+
114
+ # status
115
+ sp = subparsers.add_parser(
116
+ "status",
117
+ help="Summarize receipt files (commit, dirty, outcome, tests, shards, signed)",
118
+ )
119
+ sp.add_argument("paths", nargs="+", metavar="PATH",
120
+ help="Receipt files or directories (every *.json inside)")
121
+ sp.add_argument("--json", action="store_true", default=False)
122
+
123
+ # durations
124
+ dp = subparsers.add_parser(
125
+ "durations",
126
+ help="Fold per-test durations from receipts into a JSON map (bin-packing calibration)",
127
+ )
128
+ dp.add_argument("receipts", nargs="+", metavar="RECEIPT")
129
+ dp.add_argument("--out", required=True, metavar="PATH",
130
+ help="JSON map node_id -> seconds; merged into an existing file")
131
+
132
+ # plan
133
+ pp = subparsers.add_parser(
134
+ "plan",
135
+ help="Dry run of carry-forward: which shards of an old receipt could be "
136
+ "carried against the current tree and which must be re-run",
137
+ )
138
+ pp.add_argument("--receipt", required=True, metavar="PATH",
139
+ help="Older sharded receipt (the one you would --carry-from)")
140
+ pp.add_argument("--repo", default=".", metavar="PATH",
141
+ help="Repository root for fingerprint/ancestry checks (default: .)")
142
+ pp.add_argument("--json", action="store_true", default=False,
143
+ help="Print the plan as JSON instead of one line per shard")
109
144
 
110
145
  args = parser.parse_args()
111
146
 
147
+ if args.command == "plan":
148
+ import json
149
+
150
+ from .merge import MergeError, load_receipt, plan_carry
151
+
152
+ try:
153
+ report = plan_carry(load_receipt(args.receipt), repo_root=args.repo)
154
+ except MergeError as e:
155
+ print(f"gpu-proof plan: {e}", file=sys.stderr)
156
+ sys.exit(1)
157
+ if args.json:
158
+ print(json.dumps(report, indent=2, sort_keys=True))
159
+ else:
160
+ r = report["receipt"]
161
+ print(f"receipt {str(r.get('commit_sha'))[:12]} vs HEAD {str(r.get('head'))[:12]}: "
162
+ f"{'carryable' if r['carryable'] else 'NOT carryable'} ({r['reason']})")
163
+ for s in report["shards"]:
164
+ print(f" {s['verdict']:9s} {s['name']} ({s['node_count']} tests): {s['reason']}")
165
+ n = sum(1 for s in report["shards"] if s["verdict"] == "carryable")
166
+ print(f"{n}/{len(report['shards'])} shard(s) carryable")
167
+ sys.exit(0)
168
+
169
+ if args.command == "status":
170
+ import json
171
+ import os
172
+
173
+ from .merge import receipt_status
174
+
175
+ files = []
176
+ for p in args.paths:
177
+ if os.path.isdir(p):
178
+ files += sorted(os.path.join(p, f) for f in os.listdir(p) if f.endswith(".json"))
179
+ else:
180
+ files.append(p)
181
+ rows = [receipt_status(f) for f in files]
182
+ if args.json:
183
+ print(json.dumps(rows, indent=2, sort_keys=True))
184
+ else:
185
+ for r in rows:
186
+ if "error" in r:
187
+ print(f"{r['file']}: {r['error']}")
188
+ continue
189
+ print(f"{r['file']}: schema {r['schema']} @ {str(r['commit_sha'])[:12]}"
190
+ f"{' DIRTY' if r['dirty'] else ''}{'' if r['signed'] else ' UNSIGNED'}"
191
+ f" {r['outcome'] or '-'} tests={r['tests']} failed={r['failed']} "
192
+ f"skipped={r['skipped']} shards={len(r['shards'])} carried={r['carried']} "
193
+ f"ended={r['ended_at']}")
194
+ sys.exit(0)
195
+
196
+ if args.command == "durations":
197
+ import json
198
+
199
+ from .merge import MergeError, collect_durations, load_receipt
200
+
201
+ try:
202
+ receipts = [load_receipt(p) for p in args.receipts]
203
+ except MergeError as e:
204
+ print(f"gpu-proof durations: {e}", file=sys.stderr)
205
+ sys.exit(1)
206
+ existing = {}
207
+ try:
208
+ with open(args.out) as f:
209
+ existing = json.load(f)
210
+ except (OSError, ValueError):
211
+ existing = {}
212
+ merged = collect_durations(receipts, existing if isinstance(existing, dict) else {})
213
+ with open(args.out, "w") as f:
214
+ json.dump(merged, f, indent=2, sort_keys=True)
215
+ f.write("\n")
216
+ print(f"durations: {len(merged)} node(s) -> {args.out}")
217
+ sys.exit(0)
218
+
112
219
  if args.command == "merge":
113
220
  from .merge import MergeError, merge_receipts
114
221
 
@@ -120,6 +227,7 @@ def main():
120
227
  unsigned=args.unsigned,
121
228
  carry_from=args.carry_from,
122
229
  repo_root=args.repo,
230
+ verify_inputs=args.verify_inputs,
123
231
  )
124
232
  except MergeError as e:
125
233
  print(f"gpu-proof merge: {e}", file=sys.stderr)
@@ -21,6 +21,9 @@ class GpuProofConfig:
21
21
  github_username: Optional[str] = None
22
22
  max_age_days: int = 30
23
23
  require_gpu: bool = False
24
+ # Record each device's nvidia-smi UUID in the receipt environment. Off keeps the
25
+ # device name, driver, memory and compute capability but not the card identity.
26
+ record_gpu_uuid: bool = True
24
27
  # Sharded emission: a declared shard name + its narrow
25
28
  # fingerprint paths (None -> the global fingerprint_paths).
26
29
  shard_name: Optional[str] = None
@@ -92,7 +95,7 @@ def load_config(pytest_config) -> GpuProofConfig:
92
95
  return [p.strip() for p in raw.split(",") if p.strip()]
93
96
  return [str(p) for p in raw]
94
97
 
95
- max_age = toml_cfg.get("max_age_days")
98
+ max_age = resolve("--gpu-proof-max-age-days", "max_age_days", None)
96
99
  max_age_days = int(max_age) if max_age is not None else 30
97
100
 
98
101
  return GpuProofConfig(
@@ -117,7 +120,11 @@ def load_config(pytest_config) -> GpuProofConfig:
117
120
  ),
118
121
  github_username=resolve("--gpu-proof-github-user", "github_username", None),
119
122
  max_age_days=max_age_days,
120
- require_gpu=bool(toml_cfg.get("require_gpu", False)),
123
+ # store_true flags can only turn a setting ON (or, for the UUID, OFF).
124
+ require_gpu=bool(opt("--gpu-proof-require-gpu", False)
125
+ or toml_cfg.get("require_gpu", False)),
126
+ record_gpu_uuid=bool(toml_cfg.get("record_gpu_uuid", True))
127
+ and not bool(opt("--gpu-proof-no-gpu-uuid", False)),
121
128
  shard_name=resolve("--gpu-proof-shard", "shard_name", None),
122
129
  shard_fingerprint_paths=shard_paths,
123
130
  shard_fingerprint_extra_paths=(
@@ -49,6 +49,15 @@ def get_branch(root: str = ".") -> Optional[str]:
49
49
  return _git("rev-parse", "--abbrev-ref", "HEAD", root=root)
50
50
 
51
51
 
52
+ def get_commit_time(sha: str, root: str = ".") -> Optional[int]:
53
+ """Committer time (Unix seconds) of ``sha``, or None when git cannot answer."""
54
+ value = _git("show", "-s", "--format=%ct", sha, root=root)
55
+ try:
56
+ return int(value) if value else None
57
+ except ValueError:
58
+ return None
59
+
60
+
52
61
  def is_dirty(
53
62
  root: str = ".",
54
63
  *,