Document improvements to build-isolation setups (#15326)

## Summary

There's a lot here (maybe it should go somewhere else?), but this is a
complex topic and I wanted to include a lot of copy-pasteable examples
for common cases.

Closes https://github.com/astral-sh/uv/issues/10694.
Closes https://github.com/astral-sh/uv/issues/13959.
Closes https://github.com/astral-sh/uv/issues/15248.
Closes https://github.com/astral-sh/uv/issues/15316.
This commit is contained in:
Charlie Marsh 2025-08-18 22:15:35 +01:00 committed by GitHub
parent 0c825c5609
commit fa3c20a177
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 277 additions and 91 deletions

View File

@ -199,13 +199,49 @@ To target this environment, you'd export `UV_PROJECT_ENVIRONMENT=/usr/local`.
## Build isolation
By default, uv builds all packages in isolated virtual environments, as per
[PEP 517](https://peps.python.org/pep-0517/). Some packages are incompatible with build isolation,
be it intentionally (e.g., due to the use of heavy build dependencies, mostly commonly PyTorch) or
unintentionally (e.g., due to the use of legacy packaging setups).
By default, uv builds all packages in isolated virtual environments alongside their declared build
dependencies, as per [PEP 517](https://peps.python.org/pep-0517/).
To disable build isolation for a specific dependency, add it to the `no-build-isolation-package`
list in your `pyproject.toml`:
Some packages are incompatible with this approach to build isolation, be it intentionally or
unintentionally.
For example, packages like [`flash-attn`](https://pypi.org/project/flash-attn/) and
[`deepspeed`](https://pypi.org/project/deepspeed/) need to build against the same version of PyTorch
that is installed in the project environment; by building them in an isolated environment, they may
inadvertently build against a different version of PyTorch, leading to runtime errors.
In other cases, packages may accidentally omit necessary dependencies in their declared build
dependency list. For example, [`cchardet`](https://pypi.org/project/cchardet/) requires `cython` to
be installed in the project environment prior to installing `cchardet`, but does not declare it as a
build dependency.
To address these issues, uv supports two separate approaches to modifying the build isolation
behavior:
1. **Augmenting the list of build dependencies**: This allows you to install a package in an
isolated environment, but with additional build dependencies that are not declared by the package
itself via the [`extra-build-dependencies`](../../reference/settings.md#extra-build-dependencies)
setting. For packages like `flash-attn`, you can even enforce that those build dependencies (like
`torch`) match the version of the package that is or will be installed in the project
environment.
1. **Disabling build isolation for specific packages**: This allows you to install a package without
building it in an isolated environment.
When possible, we recommend augmenting the build dependencies rather than disabling build isolation
entirely, as the latter approach requires that the build dependencies are installed in the project
environment _prior_ to installing the package itself, which can lead to more complex installation
steps, the inclusion of extraneous packages in the project environment, and difficulty in
reproducing the project environment in other contexts.
### Augmenting build dependencies
To augment the list of build dependencies for a specific package, add it to the
[`extra-build-dependencies`](../../reference/settings.md#extra-build-dependencies) list in your
`pyproject.toml`.
For example, to build `cchardet` with `cython` as an additional build dependency, include the
following in your `pyproject.toml`:
```toml title="pyproject.toml"
[project]
@ -216,14 +252,14 @@ readme = "README.md"
requires-python = ">=3.12"
dependencies = ["cchardet"]
[tool.uv]
no-build-isolation-package = ["cchardet"]
[tool.uv.extra-build-dependencies]
cchardet = ["cython"]
```
Installing packages without build isolation requires that the package's build dependencies are
installed in the project environment _prior_ to installing the package itself. This can be achieved
by separating out the build dependencies and the packages that require them into distinct extras.
For example:
To ensure that a build dependency matches the version of the package that is or will be installed in
the project environment, set `match-runtime = true` in the `extra-build-dependencies` table. For
example, to build `deepspeed` with `torch` as an additional build dependency, include the following
in your `pyproject.toml`:
```toml title="pyproject.toml"
[project]
@ -232,48 +268,244 @@ version = "0.1.0"
description = "..."
readme = "README.md"
requires-python = ">=3.12"
dependencies = []
dependencies = ["deepspeed", "torch"]
[project.optional-dependencies]
build = ["setuptools", "cython"]
compile = ["cchardet"]
[tool.uv.extra-build-dependencies]
deepspeed = [{ requirement = "torch", match-runtime = true }]
```
This will ensure that `deepspeed` is built with the same version of `torch` that is installed in the
project environment.
Similarly, to build `flash-attn` with `torch` as an additional build dependency, include the
following in your `pyproject.toml`:
```toml title="pyproject.toml"
[project]
name = "project"
version = "0.1.0"
description = "..."
readme = "README.md"
requires-python = ">=3.12"
dependencies = ["flash-attn", "torch"]
[tool.uv.extra-build-dependencies]
flash-attn = [{ requirement = "torch", match-runtime = true }]
[tool.uv.extra-build-variables]
flash-attn = { FLASH_ATTENTION_SKIP_CUDA_BUILD = "TRUE" }
```
!!! note
The `FLASH_ATTENTION_SKIP_CUDA_BUILD` environment variable ensures that `flash-attn` is installed
from a compatible, pre-built wheel, rather than attempting to build it from source, which requires
access to the CUDA development toolkit. If the CUDA toolkit is not available, the environment variable
can be omitted, and `flash-attn` will be installed from a pre-built wheel if one is available for the
current platform, Python version, and PyTorch version.
Similarly, [`deep_gemm`](https://github.com/deepseek-ai/DeepGEMM) follows the same pattern:
```toml title="pyproject.toml"
[project]
name = "project"
version = "0.1.0"
description = "..."
readme = "README.md"
requires-python = ">=3.12"
dependencies = ["deep_gemm", "torch"]
[tool.uv.sources]
deep_gemm = { git = "https://github.com/deepseek-ai/DeepGEMM" }
[tool.uv.extra-build-dependencies]
deep_gemm = [{ requirement = "torch", match-runtime = true }]
```
The use of `extra-build-dependencies` and `extra-build-variables` are tracked in the uv cache, such
that changes to these settings will not trigger a reinstall and rebuild of the affected packages.
For example, in the case of `flash-attn`, upgrading the version of `torch` used in your project
would subsequently trigger a rebuild of `flash-attn` with the new version of `torch`.
#### Dynamic metadata
The use of `match-runtime = true` is only available for packages like `flash-attn` that declare
static metadata. If static metadata is unavailable, uv is required to build the package during the
dependency resolution phase; as such, uv cannot determine the version of the build dependency that
would ultimately be installed in the project environment.
In other words, if `flash-attn` did not declare static metadata, uv would not be able to determine
the version of `torch` that would be installed in the project environment, since it would need to
build `flash-attn` prior to resolving the `torch` version.
As a concrete example, [`axolotl`](https://pypi.org/project/axolotl/) is a popular package that
requires augmented build dependencies, but does not declare static metadata, as the package's
dependencies vary based on the version of `torch` that is installed in the project environment. In
this case, users should instead specify the exact version of `torch` that they intend to use in
their project, and then augment the build dependencies with that version.
For example, to build `axolotl` against `torch==2.6.0`, include the following in your
`pyproject.toml`:
```toml title="pyproject.toml"
[project]
name = "project"
version = "0.1.0"
description = "..."
readme = "README.md"
requires-python = ">=3.12"
dependencies = ["axolotl[deepspeed, flash-attn]", "torch==2.6.0"]
[tool.uv.extra-build-dependencies]
axolotl = ["torch==2.6.0"]
deepspeed = ["torch==2.6.0"]
flash-attn = ["torch==2.6.0"]
```
Similarly, older versions of `flash-attn` did not declare static metadata, and thus would not have
supported `match-runtime = true` out of the box. Unlike `axolotl`, though, `flash-attn` did not vary
its dependencies based on dynamic properties of the build environment. As such, users could instead
provide the `flash-attn` metadata upfront via the
[`dependency-metadata`](../../reference/settings.md#dependency-metadata) setting, thereby forgoing
the need to build the package during the dependency resolution phase. For example, to provide the
`flash-attn` metadata upfront:
```toml title="pyproject.toml"
[[tool.uv.dependency-metadata]]
name = "flash-attn"
version = "2.6.3"
requires-dist = ["torch", "einops"]
```
!!! tip
To determine the package metadata for a package like `flash-attn`, navigate to the appropriate Git repository,
or look it up on [PyPI](https://pypi.org/project/flash-attn) and download the package's source distribution.
The package requirements can typically be found in the `setup.py` or `setup.cfg` file.
(If the package includes a built distribution, you can unzip it to find the `METADATA` file; however, the presence
of a built distribution would negate the need to provide the metadata upfront, since it would already be available
to uv.)
The `version` field in `tool.uv.dependency-metadata` is optional for registry-based
dependencies (when omitted, uv will assume the metadata applies to all versions of the package),
but _required_ for direct URL dependencies (like Git dependencies).
### Disabling build isolation
Installing packages without build isolation requires that the package's build dependencies are
installed in the project environment _prior_ to building the package itself.
For example, historically, to install `cchardet` without build isolation, you would first need to
install the `cython` and `setuptools` packages in the project environment, followed by a separate
invocation to install `cchardet` without build isolation:
```console
$ uv venv
$ uv pip install cython setuptools
$ uv pip install cchardet --no-build-isolation
```
uv simplifies this process by allowing you to specify packages that should not be built in isolation
via the `no-build-isolation-package` setting in your `pyproject.toml` and the
`--no-build-isolation-package` flag in the command line. Further, when a package is marked for
disabling build isolation, uv will perform a two-phase install, first installing any packages that
support build isolation, followed by those that do not. As a result, if a project's build
dependencies are included as project dependencies, uv will automatically install them before
installing the package that requires build isolation to be disabled.
For example, to install `cchardet` without build isolation, include the following in your
`pyproject.toml`:
```toml title="pyproject.toml"
[project]
name = "project"
version = "0.1.0"
description = "..."
readme = "README.md"
requires-python = ">=3.12"
dependencies = ["cchardet", "cython", "setuptools"]
[tool.uv]
no-build-isolation-package = ["cchardet"]
```
Given the above, a user would first sync the `build` dependencies:
When running `uv sync`, uv will first install `cython` and `setuptools` in the project environment,
followed by `cchardet` (without build isolation):
```console
$ uv sync --extra build
+ cython==3.0.11
+ foo==0.1.0 (from file:///Users/crmarsh/workspace/uv/foo)
+ setuptools==73.0.1
```
Followed by the `compile` dependencies:
```console
$ uv sync --extra compile
+ cchardet==2.1.7
- cython==3.0.11
- setuptools==73.0.1
+ cython==3.1.3
+ setuptools==80.9.0
```
Note that `uv sync --extra compile` would, by default, uninstall the `cython` and `setuptools`
packages. To instead retain the build dependencies, include both extras in the second `uv sync`
invocation:
Similarly, to install `flash-attn` without build isolation, include the following in your
`pyproject.toml`:
```toml title="pyproject.toml"
[project]
name = "project"
version = "0.1.0"
description = "..."
readme = "README.md"
requires-python = ">=3.12"
dependencies = ["flash-attn", "torch"]
[tool.uv]
no-build-isolation-package = ["flash-attn"]
```
When running `uv sync`, uv will first install `torch` in the project environment, followed by
`flash-attn` (without build isolation). As `torch` is both a project dependency and a build
dependency, the version of `torch` is guaranteed to be consistent between the build and runtime
environments.
A downside of the above approach is that it requires the build dependencies to be installed in the
project environment, which is appropriate for `flash-attn` (which requires `torch` both at
build-time and runtime), but not for `cchardet` (which only requires `cython` at build-time).
To avoid including build dependencies in the project environment, uv supports a two-step
installation process that allows you to separate the build dependencies from the packages that
require them.
For example, the build dependencies for `cchardet` can be isolated to an optional `build` group, as
in:
```toml title="pyproject.toml"
[project]
name = "project"
version = "0.1.0"
description = "..."
readme = "README.md"
requires-python = ">=3.12"
dependencies = ["cchardet"]
[project.optional-dependencies]
build = ["setuptools", "cython"]
[tool.uv]
no-build-isolation-package = ["cchardet"]
```
Given the above, a user would first sync with the `build` optional group, and then without it to
remove the build dependencies:
```console
$ uv sync --extra build
$ uv sync --extra build --extra compile
+ cchardet==2.1.7
+ cython==3.1.3
+ setuptools==80.9.0
$ uv sync
- cython==3.1.3
- setuptools==80.9.0
```
Some packages, like `cchardet` above, only require build dependencies for the _installation_ phase
of `uv sync`. Others, like `flash-attn`, require their build dependencies to be present even just to
resolve the project's lockfile during the _resolution_ phase.
Some packages, like `cchardet`, only require build dependencies for the _installation_ phase of
`uv sync`. Others require their build dependencies to be present even just to resolve the project's
dependencies during the _resolution_ phase.
In such cases, the build dependencies must be installed prior to running any `uv lock` or `uv sync`
In such cases, the build dependencies can be installed prior to running any `uv lock` or `uv sync`
commands, using the lower lower-level `uv pip` API. For example, given:
```toml title="pyproject.toml"
@ -297,10 +529,10 @@ $ uv pip install torch setuptools
$ uv sync
```
Alternatively, you can provide the `flash-attn` metadata upfront via the
Alternatively, users can instead provide the `flash-attn` metadata upfront via the
[`dependency-metadata`](../../reference/settings.md#dependency-metadata) setting, thereby forgoing
the need to build the package during the dependency resolution phase. For example, to provide the
`flash-attn` metadata upfront, include the following in your `pyproject.toml`:
`flash-attn` metadata upfront:
```toml title="pyproject.toml"
[[tool.uv.dependency-metadata]]
@ -309,54 +541,6 @@ version = "2.6.3"
requires-dist = ["torch", "einops"]
```
!!! tip
To determine the package metadata for a package like `flash-attn`, navigate to the appropriate Git repository,
or look it up on [PyPI](https://pypi.org/project/flash-attn) and download the package's source distribution.
The package requirements can typically be found in the `setup.py` or `setup.cfg` file.
(If the package includes a built distribution, you can unzip it to find the `METADATA` file; however, the presence
of a built distribution would negate the need to provide the metadata upfront, since it would already be available
to uv.)
Once included, you can again use the two-step `uv sync` process to install the build dependencies.
Given the following `pyproject.toml`:
```toml title="pyproject.toml"
[project]
name = "project"
version = "0.1.0"
description = "..."
readme = "README.md"
requires-python = ">=3.12"
dependencies = []
[project.optional-dependencies]
build = ["torch", "setuptools", "packaging"]
compile = ["flash-attn"]
[tool.uv]
no-build-isolation-package = ["flash-attn"]
[[tool.uv.dependency-metadata]]
name = "flash-attn"
version = "2.6.3"
requires-dist = ["torch", "einops"]
```
You could run the following sequence of commands to sync `flash-attn`:
```console
$ uv sync --extra build
$ uv sync --extra build --extra compile
```
!!! note
The `version` field in `tool.uv.dependency-metadata` is optional for registry-based
dependencies (when omitted, uv will assume the metadata applies to all versions of the package),
but _required_ for direct URL dependencies (like Git dependencies).
## Editable mode
By default, the project will be installed in editable mode, such that changes to the source code are

View File

@ -413,12 +413,14 @@ requires-dist = ["numpy>=1.8.1", "scipy>=0.13.0", "six>=1.11.0"]
```
These declarations are intended for cases in which a package does _not_ declare static metadata
upfront, though they are also useful for packages that require disabling build isolation. In such
cases, it may be easier to declare the package metadata upfront, rather than creating a custom build
environment prior to resolving the package.
upfront, though they are also useful for packages that require
[disabling build isolation](./projects/config.md#build-isolation) In such cases, it may be easier to
declare the package metadata upfront, rather than creating a custom build environment prior to
resolving the package.
For example, you can declare the metadata for `flash-attn`, allowing uv to resolve without building
the package from source (which itself requires installing `torch`):
For example, past versions of `flash-attn` did not declare static metadata. By declaring metadata
for `flash-attn` upfront, uv can resolve `flash-attn` without building the package from source
(which itself requires installing `torch`):
```toml
[project]