Skip to content

fix(core): credit an entry that vanishes at unlink time during cache eviction - #2580

Open
LeSingh1 wants to merge 1 commit into
NVIDIA:mainfrom
LeSingh1:program-cache-eviction-credit
Open

fix(core): credit an entry that vanishes at unlink time during cache eviction#2580
LeSingh1 wants to merge 1 commit into
NVIDIA:mainfrom
LeSingh1:program-cache-eviction-credit

Conversation

@LeSingh1

@LeSingh1 LeSingh1 commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Problem

FileStreamProgramCache._enforce_size_cap walks its snapshot oldest-atime-first and subtracts each evicted entry's size from total until it is back under the cap. It handles "the file is already gone" twice, inconsistently (_file_stream.py:740-761):

            try:
                stat_now = path.stat()
            except FileNotFoundError:
                total -= size          # credited
                continue
            ...
            try:
                _unlink_with_sharing_retry(path)
                total -= size
            except FileNotFoundError:
                pass                   # NOT credited

Both branches are the same condition — another process removed the entry — observed a few microseconds apart, and the bytes are off disk either way. This backend is explicitly designed for multi-process use, and a concurrent __delitem__ or another process's eviction pass lands in exactly this window.

Not crediting the second one has two compounding effects:

  1. total stays above the cap, so the pass evicts a second, live entry that did not need to go — a needless cache miss and recompile.
  2. The pass then reseeds self._tracked_size_bytes = total with that same overcount, so the next write trips the cap early and over-evicts again.

Measured on the real module (three 30-byte entries, a 100-byte cap, one racing unlink): 2 entries survive instead of 3, and the tracker reports 90 bytes against 60 on disk.

Fix

Move the single total -= size past the handler so the FileNotFoundError branch reaches it, matching the stat-miss branch above. The PermissionError branch becomes an explicit continue: an exhausted Windows sharing-violation retry leaves the file on disk, so its bytes must stay in total. That was already the behaviour — making it explicit is what keeps the single total -= size at the end honest.

No behaviour change on the non-racing path.

Tests

test_filestream_size_cap_credits_an_entry_that_vanished_at_unlink monkeypatches _file_stream._unlink_with_sharing_retry so the pass's first victim is unlinked by "another process" in the window between our stat-guard and our own unlink — the same injection style as the existing test_filestream_cache_tracker_clamps_at_zero_under_delete_race. It asserts all three remaining entries survive and that the tracker agrees with _compute_total_size().

What I ran

Environment: macOS, no CUDA driver and no CUDA toolkit, so cuda.core cannot be built or imported here.

  • Did not run: cuda_core/tests/test_program_cache.py itself — it imports cuda.core.utils.
  • Ran (teeth check): _file_stream.py's only cuda.core dependency is ObjectCode, used for an isinstance check in _extract_bytes, so I loaded the real module with cuda.core._module stubbed and ran the new test's exact assertions against it:
    • against main: FAIL: len(cache) == 2, expected 3
    • with this change: PASS
  • Ran: a 30-trial randomised set/get/del/clear probe against the same standalone load, comparing _tracked_size_bytes to _compute_total_size() after each trial — no drift before or after the change, confirming the single-process accounting is untouched.
  • Ran: ruff check and ruff format --check on both changed files — clean, no new findings against a main baseline.
  • Checked: the two other _enforce_size_cap outcomes are unaffected — the stat-guard mismatch branch still does its own total += stat_now.st_size - size and continue, and the Windows sharing-violation branch still leaves total alone.

Overlap note: #2553 also touches _file_stream.py, but only __init__ (argument validation); the two changes are in different methods.

`_enforce_size_cap` walks its snapshot and subtracts each evicted entry's
size from `total` until it is back under the cap. It handles "the file is
already gone" twice, inconsistently:

    try:
        stat_now = path.stat()
    except FileNotFoundError:
        total -= size          # credited
        continue
    ...
    try:
        _unlink_with_sharing_retry(path)
        total -= size
    except FileNotFoundError:
        pass                   # NOT credited

Both are the same condition -- another process removed the entry -- observed a
few microseconds apart, and the bytes are off disk either way. Not crediting
the second one leaves `total` above the cap, so the pass evicts a second,
live entry that did not need to go, and then reseeds
`self._tracked_size_bytes = total` with that same overcount, which makes the
next write trip the cap early and over-evict again.

This is a documented, expected race for this backend: the class is designed
for multi-process use, and a concurrent `__delitem__` or another process's
eviction pass hits exactly this window.

The `PermissionError` branch is now an explicit `continue`: an exhausted
Windows sharing-violation retry leaves the file on disk, so its bytes must
stay in `total`. That was already the behaviour; making it explicit keeps
the single `total -= size` at the end honest.
@copy-pr-bot

copy-pr-bot Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@github-actions github-actions Bot added the cuda.core Everything related to the cuda.core module label Aug 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cuda.core Everything related to the cuda.core module

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant