Skip to content

fix(jit): invalidate stale nvcc cache artifacts by build fingerprint - #4490

Open
zhachar14h-cell wants to merge 5 commits into
flashinfer-ai:mainfrom
zhachar14h-cell:fix/jit-cache-build-fingerprint
Open

zhachar14h-cell wants to merge 5 commits into
flashinfer-ai:mainfrom
zhachar14h-cell:fix/jit-cache-build-fingerprint

Conversation

@zhachar14h-cell

@zhachar14h-cell zhachar14h-cell commented Aug 13, 2026

Copy link
Copy Markdown

Summary

FlashInfer's nvcc JIT cache (under ~/.cache/flashinfer/) is validated by Ninja's file mtime scan alone. When a wheel is reinstalled at the same version, the wheel's source files may carry older mtimes than the previously compiled artifacts. Ninja then treats the stale .so/.o as up-to-date and reuses them even though the C++/CUDA source bytes changed, producing a module linked from a mix of old and new objects. On sm_121a this manifested as a fused_moe_120.so that deadlocked cutlass_fused_moe at launch (GPU idle, scheduler stuck in epoll/futex, 300s watchdog timeout).

This PR adds a per-module meta.json build fingerprint to JitSpecNvcc. The build directory is wiped and rebuilt whenever the committed fingerprint is missing, corrupt, or different; the fingerprint is committed atomically only after a successful compile, and only as a snapshot taken before the build (a mid-build source edit can never bless objects with a post-hoc fingerprint). Ninja's fine-grained header mtime scan is preserved — this only adds a cheap content/ABI gate on top. AOT loading and FLASHINFER_DISABLE_JIT are unchanged. Old caches without meta.json get one conservative rebuild on first use after upgrade.

Fingerprint contents

  • schema version
  • FlashInfer version + git commit
  • wheel RECORD digest for files under flashinfer/ — hashes the full row (path + content sha256= + size), so a same-version reinstall with different bytes invalidates even when paths are unchanged
  • Python SOABI
  • Torch version, bundled CUDA version, libstdc++ ABI
  • TVM-FFI version (via the actual distribution name apache-tvm-ffi)
  • nvcc and host-CXX compiler identity (FLASHINFER_NVCC/nvcc, CXX/c++, launchers)
  • SHA-256 of the C++/CUDA source tree (both data/csrc and repo-tree csrc/ for source installs), recomputed on every call so editable-install edits are always observed; os.walk dirs/files are sorted for a deterministic digest
  • SHA-256 of the module's own source files (sources_sha256)
  • SHA-256 of the current rendered Ninja configuration (the same snapshot written for the build)
  • compile flags (cflags / cuda_cflags / ldflags)

Scope

  • Generated input sources that reside under a module build directory (for example, trtllm_fmha_v2/generated/) are staged and restored across invalidation, while stale compiler outputs are still removed.\n* Both the single-module JitSpecNvcc.build() and the batch precompile path build_jit_specs() run the same fingerprint gate: stale dirs are wiped before building and meta.json is committed after a successful shared ninja run, so a batch/AOT build can never copy stale artifacts. Module locks are acquired in deterministic order and held through the shared Ninja run and every metadata commit.
  • A failed build-dir wipe aborts the build (a partial directory is never blessed with a fresh fingerprint); meta.json is written via a process-unique temp file + os.replace.
  • JitSpecNvcc.is_compiled now requires a valid meta.json for JIT artifacts (AOT artifacts remain valid by construction).

Test plan

  • pytest -q tests/jit/test_jit_cpp_ext.py — new GPU-free tests cover: source content change with unchanged (older) mtime, module-source vs include-tree invalidation independently, compile-flag change, missing/corrupt meta.json, matching fingerprint preserving the build dir (Ninja owns incrementality), generated-source preservation during invalidation, failed Ninja not committing metadata, AOT path ignoring meta, the batch precompile path, current-rendered-Ninja invalidation, cross-process batch/single lock exclusion, wipe-failure abort, is_compiled semantics, JSON serializability, deterministic metadata. A built.marker sentinel asserts the stale dir is actually wiped.
  • Focused fingerprint/locking tests: 18 passed. Full file: 37 passed, 1 pre-existing environment failure due to missing flashinfer/data/csrc/batch_prefill_customize_config.jinja.
  • Pre-commit hooks all pass.
  • On DGX Spark (sm_121a): fingerprint computation overhead ~11 ms per module (well under the 100 ms budget); stale same-mtime source triggers a rebuild; an old cache without meta.json is conservatively rebuilt once and then stable; the batch path behaves identically.

Fixes #4489

Notes

Summary by CodeRabbit

  • Bug Fixes
    • Improved JIT compilation caching by detecting changes to source files, compiler settings, Python ABI, and supported toolchains.
    • Automatically rebuilds stale, missing, or corrupted build artifacts.
    • Prevents failed compilations from leaving misleading cache metadata.
  • Reliability
    • Reuses valid cached builds to avoid unnecessary recompilation.
    • Improved validation and consistency for individual and batch JIT builds.
    • Added safe coordination for concurrent builds sharing cached artifacts.
    • AOT artifacts remain available without additional cache metadata checks.
  • Documentation
    • Clarified JIT cache validation and rebuild behavior.

Summary by CodeRabbit

  • Bug Fixes

    • Improved CUDA JIT build-cache reliability by detecting changes to source files, build settings, compilers, and runtime environments.
    • Automatically discards stale or incomplete cached builds and rebuilds them when needed.
    • Added safer handling for concurrent and batch builds, reducing cache conflicts.
    • Failed compilations no longer leave behind metadata that could incorrectly validate future builds.
    • Existing AOT artifacts continue to load without the new cache validation.
  • Documentation

    • Documented JIT cache validation and invalidation behavior.

FlashInfer's nvcc JIT cache relies on Ninja's mtime scan to decide whether a
compiled module is reusable. When the same wheel is reinstalled, its source
files may carry mtimes *older* than the previously compiled artifacts, so
Ninja treats a stale `.so`/`.o` as up-to-date and reuses it even though the
C++/CUDA bytes changed. The result is a module linked from a mix of old and
new objects, which can deadlock at launch (observed on sm_121a with
`fused_moe_120.so`).

Add a per-module `meta.json` build fingerprint to `JitSpecNvcc` covering the
FlashInfer version/git commit, wheel `RECORD` hash, Python SOABI, Torch +
CUDA + libstdc++ ABI, TVM-FFI version, source-tree content hashes, the
generated Ninja file, and the compile flags. `build()` now wipes the module
build directory whenever the committed fingerprint is missing, corrupt, or
different, then rebuilds. The fingerprint is committed atomically only after a
successful compile; a failed build never marks partial artifacts valid.

Ninja's fine-grained header mtime scan is preserved; this only adds a cheap
content/ABI gate on top. AOT loading and `FLASHINFER_DISABLE_JIT` are
unchanged. Old caches without `meta.json` get one conservative rebuild on
first use after upgrade.

Fixes flashinfer-ai#4489 (NVCC JIT cache can reuse stale same-version binaries after
reinstall).

Test plan: `pytest -q tests/jit/test_jit_cpp_ext.py` (new GPU-free tests cover
source/flag/identity/ABI invalidation, corrupt/missing meta, ninja failure,
AOT behavior, determinism).
@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8f9e40d0-bcb6-4b14-864a-8e37552d42e9

📥 Commits

Reviewing files that changed from the base of the PR and between cfc8568 and 88d3420.

📒 Files selected for processing (2)
  • flashinfer/jit/core.py
  • tests/jit/test_jit_cpp_ext.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • flashinfer/jit/core.py
  • tests/jit/test_jit_cpp_ext.py

📝 Walkthrough

Walkthrough

NVCC JIT builds now use content and runtime fingerprints stored in metadata. Stale or missing metadata invalidates JIT build directories. Metadata is written only after successful compilation. Batch builds apply the same validation and persistence rules.

Changes

NVCC JIT fingerprinting

Layer / File(s) Summary
Fingerprint and metadata utilities
flashinfer/jit/core.py
Added deterministic fingerprints for source trees, wheel contents, Python ABI, dependencies, compilers, and build configuration. Added atomic metadata writes and persistent cache locks.
JIT build validation and persistence
flashinfer/jit/core.py
JitSpecNvcc now validates metadata, removes stale build directories, renders Ninja once, and records fingerprints after successful single and batch JIT builds. AOT artifacts bypass metadata validation.
Fingerprint behavior coverage and documentation
tests/jit/test_jit_cpp_ext.py, .claude/skills/add-cuda-kernel/SKILL.md, CLAUDE.md
Tests cover metadata, invalidation, recovery, failures, AOT loading, batch builds, locking, cleanup, and compiled-state validation. Documentation describes the cache-fingerprint contract.

Estimated code review effort: 4 (Complex) | ~45 minutes

Mergeability Score: 🟡 Moderate · up to 88d34

Batch JIT builds may record a valid-looking fingerprint for artifacts changed concurrently by another builder, allowing mismatched cached objects to be reused. Merge should wait for this concurrency risk to be fixed or explicitly accepted by the owner.

Suggested reviewers: sricketts, dhiraj113, aleozlx

Sequence Diagram(s)

sequenceDiagram
  participant JitSpecNvcc
  participant Metadata
  participant Ninja
  participant JITLibrary
  JitSpecNvcc->>Metadata: Validate expected fingerprints
  JitSpecNvcc->>Ninja: Invalidate stale directory and run build
  Ninja->>JITLibrary: Produce compiled library
  JitSpecNvcc->>Metadata: Atomically write metadata
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.25% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes address issue #4489 with content and ABI fingerprints, conservative rebuilds, and protection against stale same-version NVCC artifacts.
Out of Scope Changes check ✅ Passed The implementation, tests, and documentation changes are directly related to NVCC JIT cache validation and the linked issue.
Title check ✅ Passed The title clearly and concisely describes the main change: invalidating stale NVCC cache artifacts using build fingerprints.
Description check ✅ Passed The description explains the problem, implementation, scope, tests, linked issue, and reviewer-relevant notes in sufficient detail.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@flashinfer/jit/core.py`:
- Around line 212-216: Update the RECORD hashing logic in the visible
record-hash function so each flashinfer/ row includes its recorded content,
rather than hashing only the path before the first comma. Preserve filtering to
flashinfer entries, and hash the full RECORD row or at minimum the path together
with its recorded content hash so wheels with identical paths but different
bytes produce different wheel_record_hash values.
- Around line 159-191: Update the fingerprint construction around self.sources
to include a content hash for every source file, not just include_dirs. Reuse
_hash_file_sha256 or equivalent and ensure generated sources are included in the
metadata used for the JIT module identifier. Do not use _hash_source_tree_cached
or another process-lifetime memoized digest for mutable source files unless it
has content-based invalidation.

In `@tests/jit/test_jit_cpp_ext.py`:
- Around line 301-336: Update the source invalidation test using _make_nvcc_spec
to create a sentinel file in spec.build_dir after the initial build, then modify
the source and trigger the rebuild. Assert the sentinel is removed and
source_sha256 changes, rather than relying only on calls["builds"], so the test
verifies _invalidate_stale_build() removes stale build directories and fails
unless spec.sources affects the fingerprint.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ad3dc665-c1c9-4c37-ac44-4f7458579674

📥 Commits

Reviewing files that changed from the base of the PR and between 8f9ad20 and abd0236.

📒 Files selected for processing (2)
  • flashinfer/jit/core.py
  • tests/jit/test_jit_cpp_ext.py

Comment thread flashinfer/jit/core.py Outdated
Comment thread flashinfer/jit/core.py
Comment thread tests/jit/test_jit_cpp_ext.py
- Cover the batch precompile path (build_jit_specs) with the same fingerprint
  gate: wipe stale dirs before the shared ninja run and commit meta.json for
  every module after it.
- Hash the full wheel RECORD row (path + content digest + size) so a
  same-version reinstall with different bytes invalidates even when paths are
  unchanged; use the real distribution name "apache-tvm-ffi".
- Recompute the include-tree hash on every call (drop the process-wide cache)
  so editable-install source edits are always observed; sort os.walk dirs and
  files for a deterministic digest.
- Add the nvcc / host-CXX compiler identity to the fingerprint (torch.version.
  cuda does not represent the local toolchain).
- Abort instead of ignoring failures when a stale build dir cannot be wiped,
  and write meta.json atomically via a process-unique temp file.
- Snapshot the fingerprint before building and commit that snapshot, so a
  mid-build source edit can never bless objects with a post-hoc fingerprint.
- Make JitSpecNvcc.is_compiled require a valid meta.json for JIT artifacts
  (AOT artifacts remain valid by construction).
- Tests: use a built.marker sentinel to assert the stale dir is really wiped;
  add coverage for module-source vs include-tree invalidation, the batch
  path, wipe failure, and is_compiled semantics.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
flashinfer/jit/core.py (2)

530-615: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Update the JIT cache documentation.

Document the meta.json fingerprint contract, stale-directory handling, and metadata commit rule in CLAUDE.md and the relevant JIT skill documentation.

As per coding guidelines: “Keep documentation synchronized with infrastructure, API, convention, error-handling, and macro changes; update CLAUDE.md and relevant skill documentation immediately.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@flashinfer/jit/core.py` around lines 530 - 615, Update CLAUDE.md and the
relevant JIT skill documentation to describe the metadata contract implemented
by expected_meta and _meta_matches, including the fingerprint fields, stale
build-directory invalidation, treatment of directories without meta.json, and
the requirement to write meta.json only after a successful build. Keep the
documentation aligned with _invalidate_stale_build and the existing JIT cache
behavior.

Source: Coding guidelines


872-893: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Keep each module lock until its metadata is committed.

The per-module lock ends before run_ninja() and before _write_meta_atomic(). Another process can modify or remove the module directory after this code snapshots expected_meta. This batch then can commit its snapshot for an artifact produced by the other process.

Acquire module locks in deterministic order and retain them through the shared Ninja run and each metadata commit. Add a multi-process regression for a batch build racing a single-spec build of the same module.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@flashinfer/jit/core.py` around lines 872 - 893, Update the batch-build flow
around the per-module FileLock and run_ninja so every module lock is acquired in
deterministic order and held until its corresponding _write_meta_atomic commit
completes after the shared Ninja run. Prevent locks from being released when
built_specs snapshots expected_meta; preserve the existing stale-build
invalidation and metadata behavior, and add a multi-process regression covering
a batch build racing a single-spec build for the same module.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@flashinfer/jit/core.py`:
- Around line 586-596: Update the Ninja metadata flow around expected_meta,
_meta_matches, and write_ninja so the current configuration is rendered through
one shared helper before comparison. Hash that rendered content for
ninja_content_sha256 instead of hashing the existing ninja_path, and make
write_ninja write the same rendered content; add a regression that changes a
Ninja-generation input while retaining the existing .so.
- Around line 598-615: Update _invalidate_stale_build to remove the build
directory whenever meta.json is absent or _meta_matches() reports a fingerprint
mismatch, while retaining the existing logging, failure propagation, and True
return behavior. Add a regression covering an existing .so without metadata,
verifying the directory is removed so Ninja cannot treat the artifact as up to
date.

---

Outside diff comments:
In `@flashinfer/jit/core.py`:
- Around line 530-615: Update CLAUDE.md and the relevant JIT skill documentation
to describe the metadata contract implemented by expected_meta and
_meta_matches, including the fingerprint fields, stale build-directory
invalidation, treatment of directories without meta.json, and the requirement to
write meta.json only after a successful build. Keep the documentation aligned
with _invalidate_stale_build and the existing JIT cache behavior.
- Around line 872-893: Update the batch-build flow around the per-module
FileLock and run_ninja so every module lock is acquired in deterministic order
and held until its corresponding _write_meta_atomic commit completes after the
shared Ninja run. Prevent locks from being released when built_specs snapshots
expected_meta; preserve the existing stale-build invalidation and metadata
behavior, and add a multi-process regression covering a batch build racing a
single-spec build for the same module.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1b86d09d-0cd3-4754-b76f-2b71517443fa

📥 Commits

Reviewing files that changed from the base of the PR and between abd0236 and fe74c07.

📒 Files selected for processing (2)
  • flashinfer/jit/core.py
  • tests/jit/test_jit_cpp_ext.py

Comment thread flashinfer/jit/core.py Outdated
Comment thread flashinfer/jit/core.py Outdated
@zhachar14h-cell

Copy link
Copy Markdown
Author

Addressed the two outside-diff review findings in 8d2c8513:

  • Documented the NVCC meta.json fingerprint contract, missing/mismatched-directory invalidation, atomic post-success commit rule, AOT exception, and batch-lock behavior in CLAUDE.md and .claude/skills/add-cuda-kernel/SKILL.md.
  • build_jit_specs() now acquires all selected module locks in deterministic path order and retains them through the shared Ninja run and every metadata commit. A separate-interpreter regression verifies that a single-spec builder cannot acquire the module lock during either phase.

Validation: 18 focused tests passed; all pre-commit hooks passed. The full test file is 36/37, with the sole failure being the pre-existing missing flashinfer/data/csrc/batch_prefill_customize_config.jinja source-tree fixture in this checkout.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CLAUDE.md`:
- Around line 285-299: Update the later Module Caching section’s URI description
to state that the URI identifies the module configuration, while meta.json
validates cached build artifacts against the fingerprint contract. Remove the
outdated claim that the URI directly includes source_hashes, flags, and
cuda_arch, and align the related parameter-derived wording near that section.

In `@flashinfer/jit/core.py`:
- Around line 887-888: Update the lock acquisition loop over selected_specs to
use _PersistentFileLock instead of FileLock, preserving thread_local=False so
batch module locks remain persistent across releases on the shared cache.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 26e38335-d922-427f-8528-0e9285a11d2a

📥 Commits

Reviewing files that changed from the base of the PR and between fe74c07 and 8d2c851.

📒 Files selected for processing (4)
  • .claude/skills/add-cuda-kernel/SKILL.md
  • CLAUDE.md
  • flashinfer/jit/core.py
  • tests/jit/test_jit_cpp_ext.py

Comment thread CLAUDE.md
Comment thread flashinfer/jit/core.py Outdated
@zhachar14h-cell

Copy link
Copy Markdown
Author

@flashinfer-bot run

@zhachar14h-cell

Copy link
Copy Markdown
Author

@yongwww The latest commit is cfc85684; all review threads are resolved and CodeRabbit reports no actionable comments. Public workflows are currently action_required, and the author account cannot add run-ci (HTTP 403). Could you please run @flashinfer-bot run for the latest commit? Thanks.

@yongwww

yongwww commented Aug 13, 2026

Copy link
Copy Markdown
Member

@flashinfer-bot run

@zhachar14h-cell

Copy link
Copy Markdown
Author

CI fix pushed in 88d34209. The arm64/cu129 AOT failure came from cache invalidation deleting generated FMHA sources under build_dir/generated/ before Ninja ran. Invalidation now stages in-build source trees, wipes stale compiler artifacts, and restores the sources; a GPU-free regression test covers this exact layout. Local result: 37 passed / 1 known checkout-data failure; pre-commit all passed. @yongwww Could you please run @flashinfer-bot run again for the new head? Thanks.

@yongwww yongwww added run-ci and removed run-ci labels Aug 13, 2026
@zhachar14h-cell

zhachar14h-cell commented Aug 14, 2026

Copy link
Copy Markdown
Author

Update: authorized CI was subsequently started correctly for current HEAD 88d34209. Run 31726620568 completed successfully (including AOT Build Import (arm64, cu129)), and duplicate run 31727184726 has no failures with only the H100 job still queued. The linked run 31668555859 is the earlier unauthorized synchronize attempt; its red summary is expected and does not indicate a code/test failure.

@zhachar14h-cell

Copy link
Copy Markdown
Author

Hi @bkryu — would you be willing to review this PR when you have time? You authored the current JitSpec lifecycle/disk-cache work in #3874 and the Ninja JIT race fix in #2339, so this seems closest to your area. The main points needing maintainer judgment are (1) fingerprint invalidation semantics, (2) preserving generated sources under build_dir, and (3) batch-lock/metadata commit ordering. Current head is 88d34209; an authorized full public CI run passed, including arm64/cu129 AOT and GPU jobs. The older red summary is only an unauthorized skipped run. All prior actionable review comments are resolved, and cross-process locking/invalidation regressions are included. Happy to split the PR if that would make review easier. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

NVCC JIT cache can reuse stale same-version binaries after reinstall

3 participants