I totally understand this feeling, but (at least from my perspective) there is definitely an expectation and an understanding that the proposal may have to change, and perhaps significantly (if it’s ever accepted). The initial wheel variant proposals were discussed on Discourse (and then at the Packaging Summit at PyCon, though I understand that that’s also not-Discourse), and what’s happened since then is we effectively took those ideas and pulled them into a working, experimental design. We learned a lot by building that prototype that we may not have learned through discussion alone (we ran into unforeseen problems, e.g., around lockfiles; the design changed a lot; etc.). I think the design is much stronger for it, and there’s now proof that it can work for real, complex packages (like PyTorch). But this isn’t meant to be the “conclusion” of the design process, just a continuation.
Is that always true?
If I have an arbitrary wheel that has been downloaded and I’m trying to install in an arbitrary environment, and that wheel has variant metadata, shouldn’t the installer validate, by default, validate that wheel variant metadata matches that environment? And in that case doesn’t that require running the variant plugin code?
Sorry for the two quick replies, but I just wanted to add I guess that’s technically choosing whether to “select” the wheel or not, but it’s really close to being hard to distinguish for most users, I think, between arbitrary code for selection, and arbitrary code for installation. (As evidenced by how I just got myself confused on this point).
That’s a great question! I’m not sure honestly.
I think the specification should speak to this, e.g. “Installers <MUST|SHOULD|SHOULD NOT|MUST NOT> validate that a requested wheel’s variants are compatible with the current machine”. Given the discussion here, I think “SHOULD NOT” is the recommendation? I don’t think “MUST NOT” is appropriate though.
In my comment, I assumed that pip install <filename>.whl
would not validate that the variant is usable, but it is fair to say that tools will probably want to provide some sort of hint if you’re installing a variant that won’t work on your machine.
I think the difference would be asking to install <filename>.whl
vs <package-name>
. Maybe there are more cases I’m not considering though.
@zanie I can talk to the “current version” of what we are writing today.
Yes it’s always true.The variant mechanism is essentially “an add-on to the resolver logic”.
You don’t rerun the resolution if you execute pip install my_package-…-any.whl
(only scan for dependencies).
Well I don’t see (today) any reason for variant to behave in any other way (would still need to resolve dependencies).
If you explicitly request something - we should trust you and assume you know what you do.
——-
We could change that assumption if requested by the community - but I have a feeling the community will agree it should not validate a user explicit request.
As I went on to say after the end of the quoted text, this can apply before resolution. So basically, as soon as torch
enters the set of packages that needs to be resolved (and its first set of metadata is accessed), it’s identified as “needs further resolution”, which then determines that for the current platform it should be torch_xy1234
instead. No interaction with other dependencies is needed - it’s just a straight mapping that is specific to the current machine/configuration. Doesn’t even need any version resolution - if the publisher doesn’t release versions for every variant and the reference included a version (e.g. torch>2.0
) then it may just correctly fail to resolve..
If other dependencies specify a particular variant, then you’ll get file conflicts, same as if you referred to two unrelated packages that write the same files. It can be dealt with in the same way. If other dependencies refer to the generic name, they get the same variant (i.e. cache the result).
Two reasons:
- platform tag is a singular tag, defined by the Python installation, whereas variants are multi-dimensional with the vast majority of packages not requiring any dimension at all
- we can’t easily add another tag to the filenames now and have it be compatible, while we can publish new packages with new names very easily
Sorry, I wasn’t trying to take your quote out of context.
I think the complicated part is universal resolution for lock files; e.g., when solving for all possible platforms how do we know what packages we need?
Wheel variants are also trying to address cases where there are multiple valid packages, e.g., if you’re on x86-64-v4 then foo_x86-64-v4
is not the only valid package. It’s preferred, but if only foo_x86-64-v2
is available then we should take that one. I’m not sure how tractable resolution is if we need to check for a bunch of separate packages — I think that could be addressed by changes to package metadata or the Simple API but then we’re increasing scope again.
Separately, I’m a little terrified of the security posture of this approach? I can squat foo_x86-64-v4
even if I don’t own foo
and someone who does pip install foo
could pull the malicious package?
That doesn’t match existing wheel behavior for most (all?) package installers, your platform and environment are validated:
C:\> pip install .\pandas-2.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
ERROR: pandas-2.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl is not a supported wheel on this platform.
IMO this validation behavior is correct, though happy for a force or override flag, it prevents users from breaking their environments accidentally (without it I would have broken my environment many times back in 2013 when I was first learning Python).
It would also surely be useful (needed?) for users building wheels and/or variant plugins, that they can install them locally and check the validation works?
I don’t think you can do universal resolution in a lock file. You could lock all possibilities and integrate the selection logic (i.e. the arbitrary code) back into the installation logic, or (my preference) the package developer makes them all installable simultaneously (i.e. no conflicting filenames) and chooses at runtime.
In my model, whoever owns foo
chooses the names that it may resolve to. Either through metadata into a generic selector, or their own specific selector. But there isn’t a generic set of variants that get looked up for any package at all - the foo
package itself is published by the publisher, and it contains (metadata/code) the names it may be replaced by.
That’s actually a pretty decent gist of how the variant proposal works.
What makes this particularly hard isn’t selecting the variant for one project, it’s selecting a consistent set of variants across multiple projects that need to match with each other on multiple dimensions.
So NumPy & SciPy need to match on the BLAS library they’re built against. Everything with CUDA/ROCm variants need to match not only each other, but the hardware in the target machine (which may not be the machine where the environment is being put together). For CUDA at least (I’m not sure about ROCm) everything needs to be built against the same CUDA version. CPU instruction sets don’t necessarily impose cross project compatibility issues, but can make a big difference in runtime performance.
Theoretically all those things could be encoded directly into distinct package names, but then you not only end up with horrendously complicated names, you also lose the “one sdist, many binaries” aspect of the proposal that aims to emphasise that the permitted degree of behavioural divergence between wheel variants is lower than what can be expected between wheel builds for different platforms, let alone between different versions of a project or between projects with different names.
Unfortunately, until pip defaults to --only-binary :all:
we can’t guarantee that anyway.
Interesting. I guess you’re right we would not allow to install a MacOS Wheel on Linux. (And I don’t think we can force that if we wanted to for obscure reason). Is it possible to install an unsupported manylinux tag with some flag?
Funny enough - the reason why we really wanted not to run the resolution if you ask for specific has to do with building containers or large scale deployments where the “builder” may not actually have the target hardware.
To address this scenario we thought about both:
- static “frozen resolution” : think about it like”pip freeze” but for variants. It would shortcut all plugins and guarantee a totally static and reproduceable install. No plugin. No code execution. Just run the resolver in the installer.
- Variant pinning (like for version ==A.B.C) but variant style (we are still debating what form we want that syntax to take.)
Talking only about wheels, you can guarantee no arbitrary code execution in order to determine what wheel (or sdist) to fetch, or to install the wheel once it’s fetched. Yes, to build the wheel from sdist is different, but you can restrict when and where that happens with existing tools.
I really don’t see any alternative approach for this besides those projects actively trying to detect the same things, so they can reach the same conclusions independently (or alternatively, if a dependency from one to the other is involved, using a more specific dependency - e.g. scipy_cpu
explicitly depends on numpy_cpu
, rather than generic numpy
). If a user is overriding the selector by choosing them directly, then explicit dependencies will be okay, or they’ll just have to override more things.
In any case, the package developers are best placed to decide which build should be chosen for the set of dimensions that matter, and they must reduce multiple dimensions down to a single selection. The biggest challenge is if we need to factor in “other packages that are going to be installed at the same time”, but that’s going to become circular so quickly that I’m really inclined to just refuse.
Not every single scenario here has to be fully automatic. And especially on the publishing side - manual, predictable and stable usually turns out better in the long run than magical.
Yes, you can get around this — and there are valid use-cases, e.g., constructing an environment that you’re going to use on another platform.
$ uv pip install dist/example-0.1.0-py3-none-win_amd64.whl
Resolved 3 packages in 2ms
error: Failed to determine installation plan
Caused by: A path dependency is incompatible with the current platform: dist/example-0.1.0-py3-none-win_amd64.whl
hint: The wheel is compatible with Windows (`win_amd64`), but you’re on macOS (`macosx_14_0_arm64`)
$ uv pip install dist/example-0.1.0-py3-none-win_amd64.whl --python-platform windows
Resolved 3 packages in 96ms
Prepared 1 package in 2ms
Installed 3 packages in 2ms
* example==0.1.0 (from file:///Users/zb/workspace/uv/example/dist/example-0.1.0-py3-none-win_amd64.whl)
* gunicorn==23.0.0
* packaging==25.0
Yes, that’s exactly what the wheel variant proposal enables.
It just also covers factoring out those variant selection decisions into named plugins so instead of each affected library needing to implement that logic independently they can instead just specify that they use the CUDA and ROCm variant selectors (for example).
Edit to clarify: and since we don’t want to hard code a specific supported set of variant dimensions, the selector plugins get identified via PyPI project names.
Yeah, I’m happy to be proven wrong (doubtful anyone’s going to argue me wrong), but I’m pretty sure this won’t scale. It’s in a theoretically nice middle ground, but in practice I’d expect either “fully built into with the installer tool” or “fully determined by code in the package” to be the ones that work. Potentially there’s some shared libraries involved (e.g. detecting which CUDA version(s) are offered by the system), but I really think we either need to have a completely constrained set of variants or allow completely arbitrary logic.
(I hope it’s obvious that I prefer the second, but I do think the first is viable enough to put us somewhere functional. The middle-ground of “someone will write the standard plugin and tools will just use them” doesn’t really meet any of the maintenance, stability, or security needs of installers-as-in-tools or installers-as-in-people.)
Hence the complexity of the WheelNext project
The folks involved are actively writing the selector plugins needed for some of the thorniest variant selection problems (most notably PyTorch), and conducting a large scale demonstration to show they actually solve the problem they’re supposed to solve (hence this thread).
I personally wouldn’t be shocked if we do end up with a situation where there are a set of “approved” variant selectors that installation tools vendor rather than picking them up dynamically from the environment running the installation tool, but that’s a separate question from being able to distribute the design responsibility for the variant selectors themselves to the folks that understand the relevant hardware characteristics and how to query for them.
That’s more a post-experiment question than a pre-experiment one, though.
that’s actually a really good point - if the community decides this is the safest way to “deal with these plugins” - it’s absolutely doable.
Especially combined with attestations ( Attestations: A new generation of signatures on PyPI -The Trail of Bits Blog ) & trusted publishers, the security around these plugins can be decided by the community that they must meet a higher level of security to be “default ON” (opt-out)
What do you mean by “before resolution”? Do you mean, before we resolve a set of dependencies? (torch
may not be a first-party dependency. You might depend on a package that depends on vllm
which depends on torch
which depends on some nvidia
libraries, all of which have hardware-enabled variants for different GPUs. How do you ensure that these can resolve together, along with all the other dependencies? Resolution is not a purely sequential process. You may even backtrack to a state such that you no longer need vllm
at all after trying a few versions.)
What is the “straight mapping that is specific to the current machine/configuration”? Who is determining the values, and detecting them from the machine? Is every package defining its own range of values? And implementing its own logic for inferring them? Is every installer implementing that logic?
If you’re suggesting that during resolution, if we identify a package that needs this extra detection, then we run some code to detect the appropriate build for the current machine, and then incorporate that build and its dependencies into the rest of the resolution – then I think that’s just wheel variants? I may well be misunderstanding though.
The uv.lock
file in the prototype supports universal resolution with variants. We lock for all variants (like we do for platform markers) and record the necessary providers for the detection. At install time, we run those providers, then resolve the lockfile. It’s similar to markers, except that the value for the marker is provided by (e.g.) an NVIDIA package (or the user, statically). Again, it’s intended as a proof-of-concept to show that it can work.
Wheel variants do not require that this is fully automatic. That’s really a question of user experience, for installers. For example, an installer should allow users to specify variants statically.