Skip to content

fix: avoid re-downloading BMM export headers when flashinfer-cubin is installed - #2903

Merged
aleozlx merged 4 commits into
flashinfer-ai:mainfrom
yzh119:fix-bmm-header-download-with-cubin-package
Apr 3, 2026
Merged

aleozlx merged 4 commits into
flashinfer-ai:mainfrom
yzh119:fix-bmm-header-download-with-cubin-package

Conversation

@yzh119

@yzh119 yzh119 commented Mar 27, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • download_trtllm_headers() stored BMM export headers at a custom path (CUBIN_DIR/flashinfer/trtllm/batched_gemm/trtllmGen_bmm_export/), but download_artifacts() (used when building the flashinfer-cubin wheel) stores them at the canonical artifact hash path. The separate get_file() function only checked the destination path, so it never found the pre-packaged files and always re-downloaded ~17 header files from the network on every startup (×2 for each TP worker).
  • Remove download_trtllm_headers(), get_file(), and rename get_cubin()get_artifact(). Two unified primitives remain:
    • get_artifact(name, sha256) — fetches any artifact (cubins, headers, checksums, metainfo) into the canonical path, reusing flashinfer-cubin files when available
    • ensure_symlink(link, target) — maps C++ include paths to the artifact directory
  • Callers (fused_moe, moe_utils) use these two primitives directly. A get_cubin = get_artifact alias is kept for backward compatibility.

Related issue

Test plan

  • 7 unit tests covering get_artifact() local cache (cubins, checksums.txt, metainfo headers, sha256 mismatch) and the BMM header symlink pattern (no download, stale dir replacement, idempotent)
  • Verify on a gpt-oss-20b-sm100-fi-mxfp4-mxfp8-trtllm run with flashinfer-cubin installed that "Fetching file from" log messages no longer appear

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Mar 27, 2026

Copy link
Copy Markdown
Contributor

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
📝 Walkthrough

Walkthrough

Removed the per-file SHA256-validated download/cache helper and replaced bulk header-download flow with per-header get_cubin(...) retrieval plus a new ensure_symlink(...) that creates/updates a symlink to the canonical cubin cache. Added local-only tests validating cache, checksum behavior, symlink creation, replacement, and idempotency.

Changes

Cohort / File(s) Summary
Cubin loader core
flashinfer/jit/cubin_loader.py
Removed get_file(...) and the previous download_trtllm_headers(...) workflow. Added ensure_symlink(link, target) to create/update a symlink at link pointing to target, removing stale files/dirs. Shifted header download validation to per-header get_cubin(...) + meta-hash.
MOE header integration
flashinfer/jit/fused_moe.py, flashinfer/jit/moe_utils.py
Replaced bulk header download usage with per-header get_cubin(...) calls using get_meta_hash(...) and introduced BMM_EXPORT_HEADERS. Call ensure_symlink(...) to link trtllmGen_bmm_export include dir to the canonical cubin cache. Adjusted path composition to derive target under FLASHINFER_CUBIN_DIR.
Tests: local cache & symlink behavior
tests/test_cubin_local_cache.py
New tests that stub out network downloads and validate: returning canonical on-disk bytes for cubins/checksums/headers, download on checksum mismatch, creation of symlinked include dir pointing at canonical artifacts, replacement of an existing real dir, and idempotency without network calls.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • nvmbreughe
  • jiahanc
  • jimmyzho
  • bkryu
  • yongwww

Poem

🐰 Soft paws replace the scatter and the drift,
I link the headers with a tidy little shift,
No stale files linger, the cache is neat,
Tests hop around to prove the feat,
A symlinked garden—short, snug, and swift. 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 70.37% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: fixing re-downloading of BMM export headers when flashinfer-cubin is installed, which directly reflects the core problem and solution in the changeset.
Description check ✅ Passed The PR description follows the template structure with clear Summary, Related Issues, and Test Plan sections addressing the problem, solution, and verification approach.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 and usage tips.

@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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@flashinfer/jit/cubin_loader.py`:
- Around line 217-225: The fast-path copies the cached artifact with
shutil.copy(local_artifact_path, file_path), bypassing the module’s
lock/temp-file/atomic-replace behavior and risking partial writes; replace that
copy with the same atomic-write flow used by download_file (or a helper that
acquires the module lock, writes to a temp file, verifies sha256, then
os.replace into file_path) so load_cubin(local_artifact_path, sha256) remains
used to validate the artifact but the final placement of file_path is done
atomically (avoid calling shutil.copy directly).
🪄 Autofix (Beta)

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

Run ID: f164904c-e71e-405e-b8c2-f5e463b9a3e6

📥 Commits

Reviewing files that changed from the base of the PR and between 31b63bc and 1ee8dde48f23157eedf609ec6b23c83c9cf6830a.

📒 Files selected for processing (1)
  • flashinfer/jit/cubin_loader.py

Comment thread flashinfer/jit/cubin_loader.py Outdated
@yzh119
yzh119 force-pushed the fix-bmm-header-download-with-cubin-package branch from 1ee8dde to d791004 Compare March 27, 2026 16:58

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request updates the cubin loader to attempt to copy files from a local artifact directory before falling back to a network download. The review identifies a race condition in the implementation of the local file copy and suggests using the existing download_file utility to ensure atomic, concurrency-safe file operations.

I am having trouble creating individual review comments. Click here to see my feedback.

flashinfer/jit/cubin_loader.py (224-225)

high

The use of shutil.copy here is not atomic and not safe for concurrent execution. If multiple processes execute this code simultaneously for the same file_path, it can lead to a race condition where one process might read a partially written file from another process.

To ensure atomicity and prevent race conditions, you should use the existing download_file utility. It is designed for concurrency-safe file operations by using a file lock and an atomic rename operation. This change will also make the behavior consistent with how network files are handled. Using download_file also provides a graceful fallback to network download if the local copy fails for some reason.

            if download_file(local_artifact_path, file_path, session=session):
                return local_file

@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 the current code and only fix it if needed.

Inline comments:
In `@flashinfer/jit/cubin_loader.py`:
- Around line 278-284: The loop that calls get_cubin(uri_path, file_hash) for
each file (iterating header_files and using header_path) can attempt to write
nested destination files into a non-existent directory, causing
FileNotFoundError during the network fallback; ensure the canonical destination
parent directories are created before the first fallback by creating the
destination directory (derived from header_path and file name) with
os.makedirs(..., exist_ok=True) or equivalent prior to calling
get_cubin()/download_file() inside the for loop so get_cubin() or
download_file() can write the file successfully.
- Around line 286-300: The symlink removal and creation around header_dest_dir
is racy; instead, serialize replacement by creating the new symlink atomically
and then swapping it into place: ensure header_dest_dir.parent exists, create a
temporary symlink (unique name) that points to artifact_dir, then atomically
replace the target with os.replace/Path.replace from the temp symlink to
header_dest_dir so concurrent workers cannot observe a missing or
partially-updated entry; reference header_dest_dir, artifact_dir,
FLASHINFER_CUBIN_DIR and header_path and ensure you remove the temp symlink on
failure and handle existing correct symlink early-return as currently
implemented.
🪄 Autofix (Beta)

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

Run ID: ee6b3161-10bc-40a6-a156-fa0cdedce788

📥 Commits

Reviewing files that changed from the base of the PR and between 1ee8dde48f23157eedf609ec6b23c83c9cf6830a and d791004e52866d2e57b0dabda9e74096e943da74.

📒 Files selected for processing (1)
  • flashinfer/jit/cubin_loader.py

Comment thread flashinfer/jit/cubin_loader.py Outdated
Comment thread flashinfer/jit/cubin_loader.py Outdated
Comment on lines +286 to +300
# Symlink header_dest_dir -> artifact directory so C++ includes resolve.
artifact_dir = FLASHINFER_CUBIN_DIR / header_path
if header_dest_dir.is_symlink() or header_dest_dir.exists():
if (
header_dest_dir.is_symlink()
and header_dest_dir.resolve() == artifact_dir.resolve()
):
return # already correct
# Stale symlink or directory from a previous version; remove it.
if header_dest_dir.is_symlink() or header_dest_dir.is_file():
header_dest_dir.unlink()
else:
shutil.rmtree(header_dest_dir)
header_dest_dir.parent.mkdir(parents=True, exist_ok=True)
header_dest_dir.symlink_to(artifact_dir)

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.

⚠️ Potential issue | 🔴 Critical

Serialize the symlink replacement.

Lines 288-300 do an unlocked unlink/rmtree + symlink_to sequence. If two TP workers hit this path at startup, one can delete the entry after the other has checked it, or both can race on symlink_to(), which can abort JIT setup with FileExistsError / FileNotFoundError.

🔒 Proposed fix
     # Symlink header_dest_dir -> artifact directory so C++ includes resolve.
     artifact_dir = FLASHINFER_CUBIN_DIR / header_path
-    if header_dest_dir.is_symlink() or header_dest_dir.exists():
-        if (
-            header_dest_dir.is_symlink()
-            and header_dest_dir.resolve() == artifact_dir.resolve()
-        ):
-            return  # already correct
-        # Stale symlink or directory from a previous version; remove it.
-        if header_dest_dir.is_symlink() or header_dest_dir.is_file():
-            header_dest_dir.unlink()
-        else:
-            shutil.rmtree(header_dest_dir)
     header_dest_dir.parent.mkdir(parents=True, exist_ok=True)
-    header_dest_dir.symlink_to(artifact_dir)
+    with filelock.FileLock(f"{header_dest_dir}.lock", timeout=30):
+        if (
+            header_dest_dir.is_symlink()
+            and header_dest_dir.resolve() == artifact_dir.resolve()
+        ):
+            return  # already correct
+        # Stale symlink or directory from a previous version; remove it.
+        if header_dest_dir.is_symlink() or header_dest_dir.is_file():
+            header_dest_dir.unlink()
+        elif header_dest_dir.exists():
+            shutil.rmtree(header_dest_dir)
+        header_dest_dir.symlink_to(artifact_dir)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@flashinfer/jit/cubin_loader.py` around lines 286 - 300, The symlink removal
and creation around header_dest_dir is racy; instead, serialize replacement by
creating the new symlink atomically and then swapping it into place: ensure
header_dest_dir.parent exists, create a temporary symlink (unique name) that
points to artifact_dir, then atomically replace the target with
os.replace/Path.replace from the temp symlink to header_dest_dir so concurrent
workers cannot observe a missing or partially-updated entry; reference
header_dest_dir, artifact_dir, FLASHINFER_CUBIN_DIR and header_path and ensure
you remove the temp symlink on failure and handle existing correct symlink
early-return as currently implemented.

@yzh119
yzh119 force-pushed the fix-bmm-header-download-with-cubin-package branch from ed84e32 to 9fff8b3 Compare March 27, 2026 17:08

@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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/test_download_trtllm_headers.py`:
- Around line 66-155: Add a new test that covers the cache-miss/download
fallback: simulate FLASHINFER_CUBIN_DIR without the header_path populated (do
not call _populate_artifact_dir), monkeypatch
flashinfer.jit.cubin_loader.download_file to a stub that writes expected header
files into the artifact path (or a tmp dir) and records calls, then call
download_trtllm_headers("bmm", header_dest_dir, header_path, artifact_path,
checksums_txt) and assert download_file was invoked for the missing files, the
header_dest_dir becomes a symlink, and the symlinked headers contain the
expected contents; reference functions/vars download_trtllm_headers,
download_file, FLASHINFER_CUBIN_DIR, and header_dest_dir in the test.
🪄 Autofix (Beta)

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

Run ID: 33b87325-e855-432c-8a26-0a68887bb96b

📥 Commits

Reviewing files that changed from the base of the PR and between d791004e52866d2e57b0dabda9e74096e943da74 and 9fff8b386c6213ed3df3499ca5528244b089f908.

📒 Files selected for processing (1)
  • tests/test_download_trtllm_headers.py

Comment thread tests/test_download_trtllm_headers.py Outdated
Comment on lines +66 to +155
def test_no_download_when_files_present(monkeypatch, tmp_path):
"""When files exist at the artifact path, download_file must not be called."""
artifact_path = "deadbeef/batched_gemm-abc-def/"
header_path = f"{artifact_path}include/trtllmGen_bmm_export"
cubin_dir = tmp_path / "cubins"
cubin_dir.mkdir()

file_contents, checksums_txt = _build_checksums_and_files(BMM_HEADER_FILES)
_populate_artifact_dir(cubin_dir, header_path, BMM_HEADER_FILES, file_contents)

import flashinfer.jit.cubin_loader as cl

monkeypatch.setattr(cl, "FLASHINFER_CUBIN_DIR", cubin_dir)

header_dest_dir = (
cubin_dir / "flashinfer" / "trtllm" / "batched_gemm" / "trtllmGen_bmm_export"
)

with patch.object(cl, "download_file", _download_file_should_not_be_called):
download_trtllm_headers(
"bmm", header_dest_dir, header_path, artifact_path, checksums_txt
)

# Verify the symlink was created and points to the right place
assert header_dest_dir.is_symlink()
assert header_dest_dir.resolve() == (cubin_dir / header_path).resolve()

# Verify all headers are accessible through the symlink
for f in BMM_HEADER_FILES:
assert (header_dest_dir / f).exists(), f"Header {f} not accessible via symlink"
assert (header_dest_dir / f).read_bytes() == file_contents[f]


def test_stale_directory_replaced_by_symlink(monkeypatch, tmp_path):
"""A pre-existing real directory at header_dest_dir gets replaced by a symlink."""
artifact_path = "deadbeef/batched_gemm-abc-def/"
header_path = f"{artifact_path}include/trtllmGen_bmm_export"
cubin_dir = tmp_path / "cubins"
cubin_dir.mkdir()

file_contents, checksums_txt = _build_checksums_and_files(BMM_HEADER_FILES)
_populate_artifact_dir(cubin_dir, header_path, BMM_HEADER_FILES, file_contents)

import flashinfer.jit.cubin_loader as cl

monkeypatch.setattr(cl, "FLASHINFER_CUBIN_DIR", cubin_dir)

header_dest_dir = (
cubin_dir / "flashinfer" / "trtllm" / "batched_gemm" / "trtllmGen_bmm_export"
)
# Simulate leftover directory from the old download_trtllm_headers behavior
header_dest_dir.mkdir(parents=True, exist_ok=True)
(header_dest_dir / "stale_file.h").write_text("old")

with patch.object(cl, "download_file", _download_file_should_not_be_called):
download_trtllm_headers(
"bmm", header_dest_dir, header_path, artifact_path, checksums_txt
)

assert header_dest_dir.is_symlink()
assert not (header_dest_dir / "stale_file.h").exists()


def test_idempotent(monkeypatch, tmp_path):
"""Calling download_trtllm_headers twice is a no-op the second time."""
artifact_path = "deadbeef/batched_gemm-abc-def/"
header_path = f"{artifact_path}include/trtllmGen_bmm_export"
cubin_dir = tmp_path / "cubins"
cubin_dir.mkdir()

file_contents, checksums_txt = _build_checksums_and_files(BMM_HEADER_FILES)
_populate_artifact_dir(cubin_dir, header_path, BMM_HEADER_FILES, file_contents)

import flashinfer.jit.cubin_loader as cl

monkeypatch.setattr(cl, "FLASHINFER_CUBIN_DIR", cubin_dir)

header_dest_dir = (
cubin_dir / "flashinfer" / "trtllm" / "batched_gemm" / "trtllmGen_bmm_export"
)

with patch.object(cl, "download_file", _download_file_should_not_be_called):
download_trtllm_headers(
"bmm", header_dest_dir, header_path, artifact_path, checksums_txt
)
download_trtllm_headers(
"bmm", header_dest_dir, header_path, artifact_path, checksums_txt
)

assert header_dest_dir.is_symlink()

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.

⚠️ Potential issue | 🟡 Minor

Add explicit coverage for the cache-miss/download fallback path.

Current tests only validate the “all files already local” path (download_file is always forced to fail). That leaves the missing-file flow untested in this file.

✅ Suggested test addition
+import pathlib
@@
+def test_download_when_files_missing(monkeypatch, tmp_path):
+    """When artifact files are missing, download_file should be used to populate them."""
+    artifact_path = "deadbeef/batched_gemm-abc-def/"
+    header_path = f"{artifact_path}include/trtllmGen_bmm_export"
+    cubin_dir = tmp_path / "cubins"
+    cubin_dir.mkdir()
+
+    file_contents, checksums_txt = _build_checksums_and_files(BMM_HEADER_FILES)
+
+    import flashinfer.jit.cubin_loader as cl
+    monkeypatch.setattr(cl, "FLASHINFER_CUBIN_DIR", cubin_dir)
+
+    def _fake_download_file(uri, dest, session=None):
+        rel = pathlib.Path(dest).relative_to(cubin_dir).as_posix()
+        name = rel.removeprefix(f"{header_path}/")
+        p = pathlib.Path(dest)
+        p.parent.mkdir(parents=True, exist_ok=True)
+        p.write_bytes(file_contents[name])
+
+    header_dest_dir = (
+        cubin_dir / "flashinfer" / "trtllm" / "batched_gemm" / "trtllmGen_bmm_export"
+    )
+
+    with patch.object(cl, "download_file", side_effect=_fake_download_file) as mocked:
+        download_trtllm_headers(
+            "bmm", header_dest_dir, header_path, artifact_path, checksums_txt
+        )
+
+    assert mocked.call_count > 0
+    assert header_dest_dir.is_symlink()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_download_trtllm_headers.py` around lines 66 - 155, Add a new test
that covers the cache-miss/download fallback: simulate FLASHINFER_CUBIN_DIR
without the header_path populated (do not call _populate_artifact_dir),
monkeypatch flashinfer.jit.cubin_loader.download_file to a stub that writes
expected header files into the artifact path (or a tmp dir) and records calls,
then call download_trtllm_headers("bmm", header_dest_dir, header_path,
artifact_path, checksums_txt) and assert download_file was invoked for the
missing files, the header_dest_dir becomes a symlink, and the symlinked headers
contain the expected contents; reference functions/vars download_trtllm_headers,
download_file, FLASHINFER_CUBIN_DIR, and header_dest_dir in the test.

@yzh119
yzh119 force-pushed the fix-bmm-header-download-with-cubin-package branch 2 times, most recently from e6673bf to 60961d8 Compare March 27, 2026 17:29

@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.

🧹 Nitpick comments (1)
tests/test_cubin_local_cache.py (1)

37-39: Consider extracting repeated setup to a pytest fixture.

The pattern of importing flashinfer.jit.cubin_loader as cl and monkeypatching FLASHINFER_CUBIN_DIR is repeated in every test. A fixture could reduce this boilerplate:

♻️ Optional refactor using a fixture
import pytest

`@pytest.fixture`
def cubin_loader_with_tmp_dir(monkeypatch, tmp_path):
    """Provides cubin_loader module with FLASHINFER_CUBIN_DIR set to a temp directory."""
    import flashinfer.jit.cubin_loader as cl
    cubin_dir = tmp_path / "cubins"
    cubin_dir.mkdir()
    monkeypatch.setattr(cl, "FLASHINFER_CUBIN_DIR", cubin_dir)
    return cl, cubin_dir

def test_get_cubin_no_download_when_file_present(cubin_loader_with_tmp_dir):
    """get_cubin() returns local bytes without calling download_file."""
    cl, cubin_dir = cubin_loader_with_tmp_dir
    # ... rest of test using cl and cubin_dir

This is purely optional—the current approach is clear and explicit about what each test sets up.

Also applies to: 60-62, 83-85, 106-108, 185-187, 218-220, 248-250

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_cubin_local_cache.py` around lines 37 - 39, Extract the repeated
import and monkeypatch into a pytest fixture that imports
flashinfer.jit.cubin_loader as cl, creates a temporary cubin_dir (using
tmp_path), monkeypatches cl.FLASHINFER_CUBIN_DIR to that dir, and returns (cl,
cubin_dir); then update tests that currently import cl and call
monkeypatch.setattr(cl, "FLASHINFER_CUBIN_DIR", cubin_dir) to accept the fixture
(e.g., cubin_loader_with_tmp_dir) and use the returned cl and cubin_dir instead.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@tests/test_cubin_local_cache.py`:
- Around line 37-39: Extract the repeated import and monkeypatch into a pytest
fixture that imports flashinfer.jit.cubin_loader as cl, creates a temporary
cubin_dir (using tmp_path), monkeypatches cl.FLASHINFER_CUBIN_DIR to that dir,
and returns (cl, cubin_dir); then update tests that currently import cl and call
monkeypatch.setattr(cl, "FLASHINFER_CUBIN_DIR", cubin_dir) to accept the fixture
(e.g., cubin_loader_with_tmp_dir) and use the returned cl and cubin_dir instead.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b9eb6734-8b52-4a80-933a-b71e7ce0310d

📥 Commits

Reviewing files that changed from the base of the PR and between 9fff8b386c6213ed3df3499ca5528244b089f908 and e6673bf4b79aa3f9725dca3277ad69d056c2c9da.

📒 Files selected for processing (1)
  • tests/test_cubin_local_cache.py

@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

♻️ Duplicate comments (1)
flashinfer/jit/cubin_loader.py (1)

222-242: ⚠️ Potential issue | 🟠 Major

Race condition in symlink creation/replacement remains unaddressed.

The ensure_symlink() function performs an unlocked sequence of unlink()/rmtree() followed by symlink_to(). When multiple TP workers start concurrently, they can race on this sequence, causing FileExistsError or FileNotFoundError if one worker deletes while another is creating.

Consider using atomic symlink replacement or a file lock:

🔒 Proposed fix using atomic symlink replacement
 def ensure_symlink(
     link: Union[str, pathlib.Path], target: Union[str, pathlib.Path]
 ) -> None:
     """Create or update a symlink, removing any stale file/directory at *link*.

     This is used to map C++ include paths (e.g.
     ``CUBIN_DIR/flashinfer/trtllm/batched_gemm/trtllmGen_bmm_export``) to the
     canonical artifact directory where ``get_cubin()`` stores downloaded files.
     """
     link = pathlib.Path(link)
     target = pathlib.Path(target)
+    link.parent.mkdir(parents=True, exist_ok=True)
+
+    # Fast path: already correct
     if link.is_symlink() or link.exists():
         if link.is_symlink() and link.resolve() == target.resolve():
             return  # already correct
-        # Stale symlink or directory from a previous version; remove it.
-        if link.is_symlink() or link.is_file():
-            link.unlink()
-        else:
-            shutil.rmtree(link)
-    link.parent.mkdir(parents=True, exist_ok=True)
-    link.symlink_to(target)
+
+    # Atomic replacement via temp symlink + os.replace
+    import uuid
+    tmp_link = link.parent / f".{link.name}.{uuid.uuid4().hex}.tmp"
+    try:
+        tmp_link.symlink_to(target)
+        # os.replace is atomic on POSIX; on Windows it may fail if link is a directory
+        if link.is_dir() and not link.is_symlink():
+            shutil.rmtree(link)
+        os.replace(tmp_link, link)
+    except Exception:
+        if tmp_link.exists() or tmp_link.is_symlink():
+            tmp_link.unlink()
+        raise
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@flashinfer/jit/cubin_loader.py` around lines 222 - 242, The ensure_symlink
function has a race between removing a stale path and creating the symlink; fix
it by making symlink creation atomic: create a temporary symlink (e.g., in the
same directory with a unique name) that points to the target and then atomically
replace the final link with os.replace (or pathlib.Path.replace) so other
processes either see the old or new symlink but never a missing/broken state;
alternatively, guard the remove+create sequence with a filesystem lock on
link.parent (using a lockfile) to serialize concurrent workers—apply this change
inside ensure_symlink to replace the unlink()/rmtree() + symlink_to() sequence.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@flashinfer/jit/fused_moe.py`:
- Around line 256-268: The loop that calls get_cubin(...) for headers under
bmm_export_path can fail because download_file() doesn't create parent
directories; before each get_cubin(...) call (inside the loop over
BMM_EXPORT_HEADERS in fused_moe.py) ensure the target parent directory exists
(mkdir(parents=True, exist_ok=True)) for the path where the header will be
written (derive it from include_path / bmm_export_path / header) so nested paths
like "trtllm/gen/CommonUtils.h" won't raise FileNotFoundError; update the code
around the bmm_export_path loop (referencing BMM_EXPORT_HEADERS, get_cubin(),
get_meta_hash(checksum, header), and ensure_symlink) to create the parent
directories prior to downloading each header.

In `@flashinfer/jit/moe_utils.py`:
- Around line 44-58: Duplicate header-fetching logic in moe_utils.py and
fused_moe.py should be consolidated: create a helper
download_bmm_export_headers(...) in cubin_loader.py that accepts artifact_path,
checksum_hash, and bmm_export_headers and performs the checksum retrieval (using
get_cubin and get_meta_hash), loops over BMM_EXPORT_HEADERS, creates parent
directories (header_full_path.parent.mkdir(parents=True, exist_ok=True)) before
calling get_cubin for each header to handle nested paths, and then calls
ensure_symlink to link into
jit_env.FLASHINFER_CUBIN_DIR/flashinfer/trtllm/batched_gemm/trtllmGen_bmm_export;
replace the duplicated blocks in moe_utils.py and fused_moe.py with a call to
download_bmm_export_headers(ArtifactPath.TRTLLM_GEN_BMM,
CheckSumHash.TRTLLM_GEN_BMM, BMM_EXPORT_HEADERS).

---

Duplicate comments:
In `@flashinfer/jit/cubin_loader.py`:
- Around line 222-242: The ensure_symlink function has a race between removing a
stale path and creating the symlink; fix it by making symlink creation atomic:
create a temporary symlink (e.g., in the same directory with a unique name) that
points to the target and then atomically replace the final link with os.replace
(or pathlib.Path.replace) so other processes either see the old or new symlink
but never a missing/broken state; alternatively, guard the remove+create
sequence with a filesystem lock on link.parent (using a lockfile) to serialize
concurrent workers—apply this change inside ensure_symlink to replace the
unlink()/rmtree() + symlink_to() sequence.
🪄 Autofix (Beta)

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

Run ID: 5bb200d6-22b4-44bc-a987-ff871fa25a57

📥 Commits

Reviewing files that changed from the base of the PR and between e6673bf4b79aa3f9725dca3277ad69d056c2c9da and 60961d8a6e15fa58af40adf498b2e17c55b0be5e.

📒 Files selected for processing (4)
  • flashinfer/jit/cubin_loader.py
  • flashinfer/jit/fused_moe.py
  • flashinfer/jit/moe_utils.py
  • tests/test_cubin_local_cache.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/test_cubin_local_cache.py

Comment thread flashinfer/jit/fused_moe.py Outdated
Comment on lines 256 to 268
# Fetch BMM export headers via get_cubin() and symlink for C++ includes.
bmm_export_path = f"{include_path}/trtllmGen_bmm_export"
for header in BMM_EXPORT_HEADERS:
h = get_cubin(f"{bmm_export_path}/{header}", get_meta_hash(checksum, header))
assert h, f"{header} not found"
ensure_symlink(
jit_env.FLASHINFER_CUBIN_DIR
/ "flashinfer"
/ "trtllm"
/ "batched_gemm"
/ "trtllmGen_bmm_export"
)

download_trtllm_headers(
"bmm", header_dest_dir, header_path, ArtifactPath.TRTLLM_GEN_BMM, checksum
/ "trtllmGen_bmm_export",
jit_env.FLASHINFER_CUBIN_DIR / bmm_export_path,
)

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.

⚠️ Potential issue | 🔴 Critical

Create parent directories before downloading nested header files.

The loop calls get_cubin() for headers with nested paths (e.g., trtllm/gen/CommonUtils.h). The download_file() function does not create parent directories, so on a clean cache the first download for a nested path will fail with FileNotFoundError.

🐛 Proposed fix to ensure directories exist
     # Fetch BMM export headers via get_cubin() and symlink for C++ includes.
     bmm_export_path = f"{include_path}/trtllmGen_bmm_export"
     for header in BMM_EXPORT_HEADERS:
+        header_full_path = jit_env.FLASHINFER_CUBIN_DIR / bmm_export_path / header
+        header_full_path.parent.mkdir(parents=True, exist_ok=True)
         h = get_cubin(f"{bmm_export_path}/{header}", get_meta_hash(checksum, header))
         assert h, f"{header} not found"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@flashinfer/jit/fused_moe.py` around lines 256 - 268, The loop that calls
get_cubin(...) for headers under bmm_export_path can fail because
download_file() doesn't create parent directories; before each get_cubin(...)
call (inside the loop over BMM_EXPORT_HEADERS in fused_moe.py) ensure the target
parent directory exists (mkdir(parents=True, exist_ok=True)) for the path where
the header will be written (derive it from include_path / bmm_export_path /
header) so nested paths like "trtllm/gen/CommonUtils.h" won't raise
FileNotFoundError; update the code around the bmm_export_path loop (referencing
BMM_EXPORT_HEADERS, get_cubin(), get_meta_hash(checksum, header), and
ensure_symlink) to create the parent directories prior to downloading each
header.

Comment thread flashinfer/jit/moe_utils.py Outdated
Comment on lines 44 to 58
checksum = get_cubin(
f"{ArtifactPath.TRTLLM_GEN_BMM}/checksums.txt", CheckSumHash.TRTLLM_GEN_BMM
)
bmm_export_path = f"{ArtifactPath.TRTLLM_GEN_BMM}/include/trtllmGen_bmm_export"
for header in BMM_EXPORT_HEADERS:
h = get_cubin(f"{bmm_export_path}/{header}", get_meta_hash(checksum, header))
assert h, f"{header} not found"
ensure_symlink(
jit_env.FLASHINFER_CUBIN_DIR
/ "flashinfer"
/ "trtllm"
/ "batched_gemm"
/ "trtllmGen_bmm_export",
f"{ArtifactPath.TRTLLM_GEN_BMM}/include/trtllmGen_bmm_export",
ArtifactPath.TRTLLM_GEN_BMM,
get_cubin(
f"{ArtifactPath.TRTLLM_GEN_BMM}/checksums.txt", CheckSumHash.TRTLLM_GEN_BMM
),
jit_env.FLASHINFER_CUBIN_DIR / bmm_export_path,
)

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.

⚠️ Potential issue | 🔴 Critical

Duplicate header-fetching logic with fused_moe.py; also missing parent directory creation.

This block is nearly identical to fused_moe.py lines 243-268. Consider extracting into a shared helper (e.g., download_bmm_export_headers() in cubin_loader.py) to avoid duplication and ensure consistent behavior.

Additionally, the same issue applies here: nested header paths like trtllm/gen/CommonUtils.h require parent directory creation before get_cubin() is called.
,

♻️ Proposed refactor: extract shared helper

In cubin_loader.py, add a helper:

def download_bmm_export_headers(
    artifact_path: str,
    checksum_hash: str,
    bmm_export_headers: list,
) -> None:
    """Download BMM export headers and create symlink for C++ includes."""
    from .env import FLASHINFER_CUBIN_DIR
    
    checksum = get_cubin(f"{artifact_path}/checksums.txt", checksum_hash)
    assert checksum, f"Failed to get checksums.txt from {artifact_path}"
    
    bmm_export_path = f"{artifact_path}/include/trtllmGen_bmm_export"
    for header in bmm_export_headers:
        header_full_path = FLASHINFER_CUBIN_DIR / bmm_export_path / header
        header_full_path.parent.mkdir(parents=True, exist_ok=True)
        h = get_cubin(f"{bmm_export_path}/{header}", get_meta_hash(checksum, header))
        assert h, f"{header} not found"
    
    ensure_symlink(
        FLASHINFER_CUBIN_DIR / "flashinfer" / "trtllm" / "batched_gemm" / "trtllmGen_bmm_export",
        FLASHINFER_CUBIN_DIR / bmm_export_path,
    )

Then in both fused_moe.py and moe_utils.py:

from .cubin_loader import download_bmm_export_headers
from .fused_moe import BMM_EXPORT_HEADERS
from ..artifacts import ArtifactPath, CheckSumHash

download_bmm_export_headers(ArtifactPath.TRTLLM_GEN_BMM, CheckSumHash.TRTLLM_GEN_BMM, BMM_EXPORT_HEADERS)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@flashinfer/jit/moe_utils.py` around lines 44 - 58, Duplicate header-fetching
logic in moe_utils.py and fused_moe.py should be consolidated: create a helper
download_bmm_export_headers(...) in cubin_loader.py that accepts artifact_path,
checksum_hash, and bmm_export_headers and performs the checksum retrieval (using
get_cubin and get_meta_hash), loops over BMM_EXPORT_HEADERS, creates parent
directories (header_full_path.parent.mkdir(parents=True, exist_ok=True)) before
calling get_cubin for each header to handle nested paths, and then calls
ensure_symlink to link into
jit_env.FLASHINFER_CUBIN_DIR/flashinfer/trtllm/batched_gemm/trtllmGen_bmm_export;
replace the duplicated blocks in moe_utils.py and fused_moe.py with a call to
download_bmm_export_headers(ArtifactPath.TRTLLM_GEN_BMM,
CheckSumHash.TRTLLM_GEN_BMM, BMM_EXPORT_HEADERS).

@yzh119
yzh119 force-pushed the fix-bmm-header-download-with-cubin-package branch from 60961d8 to 74c8305 Compare March 27, 2026 17:40
… installed

`download_trtllm_headers()` stored BMM export headers at a custom path
that didn't match where `download_artifacts()` (and the flashinfer-cubin
wheel) puts them. This caused ~17 header files to be re-downloaded from
the network on every startup.

Replace `download_trtllm_headers()` and `get_file()` with two unified
primitives:

- `get_artifact()` (renamed from `get_cubin()`) fetches any artifact
  into the canonical path, reusing flashinfer-cubin files when available
- `ensure_symlink()` maps C++ include paths to the artifact directory

Callers (fused_moe, moe_utils) use these two primitives directly.
A backward-compatible `get_cubin = get_artifact` alias is kept.

Fixes: vllm-project/vllm#38110

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@aleozlx

aleozlx commented Mar 27, 2026

Copy link
Copy Markdown
Member

question: why did this PR touch the cutlass submodule?

@aleozlx aleozlx self-assigned this Mar 27, 2026

@aleozlx aleozlx left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lgtm except for the one question above

@flashinfer-bot

Copy link
Copy Markdown
Collaborator

[FAILED] Pipeline #47272725: 7/20 passed

@aleozlx aleozlx added the v0.6.8 release blocker label for 0.6.8 label Apr 1, 2026

@jimmyzho jimmyzho 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.

lgtm, we could address different hash in a followup

@aleozlx

aleozlx commented Apr 2, 2026

Copy link
Copy Markdown
Member

s different hash i

which different hash did you mean? about cutlass or bmm artifacts?

@jimmyzho

jimmyzho commented Apr 2, 2026

Copy link
Copy Markdown
Contributor

s different hash i

which different hash did you mean? about cutlass or bmm artifacts?

Different bmm artifacts

@aleozlx

aleozlx commented Apr 2, 2026

Copy link
Copy Markdown
Member

oh i think you mean the comment you left earlier.. i have addressed locally. let me push it

@aleozlx

aleozlx commented Apr 2, 2026

Copy link
Copy Markdown
Member

i'm making a pass at other chatbot comments to double check...

@aleozlx

aleozlx commented Apr 3, 2026

Copy link
Copy Markdown
Member

standby as i test the full workflow locally to double check everything

@aleozlx

aleozlx commented Apr 3, 2026

Copy link
Copy Markdown
Member

/bot run

@flashinfer-bot

Copy link
Copy Markdown
Collaborator

GitLab MR !474 has been updated with latest changes, and the CI pipeline #47589987 is currently running. I'll report back once the pipeline job completes.

@aleozlx

aleozlx commented Apr 3, 2026

Copy link
Copy Markdown
Member

cmds

1 pip install --upgrade setuptools
2 pip install --no-build-isolation -e .
3 pushd flashinfer-cubin/
4 pip install --no-build-isolation -e . -v
5 export FLASHINFER_NO_DOWNLOAD=1
6 export FLASHINFER_LOGGING_LEVEL=debug
7 popd
8 pytest tests/moe/test_trtllm_gen_fused_moe.py -v --log-cli-level=DEBUG

@aleozlx

aleozlx commented Apr 3, 2026

Copy link
Copy Markdown
Member

local tests are looking good, no download observed by the aforementioned approach

@aleozlx
aleozlx enabled auto-merge (squash) April 3, 2026 01:48
@aleozlx

aleozlx commented Apr 3, 2026

Copy link
Copy Markdown
Member

disclaimer on the last commit: it says debug but the debug var introduced is harmless and can help future debugging this area. hence leaving it in

@aleozlx

aleozlx commented Apr 3, 2026

Copy link
Copy Markdown
Member

i'll send out a .post1 release that cherry picks it to resolve the filed issue, as planned, once this gets merged

@aleozlx

aleozlx commented Apr 3, 2026

Copy link
Copy Markdown
Member

irrelevant err on gb200/300 (likely infra error)

FileExistsError: [Errno 17] File exists: '/workspace/junit'
❌ FAILED: tests/moe/test_trtllm_cutlass_fused_moe.py
==========================================
[93/118] Processing: tests/moe/test_trtllm_gen_fused_moe.py

@flashinfer-bot

Copy link
Copy Markdown
Collaborator

[FAILED] Pipeline #47589987: 10/20 passed

@aleozlx

aleozlx commented Apr 3, 2026

Copy link
Copy Markdown
Member

AOT builds has timed out

@aleozlx
aleozlx merged commit e0f3729 into flashinfer-ai:main Apr 3, 2026
50 of 58 checks passed
@johnnynunez johnnynunez mentioned this pull request Apr 4, 2026
5 tasks
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.

0.6.7 Escaped artifacts causing assertion error with trtllm-gen MoE/GEMM/attention at runtime in air-gaped env

4 participants