Repository navigation
[BUG]: CI may build cuda.core against published cuda-bindings instead of the same-commit wheel #2468
Description
codex gpt-5.6-sol
Type of bug
Silent failure
Component
Infrastructure
Describe the bug
The wheel and sdist CI workflows build a cuda-bindings development wheel from the current checkout and expose its directory through PIP_FIND_LINKS when building cuda.core. However, the cuda.core build backend requests only:
cuda-bindings==<CUDA major>.*
PIP_FIND_LINKS adds candidates; it does not prioritize those candidates over the package index or enable prerelease selection. When a stable release satisfies the requirement, pip can therefore select the published stable wheel instead of the same-checkout .dev wheel.
This means cuda.core can be Cythonized against declarations from an older published cuda-bindings package even though CI has just built a newer cuda-bindings wheel from the PR. The resulting green build does not validate that the current cuda.core and cuda-bindings sources are compatible.
This occurred in PR #2395 (and was then discovered while locally testing PR #2393):
- The same-checkout bindings artifact was
13.3.2.dev.... - The
cuda.corebuild requirement wascuda-bindings==13.*. - Stable
cuda-bindings 13.3.1was available from the package index. - The published 13.3.1
cydriver.pxddeclaredCUDA_MEMCPY3D.srcHostasconst void*. - The current in-tree
cydriver.pxddeclared the field asvoid*. - Consequently,
params.srcHost = <const void*>...compiled in CI but failed
when Cython consumed the current in-tree PXD.
The failure from a same-source editable QA build is recorded in cuda-python_qa_bindings_linux_2026-07-31+003242_build_log.txt:
Assigning to 'void *' from 'const void *' discards const qualifier
The standard wheel tests do not expose the problem. They install the current bindings wheel only after the core wheel has already been Cythonized. The const qualification does not change the struct layout or ABI, so runtime testing continues to pass.
The paired pixi source-build workflow could catch this class of incompatibility, but its pull-request path filter does not include ordinary cuda_core/cuda/**/*.pyx changes.
How to reproduce
- Build a
.devcuda-bindingswheel from the current checkout. - Leave the normal package index enabled and expose the wheel only through
PIP_FIND_LINKS. - Build
cuda.core; its backend requestscuda-bindings==13.*. - Observe that the build succeeds against the published stable bindings PXD.
- Editable-install the current bindings source and then build the same core source.
- Observe that Cython instead consumes the in-tree PXD and reports the discarded-const error.
Expected behavior
When CI builds cuda.bindings and cuda.core from the same checkout, the core build must use the exact bindings artifact produced from that checkout. CI should fail if it cannot resolve that artifact, and the selected bindings version and location should be visible in the build log.
Possible fixes
- Make the dynamic core build requirement select the exact same-checkout bindings version or artifact. A direct local requirement or exact constraint is more robust than relying on
PIP_FIND_LINKS; merely enabling prereleases would still leave room for a different index candidate to win later. - Add a build-time assertion that reports and validates the resolved
cuda-bindingsversion and PXD location. - Expand the paired source-build workflow's pull-request path filter to cove ordinary
cuda_coreCython sources.
Activity
rwgk commentedon Aug 5, 2026
This is not a new problem, although the earlier cases were not identical.
In January, #1454 documented a closely related provenance failure: during an isolated build, a local or just-built
cuda-bindingscould be displaced by a published PyPI candidate. That PR included a proposed safeguard incuda_core/build_hooks.py, but the broader change did not land. The review also identified an important weakness in that implementation: passively inspecting whethercuda-bindingswas already installed would rarely work under normal PEP 517 build isolation.The same broader incident also produced #1470, which explored fail-fast validation for invalid development versions. Both #1454 and #1470 were closed without merging. A narrower wheel-install CI correction was extracted into #1493 and did merge. Those changes addressed parts of the incident, but the more general dependency-provenance risk remained. I think this history is useful to consider because it shows both that accidental PyPI substitution is a recurring risk and that the solution must operate correctly under build isolation.
We now have two PRs addressing the immediate problem in this issue:
cuda-pathfinderandcuda-bindingswheels. It keeps artifact-selection policy in CI and integrates it with the selective-build work.cuda-bindingsbuild requirement point directly at a selected local wheel, applies that protection to wheel, sdist, and coverage builds, and reports the imported bindings version and location.Both approaches improve materially on
PIP_FIND_LINKSandPIP_PRE, neither of which identifies the exact artifact that CI intends to validate. #2464 has the cleaner CI-owned selection mechanism and also coverscuda-pathfinder, but its current exact-artifact protection is confined to the wheel workflow. #2470 has more complete workflow coverage and diagnostics, but puts CI-specific artifact-selection behavior into the distributedcuda_corebuild backend and addresses onlycuda-bindings.Rather than choosing one PR wholesale, I think we should take the historical incident, this issue, and the complementary strengths of #2464 and #2470 into account in a focused alternative. The goal would be to make accidental PyPI substitution less likely more generally.
The immediate failure in #2468 concerns
cuda.corecompiling against the wrongcuda-bindings, but the same resolver pattern can affect any CI stage that builds one same-checkout CUDA Python package against another while a public index remains available. We should therefore minimize accidental PyPI substitution more generally, within a clear boundary.Core invariant
What to combine
From #2464, I propose retaining:
cuda-pathfinderandcuda-bindings;From #2470, I propose retaining:
cuda-bindingsversion and location.I propose using exact CI-supplied constraints as the single artifact-selection mechanism. The normal
cuda.corebuild-backend requirement would remain unchanged, while the CI caller would constrain that requirement to an exact local wheel. The build hook would retain the provenance diagnostic, but would not need a CI-specific wheel-directory selection contract.PIP_FIND_LINKSmay remain useful for discovery in other contexts, but it should not be considered sufficient protection against substitution.PIP_PRE=1is also insufficient because it only makes prereleases eligible; it does not select the intended artifact.Proposed implementation shape
Add one small, cross-platform, tested helper that generates constraint files from selected local wheel artifacts.
The helper should:
cuda-bindings @ file:///.../cuda_bindings-....whl;Generate constraints for each dependency edge rather than one global file. For example:
cuda-bindings: constraincuda-pathfinder;cuda-core: constrain currentcuda-pathfinderand currentcuda-bindings;cuda-core: constrain the selected pathfinder and previous-major bindings artifacts.Supply both
PIP_BUILD_CONSTRAINTandPIP_CONSTRAINTto the relevant build and install steps. The protected workflows should explicitly provide pip 25.3 or newer, where dedicated build constraints are supported.Apply the mechanism to every same-checkout build path, including:
Workflows whose purpose is to verify packages from PyPI or TestPyPI should remain explicitly outside this policy.
Preserve the
cuda_core/build_hooks.pydiagnostic aftercuda.bindingsis imported:This records what Cython actually consumed, complementing the helper's record of the wheel selected for installation.
Test the helper's successful and failure paths, including missing, ambiguous, wrong-distribution, wrong-major, and spaces-in-path cases. The real Linux and Windows wheel, sdist, and coverage jobs should then provide integration coverage that pip honors the generated constraints.
Keep public indexes enabled. Globally setting
PIP_NO_INDEXwould be unnecessarily broad because these environments still need third-party packages and some workflows intentionally verify released CUDA Python packages. Exact constraints should protect only the internal artifacts selected for same-checkout testing.Proposed PR strategy
I propose preparing a new draft PR from current
main, focused specifically on exact same-checkout dependency provenance.__version__validation: import-time assertions, unit tests, PyPI fallback prevention, CI hardening #1454, Add version validation to prevent silent setuptools-scm fallback versions #1470, ci: add selective wheel build plumbing #2464, ci: build cuda.core against the exact same-checkout cuda-bindings wheel #2470, and this issue.rwgk commentedon Aug 6, 2026
Analysis of PR 2509 provenance output in main CI before PR 2510
Conclusion
The new diagnostics from PR 2509 make the accidental package-index substitution unambiguous.
In the successful main CI run 31071375906, the previous-CTK
cuda.corebuild had a localcuda-bindings 12.9.8.dev9+ga4d338b9awheel available, but 18 matrix jobs reported that the build actually imported releasedcuda-bindings 12.9.7. The configured index is NVIDIA's PyPI proxy/cache, so this is more precisely an index-sourced released wheel rather than the provided local CI artifact; saying "from PyPI" is reasonable shorthand.The run also shows the same problem even more consistently for
cuda-pathfinder: every provenance print reports released1.6.0, although the run built1.6.1.dev44+g979b65fb2from the checkout.This is a strong before-state validation for PR 2510. It does not, of course, exercise PR 2510 itself.
Run inspected
CI, triggered by a push tomain#2512, merged after PR 2509)I inspected all 24 wheel-build jobs: eight interpreter variants (
3.10,3.11,3.12,3.13,3.14,3.14t,3.15, and3.15t) on each oflinux-64,linux-aarch64, andwin-64.Direct
cuda-bindingsevidenceEvery wheel-build job had the previous-branch artifact
cuda-bindings 12.9.8.dev9+ga4d338b9aavailable locally before buildingcuda.coreagainst the previous CTK. The build requirement wascuda-bindings==12.*, and the local artifact directory was supplied throughPIP_FIND_LINKS.The result split by interpreter compatibility:
cuda-bindings 12.9.7cuda-bindings 12.9.8.dev9+ga4d338b9aThe Linux x86-64 Python 3.10 job provides a compact example:
See Build linux-64, CUDA 13.3.0 / py3.10.
Windows shows the same result:
See Build win-64, CUDA 13.3.0 / py3.10. The Linux aarch64 Python 3.10 job reports the same
12.9.7result in job 92527767778.The Python 3.15 rows are a useful control. They report the exact local dev version, for example:
See Build linux-64, CUDA 13.3.0 / py3.15. This is consistent with no compatible
12.9.7wheel being available for Python 3.15, leaving the local dev wheel as the usable candidate. The logs establish the selection, not that resolver rationale. This contrast is especially helpful: CI provenance currently depends on what compatible releases happen to exist on the package index.The temporary
build-envpath alone would not prove the source, because a local wheel would also be installed into that isolated environment. The decisive evidence is the version mismatch:12.9.7was imported even though the local artifact was12.9.8.dev9+ga4d338b9a.Why the pre-2510 behavior occurs
PIP_FIND_LINKSadds a directory to pip's candidate sources; it does not make the directory exclusive or give its candidates provenance priority over the configured index. The checkout and previous-branch wheels use development versions, while stable released candidates are present on the index. With requirements such ascuda-bindings==12.*, pip can therefore choose the released candidate instead of the local dev wheel.PIP_PRE=1would make prereleases eligible, but it would still not encode the desired provenance invariant. It would also broaden prerelease selection for dependencies generally.PR 2510 instead:
cuda-bindings @ file:///.../cuda_bindings-...whl;PIP_BUILD_CONSTRAINTandPIP_CONSTRAINTso isolated PEP 517 build dependencies are covered;cuda-pathfinderwhen buildingcuda-bindings, and to both internal wheels when building current- and previous-CTKcuda.core, including the sdist and coverage workflows;Diagnostic caveats
There are two limitations in what this run displays:
Using cuda-bindings 13.xline. They requestcuda-bindings==13.*and build successfully, but PR 2509's bindings print is inside a best-effort import guarded byexcept ImportError. The logs do not establish why that import did not produce a line, and they do not directly identify the selected 13.x version. The previous-CTK12.xresult and the pathfinder results are nevertheless conclusive evidence of substitution.PIP_FIND_LINKS, so PR 2510 appropriately covers them, but the counts above (96 messages total) refer only to the 24 wheel-build jobs.The missing 13.x print is worth remembering when reading future logs: the absence of a provenance line does not establish that the local wheel was selected. PR 2510 improves observability here as well because its constraint-generation step prints the exact wheel selected before the build, independently of whether
import cuda.bindingssucceeds in the backend hook.Recommendation
The run provides exactly the before-state evidence that PR 2510 is intended to address. I see no reason in these logs to delay the already-approved PR 2510.
After PR 2510 is merged, I would verify one main run for:
Selected cuda-pathfinder==1.6.1.dev... from ... as file://...in the constraint-generation steps;Selected cuda-bindings==13.3.2.dev...for the current-CTK build;Selected cuda-bindings==12.9.8.dev...for the previous-CTK build;That follow-up would confirm the after-state; this run already documents the problem very clearly.
cuda-pathfinderbaseline and broader PR 2510 policyThe pre-2510 production-wheel result is uniform:
cuda-pathfinder 1.6.1.dev44+g979b65fb2from the checked-out main commit.cuda-bindingsbuild, the current-CTKcuda.corebuild, and the previous-CTKcuda.corebuild.cuda-pathfinder 1.6.0from a temporary PEP 517 build environment.cuda_pathfinder-1.6.0wheel from the configured package index was selected.For example, the Linux x86-64 Python 3.10 job on main first builds
1.6.1.dev44+g979b65fb2, but the three dependent builds report:These messages are useful provenance, but this was not another instance of the issue 2468 failure under the pre-2510 workflow contract. In these production-wheel jobs, the workflow did not make the just-built pathfinder wheel available to the isolated
cuda-bindingsandcuda.corebuilds, so resolvingcuda-pathfinder>=1.5from the configured index was expected. That differs from the problematiccuda-bindingsedge: a local bindings wheel directory was supplied, yet pip was still free to choose a released wheel from PyPI.Build isolation explains why the imported package lives under
/tmp/build-env-*; it does not require that the package originate on PyPI. A build frontend can install either an index candidate or an exact local-wheel reference into the isolated environment.PR 2510 intentionally broadens the same-checkout policy to include pathfinder. In the representative PR 2510 Linux x86-64 Python 3.10 job, each of the three protected dependency edges selects
cuda-pathfinder 1.6.1.dev45+gf39b4c008through an exactfile:///host/...wheel constraint. Each corresponding build-hook message then reports that same version from its isolated build environment. The same constraint mechanism selects the appropriate exact localcuda-bindingswheel for the current- and previous-CTKcuda.corebuilds.The before-state therefore documents an expected index-based pathfinder baseline, while the PR 2510 job demonstrates a deliberate policy change: dependencies produced by the same checkout are selected exactly and cannot be silently replaced by similarly named packages from an index. Build isolation is preserved; dependency provenance becomes deterministic. If compatibility with a released
cuda-pathfinderis important, it should be exercised by an explicit test configuration rather than as an incidental resolver outcome in the same-checkout build.rwgk commentedon Aug 6, 2026
Before/after CI evidence for exact local-wheel constraints
This is a follow-up to the earlier pre-2510 log analysis. I compared two adjacent points in the current stack:
This gives us a useful controlled comparison: #2520 shows what the isolated build frontend selected under the old policy, and #2510 shows what changes when each internal build dependency is constrained to one exact wheel file.
Scope
I counted isolated build-dependency consumers, not the downstream jobs that test already-built artifacts:
linux-64,linux-aarch64, andwin-64) x 8 Python variants (3.10, 3.11, 3.12, 3.13, 3.14, 3.14t, 3.15, and 3.15t).cuda.bindings, current-CTKcuda.core, and previous-CTKcuda.core: 72 isolated consumer builds and 120 internal dependency resolutions in total.cuda.bindingsandcuda.corefrom their sdists: 4 additional consumer builds and 6 internal dependency resolutions.Results at a glance
cuda.bindingscuda-pathfinder 1.6.0, not the wheel built in that jobcuda.core1.6.0. Bindings: 18/24 released13.3.1; 6/24 local dev wheelcuda.core1.6.0. Bindings: 18/24 released12.9.7; 6/24 intended previous-branch dev wheelcuda.bindingspip wheeloutputcuda.corepip wheeloutputFor the 24-job wheel matrix, this can be summarized as follows:
Selected ...linesUsing cuda-pathfinder ...linesUsing cuda-bindings ...linesThe 12 incidental matches in the before run are six current-core and six previous-core bindings resolutions for Python 3.15 and 3.15t. They matched because there was no compatible released wheel available, not because the workflow required the local artifact.
Before: compatible candidates could still win
PR #2520 built local
cuda-pathfinder 1.6.1.dev45+g9ef67b610and currentcuda-bindings 13.3.2.dev163+g9ef67b610. It also downloaded previous-branchcuda-bindings 12.9.8.dev9+ga4d338b9afor the previous-CTK core build.Across all 24 wheel jobs, the backend provenance was completely consistent:
1.6.0, covering the bindings build plus both core builds in every job.cuda-bindings 13.3.1, and 18/24 previous-core builds used released12.9.7.The pattern was identical on Linux x86-64, Linux aarch64, and Windows. Representative examples are Linux x86-64 / Python 3.10, Linux aarch64 / Python 3.10, Windows / Python 3.10, and Linux x86-64 / Python 3.15.
The imported packages correctly appear under temporary PEP 517 build-environment
site-packagesdirectories. The evidence of substitution is the version mismatch against the wheel produced or downloaded by that job, not the temporary import path itself.This confirms the underlying problem:
PIP_FIND_LINKSmade the local wheel available as another candidate, but did not require it. A compatible released distribution could silently win.PIP_PRE=1would broaden prerelease eligibility, but would still not express the required artifact identity.After: exact selection and backend import agree
PR #2510 built local
cuda-pathfinder 1.6.1.dev47+g5fcc5fd8dand currentcuda-bindings 13.3.2.dev165+g5fcc5fd8d, and downloaded previous-branchcuda-bindings 12.9.8.dev9+ga4d338b9ainto a separate directory.All 24 production jobs succeeded. Each job emitted exactly five constraint-generator selection lines and five matching backend provenance lines:
That is 120 exact artifact selections and 120 matching backend imports, with no version or ordering anomalies across the full platform/Python matrix. For example, the Linux x86-64 / Python 3.10 job reports direct-file selections for all five dependencies, followed by
Using ...lines with the same versions from the isolated build environments. Linux container paths are mapped tofile:///host/...; Windows usesfile:///D:/a/...paths.The two evidence channels are separate and complementary:
write_wheel_constraints.pyreports which exact local artifact it wrote into the constraint file.Agreement between them across all 120 wheel-matrix dependency resolutions is the strongest runtime evidence that the policy is working as intended.
sdist builds
Both before-run sdist jobs succeeded (Linux, Windows), but their non-verbose
pip wheelcommands suppressed successful backend stderr. Their zeroUsing ...lines are therefore an observability limitation, not evidence for or against substitution.After #2510, the corresponding Linux and Windows jobs each emitted three constraint-generator selection lines: exact local pathfinder for the bindings build, followed by exact local pathfinder and bindings for the core build. All 6/6 selections became direct-reference constraint entries, and both jobs completed successfully under those constraints. Backend
Using ...lines remain suppressed at the current pip verbosity, but exact selection is now explicit before each build starts.Coverage qualification
Neither PR CI run executes
.github/workflows/coverage.yml; that workflow runs on a schedule or by manual dispatch. The latest relevant pre-fix coverage run 31060413129 independently demonstrated the same risk: Linux selected released pathfinder/bindings packages, and Windows downloaded publiccuda-bindings 13.4.0b1despite a local13.3.2.dev160+g418595b78wheel being available. The subsequent Windows core compile failure is consistent with that unintended version mismatch, although the log alone does not prove causality.PR #2510 updates all four relevant coverage consumers (Linux bindings/core and Windows bindings/core) to use the same exact-wheel mechanism. There is no coverage run at the PR head, however, so runtime confirmation for this separate workflow remains outstanding. I have not included coverage in the 120 production-wheel or 6 sdist dependency counts.
CI status
build smoke (cu13, linux-64, CPU)failure is not part of this analysis.actions/setup-pythonlooked for~3.15.0-0in the runner's local tool cache. The same matrix cell passed in earlier runs, so I reran only that job; the rerun completed successfully.At the final check, the only failed check reported for either PR was its respective instance of that excluded source-build smoke failure.
Conclusion
The before run shows that successful builds can silently substitute compatible released packages for artifacts built in the same workflow. The after run changes the policy from "consider these wheels" to "use these exact files," and the production-wheel logs cross-check each selected file against what the isolated backend actually imported.
For every wheel and sdist consumer exercised here, the enforced policy now satisfies the core invariant: an internal build dependency resolves to the exact wheel produced or downloaded for that workflow, not merely to any compatible distribution available from an index.
The one remaining runtime-validation item is the independently scheduled coverage workflow.