Skip to content

build: replace scikit-build-core + CMake with the in-tree setuptools backend - #291

Open
demandal25 wants to merge 7 commits into
amd-integrationfrom
build-system-setuptools
Open

build: replace scikit-build-core + CMake with the in-tree setuptools backend#291
demandal25 wants to merge 7 commits into
amd-integrationfrom
build-system-setuptools

Conversation

@demandal25

@demandal25 demandal25 commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

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-tree build_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 then CMakeLists.txt only installed headers and created the flashinfer/include symlink, and flashinfer/CMakeLists.txt was eight lines of message() referencing a ${PROJECT_VERSION_FULL} that nothing sets. We required a CMake toolchain, scikit-build-core's experimental = true, and a _skbuild tree in order to copy headers and make one symlink.

What changed

  • build_backend.py — replaced the orphaned upstream-CUDA version (which read a version.txt that does not exist and wrote the upstream flashinfer/data/ layout) with a thin setuptools.build_meta wrapper whose only extra jobs are materializing flashinfer/include and restoring an editable symlink it replaced.
  • pyproject.toml[build-system] now uses build_backend with backend-path = ["."]; [tool.scikit-build] and [tool.scikit-build.sdist] removed; [tool.setuptools] with explicit packages.find and package-data added. torch and numpy dropped from build requires — nothing is compiled at build time, so they were never needed. license-files uses the PEP 639 [project] key.
  • MANIFEST.in — new. setuptools only ships declared package data, so the top-level include/ tree, the backend itself, and the generated _version.py have to be named for the sdist. It also has to re-create the pruning that [tool.scikit-build.sdist].exclude used to do: setuptools-scm's file finder contributes every git-tracked file, so without explicit prunes the tarball picks up .github, .devcontainer, profiler, rocm_profiler and Jenkinsfile.
  • CMakeLists.txt, flashinfer/CMakeLists.txt, include/flashinfer/configure.h.in — deleted. The last was a configure_file template nothing configures.
  • .pre-commit-config.yamlcmake-format hook removed (no CMake files remain).
  • .devcontainer/rocm/Dockerfile, docker/Dockerfile.rocm_ci — drop cmake and scikit-build-core from the installed build tooling.

build_utils.py is kept. It looks orphaned from the root's perspective, but flashinfer-cubin/build_backend.py:13 and flashinfer-jit-cache/build_backend.py:27 each prepend the repository root to sys.path and import get_git_version from it at module load, so it is a shared helper for those two sub-wheel backends.

Architecture / design notes

Why flashinfer/include is load-bearing at runtime, not just build time. get_include_paths.get_include() resolves <pkg>/include, which becomes FLASHINFER_INCLUDE_DIR in flashinfer/jit/env.py and ends up as the -isystem flag on every HIP JIT compile. If it is missing or dangling, every JIT build fails.

The backend materializes it three different ways on purpose:

Build Form Why
editable relative symlink to ../include edits to include/ are picked up with no rebuild; matches the manual worktree setup in CLAUDE.md, and stays valid under a container bind mount
wheel real recursive copy, filtered to .cuh/.h/.hpp setuptools will not follow a symlink into a wheel; the filter mirrors the retired CMake install(DIRECTORY ...) rule so wheel contents are unchanged
sdist cleared the sdist ships the source layout (top-level include/ via MANIFEST.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 one

The 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/include left in the tree is what get_include() resolves on any later in-tree run, shadowing edits under include/ 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_wheel does not materialize headers at all: metadata comes from [project].

package-data is the sharp edge. scikit-build-core's wheel.packages = ["flashinfer"] swept flashinfer/csrc_rocm/** into the wheel implicitly; setuptools does not. Both csrc_rocm (via get_csrc_dir()) and include (via get_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.find patterns must be anchored. wheel.packages = ["flashinfer"] was an exact name; the setuptools equivalent is a glob, and [tool.setuptools.packages.find] defaults namespaces = true so it matches plain directories. "flashinfer*" therefore also matched the sibling flashinfer-cubin/ and flashinfer-jit-cache/ project roots and shipped their build files inside the root wheel. ["flashinfer", "flashinfer.*"] is the fix. Setting namespaces = false instead is a trap: flashinfer/cute_dsl and flashinfer/tuning_configs have no __init__.py, so disabling namespaces demotes their modules to incidental data that only survives via setuptools-scm's git file finder.

wheel is deliberately absent from [build-system].requires. setuptools has provided bdist_wheel itself since 70.1, and python -m build --no-isolation — which docker/Dockerfile.rocm_ci runs — 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.txt is gone as intended; NOTICE is now shipped under .dist-info/licenses/ where the old wheel.license-files = ["LICENSE"] omitted it — kept deliberately, since Apache-2.0 §4(d) requires NOTICE content to travel with redistributions; flashinfer/profiler/__init__.py is now included, fixing a pre-existing hole that made import flashinfer.profiler fail on an installed wheel; and the tag moves from py3-none-linux_x86_64 to py3-none-any, because the old platform tag was an artifact of CMake running and nothing is compiled.

Adding the four licenses/*.txt texts was implemented and backed out: under python -m build setuptools writes License-File metadata for all six and then copies only two into the archive, so the wheel contradicts its own METADATA. pip wheel handles it correctly. The third-party texts still ship in the sdist.

Versioning is unchanged. [tool.setuptools_scm] still writes flashinfer/_version.py via scripts/git_describe_rocm.py; only the scikit-build-core-specific metadata.version.provider key is dropped. The upstream version.txt / _build_meta.py scheme is deliberately not adopted.

Test plan

Run against setuptools 84 / setuptools-scm 9 / py3.12, with the wheel package uninstalled to exercise the requires change:

  • python -m build --no-isolation --wheel → 223 headers under flashinfer/include/, 60 files under flashinfer/csrc_rocm/, plus _version.py and py.typed; no SetuptoolsDeprecationWarning
  • pip wheel . --no-build-isolation → identical counts, and the .dist-info/licenses/ contents match the License-File entries in METADATA on both routes
  • wheel inventory diffed in full against the previous configuration, not just spot-counted: top_level.txt is flashinfer alone, no sibling-project files, and the real package is unchanged at 414 flashinfer/ entries with cute_dsl and tuning_configs present
  • python -m build --sdist → 899 files; ships top-level include/ (252), build_backend.py, build_utils.py, _version.py, MANIFEST.in and licenses/, and does not contain the generated flashinfer/include copy
  • sdist pruning verified by inspection: .github, .devcontainer, profiler, rocm_profiler, scripts, tests, docs, Jenkinsfile, CHANGELOG.md all at zero files
  • wheel built from that sdist, with git removed from PATH → same 223 / 60 counts and the correct version, confirming _version.py travels with the tarball
  • pip install --no-build-isolation -e . on a checkout with flashinfer/include deleted → recreates it as a relative symlink
  • checkout state A/B'd over the full matrix — 3 prior states (symlink / 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 wheel
  • pip install . and pip install -e . both leave flashinfer/include -> ../include intact
  • ERROR Missing dependencies: wheel reproduced first with wheel listed in requires, then confirmed fixed by dropping it — this is what docker/Dockerfile.rocm_ci's python -m build --no-isolation would have hit
  • both sub-wheel backends import again after restoring build_utils.py (ModuleNotFoundError reproduced 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_SUFFIXES fails 1, dropping the finally-clause _clear fails 8)
  • pre-commit run -a → all hooks pass

Not covered

  • The packaging checks above ran in a plain venv on the host, not in the ROCm devcontainer — Docker was unreachable from that session. Nothing in this change needs a GPU or a HIP toolchain, but it does mean the setuptools version under test (84) is whatever pip resolved rather than whatever the image pins.
  • A GPU-backed JIT compile against the installed wheel was not run. The -isystem path 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.
  • No ROCm CI workflow exists in-tree (docker/Dockerfile.rocm_ci is not built by anything here), so this change is not covered by automation; the checks above were manual.

…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>
Copilot AI lite review requested due to automatic review settings August 21, 2026 21:36

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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].exclude list 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, but flashinfer/version.py still imports only that module and flashinfer/__main__.py still reads from version. Since this backend now only lets setuptools-scm write _version.py, the CUDA package path and python -m flashinfer fall back to 0.0.0+unknown after installation. Update version.py to 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 clears flashinfer/include and relies on MANIFEST.in's top-level include/ 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:27 and flashinfer-cubin/build_backend.py:13 prepend the repository root to sys.path and then import build_utils.get_git_version. Their backend modules are imported before any wheel hook runs, so both release-wheel builds now fail with ModuleNotFoundError. 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.

Comment thread build_backend.py
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>
Copilot AI review requested due to automatic review settings August 21, 2026 21:50
@demandal25

demandal25 commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator Author

Response to suppressed review comments

build_utils.py:1 — accepted, fixed in caffc88b. Same finding as the inline comment. Reproduced first: both flashinfer-cubin/build_backend.py:13 and flashinfer-jit-cache/build_backend.py:27 prepend the repo root to sys.path and import build_utils.get_git_version at module load, so the deletion broke them before any PEP 517 hook ran. File restored.

MANIFEST.in:35 — accepted, fixed in caffc88b. Confirmed empirically rather than assumed: setuptools-scm’s file finder contributes every git-tracked file, so the sdist had picked up .github (13 files), .devcontainer (11), profiler (5), rocm_profiler (2) and Jenkinsfile. Added the missing prunes; the tarball goes 935 → 898 files with all of those now at zero.

build_backend.py:20 — accepted, fixed in caffc88b. The module docstring did contradict _prepare_for_sdist, which clears the directory rather than copying. Split the sdist case out.

build_backend.py:27 — declined. The premise is that this PR stops flashinfer/_build_meta.py being generated, regressing flashinfer/version.py and python -m flashinfer to 0.0.0+unknown. That generation never happened on this path: on amd-integration the root backend is scikit_build_core.build with no backend-path, so the root build_backend.py is not importable as a backend and its module-level _create_build_metadata() never runs during a root build. _build_meta.py is also gitignored, so it is not shipped.

Whatever python -m flashinfer reports today, it reports the same after this PR — the behaviour is unchanged, and the ROCm import path uses flashinfer/_version.py via __init__.py regardless. Fixing the CUDA-path version.py/_build_meta coupling is a real cleanup, but it is pre-existing and out of scope here.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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/*.txt file to LICENSE.*.txt before a wheel build. With this setting now limited to the root LICENSE and no replacement copy step, the four third-party license texts under licenses/ are retained only in the sdist and disappear from the wheel. Preserve them as wheel license files (for example by matching licenses/*.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 separate wheel package for wheel builds, but this replacement install list does not add it, so a clean image may fail at bdist_wheel. Add wheel to 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 running pip wheel . after pip install -e . leaves the still-active editable install pointing at stale headers, so later edits under include/ 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 (and FLASHINFER_LOCAL_VERSION) when writing the package version, but this backend ignores both. The existing nightly release job still exports FLASHINFER_DEV_RELEASE_SUFFIX for the root python -m build step (.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-isolation below, so the build-system requirements are not installed into the environment. After removing scikit-build-core, the new backend still requires the separate wheel package for setuptools.build_meta to provide bdist_wheel, but this image no longer installs it; a clean image can fail with an unrecognized bdist_wheel command. Add wheel to 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 && \

Comment thread pyproject.toml
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>
Copilot AI review requested due to automatic review settings August 22, 2026 05:26
@demandal25

Copy link
Copy Markdown
Collaborator Author

Response to suppressed review comments (review 4997744183)

build_backend.py:67 — accepted, fixed in b5b1426b. Correct, and reproduced: pip builds a local directory in place, so pip wheel . in a checkout that already had pip install -e . replaced the live flashinfer/include symlink with a frozen copy, and later edits under include/ stopped being picked up. The wheel and sdist hooks now snapshot an existing symlink and restore it on the way out. A/B on the same checkout: before, include is: real dir; after, include is: symlink -> ../include.

.devcontainer/rocm/Dockerfile:91 and docker/Dockerfile.rocm_ci:54 — accepted, fixed differently in b5b1426b. The failure is real but the diagnosis is not: setuptools has shipped its own bdist_wheel since 70.1, so wheel is not needed to build one. What actually breaks is python -m build --no-isolation, which validates that every entry in [build-system].requires is installed — and this PR listed wheel there. Reproduced against setuptools 84 with no wheel package: ERROR Missing dependencies: wheel. Dropping the unused requirement fixes both images; installing wheel into them would have satisfied a requirement nothing uses. Both routes verified afterwards with wheel uninstalled — python -m build --no-isolation --wheel and pip wheel . each produce 223 headers and 60 csrc_rocm files.

pyproject.toml:64 — partly accepted, fixed in b5b1426b; the licenses/*.txt part was tried and reverted. The regression premise is wrong — [tool.scikit-build] wheel.license-files = ["LICENSE"] means those four texts were not in the wheel before this PR either. But the line was worth changing for another reason: [tool.setuptools] license-files is deprecated in favour of [project] license-files on setuptools>=77 and emitted a warning on every build, so it moved.

Adding licenses/*.txt was implemented, measured, and backed out, because it produces an inconsistent wheel under the command CI runs:

$ python -m build --no-isolation --wheel      # with license-files = [..., "licenses/*.txt"]
IN ARCHIVE : 2 ['LICENSE', 'NOTICE']
IN METADATA: 6 ['LICENSE', 'NOTICE', 'licenses/LICENSE.cutlass.txt',
                'licenses/LICENSE.flashattention3.txt', 'licenses/LICENSE.fmt.txt',
                'licenses/LICENSE.spdlog.txt']

setuptools writes the License-File metadata for all six and then copies only two, so the wheel contradicts its own METADATA. pip wheel . handles it correctly, but docker/Dockerfile.rocm_ci uses python -m build. Shipping a wheel that promises files it does not contain is worse than the status quo, so license-files stays at ["LICENSE", "NOTICE"] — byte-identical to what the wheel carried before. The third-party texts remain in the sdist. Worth revisiting once that setuptools behaviour is fixed upstream.

build_backend.py:30 — declined. The claim is that nightly wheels lose their date suffix because this backend ignores FLASHINFER_DEV_RELEASE_SUFFIX. That job does not run today: .github/workflows/nightly-release.yml opens its setup step with VERSION=$(cat version.txt | tr -d '[:space:]'), and version.txt is not in this tree (git ls-files version.txt is empty) — every other job in the workflow is needs: setup. The env var was also already inert for the root build before this PR, for the same reason as the _build_meta comment in the previous round: build_backend.py was not the configured backend, so nothing read it. Rewiring the release pipeline onto setuptools-scm is real work and belongs in its own change.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 *.txt affects the sdist file list only; it does not copy the top-level licenses/ directory into a wheel. The proposed wheel therefore contains NOTICE but none of the licenses/LICENSE.*.txt files 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/licenses directory as well.
recursive-include licenses *.txt

build_backend.py:122

  • When the checkout has no pre-existing editable symlink, this finally block does nothing after the wheel/metadata hook, so the generated real copy remains under flashinfer/include. get_include() resolves that path at runtime, meaning a subsequent source-tree test or JIT run after pip wheel . uses a frozen header snapshot and no longer sees edits under top-level include/. Clear the generated path on exit when prior is None (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>
Copilot AI review requested due to automatic review settings August 22, 2026 12:10
@demandal25

Copy link
Copy Markdown
Collaborator Author

Response to suppressed review comments (review 4999194584)

build_backend.py:122 — accepted, and it exposed a worse bug than the one reported. Fixed in b86445cf. The report is right that the finally did nothing when no symlink pre-existed. Running a self code-review on that commit surfaced the sharper case: with flashinfer/include a real directory, build_sdist cleared it and neither branch fired, so the checkout lost the directory outright — while the docstring claimed "a real copy that was already there is left as a real copy". Reproduced before fixing:

  symlink  + sdist : symlink -> ../include  -> symlink -> ../include
  realdir  + sdist : real dir               -> ABSENT
  realdir  + wheel : real dir               -> real dir

The A/B that signed off on the previous commit had exercised pip wheel only, which re-materializes a copy in build_wheel and masks it.

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 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. The existed flag and the os import go away with it (pathlib has Path.readlink).

Re-verified over the full matrix this time (3 prior states x 2 hooks, not 3 x 1): symlink round-trips on both build_wheel and build_sdist; real dir and absent both end absent on both. Wheel contents unchanged at 223 headers / 60 csrc_rocm files in every case, including a wheel built from the sdist with no git on PATH. pip install . and pip install -e . both still leave the relative symlink.

The same review also found that prepare_metadata_for_build_wheel was materializing the whole header tree for nothing — metadata comes from [project], and with the restore in place the copy was created and deleted inside that one hook, costing a 223-file copytree per install. Removed; the emitted .dist-info is byte-identical without it.

MANIFEST.in:23 — declined, third time, but the underlying point is real and should be its own change. The premise that this PR drops the texts is still wrong: [tool.scikit-build] wheel.license-files = ["LICENSE"] means they were never in the wheel on amd-integration either, so nothing regresses here.

The suggested remedies do not work as stated. Listing them under license-files was implemented and measured on setuptools 84 and again on 80.9.0 — python -m build writes all six License-File: entries into METADATA and copies two, so the wheel contradicts its own metadata; pip wheel copies all six. Shipping them as package data instead puts them at flashinfer/licenses/, which is not where any license tooling looks.

What is genuinely true, and worth recording: the wheel ships NOTICE, whose text says "See licenses/ for text of these licenses", and the wheel has no licenses/. That pointer has dangled since before this PR. Fixing it properly means picking a layout that both python -m build and pip wheel agree on, which is a licensing-hygiene change on its own merits and does not belong in a build-system swap. Tracking it separately rather than smuggling a metadata inconsistency into this one.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 discover flashinfer-cubin/flashinfer_cubin and flashinfer-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_SUFFIX for python -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 supported SETUPTOOLS_SCM_PRETEND_VERSION value 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>
Copilot AI review requested due to automatic review settings August 22, 2026 15:40
@demandal25

Copy link
Copy Markdown
Collaborator Author

Response to suppressed review comments (review 5000164297)

pyproject.toml:66 — accepted, and it was a real regression from this PR. Fixed in 2ddf71aa. Good catch. [tool.setuptools.packages.find] defaults namespaces to true, so the finder matches plain directories and the flashinfer* glob picked up the sibling project roots. The built wheel contained flashinfer-cubin/build_backend.py, flashinfer-cubin/pyproject.toml, flashinfer-cubin/.gitignore and the flashinfer-jit-cache/ equivalents, and top_level.txt read:

flashinfer
flashinfer-cubin
flashinfer-jit-cache

This is a regression rather than pre-existing behaviour: scikit-build-core's wheel.packages = ["flashinfer"] was an exact name, not a pattern.

The fix is to anchor the patterns — include = ["flashinfer", "flashinfer.*"]. Worth recording that the obvious companion change is wrong: a first attempt also set namespaces = false, and self-review showed that to be redundant and harmful. Redundant because a hyphenated directory cannot match flashinfer.* anyway. Harmful because flashinfer/cute_dsl and flashinfer/tuning_configs are genuine namespace packages with no __init__.py, so disabling namespaces demotes their five modules to incidental data files that survive only through setuptools-scm's git file finder — a build from a git-less export would drop them silently. It also emitted 28 absent from the packages configuration warnings per build against 0 for the anchored include alone.

Measured across all three variants:

flashinfer* + namespaces = false anchored only
flashinfer/ entries 414 414 414
sibling files in wheel 8 0 0
setuptools warnings 0 28 0

top_level.txt is now flashinfer alone, the real package is unchanged (none lost, none gained), and the wheel still carries 223 headers and 60 csrc_rocm files.

For completeness: the sdist is unaffected by package discovery and still ships the sibling projects, csrc/ and other development files. That predates this PR and contradicts the intent stated in MANIFEST.in, so it is worth a follow-up rather than a change here.

pyproject.toml:63 — declined, third time. .github/workflows/nightly-release.yml cannot reach the step that exports FLASHINFER_DEV_RELEASE_SUFFIX: its setup job begins VERSION=$(cat version.txt | tr -d '[:space:]'), version.txt is not in this tree (git ls-files version.txt is empty), and every other job is needs: setup. The variable was equally inert before this PR, because the root build_backend.py was not the configured backend and nothing read it.

The suggested remedy would also not work as written: SETUPTOOLS_SCM_PRETEND_VERSION overrides the version outright rather than appending a .devN suffix to a base read from version.txt, so it does not preserve the workflow's contract. Rewiring the release pipeline onto setuptools-scm is real work with its own test surface and belongs in a separate change.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 passes FLASHINFER_INCLUDE_DIR as -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 adds NOTICE as a project license file. Setuptools will therefore add NOTICE under .dist-info/licenses, so the wheel is not byte-identical to the previous configuration as claimed in the PR description. Either keep only LICENSE or 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>
Copilot AI review requested due to automatic review settings August 22, 2026 16:17
@demandal25

Copy link
Copy Markdown
Collaborator Author

Response to suppressed review comments (review 5000505481)

build_backend.py:12 — accepted, fixed in 73f553a7. Correct, it is -isystem (flashinfer/jit/cpp_ext.py:128 puts FLASHINFER_INCLUDE_DIR in system_includes). Docstring updated.

build_backend.py:150 — accepted, added in 73f553a7. Fair: two regressions landed in this code across two rounds with nothing to catch them. tests/rocm_tests/test_build_backend.py covers 4 prior states x 2 prepare steps, restore-on-exception, all three _HEADER_SUFFIXES entries, the relative-symlink requirement, and the packages.find exclusion. A/B'd by mutation rather than assumed: reverting the restore logic fails 3 of 14, narrowing _HEADER_SUFFIXES fails 1, dropping the finally-clause _clear fails 8. It sits in tests/rocm_tests/ because that is what testpaths covers — at tests/ top level it would have been collected by nothing.

pyproject.toml:14 — you are right and I was wrong. I had claimed the license set was unchanged; that was inferred from bdist_wheel's default globs and never measured. Building the old configuration settles it — scikit-build-core builds the wheel itself and honoured wheel.license-files = ["LICENSE"] exactly, so the old wheel had LICENSE only and this PR does add NOTICE.

Keeping NOTICE, because Apache-2.0 §4(d) requires NOTICE content to travel with redistributions, so shipping it is the more correct behaviour. The PR description's compatibility claim has been corrected rather than the config.

That same old-vs-new diff turned up two more differences worth recording, neither previously noted:

  • flashinfer/profiler/__init__.py was missing from the old wheel despite being a tracked package, so import flashinfer.profiler failed on an install. This PR fixes it.
  • The wheel tag moves from py3-none-linux_x86_64 to py3-none-any. The old platform tag came from scikit-build-core marking the wheel platlib because CMake ran; nothing is compiled, so the pure tag is correct. The Operating System :: POSIX :: Linux classifier still signals the real constraint.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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_sdist hooks or inspect an actual artifact. A regression in the new setuptools package-data/MANIFEST.in integration (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 existing flashinfer/include directory 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 actual build_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—tomllib is unavailable, so pytest.importorskip skips 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())

Comment thread tests/rocm_tests/test_build_backend.py Outdated
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>
Copilot AI review requested due to automatic review settings August 22, 2026 22:59
@demandal25

Copy link
Copy Markdown
Collaborator Author

Response to suppressed review comments (review 5000571264)

tests/rocm_tests/test_build_backend.py:116 — accepted, fixed in 4bfbaa76. Right: tomllib is 3.11+, requires-python allows 3.10, so that assertion was skipping unconditionally on the minimum interpreter. Now falls back to tomli. It is not a declared dependency, so 3.10 without it still skips — but the skip is the exception now, not the rule.

build_backend.py:144 — declined. prepare_metadata_for_build_editable creating the symlink is not a stray side effect: the symlink is the terminal state an editable install wants, it is what CLAUDE.md documents for a worktree, and the operation is idempotent. Deferring it to build_editable would mean a frontend that stops after metadata leaves the checkout with no flashinfer/include — strictly worse than leaving the correct one.

build_backend.py:150 — declined for this PR, and worth doing separately. The point is fair; the unit tests assert checkout postconditions, not artifact contents. But an artifact-inventory test builds a real wheel and sdist plus a wheel from that sdist, which is minutes of runtime and a torch-free lane this repo does not have — too much to bolt onto a build-system swap. That matrix was run manually here (223 headers / 60 csrc_rocm on every path, sdist-to-wheel with git off PATH, and a full diff against a wheel built from the old scikit-build-core config); the gap is that it is not automated.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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/include pointing to an absolute source path; after build_wheel or build_sdist, that absolute link is recreated and get_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 ../include target (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 ship licenses/*.txt; MANIFEST.in only affects the sdist and license-files lists only the two root files. This leaves NOTICE'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 is git_describe_command (as used by amd-flashinfer-jit-cache/pyproject.toml:54), so this backend will not run scripts/git_describe_rocm.py; builds from ROCm tags/commit distances can therefore get different version strings despite the stated unchanged versioning. Rename that option to git_describe_command and add a version assertion for a ROCm tag.
build-backend = "build_backend"

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants