build: replace scikit-build-core + CMake with the in-tree setuptools backend - #291
build: replace scikit-build-core + CMake with the in-tree setuptools backend#291demandal25 wants to merge 7 commits into
Conversation
…backend The root package used scikit-build-core + CMake while every sub-wheel project (amd-flashinfer-jit-cache, flashinfer-jit-cache, flashinfer-cubin) already used an in-tree build_backend.py, so the tree carried two build systems. The CMake half no longer earned it. Compiled targets were removed in f00470c ("Tech debt removal"); since then CMakeLists.txt only installed headers and created the flashinfer/include symlink, and flashinfer/CMakeLists.txt was eight lines of message(). That required a CMake toolchain, scikit-build-core's experimental mode and a _skbuild tree to copy headers and make one symlink. The new root build_backend.py wraps setuptools.build_meta and does exactly one extra thing: materialize flashinfer/include -- a relative symlink for editable installs, a real filtered copy for wheels (setuptools will not follow a symlink into a wheel). This is load-bearing at runtime, not just build time: get_include_paths.get_include() feeds FLASHINFER_INCLUDE_DIR, which becomes the -I flag on every HIP JIT compile. Versioning is unchanged (setuptools-scm -> flashinfer/_version.py); the upstream version.txt / _build_meta.py scheme is deliberately not adopted. package-data now names flashinfer/csrc_rocm and flashinfer/include explicitly. scikit-build-core's wheel.packages swept those in implicitly and setuptools does not, so omitting them would ship a wheel whose JIT cannot compile. Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR replaces the root CMake/scikit-build-core packaging flow with an in-tree setuptools backend and removes obsolete CMake tooling.
Changes:
- Adds setuptools packaging and explicit package-data handling.
- Adds sdist manifest rules and header materialization.
- Removes CMake files, utilities, hooks, and related dependencies.
Blocking findings: deleting build_utils.py breaks both sub-wheel backends with ModuleNotFoundError. This was reported as critical with 3 votes on build_backend.py and 1 vote on build_utils.py. Keep the shared module or update both consumers.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Summary |
|---|---|
pyproject.toml |
Configures the in-tree setuptools backend and package data. |
MANIFEST.in |
Defines source-distribution contents. |
include/flashinfer/configure.h.in |
Removes the unused CMake template. |
flashinfer/CMakeLists.txt |
Removes obsolete package CMake configuration. |
docker/Dockerfile.rocm_ci |
Removes obsolete build dependencies. |
CMakeLists.txt |
Removes obsolete root CMake configuration. |
build_utils.py |
Removes the shared build helper; this breaks remaining sub-wheel backends. |
build_backend.py |
Implements setuptools hooks and header materialization; its removal of build_utils.py breaks sub-wheel builds. |
.pre-commit-config.yaml |
Removes the CMake formatting hook. |
.devcontainer/rocm/Dockerfile |
Removes obsolete build dependencies. |
Suppressed comments (4)
MANIFEST.in:35
- The removed
[tool.scikit-build.sdist].excludelist is not preserved here. Since setuptools-scm contributes tracked files to the sdist,.github,.devcontainer,profiler,Jenkinsfile, and other development/build files are now included because this manifest does not prune or exclude them. Please carry over the old exclusions so release sdists do not grow to contain CI and container machinery.
prune tests
prune docs
prune benchmarks
prune examples
prune 3rdparty
prune ci
prune docker
build_backend.py:27
- This removal changes the existing version API: the old backend generated
flashinfer/_build_meta.py, butflashinfer/version.pystill imports only that module andflashinfer/__main__.pystill reads fromversion. Since this backend now only lets setuptools-scm write_version.py, the CUDA package path andpython -m flashinferfall back to0.0.0+unknownafter installation. Updateversion.pyto use_version.py(while preserving git-version behavior) or continue generating the module.
Versioning is handled entirely by setuptools-scm via ``[tool.setuptools_scm]``
(which writes ``flashinfer/_version.py``). This backend deliberately does not
implement the upstream ``version.txt`` / ``_build_meta.py`` scheme.
build_backend.py:20
- This mode summary says an sdist gets a real recursive copy, but
_prepare_for_sdist()below deliberately clearsflashinfer/includeand relies onMANIFEST.in's top-levelinclude/tree. The docstring should describe the sdist case separately so it does not contradict the build behavior.
- wheel / sdist -> a real recursive copy, because a symlink is not followed into
a wheel and would ship a dangling link.
build_utils.py:1
- This deletion breaks the two other in-tree backends:
flashinfer-jit-cache/build_backend.py:27andflashinfer-cubin/build_backend.py:13prepend the repository root tosys.pathand then importbuild_utils.get_git_version. Their backend modules are imported before any wheel hook runs, so both release-wheel builds now fail withModuleNotFoundError. Keep this shared module or move the helper and update both consumers before deleting it.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Three fixes from the review of the previous commit. build_utils.py must stay. flashinfer-cubin/build_backend.py:13 and flashinfer-jit-cache/build_backend.py:27 each prepend the repository root to sys.path and then import build_utils.get_git_version at module load, so removing the root copy made both sub-wheel builds fail with ModuleNotFoundError before any PEP 517 hook could run. Reproduced, then fixed by restoring the file: it is a shared helper for those backends, not an orphan. MANIFEST.in now prunes the development and CI machinery. setuptools-scm's file finder contributes every git-tracked file to the sdist, so dropping [tool.scikit-build.sdist].exclude silently pulled .github (13 files), .devcontainer (11), profiler (5), rocm_profiler (2) and Jenkinsfile into the tarball. Verified: 935 -> 898 files, with all of the above now at zero. The build_backend.py module docstring claimed sdist gets a real recursive copy, which contradicted _prepare_for_sdist clearing the directory. Split the sdist case out so the doc matches the behavior. Co-Authored-By: Claude <noreply@anthropic.com>
|
Response to suppressed review comments
Whatever |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.
Suppressed comments (5)
Previously missed (1) — in code that hasn't changed since the last review.
pyproject.toml:64
- The previous backend explicitly copied every
licenses/*.txtfile toLICENSE.*.txtbefore a wheel build. With this setting now limited to the rootLICENSEand no replacement copy step, the four third-party license texts underlicenses/are retained only in the sdist and disappear from the wheel. Preserve them as wheel license files (for example by matchinglicenses/*.txt) so the artifact does not lose its bundled dependency notices.
license-files = ["LICENSE"]
.devcontainer/rocm/Dockerfile:91
- The development image is used with the documented
pip install --no-build-isolation -ve .flow, which skips installing[build-system].requires. The new setuptools backend needs the separatewheelpackage for wheel builds, but this replacement install list does not add it, so a clean image may fail atbdist_wheel. Addwheelto the explicit development dependencies.
/bin/micromamba run -n ${MAMBA_ENV_NAME} pip install --no-cache-dir ninja "setuptools>=80" "setuptools-scm>=9.2" pre-commit numpy pytest pytest-cov pytest-xdist pytest-rerunfailures pybind11 ruff && \
build_backend.py:67
_prepare_for_wheel()runs in the checkout and_clear()replaces an existing editable symlink with a real snapshot. Thus runningpip wheel .afterpip install -e .leaves the still-active editable install pointing at stale headers, so later edits underinclude/are ignored until editable installation is repeated. Preserve and restore the prior symlink (including around metadata/sdist hooks), or materialize the wheel inputs in a temporary build tree instead of mutating the checkout.
_clear(_pkg_include)
if use_symlink:
# Relative, so the link stays valid if the checkout is moved or bind
# mounted at a different path inside a container.
_pkg_include.symlink_to(Path("..") / "include", target_is_directory=True)
build_backend.py:30
- The old root backend honored
FLASHINFER_DEV_RELEASE_SUFFIX(andFLASHINFER_LOCAL_VERSION) when writing the package version, but this backend ignores both. The existing nightly release job still exportsFLASHINFER_DEV_RELEASE_SUFFIXfor the rootpython -m buildstep (.github/workflows/nightly-release.yml:67-73), so nightly wheels lose their date/dev suffix and the documented versioning is not unchanged. Preserve the environment-variable behavior or update the release/versioning pipeline together with this change.
Versioning is handled entirely by setuptools-scm via ``[tool.setuptools_scm]``
(which writes ``flashinfer/_version.py``). This backend deliberately does not
implement the upstream ``version.txt`` / ``_build_meta.py`` scheme.
docker/Dockerfile.rocm_ci:54
- This CI image invokes
python -m build --no-isolationbelow, so the build-system requirements are not installed into the environment. After removing scikit-build-core, the new backend still requires the separatewheelpackage forsetuptools.build_metato providebdist_wheel, but this image no longer installs it; a clean image can fail with an unrecognizedbdist_wheelcommand. Addwheelto the explicit image dependencies.
~/.local/bin/micromamba run -n ${MAMBA_ENV_NAME} pip install pybind11 build ninja "setuptools>=80" setuptools-scm numpy pytest pytest-xdist && \
Three fixes from the second review round. `wheel` leaves [build-system].requires. setuptools has provided bdist_wheel itself since 70.1, so it was never needed — but `python -m build --no-isolation` validates that every listed requirement is installed, and both container images install the build tooling explicitly. Reproduced against setuptools 84 with no `wheel` package: `ERROR Missing dependencies: wheel`, which is exactly what docker/Dockerfile.rocm_ci would have hit. Dropping the entry fixes it; adding `wheel` to the images would have papered over a requirement nothing uses. license-files moves from [tool.setuptools] to [project]. setuptools>=77 deprecates the former and warns on every build. Wheel contents are unchanged (LICENSE + NOTICE, which the previous config also produced via bdist_wheel's default globs) — deliberately so. Listing `licenses/*.txt` as the review suggested was tried and reverted: under `python -m build` setuptools emits the License-File metadata for those four files and then does not copy them into the archive, so the wheel contradicts its own METADATA. `pip wheel` handles it correctly, but the CI image uses `python -m build`. Wheel and sdist hooks now restore an editable symlink they replaced. pip builds a local directory in place, so `pip wheel .` in a checkout that already had `pip install -e .` swapped the live `flashinfer/include` symlink for a frozen copy, and later edits under `include/` silently stopped being picked up. A/B confirmed: before, the checkout is left holding a real directory; after, the symlink survives. Co-Authored-By: Claude <noreply@anthropic.com>
|
Response to suppressed review comments (review 4997744183)
Adding setuptools writes the
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.
Suppressed comments (2)
MANIFEST.in:23
recursive-include licenses *.txtaffects the sdist file list only; it does not copy the top-levellicenses/directory into a wheel. The proposed wheel therefore containsNOTICEbut none of thelicenses/LICENSE.*.txtfiles that NOTICE explicitly says contain the bundled third-party license text. Please arrange for those texts to be included in the wheel as package data or under the wheel's.dist-info/licensesdirectory as well.
recursive-include licenses *.txt
build_backend.py:122
- When the checkout has no pre-existing editable symlink, this
finallyblock does nothing after the wheel/metadata hook, so the generated real copy remains underflashinfer/include.get_include()resolves that path at runtime, meaning a subsequent source-tree test or JIT run afterpip wheel .uses a frozen header snapshot and no longer sees edits under top-levelinclude/. Clear the generated path on exit whenpriorisNone(and only recreate the symlink when one existed).
_clear(_pkg_include)
_pkg_include.symlink_to(prior, target_is_directory=True)
Follow-up to b5b1426, rewritten after self-review found the first version wrong. b5b1426 restored only a pre-existing symlink. Self-review caught the case it missed: with flashinfer/include a real directory, build_sdist cleared it and neither restore branch fired, so the checkout lost the directory outright. The A/B that signed off on b5b1426 had exercised pip wheel only, which re-creates a copy in build_wheel and hid it. Reproduced as realdir + sdist: real dir -> ABSENT before this commit. Collapsing the three-state contract to two fixes it and is what the motivation argued for anyway: a symlink goes back, everything else is cleared. A leftover real copy is exactly the frozen snapshot get_include() would resolve on a later in-tree run, so clearing it trades a silently stale header tree for one that fails the JIT loudly. That also drops the `existed` flag and the `os` import (pathlib has Path.readlink). prepare_metadata_for_build_wheel no longer materializes headers. Metadata comes from [project], so the copy had no consumer even before this change; with the restore in place it was created and deleted inside the one hook, costing a 223-file copytree per install for nothing. Confirmed the emitted .dist-info is byte-identical without it. Verified across the full matrix this time - 3 prior states x 2 hooks, not 3 x 1: symlink round-trips, real dir and absent both end absent, on both sdist and wheel. Wheel contents unchanged at 223 headers / 60 csrc_rocm in all three, including a wheel built from the sdist with no git on PATH; pip install . and pip install -e . both still leave the relative symlink. Comments trimmed throughout to the project's brevity rule; the measurements behind them live here instead. Co-Authored-By: Claude <noreply@anthropic.com>
|
Response to suppressed review comments (review 4999194584)
The A/B that signed off on the previous commit had exercised Rather than add a third arm, the contract collapses to two: a symlink is put back, everything else is cleared. That is what the original motivation argued for anyway — a leftover real copy is precisely the frozen snapshot Re-verified over the full matrix this time (3 prior states x 2 hooks, not 3 x 1): symlink round-trips on both The same review also found that
The suggested remedies do not work as stated. Listing them under What is genuinely true, and worth recording: the wheel ships |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.
Suppressed comments (2)
Previously missed (1) — in code that hasn't changed since the last review.
pyproject.toml:66
- The
flashinfer*pattern is too broad for a flat repository with the sibling sub-wheel projects: setuptools' namespace-aware finder can also discoverflashinfer-cubin/flashinfer_cubinandflashinfer-jit-cache/flashinfer_jit_cache(both contain__init__.py). That puts sub-wheel modules into the root wheel under hyphenated paths and changes the root package contents; restrict the pattern to the root package namespace.
include = ["flashinfer*"]
pyproject.toml:63
- The root nightly workflow still exports
FLASHINFER_DEV_RELEASE_SUFFIXforpython -m build(.github/workflows/nightly-release.yml:69), but this backend now delegates versioning to setuptools-scm and never translates that variable into a version. Daily builds therefore keep the SCM-derived version instead of the requested date suffix, so published nightly artifacts can collide. Preserve the workflow contract (for example by setting a supportedSETUPTOOLS_SCM_PRETEND_VERSIONvalue before metadata generation) or update the release workflow and its versioning contract.
build-backend = "build_backend"
backend-path = ["."]
Copilot flagged include = ["flashinfer*"] as too broad and was right; the
measurement is worse than the report. [tool.setuptools.packages.find] defaults
namespaces to true, so the finder matches plain directories, and the glob picked
up the sibling flashinfer-cubin/ and flashinfer-jit-cache/ project roots. The
wheel shipped their build_backend.py, pyproject.toml and .gitignore, and
top_level.txt read:
flashinfer
flashinfer-cubin
flashinfer-jit-cache
A regression from this PR: scikit-build-core's wheel.packages = ["flashinfer"]
was an exact name, not a pattern.
Anchoring the patterns to flashinfer / flashinfer.* is the whole fix. A first
attempt also set namespaces = false; self-review showed that to be both
redundant and harmful, and it was dropped. Redundant because a hyphenated
directory cannot match "flashinfer.*" anyway. Harmful because flashinfer/cute_dsl
and flashinfer/tuning_configs are real namespace packages with no __init__.py:
turning namespaces off demotes their five modules to incidental data files that
survive only via setuptools-scm's git file finder, so a build from a git-less
export would silently drop them. It also emitted 28 "absent from the packages
configuration" warnings per build, against 0 for the anchored include alone.
Measured on all three variants: top_level.txt is now just flashinfer, no sibling
files, and the real package is untouched at 414 flashinfer/ entries — none lost,
none gained, still 223 headers and 60 csrc_rocm files, cute_dsl and
tuning_configs both present.
Missed earlier because the wheel checks only counted what should be present and
never looked at what should not be.
Co-Authored-By: Claude <noreply@anthropic.com>
|
Response to suppressed review comments (review 5000164297)
This is a regression rather than pre-existing behaviour: scikit-build-core's The fix is to anchor the patterns — Measured across all three variants:
For completeness: the sdist is unaffected by package discovery and still ships the sibling projects,
The suggested remedy would also not work as written: |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.
Suppressed comments (3)
build_backend.py:12
- This newly added explanation names
-I, but the HIP JIT generator passesFLASHINFER_INCLUDE_DIRas-isystem(flashinfer/jit/cpp_ext.py:127-128). Please use the actual flag (or say “include flag”) so the backend documentation does not describe the generated command incorrectly.
in ``flashinfer/jit/env.py`` and ends up as the ``-I`` flag on every HIP JIT
build_backend.py:150
- The new PEP 517 hooks are the only code that controls whether the runtime header tree is copied, cleared, and restored, but this behavior has no automated regression test in the repository; the described source-state matrix is manual-only. A future change to hook ordering or cleanup can therefore produce an installable wheel with missing/stale headers or leave the checkout in the wrong state. Add a lightweight packaging test that invokes the wheel/sdist/editable hooks and asserts the archive contents plus the symlink/absent postconditions.
def build_wheel(wheel_directory, config_settings=None, metadata_directory=None):
with _restoring_pkg_include():
_prepare_for_wheel()
return _orig.build_wheel(wheel_directory, config_settings, metadata_directory)
pyproject.toml:14
- The removed scikit-build configuration explicitly set
wheel.license-files = ["LICENSE"], whereas this addsNOTICEas a project license file. Setuptools will therefore addNOTICEunder.dist-info/licenses, so the wheel is not byte-identical to the previous configuration as claimed in the PR description. Either keep onlyLICENSEor update the compatibility claim and inventory expectations.
license-files = ["LICENSE", "NOTICE"]
Two regressions shipped in _restoring_pkg_include across two review rounds with
nothing to catch them, so add the guard Copilot asked for. Pure filesystem
logic: the backend module is loaded by path and its _src_include/_pkg_include
globals are rebound to a tmp_path, so the real checkout is never touched.
Lives in tests/rocm_tests/ because that is what [tool.pytest.ini_options]
testpaths covers - a file at tests/ top level is collected by nothing, which
self-review caught before it shipped as a guard that never runs.
Covers 4 prior states (symlink, real dir, regular file, absent) x 2 prepare
steps, restore-on-exception, all three entries in _HEADER_SUFFIXES, the
relative-symlink requirement, and that packages.find still excludes the sibling
projects.
A/B'd by mutation: reverting to the bae7c593 restore logic fails 3 of 14
(including realdir + wheel leaving the stale copy that started this), narrowing
_HEADER_SUFFIXES to {".cuh"} fails 1, and dropping the finally-clause _clear
fails 8. Two cells are known-weak and kept for documentation rather than
protection: realdir + sdist passes either way because the old bug's symptom
matches the new contract, and absent + sdist has no reachable mutation.
Also corrects the module docstring: FLASHINFER_INCLUDE_DIR reaches hipcc as
-isystem, not -I (flashinfer/jit/cpp_ext.py:128).
Co-Authored-By: Claude <noreply@anthropic.com>
|
Response to suppressed review comments (review 5000505481)
Keeping That same old-vs-new diff turned up two more differences worth recording, neither previously noted:
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.
Suppressed comments (3)
build_backend.py:150
- The automated tests exercise only the private preparation helpers; they never invoke the PEP 517
build_wheel/build_sdisthooks or inspect an actual artifact. A regression in the new setuptoolspackage-data/MANIFEST.inintegration (for example, headers missing from a wheel or an sdist that cannot rebuild a wheel) would therefore pass the suite. Please add a non-GPU integration test that builds and inventories both artifacts, including the sdist-to-wheel path.
def build_wheel(wheel_directory, config_settings=None, metadata_directory=None):
with _restoring_pkg_include():
_prepare_for_wheel()
return _orig.build_wheel(wheel_directory, config_settings, metadata_directory)
build_backend.py:144
- This metadata-only PEP 660 hook mutates the checkout:
_prepare_for_editable()clears any existingflashinfer/includedirectory and creates a symlink even when the frontend is only requesting metadata (and can leave that mutation behind if the later build is not run). The actualbuild_editable()hook already performs this preparation, so metadata generation should remain side-effect-free and defer it to the build hook.
_prepare_for_editable()
return _orig.prepare_metadata_for_build_editable(
metadata_directory, config_settings
)
tests/rocm_tests/test_build_backend.py:116
- On Python 3.10—the minimum declared version and the version used by the release workflows—
tomllibis unavailable, sopytest.importorskipskips the only assertion that verifies the anchored package patterns. This leaves the sibling-package regression untested in that supported environment; use a 3.10-compatible parser (or add its backport to the test dependencies) instead of unconditionally skipping.
tomllib = pytest.importorskip("tomllib")
cfg = tomllib.loads((_REPO_ROOT / "pyproject.toml").read_text())
The file said "no torch, no GPU". The assertions are torch-free, but it lives
under tests/rocm_tests/ and tests/conftest.py imports torch at module scope, so
collection needs it either way. There is no CPU-only lane to move it to.
Also swap pytest.importorskip("tomllib") for a tomllib-then-tomli fallback.
tomllib is 3.11+, requires-python allows 3.10, so on the minimum interpreter the
sibling-package assertion was skipping unconditionally. tomli is not a declared
dependency, so 3.10 still skips where it is absent — but the skip is now the
exception rather than the rule.
Co-Authored-By: Claude <noreply@anthropic.com>
|
Response to suppressed review comments (review 5000571264)
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.
Suppressed comments (3)
build_backend.py:110
- This restores whatever target the old symlink had verbatim. A checkout migrated from the deleted CMake editable hook can still have
flashinfer/includepointing to an absolute source path; afterbuild_wheelorbuild_sdist, that absolute link is recreated andget_include().resolve()will keep pointing at the old path when the checkout is moved or bind-mounted. Normalize a preserved generated link to the required relative../includetarget (or preserve it only after validating that target).
prior = _pkg_include.readlink() if _pkg_include.is_symlink() else None
pyproject.toml:14
- The wheel now ships
NOTICE(under.dist-info/licenses/) but does not shiplicenses/*.txt;MANIFEST.inonly affects the sdist andlicense-fileslists only the two root files. This leavesNOTICE's “See licenses/” reference dangling in the wheel, while the wheel includes adapted/bundled third-party headers such as CUTLASS whose BSD/MIT terms require the corresponding notices in binary-distribution documentation. Please package these texts in the wheel as well using a mechanism that works for both build frontends, rather than keeping them sdist-only.
# Adding licenses/*.txt here makes `python -m build` write License-File
# metadata it does not copy, so the wheel contradicts its own METADATA.
license-files = ["LICENSE", "NOTICE"]
pyproject.toml:62
- Switching the build backend makes setuptools-scm resolve the root version directly, but the existing root config still spells the custom command as
scm.git.describe_command(pyproject.toml:92). The setuptools-scm option isgit_describe_command(as used byamd-flashinfer-jit-cache/pyproject.toml:54), so this backend will not runscripts/git_describe_rocm.py; builds from ROCm tags/commit distances can therefore get different version strings despite the stated unchanged versioning. Rename that option togit_describe_commandand add a version assertion for a ROCm tag.
build-backend = "build_backend"
Summary
The root package built with scikit-build-core + CMake while every sub-wheel project (
amd-flashinfer-jit-cache,flashinfer-jit-cache,flashinfer-cubin) already used an in-treebuild_backend.py, so the tree carried two build systems. This converges the root onto the same plain-setuptools backend and deletes the CMake machinery.The CMake half no longer earned its keep. Compiled targets were removed in
f00470c1("Tech debt removal"); since thenCMakeLists.txtonly installed headers and created theflashinfer/includesymlink, andflashinfer/CMakeLists.txtwas eight lines ofmessage()referencing a${PROJECT_VERSION_FULL}that nothing sets. We required a CMake toolchain, scikit-build-core'sexperimental = true, and a_skbuildtree in order to copy headers and make one symlink.What changed
build_backend.py— replaced the orphaned upstream-CUDA version (which read aversion.txtthat does not exist and wrote the upstreamflashinfer/data/layout) with a thinsetuptools.build_metawrapper whose only extra jobs are materializingflashinfer/includeand restoring an editable symlink it replaced.pyproject.toml—[build-system]now usesbuild_backendwithbackend-path = ["."];[tool.scikit-build]and[tool.scikit-build.sdist]removed;[tool.setuptools]with explicitpackages.findandpackage-dataadded.torchandnumpydropped from build requires — nothing is compiled at build time, so they were never needed.license-filesuses the PEP 639[project]key.MANIFEST.in— new. setuptools only ships declared package data, so the top-levelinclude/tree, the backend itself, and the generated_version.pyhave to be named for the sdist. It also has to re-create the pruning that[tool.scikit-build.sdist].excludeused to do: setuptools-scm's file finder contributes every git-tracked file, so without explicit prunes the tarball picks up.github,.devcontainer,profiler,rocm_profilerandJenkinsfile.CMakeLists.txt,flashinfer/CMakeLists.txt,include/flashinfer/configure.h.in— deleted. The last was aconfigure_filetemplate nothing configures..pre-commit-config.yaml—cmake-formathook removed (no CMake files remain)..devcontainer/rocm/Dockerfile,docker/Dockerfile.rocm_ci— dropcmakeandscikit-build-corefrom the installed build tooling.build_utils.pyis kept. It looks orphaned from the root's perspective, butflashinfer-cubin/build_backend.py:13andflashinfer-jit-cache/build_backend.py:27each prepend the repository root tosys.pathand importget_git_versionfrom it at module load, so it is a shared helper for those two sub-wheel backends.Architecture / design notes
Why
flashinfer/includeis load-bearing at runtime, not just build time.get_include_paths.get_include()resolves<pkg>/include, which becomesFLASHINFER_INCLUDE_DIRinflashinfer/jit/env.pyand ends up as the-isystemflag on every HIP JIT compile. If it is missing or dangling, every JIT build fails.The backend materializes it three different ways on purpose:
../includeinclude/are picked up with no rebuild; matches the manual worktree setup in CLAUDE.md, and stays valid under a container bind mount.cuh/.h/.hppinstall(DIRECTORY ...)rule so wheel contents are unchangedinclude/viaMANIFEST.in); building a wheel from that sdist re-creates the copy. Leaving a real copy would duplicate the whole header tree in the tarball, and leaving a symlink would ship a dangling oneThe wheel and sdist hooks build in the checkout, so they do not leave a copy behind. pip builds a local directory in place. A generated
flashinfer/includeleft in the tree is whatget_include()resolves on any later in-tree run, shadowing edits underinclude/with a frozen snapshot. So a symlink is put back as a symlink and anything else is cleared — absent fails the JIT loudly, stale does not.prepare_metadata_for_build_wheeldoes not materialize headers at all: metadata comes from[project].package-datais the sharp edge. scikit-build-core'swheel.packages = ["flashinfer"]sweptflashinfer/csrc_rocm/**into the wheel implicitly; setuptools does not. Bothcsrc_rocm(viaget_csrc_dir()) andinclude(viaget_include()) are resolved at runtime, so omitting either produces a wheel that installs cleanly and then fails at the first JIT compile. They are now named explicitly.packages.findpatterns must be anchored.wheel.packages = ["flashinfer"]was an exact name; the setuptools equivalent is a glob, and[tool.setuptools.packages.find]defaultsnamespaces = trueso it matches plain directories."flashinfer*"therefore also matched the siblingflashinfer-cubin/andflashinfer-jit-cache/project roots and shipped their build files inside the root wheel.["flashinfer", "flashinfer.*"]is the fix. Settingnamespaces = falseinstead is a trap:flashinfer/cute_dslandflashinfer/tuning_configshave no__init__.py, so disabling namespaces demotes their modules to incidental data that only survives via setuptools-scm's git file finder.wheelis deliberately absent from[build-system].requires. setuptools has providedbdist_wheelitself since 70.1, andpython -m build --no-isolation— whichdocker/Dockerfile.rocm_ciruns — hard-fails on any listed requirement that is not installed. Listing it would force both container images to install a package nothing uses.Wheel contents versus the old scikit-build-core build, measured by building both and diffing (not inferred):
flashinfer/CMakeLists.txtis gone as intended;NOTICEis now shipped under.dist-info/licenses/where the oldwheel.license-files = ["LICENSE"]omitted it — kept deliberately, since Apache-2.0 §4(d) requires NOTICE content to travel with redistributions;flashinfer/profiler/__init__.pyis now included, fixing a pre-existing hole that madeimport flashinfer.profilerfail on an installed wheel; and the tag moves frompy3-none-linux_x86_64topy3-none-any, because the old platform tag was an artifact of CMake running and nothing is compiled.Adding the four
licenses/*.txttexts was implemented and backed out: underpython -m buildsetuptools writesLicense-Filemetadata for all six and then copies only two into the archive, so the wheel contradicts its own METADATA.pip wheelhandles it correctly. The third-party texts still ship in the sdist.Versioning is unchanged.
[tool.setuptools_scm]still writesflashinfer/_version.pyviascripts/git_describe_rocm.py; only the scikit-build-core-specificmetadata.version.providerkey is dropped. The upstreamversion.txt/_build_meta.pyscheme is deliberately not adopted.Test plan
Run against setuptools 84 / setuptools-scm 9 / py3.12, with the
wheelpackage uninstalled to exercise the requires change:python -m build --no-isolation --wheel→ 223 headers underflashinfer/include/, 60 files underflashinfer/csrc_rocm/, plus_version.pyandpy.typed; noSetuptoolsDeprecationWarningpip wheel . --no-build-isolation→ identical counts, and the.dist-info/licenses/contents match theLicense-Fileentries in METADATA on both routestop_level.txtisflashinferalone, no sibling-project files, and the real package is unchanged at 414flashinfer/entries withcute_dslandtuning_configspresentpython -m build --sdist→ 899 files; ships top-levelinclude/(252),build_backend.py,build_utils.py,_version.py,MANIFEST.inandlicenses/, and does not contain the generatedflashinfer/includecopy.github,.devcontainer,profiler,rocm_profiler,scripts,tests,docs,Jenkinsfile,CHANGELOG.mdall at zero filesgitremoved fromPATH→ same 223 / 60 counts and the correct version, confirming_version.pytravels with the tarballpip install --no-build-isolation -e .on a checkout withflashinfer/includedeleted → recreates it as a relative symlinksymlink/ real directory / absent) x 2 hooks (build_wheel,build_sdist). A symlink round-trips; the other two end absent. Every cell produces the same 223 / 60 wheelpip install .andpip install -e .both leaveflashinfer/include -> ../includeintactERROR Missing dependencies: wheelreproduced first withwheellisted in requires, then confirmed fixed by dropping it — this is whatdocker/Dockerfile.rocm_ci'spython -m build --no-isolationwould have hitbuild_utils.py(ModuleNotFoundErrorreproduced first, then confirmed fixed)tests/rocm_tests/test_build_backend.py— 14 tests over the prior-state x hook matrix, A/B'd by mutation (reverting the restore logic fails 3, narrowing_HEADER_SUFFIXESfails 1, dropping the finally-clause_clearfails 8)pre-commit run -a→ all hooks passNot covered
-isystempath this PR is responsible for is exercised indirectly by the editable-install symlink check and the wheel content checks, but an end-to-end kernel build would be a stronger signal and is worth doing before merge if a node is free.docker/Dockerfile.rocm_ciis not built by anything here), so this change is not covered by automation; the checks above were manual.