🗂️

Pythonのプロジェクト管理ツール uv のv0.5.3までの便利な機能 - dependencies編

2024/12/09に公開

Python Advent Calendar 2024の7日目です。

この記事ではuvについて、v0.5.3までのアップデートで個人的に便利だった機能を依存関係に焦点を当ててまとめました。

uvは高速なPythonパッケージとプロジェクト管理ツールです。2024年8月20日にuvのバージョンがv0.3.0にアップデートされて以来、広く使われるようになりました。

以前に以下の記事をまとめています。これからuvを使いたい方などに参考になれば嬉しいです!また、公式のドキュメントがしっかりと整備されているのでhttps://docs.astral.sh/uv/getting-started/から使ってみることをオススメします。

https://zenn.dev/turing_motors/articles/594fbef42a36ee

現在のuvのバージョンがv0.5.3より前のバージョンであれば、以下のコマンドでuvをアップデートしましょう。

uv self update

1. PyTorchを例にindex-urlを指定するパッケージのインストール

Using uv with PyTorch - uvとして、uvでのPyTorchのインストール方法が公式ドキュメントがまとめられています。

この中で、そもそもPyTorchのインストールが特殊な理由として以下の2つが挙げています。

  1. 多くのPythonパッケージは、PyPI (Python Package Index)でホストされています。一方で、PyTorchの場合は専用のindexでホストされています(https://download.pytorch.org/whl/cpu)。PyTorchのインストールの際には、このindexを指定するようにする必要があります。
  2. PyTorchはアクセラレータごとに異なるwheelがビルドされて、そのwheelはローカルバージョンで指定されています(e.g., 2.5.1+cpu, 2.5.1+cu121)。各wheelは異なるindexで公開されて、異なるアクセラレータ環境ごとにindexを指定する必要があります(e.g., https://download.pytorch.org/whl/cu121)。

そのため通常のuv add torchでは、PyPIでホストされているPyTorchのパッケージがインストールされます。ここで、

In this case, PyTorch would be installed from PyPI, which hosts CPU-only wheels for Windows and macOS, and GPU-accelerated wheels on Linux (targeting CUDA 12.4)

とあることからtorch=2.5.1では、PyPI上ではWindowsやmacOSではCPU、Linux環境ではCUDA 12.4でビルドされたwheelが公開されているらしいです。それ以外の組み合わせで行う場合を紹介されています。

1.1 tool.uv.indexを用いたPyPI以外のindexの指定

uvのv0.4.23から新しいindex指定が導入されて、pipスタイルの--index-url--extra-index-urlの設定オプションに相当する機能です。

最初にtool.uv.indexを用いて名前付きのindexを定義します。今回はCUDA 12.4のPyTorchのindexを指定します。このURLはPyTorchの場合はPrevious PyTorch Versionsを参照するか、uvのドキュメントを参照してください (Using a PyTorch index - uv)。なお、以下のnameは任意の文字列です。

[[tool.uv.index]]
name = "pytorch-cu124"
url = "https://download.pytorch.org/whl/cu124"
explicit = true

tool.uv.sourcesで、パッケージごとにどのindexからインストールするかを指定します。今回は上記で指定した名前付きindexを指定します。

[tool.uv.sources]
torch = { index = "pytorch-cu124" }
torchvision = { index = "pytorch-cu124" }

これによりtorchとtorchvisionは、https://download.pytorch.org/whl/cu124でホストされているパッケージからインストールされます。後はuv add torch torchvisionを実行することでパッケージをdependenciesに追加して、インストールすることが可能です。この操作はpipでの以下の操作と同じです。

pip install torch torchvision --index-url https://download.pytorch.org/whl/cu124

1.2 explicit = true[tool.uv.sources]に指定されていないパッケージはデフォルトのindex(PyPI)を使用する

tool.uv.indexにはexplicitという設定があります。これをtrueに設定することにより、[tool.uv.sources]で指定されていないパッケージはデフォルトのPyPIでホストされているパッケージをインストールします。PyTorchの場合はexplicit = trueが推奨されています。

そもそもなぜこの設定が必要なのかは、指定したindex urlにインストールしたいパッケージ以外もホストされている場合があり、余計な依存関係の競合が発生するおそれがあるためです。

explicit = trueでなければ、tool.uv.sourcesに指定されていないパッケージもこのindex URLからインストールします。PyTorchのindex URLにはnumpyや、jinja2やnetworkxなどもホストされています。ただ、それらのパッケージは一部バージョンしかホストされていません。例えばnumpyであれば2.0.0以上のバージョンはホストされていないため、他のパッケージの要求と一致しない場合があります。そのためPyTorch関連パッケージ以外はPyPIを参照するよう、explicit = trueに設定することが望ましいです。

1.3 Environment markersでOSごとにindexの指定を変更する

tool.uv.sourcesにはmarkerとしてenvironment markersを使用することができます。この機能は、特にmacOSでCPU版のtorchを指定する際に便利です。またPyTorch以外のパッケージにももちろん使えるため、クロスプラットフォームでの開発時に便利な機能です。

Environment markersについては後述するとして、先にConfiguring accelerators with environment markers - uvのpyproject.tomlを見てみましょう。

[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12.0"
dependencies = [
  "torch>=2.5.1",
  "torchvision>=0.20.1",
]

[tool.uv.sources]
torch = [
  { index = "pytorch-cpu", marker = "platform_system == 'Windows'" },
  { index = "pytorch-cu124", marker = "platform_system == 'Linux'" },
]
torchvision = [
  { index = "pytorch-cpu", marker = "platform_system == 'Windows'" },
  { index = "pytorch-cu124", marker = "platform_system == 'Linux'" },
]

[[tool.uv.index]]
name = "pytorch-cpu"
url = "https://download.pytorch.org/whl/cpu"
explicit = true

[[tool.uv.index]]
name = "pytorch-cu124"
url = "https://download.pytorch.org/whl/cu124"
explicit = true

これはOSがWindowsであればpytorch-cpuで指定されているindex URLを使用し、Linuxであればpytorch-cu124で指定されているindex URLを使用するようになります。uv syncのコマンドだけで柔軟にtorchのindexを変更してくれます。

では、enviromental makerについてです。そもそもuvはPEP 508に基づいたDependency specifiersの記述が可能で、その記述の1つにenviromental makerがあります。これにより、OSやCPUアーキテクチャなどシステムの環境に合わせて依存関係を記述することが可能になります。

markerとして、sys_platform (e.g., Linuxなら'linux', macOSなら'darwin')やplatform_machine (e.g., 'x86_64', 'aarch64', 'amd64')、前述したplatform_system (e.g., 'Linux', 'Darwin', 'Windows')などが使用できます。他のmarkerなど詳細については、Python Packaging User Guideの環境マーカを参照してください。

このmarkerはandor()で組み合わせた条件式で記述することができます。これにより、OSはLinuxかつCPUアーキテクチャはx86_64のときはこのindexでインストールする、といったことが可能になります。

一例として以下の記事では、macOSまたはaarch64のLinuxではCPU版の、x86_64のLinuxではCUDA版のPyTorchを切り替えるを以下のように記述されています。

[tool.uv.sources]
torch = [
    { index = "torch-cuda", marker = "sys_platform == 'linux' and platform_machine == 'x86_64'"},
    { index = "torch-cpu", marker = "sys_platform == 'darwin' or (sys_platform == 'linux' and platform_machine == 'aarch64')"},
]

https://zenn.dev/mjun0812/articles/b32f870bb3cdbf

2. optional-dependenciesを使う

Optional dependencies - uv

特定のパッケージはextras_requireとしてデフォルトの依存関係を減らし、明示的にインストールするパッケージを要求します。例えばpandasであれば、Excel parserが必要ならpip install pandas[excel]、Excel parserとplot機能が必要であればpip install pandas[plot, excel]のように明示的に追加します(pyproject.toml)。

uvではpyproject.toml内で[project.optional-dependencies]に依存関係を記述します。

コマンドはuv add時に--optional <extra名>を指定することです。

uv add "numpy<2.0.0" --optional numpy-1

すると[project.optional-dependencies]numpy-1として追加されます。

[project]
name = "project"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12.0"
dependencies = []

[project.optional-dependencies]
numpy-1 = [
    "numpy<2.0.0",
]

このextrasに記述された依存パッケージをインストールするにはuv sync --all-extrasで実行可能で、特定のextrasだけインストールするにはuv sync --extra numpy-1のように指定することが可能です。

2.1 競合するoptional-dependenciesはconflictsを宣言する

Conflicting dependencies - uv

uvではoptional-dependenciesは相互に互換性があることが要求されます。そのため以下のように互換性のないパッケージのバージョンを指定するとエラーが発生します。

[project]
name = "project"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12.0"
dependencies = []

[project.optional-dependencies]
numpy-1 = [
    "numpy<2.0.0",
]
numpy-2 = [
    "numpy>=2.0.0",
]
$ uv sync
  × No solution found when resolving dependencies:
  ╰─▶ Because project[numpy-2] depends on numpy>=2.0.0 and project[numpy-1] depends on numpy<2.0.0, we can conclude that project[numpy-1] and project[numpy-2]
      are incompatible.
      And because your project requires project[numpy-1] and project[numpy-2], we can conclude that your projects's requirements are unsatisfiable.

ここでuvのv0.5.3からtool.uv.conflicts追加されました。conflictsを用いることで競合しているextrasを明示的に記載でき、このエラーを回避することができます。

[project]
name = "project"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12.0"
dependencies = []

[project.optional-dependencies]
numpy-1 = [
    "numpy<2.0.0",
]
numpy-2 = [
    "numpy>=2.0.0",
]

[tool.uv]
conflicts = [
    [
      { extra = "numpy-1" },
      { extra = "numpy-2" },
    ],
]

もちろんこの2つの依存関係を同時にインストールすることはできません。

$ uv sync --all-extras
  Resolved 3 packages in 4ms
  error: extra `numpy-1`, extra `numpy-2` are incompatible with the declared conflicts: {`project[numpy-1]`, `project[numpy-2]`}

2.2 uv sync --extraでインストールするPyTorchを切り替える

Configuring accelerators with optional dependencies - uv

tool.uv.indexやoptional-dependenciesを用いることで、uv sync --extra cpuuv sync --extra cu124で切り替えられます。

以下のようにpyproject.tomlを設定することで可能です。tool.uv.sourcesにおいて、extraとしてoptional-dependenciesを指定することが可能です。

[project]
name = "project"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12.0"
dependencies = []

[project.optional-dependencies]
cpu = [
  "torch>=2.5.1",
]
cu124 = [
  "torch>=2.5.1",
]

[tool.uv]
conflicts = [
  [
    { extra = "cpu" },
    { extra = "cu124" },
  ],
]

[tool.uv.sources]
torch = [
  { index = "pytorch-cpu", extra = "cpu" },
  { index = "pytorch-cu124", extra = "cu124" },
]

[[tool.uv.index]]
name = "pytorch-cpu"
url = "https://download.pytorch.org/whl/cpu"
explicit = true

[[tool.uv.index]]
name = "pytorch-cu124"
url = "https://download.pytorch.org/whl/cu124"
explicit = true

3. 複数のdev-dependenciesを使い分ける

uvのv0.4.27からDependency groupsという機能が追加されました。これまでは単一だったdevelopment dependencies(開発用の依存関係; dev-dependencies)が、複数グループで設定できる機能です。

そもそもoptional-dependenciesではなく、dev-dependenciesはどのようなときに便利でしょうか。理由の1つとして、パッケージをビルドして公開するときにlinterやtest用のパッケージなどの余計な依存関係を含めずに済むことが挙げられます。

3.1 uv add --group[dependency-groups]に追加する

コマンドはuv add時に--gourp <グループ名>を指定することです。

uv add ruff --group lint
uv add pytest --group test

するとdependency-groupsにグループごとにパッケージが追加されます。

[project]
name = "project"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12.0"
dependencies = []

[dependency-groups]
lint = [
    "ruff>=0.8.2",
]
test = [
    "pytest>=8.3.4",
]

また、以前までのバージョンと同様にuv add --devも可能です。この場合はdevというグループに追加されます。

このグループに記述された依存パッケージをインストールするにはuv sync --all-groupsで実行可能であり、特定のグループだけインストールするにはuv sync --group lintのように指定することが可能です。

また、default-groupsとしてuv syncに含めることができます。以下の記述でuv syncをするとdevlintのグループ内の依存パッケージも加えてインストールします。

[tool.uv]
default-groups = ["dev", "lint"]

4.--no-build-isolationが必要なパッケージのインストール

Build isolation - uv

近年の多くのパッケージではPEP 517に従い、隔離された仮想環境でビルドします。一方で、flash-attentionなど一部パッケージでは意図的に現在の仮想環境内でビルドすることを要求する場合があります。

pipであればビルドに必要なパッケージ(e.g., setuptools, packaging)を先に仮想環境に追加した後に、pip install--no-build-isolationのオプションを追加することで実行できます。

uvの場合はどうすればいいか、実際にflash-attentionをインストールしてみましょう。

4.1 flash-attentionインストール

まず最初にPyTorchをtool.uv.indexを指定して追加します。

[project]
name = "project"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12.0"
dependencies = [
    "torch>=2.5.1"
]

[tool.uv.sources]
torch = [
  { index = "pytorch-cu124" }
]

[[tool.uv.index]]
name = "pytorch-cu124"
url = "https://download.pytorch.org/whl/cu124"
explicit = true

次にビルドに必要なパッケージをインストールします。flash-attnのInstallation and featuresには、packaging, ninja, psutilが必要なのでoptional-dependenciesに追加します。

uv add packaging ninja psutil --optional build

flash-attnを追加する前に便利な設定としてtool.uv.no-build-isolation-packagetool.uv.dependency-metadataを追記します。なお、この設定は行わなくてもインストールできます。

Build isolation - uv

uvの場合はuv adduv sync--no-build-isolationのオプションを追加することができて、現在の仮想環境内でビルドするようにできます。ただ毎回このオプションを追加するのが面倒であり、特定のパッケージを選択的に付けたいためtool.uv.no-build-isolation-packageを使います。以下のようにpyproject.tomlに記載します。

[tool.uv]
no-build-isolation-package = ["flash-attn"]

Dependency metadata - uv

依存関係を解決する際に各パッケージのMETADATA(メタデータ)を用います。このメタデータはindexに静的ファイルとして用意されていることが多いですが、source distributionsのみ配布しているパッケージの場合はメタデータが存在しないことがあります。その場合はパッケージをビルドする必要があり、依存関係解決に時間が掛かる、正常にビルドできない、など問題が発生する可能性があります。

uvではtool.uv.dependency-metadataとしてメタデータを明示的に記述することができます。以下のようにpyproject.tomlに記載します。

[[tool.uv.dependency-metadata]]
name = "flash-attn"
version = "2.6.3"
requires-dist = ["torch", "einops"]

nameはパッケージ名、versionはそのパッケージのバージョン、requires-distはパッケージの使用時に必要な依存パッケージを指定します。なおsetup.pyであればinstall_requiresに相当し、uvなどのpyproject.tomlであればdependenciesに相当するパッケージです。

dependency-metadataの宣言はメタデータが存在しない場合を対象としていますが、no-build-isolationが必要なパッケージに役にたつケースがあります。flash-attnはこの例の一つです。

いよいよflash-attnのインストールです。以下のコマンドを実行しましょう。バージョンは2.6.3に固定していますが、任意のバージョンで選択してください。

uv add flash-attn==2.6.3 --optional compile

インストールまで時間が掛かると思いますが、無事インストールできたら完了です。pyproject.tomlは以下のようになります。

[project]
name = "project"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12.0"
dependencies = [
    "torch>=2.5.1",
]

[project.optional-dependencies]
build = [
    "ninja>=1.11.1.2",
    "packaging>=24.2",
    "psutil>=6.1.0",
]
compile = [
    "flash-attn==2.6.3",
]

[[tool.uv.index]]
name = "pytorch-cu124"
url = "https://download.pytorch.org/whl/cu124"
explicit = true

[tool.uv.sources]
torch = { index = "pytorch-cu124" }

[tool.uv]
no-build-isolation-package = ["flash-attn"]

[[tool.uv.dependency-metadata]]
name = "flash-attn"
version = "2.6.3"
requires-dist = ["torch", "einops"]

また、先にこのpyproject.tomlがある場合は2段階のuv syncを実行します。

uv sync --extra build
uv sync --extra build --extra compile

以上で無事にflash-attnがインストールできるようになりました。

5. おわりに

この記事ではuvのv0.5.3までの便利な機能を、特にdependenciesに関係するところを紹介しました。uvはそれ以外にも自作パッケージビルドや、workspacesなど便利な機能があるので別の記事で紹介したいと思います。不明点などあればお気軽にコメントしていただけると嬉しいです!

Tech Blog - Turing

Discussion