Skip to content

gpucheck v1.0 — Apple MPS backend, stride fuzzing, dashboard - #2

Merged
Akasxh merged 30 commits into
mainfrom
release/v1.0
May 7, 2026
Merged

gpucheck v1.0 — Apple MPS backend, stride fuzzing, dashboard#2
Akasxh merged 30 commits into
mainfrom
release/v1.0

Conversation

@Akasxh

@Akasxh Akasxh commented May 1, 2026

Copy link
Copy Markdown
Owner

Summary

gpucheck v1.0.0rc1 — first major release. Four parallel engineering tracks delivered:

  • Track A — MPS backend (feat/track-a-mps): gpucheck.backends.{Backend, CUDABackend, MPSBackend}, @devices("mps"), deadlock-safe MPS event timing (avoids pytorch#162872), 2× MPS tolerance overlay (PROVISIONAL — pending P99 calibration), 12-entry [tool.gpucheck.mps.xfail], [mps] / [apple] install extras (torch>=2.6).
  • Track B — Stride fuzzing (feat/track-b-strides): fuzz_strides() deterministic 7-category corpus, StrideStrategy for Hypothesis, parametrize_gpu(stride_categories=...) wiring.
  • Track C — Thread-safety (feat/track-c-thread-safety): tolerance_context() backed by contextvars.ContextVar (safe for pytest-xdist, threads, asyncio). Mitigates security finding TM-E1 (path-injection via CUDA_HOME/CUDA_PATH).
  • Track D — Release bundle (feat/track-d-bundle): HTML dashboard reporter, assert_deterministic / @requires_determinism, committed uv.lock (DEP-1), CI permissions hardening (CFG-2), reporting test coverage 0% → 98%.

Test count: 117 → 224 (+107). All ruff E/F/W/I/N/UP/B/A/SIM/TCH + mypy strict clean.

Dogfooding: Real MPS run on this Mac at dist/dashboard-v1.0.html — matmul fp32 throughput 63 → 1133 → 1703 GFLOPs at 256/1024/2048 with the deadlock-safe event timer.

Bug-finding swarm: 26 kernels × 250 iterations = 6 500 iterations on real MPS. UPSTREAM.md verdict: 0 issues filed — all swarm-flagged divergences are tolerance-recalibration territory (1-5× atol), not upstream PyTorch bugs. The PROVISIONAL 2× multiplier needs P99 calibration on the M-machine; conv2d stride=slice fp16/bf16 is a candidate xfail entry.

Team protocol: session ran on claude-forge — see .claude/teams/{research,engineering,security,testing,docs,forge}/v1.0/ for the full evidence trail (SYNTHESIS.md, FINDINGS.md, DIFF_LOG.md, swarm/RESULTS_*.md, evaluator.md per team).

Test plan

  • uv run pytest -q → 224 passed, 1 skipped (was 117)
  • MPS-tagged tests on this Apple Silicon Mac → 30/31 pass (1 SKIPPED is cuda0 test, no NVIDIA hardware)
  • ruff + mypy strict clean
  • Reporting coverage 0% → 98%
  • HTML dashboard renders (verified dist/dashboard-v1.0.html, real benchmarks)
  • Determinism sanitizer (assert_deterministic) tested
  • Thread-safety regression test under ThreadPoolExecutor
  • TestPyPI upload — deferred (~/.pypirc not configured locally)

🤖 Generated with Claude Code

Akasxh and others added 30 commits May 1, 2026 09:51
Track-A of the gpucheck v1.0 release introduces an MPS backend wired
through a structural Backend Protocol. The benchmark fixture uses
device-level torch.mps.synchronize() instead of per-event Event sync to
avoid the deadlock documented in pytorch#162872 (research SYNTHESIS §3).

Major additions:
- New gpucheck.backends package with Backend / EventTimer Protocols and
  CUDABackend / MPSBackend implementations.
- @Devices("mps") and @Devices("all") parametrize across CUDA + MPS.
- assert_close GPU fast-path widened to MPS tensors (no CPU transfer).
- compute_tolerance accepts device_type="mps" and applies a PROVISIONAL
  2x dtype-aware multiplier (calibration plan in SYNTHESIS §7).
- [tool.gpucheck.mps.xfail] config block in pyproject.toml ships the 12
  known-broken kernels from SYNTHESIS §2 as a living document.
- gpucheck.is_mps_xfailed("op.subcategory") queries the xfail registry.
- pyproject.toml extras: [mps] and [apple] (torch>=2.6 floor).
- GPUInfo gains a backend: str field; MPS-derived instances populate
  architecture="Apple-Silicon", compute_capability=(0, 0), tensor_core_generation=None.
- gpu_benchmark fixture branches on cuda_avail vs mps_avail; MPS path
  uses time.perf_counter() between torch.mps.synchronize() calls.

Security findings touched:
- N1 / N2 (xcrun metal subprocess hardening): out-of-scope, gpucheck
  does not shell out to xcrun (PyTorch handles that internally).
- N3 (mach task_info): out-of-scope, MPS memory comes from
  torch.mps.current_allocated_memory and psutil RSS.
- N4 (supply chain): partially mitigated via torch>=2.6 floor on [mps].
- N5 (MPS dispatch sanitizer): out-of-scope until Apple ships one.

Test count: 117 baseline -> 147 passing (+30 net new MPS-aware tests).
ruff and mypy strict pass clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Track-B of the gpucheck v1.0 release fills the documented "no stride /
contiguity fuzzing" gap (CLAUDE.md weaknesses). Adds a deterministic
seven-category corpus and a Hypothesis StrideStrategy, wired into
parametrize_gpu via a new stride_categories= keyword.

Categories (priority order):
  row_major, column_major, broadcast, transpose, slice, non_contig, gather

Each category exercises a different code path inside PyTorch's kernel
dispatcher. Test authors can write:

    @parametrize_gpu(
        dtypes=("float32",), shapes=((64, 64),),
        stride_categories=("row_major", "transpose", "broadcast"),
    )
    def test_layout_invariant(dtype, shape, device, stride_category):
        t = fuzz_strides_for_category(shape, dtype, stride_category, device=device)
        ...

New module:
- src/gpucheck/fuzzing/strides.py with fuzz_strides, fuzz_strides_for_category,
  StrideStrategy, CATEGORIES.

Tests added:
- tests/test_fuzz_strides.py (17 tests covering each category and edge cases)
- tests/test_fuzz_strides_hypothesis.py (3 Hypothesis property tests)
- tests/test_parametrize_gpu_strides.py (3 wiring tests)

Test count: 117 baseline -> 139 passing (+22 net new). ruff and mypy strict
pass clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… TM-E1

Track-C of the gpucheck v1.0 release fixes the documented
"Thread-safety issue in tolerance override stack" gap (CLAUDE.md weakness)
and addresses security finding TM-E1.

Tolerance override stack:
- Replace module-level list `_tolerance_overrides` with a
  `contextvars.ContextVar`. Each OS thread (and each asyncio task that
  copies the current context) sees its own override stack.
- `tolerance_context(atol, rtol)` now uses ContextVar.set / .reset(token),
  which is exception-safe by construction.
- The user-facing API is unchanged: `with tolerance_context(...): ...`

TM-E1 mitigation in sanitizers/race.py:
- _find_compute_sanitizer normalizes CUDA_HOME / CUDA_PATH via
  os.path.realpath and validates against _CUDA_HOME_ALLOWLIST
  (/usr/local/cuda, /opt/nvidia/cuda, /opt/cuda).
- A symlink pointing outside the allowlist is correctly rejected.
- A path that lookalikes a prefix (e.g. /usr/local/cuda-evil) is rejected
  by exact-prefix-with-separator matching.
- Rejected paths emit a RuntimeWarning explaining the rejection.

Tests added:
- tests/test_tolerance_thread_safety.py
  - test_tolerance_context_is_thread_isolated: 4-thread Barrier-coordinated
    stress test; fails on the unfixed plain-list code, passes on ContextVar.
  - test_tolerance_context_pop_is_correct_after_exception
  - test_tolerance_context_nesting_in_single_thread
- tests/test_race_cuda_home_allowlist.py (6 tests covering the canonical
  paths, lookalikes, missing PATH, allowlisted binary, symlink attack)

Test count: 117 baseline -> 126 passing (+9 net new). ruff and mypy strict
pass clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… hardening

Track-D of the gpucheck v1.0 release closes four documented gaps in one
shot, splits across the reporting and sanitizers packages plus CI plumbing.

D.1 reporting test coverage 0 -> 98%
- tests/test_reporting_console.py (10 tests) — Rich-based renderer.
- tests/test_reporting_json.py (6 tests) — RunRecord schema, compare_runs
  classifies regression / ok / new / removed / div-by-zero.
- tests/test_reporting_ci.py (8 tests) — GitHub Actions annotations,
  JUnit XML well-formedness, PR comment Markdown.

D.2 reporting/html.py — static HTML dashboard
- Self-contained HTML (no external CSS/JS, no fetches at view time).
- Inline SVG bar chart for benchmark medians.
- Test results / benchmarks / memory / comparison sections.
- Empty data handled gracefully.
- HTMLParser well-formedness verified; XSS escaping verified.

D.3 sanitizers/determinism.py — assert_deterministic + @requires_determinism
- Seeds random / numpy / torch (CPU+CUDA+MPS) before each invocation.
- Compares torch.Tensor / tuple / list / scalar outputs byte-identically.
- DeterminismError surfaces SYNTHESIS §4 best-effort caveat in failure msg.
- 8 tests covering pass/fail/exception/nesting/torch-tensor paths.

D.4 DEP-1 mitigation — uv.lock committed
- 1220-line lock file from existing dev environment.
- CI now installs via `uv sync --frozen --extra dev`.

D.5 CFG-2 mitigation — .github/workflows/ci.yml
- Top-level `permissions: contents: read` block.
- Default GITHUB_TOKEN is no longer write-all.

Test count: 117 baseline -> 157 passing (+40 net new).
Reporting coverage: 0% -> 98% (target was 90%).
ruff and mypy strict pass clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Resolves tolerances.py conflict between Track-A (MPS multipliers + xfail
registry) and Track-C (ContextVar override stack). C's ContextVar wins
for `_tolerance_overrides`; A's `_MPS_TOLERANCE_MULTIPLIERS` and xfail
registry stack alongside. Verified: 16 tests pass across both feature
sets (test_tolerance_thread_safety + test_assert_close_mps + test_mps_xfail).
…xtra

Track-D committed uv.lock generated against pre-A pyproject.toml. Phase 3
merge brings in the [mps] / [apple] extras with torch>=2.6 floor. Regen
captures these for reproducible installs.
Document the four v1.0 tracks (MPS backend, stride fuzzing, thread-safe
tolerances, release bundle) with citations to engineering DIFF_LOG and
research SYNTHESIS. Reconciles the prior "8 real bugs" claim with the
verified count (2 externally filed: triton#9838, triton#9839).
…commits

Cover dev setup (uv sync --frozen), test running (224 passing, MPS
auto-runs on Apple Silicon), code style (ruff + mypy strict), commit
convention transition (Conventional Commits going forward; legacy
'[ Type ] :' bracket history unchanged), PR process, expert system,
and the engineering / docs team protocols at ~/.claude/teams/.
Documents the new Backend Protocol entry points (still backwards
compatible with detect_gpus()), GPUInfo.backend field default,
@Devices() MPS auto-detection, ContextVar-based tolerance overrides,
[tool.gpucheck.mps.xfail] registry, the PROVISIONAL 2x MPS multiplier,
new fuzz_strides API, and the committed uv.lock for reproducible installs.
…inter

Update the lead paragraph to reflect MPS as first-class, add the [mps]
install extra, replace the AMD ROCm 'not supported yet' caveat with
explicit MPS-supported / ROCm-and-XPU-planned phrasing, and reconcile the
"8 real bugs" claim with the visible table via a footnote citing the
2 externally verified upstream issues (triton#9838 open, triton#9839
closed) per research SYNTHESIS Sub-Q 8.
… bundle

Move MPS, stride/contiguity fuzzing, thread-safety, HTML reporting, and
determinism out of "Known Weaknesses & Gaps" — they all shipped in v1.0.
Add a new "v1.0 highlights" section summarising the four engineering
tracks. Update Git Conventions to formalize Conventional Commits going
forward (legacy bracket-style history unchanged). Update architecture
overview with the new backends/ package. Update PyPI line to v1.0.0rc1.
CI lint job (mypy strict on Linux py3.12) flagged
`src/gpucheck/fuzzing/strides.py:295` unused-ignore. Local verifier
during Track B did not surface this — likely because hypothesis was
installed lazily after that file was written. Removing the ignore is
the minimal fix; the decorator is now correctly inferred.
…-05 — IMPL_PLAN_v1.1)

memory_guard() previously yielded a private _MutableReport class, leaking an
underscore-prefixed type through a public API and forcing callers to depend
on an unstable name. Rename to MemoryGuardReport (slots dataclass) and
re-export from gpucheck.sanitizers.

Avoids collision with fixtures.profiler.MemoryReport (frozen, fixture-side
summary) by using a distinct name; the existing MemoryReport backward-compat
alias on SanitizerMemoryReport is preserved.

Refs: detector-files Top-10 #8, synthesist ISS-15, planner row 119.
… (T-01)

Replace the module-level `try: import torch as _torch / _has_torch` block in
`src/gpucheck/assertions/close.py` with a cached `_torch_mod()` helper that
performs the import only when an API needs it. Both call sites inside
`assert_close` (device-type detection and the GPU fast-path) bind
`_torch = _torch_mod()` at the top of the function and guard with
`_torch is not None`.

Verified via `python -c "import gpucheck.assertions.close; import sys;
assert 'torch' not in sys.modules"`. All 32 tests in test_assertions.py
remain green.

Source: detector-files Top-3 #1; synthesist ISS-08; planner T-01.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ontract (T-04 — detector-files #3)

Replaces 7 bare `except Exception:` sites with narrow tuples + debug-level
log messages so silent fallbacks can be diagnosed:

backends/mps.py
  L99   torch.mps.device_count()      -> (RuntimeError, AttributeError)
  L137  current_allocated_memory()    -> (RuntimeError, AttributeError)
  L141  driver_allocated_memory()     -> (RuntimeError, AttributeError)
  L148  recommended_max_memory()      -> (RuntimeError, AttributeError)
  L191  recommended_max_memory()      -> (RuntimeError, AttributeError)
arch/detection.py
  L157  pynvml CUDA driver version    -> (pynvml.NVMLError, AttributeError)
  L229  torch.cuda.mem_get_info()     -> (RuntimeError, AttributeError)

All sites now log at DEBUG before returning the documented fallback so
"why is recommended_max_memory zero on this build?" is answerable.

Acceptance:
  ruff check src/ tests/  -> All checks passed
  mypy src/               -> Success: no issues found in 41 source files
  pytest -q               -> 224 passed, 1 skipped (unchanged baseline)
…ed (T-07 — IMPL_PLAN_v1.1)

The README claimed `pytest tests/gpu_integration/` auto-skips without a GPU,
but on MPS hosts the suite collected and failed 52 tests on CUDA-specific
calls (docs-tester finding R-B21 / T-B5).

Add a `pytest_collection_modifyitems` hook in
`tests/gpu_integration/conftest.py` that skips every collected item unless:

- CUDA is available, or
- MPS is available *and* `--mps-integration` was passed.

Add the `--mps-integration` flag for opt-in MPS execution. The default
top-level `pytest -q` already ignores this directory via `pyproject.toml`
addopts; this hook honors the README claim when the directory is invoked
explicitly.
…failure (T-08 — IMPL_PLAN_v1.1)

`_load_pyproject_config` previously caught bare `Exception` and silently
swallowed it, masking real misconfiguration (security finding PM-2). Narrow
to `(OSError, tomllib.TOMLDecodeError)` and emit a `UserWarning` so users
see *why* their tolerance overrides were ignored. Programmer errors
(AttributeError, TypeError, etc.) now propagate as expected.

The absent-file branch still returns silently — that is the documented
fall-through path to built-in defaults.

Refs: security-postmerge PM-2, planner T-08.
…(T-02 — PM-4)

Insert `.contiguous()` between `.cpu()` and `.numpy()` on both torch.Tensor
branches of `_to_numpy` (the primary `hasattr(tensor, "detach")` branch and
the dlpack fallback inside `__cuda_array_interface__`). torch <2.1 raises
RuntimeError ("input array is not C-contiguous") when `.numpy()` is called
on stride-fuzzed / sliced / transposed tensors. Preventive on newer torch —
known to fire on older PyTorch.

Add `tests/test_assert_close_contiguous.py` with 6 parametrized cases
(slice / transpose / broadcast × `_to_numpy` direct / `assert_close`
end-to-end). Test count 224 → 230 passed, 1 skipped.

Source: security-postmerge PM-4; planner T-02; synthesist ISS-18.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ector-files #1, partial)

Two parallel detection stacks lived at:
  - arch/detection.py:133,206  (pynvml + torch -> list[GPUInfo])
  - fixtures/gpu.py:43,90      (pynvml + torch -> GPUDevice)

Drift risk: the two stacks had different fallbacks (NVMLError vs
RuntimeError vs AssertionError catches) and different "no backend"
behavior (one warned, the other silently returned None).

Now there is exactly one detection codepath:

  arch/detection._detect_gpus_or_warn() -> list[GPUInfo] | None
    `-- arch/detection.detect_gpus() (lru_cache wrapper, public API,
                                      maps None -> [])
    `-- fixtures/gpu.detect_gpu()      (calls detect_gpus(), adapts the
                                      first GPUInfo to the smaller
                                      GPUDevice via _to_device())

Public API preserved:
  - arch.detect_gpus() still returns list[GPUInfo] (empty list on no
    backend; was already that way).
  - fixtures.gpu.detect_gpu() still returns GPUDevice | None.
  - The "no detection backend available" UserWarning is preserved (and
    is now lru_cache-deduped, firing at most once per session via
    detect_gpus()).

The plugin.py shim (`_lazy_detect_gpus` / `_gpu_available` / `_gpu_count`
at plugin.py:10-22) is owned by another agent per task instructions and
left untouched; the helper this commit introduces is the foundation that
agent's plugin.py refactor can call into.

Removed:
  - fixtures/gpu._detect_gpu_pynvml (~45 LOC)
  - fixtures/gpu._detect_gpu_torch  (~25 LOC)

Acceptance:
  ruff check src/gpucheck/arch/ src/gpucheck/fixtures/gpu.py -> clean
  mypy src/                                                  -> clean
  pytest -q                                                  -> 230 passed,
                                                                1 skipped
                                                                (unchanged)
…(T-21 — api-dx #2)

api-dx-grade weak API #2 flagged @require_arch (singular) as
inconsistent with @requires_determinism (plural). Both are decorators
of the same shape but spelled differently — a typo footgun that silently
skips tests instead of erroring.

This commit:
- Adds @requires_arch as the canonical (plural) form. Same semantics as
  @require_arch, same alias expansion ("Blackwell" -> {"blackwell-dc",
  "blackwell-consumer"}), same skip-with-detected-arch error string.
- Keeps @require_arch as a backward-compatible wrapper that emits a
  DeprecationWarning on use; will be removed in v1.2.
- Re-exports both names from gpucheck.arch (and arch.compatibility).
- Documents the addition + deprecation in CHANGELOG.md under
  [Unreleased].

The existing test suite continues to use @require_arch (verified by
grep); those tests pass unchanged and surface the new
DeprecationWarning, which mypy/ruff/pytest collect as informational —
no test fails.

Acceptance:
  ruff check src/                  -> All checks passed
  mypy src/                        -> Success: no issues found in 41 source files
  pytest -q                        -> 233 passed, 1 skipped (no regression)
  pytest -W default tests/test_arch.py
                                   -> @require_arch sites now emit
                                      DeprecationWarning("@require_arch
                                      (singular) is deprecated and will
                                      be removed in v1.2; use
                                      @requires_arch (plural) for naming
                                      consistency with
                                      @requires_determinism.")
… ~30 mutants)

Add `TestMismatchReportPinnedNumerics` to tests/test_assertions.py with
three new tests that lock down exact numeric values in
`format_mismatch_report` output:

1. `test_max_abs_error_value_is_pinned` — pins max abs error (4.5) and
   mean abs error (2.5) values, plus row labels.
2. `test_mismatch_count_and_location_are_pinned` — pins
   `5 / 6 (83.33%)` count/total/percentage and 2-D max-error location
   `(1, 2)` (forces unravel_index axis mutations to fail).
3. `test_histogram_present_with_pinned_bucket_and_count` — pins
   histogram bucket label `[1e-3, 1e-2)` and count 3 (ANSI-stripped).

Targets `assertions/reporting.py` mutator survivors documented in
`EVIDENCE/mutator-survivors.md` top-leverage #1 (~30 surviving mutants).
No source changes to reporting.py — tests-only per planner T-10.

Test count 230 → 233 passed, 1 skipped.
ruff: clean. mypy: clean.

Source: planner T-10; mutator-survivors top-leverage #1; synthesist ISS-25.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…roup

Final summary: 233 passed, 1 skipped; mutmut baseline 169 (unchanged);
ruff + mypy clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…-N1 — review BLOCKER)

External review N1: the `baseline_2x=True` branch in `assert_close` was
deriving its own k_dim scaler via `sqrt(k_dim)` while every other code
path (and the canonical `compute_tolerance` helper) uses
`sqrt(max(k_dim, 1) / 128)`. At k_dim=4096 the two diverge by
`sqrt(128) ≈ 11.3×`, silently making `baseline_2x=True` ~11× more
permissive than the un-doubled call. The headline FlashAttention
example (`assert_close(out, ref, k_dim=4096, baseline_2x=True)`) hit
this immediately — silent-correctness regression in the most-prominent
public assertion API.

Fix: drop the open-coded scaling block in `close.py:172-183` and route
through `compute_tolerance(dtype, k_dim=..., device_type=...)` for the
canonical scale, then multiply atol/rtol by 2 only if `baseline_2x` is
set. Identical semantics to the non-`baseline_2x` path; just doubled.

Regression test: `TestBaseline2xKDimScaling` in
`tests/test_assertions.py` — parametrized over k_dim ∈ {128, 1024, 4096}.
Pins the contract that `baseline_2x` is exactly 2× canonical, NOT
`2 × sqrt(128)`× canonical. Tests both the pass boundary
(diff = 1.5× canonical) and the fail boundary (diff = 2.5× canonical).

Verification: 233 → 239 tests passing; ruff + mypy clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…view BLOCKER)

External review A1: the canonical category constants in
`src/gpucheck/fuzzing/strides.py` use snake_case (`row_major`,
`broadcast`, `non_contig`, ...), but `MIGRATION.md` §7 examples named
them in kebab-case (`row-major`, `broadcast-induced`,
`contiguous-after-clone`, `gather-induced`). Every copy-pasted
MIGRATION snippet hit `ValueError: Unknown stride category
'broadcast-induced'`.

Fix option (a) chosen — accept BOTH forms at the API boundary and
normalize to the canonical snake_case internally; deprecation-warn on
non-canonical input. Rationale:

- Real users have already copy-pasted the kebab examples into their
  codebases. Hard-failing them on upgrade is a worse first-impression
  than carrying a one-tier alias table.
- `_canonicalize_category()` is a pure-string lookup; zero runtime cost
  on the canonical path.
- DeprecationWarning gives users a discoverable path off the alias.
- The public `CATEGORIES` constant remains snake_case (Python
  convention; matches the in-tree code style).

Also rewrites `MIGRATION.md` §7 to use the canonical snake_case names
and adds a note that kebab forms are deprecated aliases. Drops the
nonsense `n=20` from the fuzz_strides example (corollary of D4).

Test: `tests/test_fuzz_strides.py::test_fuzz_strides_for_category_accepts_kebab_alias`
parametrizes 7 alias→canonical pairs; asserts both spellings produce
the same tensor (same seed) and that DeprecationWarning fires for the
kebab form. A second test exercises `fuzz_strides(categories=...)`
with mixed kebab inputs.

Verification: 239 → 247 tests passing; ruff + mypy clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…(B-S1 — review BLOCKER)

External review S1: `src/gpucheck/plugin.py:_load_pyproject_config`
falls back to `import tomli as tomllib` on Python 3.10 (which lacks
`tomllib` in the stdlib). But `tomli` was NOT declared in
`[project.dependencies]` of `pyproject.toml`. On a fresh
`pip install gpucheck` on 3.10 with no transitive coverage of `tomli`,
the import fails — and the previously-broad `except` (now narrowed by
T-08) silently no-oped the user's `[tool.gpucheck.tolerances]` and
`[tool.gpucheck.mps.xfail]` overlays. Confidence-in-test failure
mode (OWASP-A05 configuration trust).

Fix: add `'tomli>=2.0; python_version < "3.11"'` to
`[project.dependencies]` using PEP 508 marker syntax. The marker
ensures Python 3.11+ does NOT pull tomli (stdlib `tomllib` is
preferred there).

Test: `tests/test_plugin_tomli.py::test_tomli_is_importable_on_python_310`
asserts `tomli` is importable on the 3.10 path (skipped on 3.11+).
A second smoke test exercises the loader on whichever TOML backend
the running interpreter resolves to, pinning that the
`[tool.gpucheck.tolerances]` overlay actually applies end-to-end.

Verification: 247 → 248 passed, 1 → 2 skipped (the 3.10-only test
correctly skips on the 3.12 runner); ruff + mypy clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…t MIGRATION example (B-A2 — review BLOCKER)

External review A2: two cooperating defects in the determinism API.

(1) MIGRATION.md §8 example invoked `assert_deterministic` with three
    kwargs that don't exist: `args=`, `runs=`, `atol=`. The real
    signature is `assert_deterministic(fn, *args, n=3, seed=0,
    **kwargs)`. The bogus kwargs would forward into the user's kernel
    via `**kwargs`, crashing on the first call.

(2) The function compared outputs via `torch.equal` (bit-exact) with
    no tolerance knob. Research SYNTHESIS §4 documents that MPS is
    *best-effort* deterministic — pytorch#181936 / #170837 / #177116
    show real run-to-run drift on Apple Silicon. Byte-equality is the
    wrong default contract for the headline MPS use case the docs
    advertise.

Fix:

- Add `atol: float = 0.0` and `rtol: float = 0.0` parameters to
  `assert_deterministic`. When both are zero (the back-compat
  default), comparison stays bit-exact (`torch.equal`). When either
  is non-zero, comparison switches to `torch.allclose(..., atol, rtol)` —
  the right contract for MPS.
- Forward atol/rtol through `requires_determinism` so the decorator
  form gets the same knob.
- Updated the failure message to identify which mode raised
  (`mode=byte-identical` vs `mode=allclose(atol=..., rtol=...)`) so
  CI logs are self-explanatory.
- Rewrote MIGRATION.md §8 example to use the real signature
  (positional `*args`, `n=`, optional `atol=`/`rtol=`).

Tests (5 new):
- byte-identical default rejects 1-ULP drift
- atol > drift => pass
- atol < drift => DeterminismError with `allclose` in the message
- rtol path covered separately
- decorator forwards atol correctly

Verification: 248 → 253 passed, 2 skipped; ruff + mypy clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
`uv` re-resolved the lockfile when `tomli; python_version < "3.11"`
was added to `[project.dependencies]` in commit 0da3fd4 (B-S1).
The single line added to uv.lock is the marker-gated tomli
inclusion under the gpucheck `dependencies` and `requires-dist`
sections. No code change; lockfile-only.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ctor

5K-sample calibration on M5 (drift_histogram_5k.json) confirmed the 2×
starting multiplier within ±6% for matmul; flagged conv2d as an outlier
(+225%/+75%/+70% over v3 projection). The per-(kernel, dtype) refactor
required to address conv2d ships in v1.1 as task T-24, not v1.0.

Comment-only — no behavior change. PROVISIONAL flag intact.
@Akasxh
Akasxh merged commit 763a437 into main May 7, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant