Skip to content

Commit 902af4e

Browse files
committed
Support modern uv project management (uv init + uv add)
- Update MaxText installation scripts to detect 'uv.lock' and use 'uv add --frozen'. - Consolidate uv detection and installation logic into a shared 'uv_utils.py' module. - Improve uv detection robustness by prioritizing 'python -m uv' and path lookup. - Update docs/install_maxtext.md with modern uv workflow instructions and fix outdated paths. - Fix macOS installation by dynamically skipping legacy MaxText directory.
1 parent 4cf5bee commit 902af4e

7 files changed

Lines changed: 196 additions & 111 deletions

File tree

build_hooks.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"""Custom build hooks for PyPI."""
1616

1717
import os
18+
import sys
1819
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
1920

2021
TPU_REQUIREMENTS_PATH = "src/dependencies/requirements/generated_requirements/tpu-requirements.txt"
@@ -33,9 +34,27 @@ def get_tpu_dependencies():
3334

3435

3536
class CustomBuildHook(BuildHookInterface):
36-
"""A custom hook to inject TPU dependencies into the core wheel dependencies."""
37+
"""A custom hook to handle platform-specific package configuration for MaxText."""
3738

3839
def initialize(self, version, build_data): # pylint: disable=unused-argument
39-
tpu_deps = get_tpu_dependencies()
40-
build_data["dependencies"] = tpu_deps
41-
print(f"Successfully injected {len(tpu_deps)} TPU dependencies into the wheel's core requirements.")
40+
"""Adjusts the build_data dictionary to customize the wheel's package structure."""
41+
# The following TPU dependency injection is disabled because TPU-specific requirements
42+
# are now managed via optional dependencies (extras) in pyproject.toml
43+
# (e.g., pip install maxtext[tpu]).
44+
# tpu_deps = get_tpu_dependencies()
45+
# build_data["dependencies"] = tpu_deps
46+
# print(f"Successfully injected {len(tpu_deps)} TPU dependencies into the wheel's core requirements.")
47+
48+
# macOS specific logic to avoid case-sensitivity issues with MaxText and maxtext directories
49+
build_data["force_include"] = build_data.get("force_include", {})
50+
if sys.platform == "darwin":
51+
print("macOS detected. Skipping legacy MaxText shims to avoid case-sensitivity conflicts.")
52+
# Always include the __init__.py in the lowercase 'maxtext' package on macOS.
53+
# This ensures that 'import maxtext' (and thus 'import MaxText' on macOS)
54+
# has the proper version and metadata.
55+
build_data["force_include"]["src/MaxText/__init__.py"] = "maxtext/__init__.py"
56+
else:
57+
# On other platforms, include 'src/MaxText' as its own top-level package for legacy support.
58+
# We do NOT add __init__.py to 'maxtext' here to maintain exact parity with previous builds.
59+
print("Included src/MaxText as a top-level package for non-macOS platforms.")
60+
build_data["force_include"]["src/MaxText"] = "MaxText"

docs/install_maxtext.md

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ MaxText offers following installation modes:
2424
3. maxtext[tpu-post-train]. Used for post-training on TPUs. Currently, this option should also be used for running vllm_decode on TPUs.
2525
4. maxtext[runner]. Used for building MaxText's Docker images and scheduling workloads through XPK.
2626

27-
## From PyPI (Recommended)
27+
## From PyPI (Recommended on Linux)
2828

2929
This is the easiest way to get started with the latest stable version.
3030

@@ -45,7 +45,7 @@ install_maxtext_tpu_github_deps
4545

4646
# Option 2: Installing maxtext[cuda12]
4747
uv pip install maxtext[cuda12] --resolution=lowest
48-
install_maxtext_cuda12_github_dep
48+
install_maxtext_cuda12_github_deps
4949

5050
# Option 3: Installing maxtext[tpu-post-train]
5151
uv pip install maxtext[tpu-post-train] --resolution=lowest
@@ -55,12 +55,33 @@ install_maxtext_tpu_post_train_extra_deps
5555
uv pip install maxtext[runner] --resolution=lowest
5656
```
5757

58-
> **Note:** The `install_maxtext_tpu_github_deps`, `install_maxtext_cuda12_github_dep`, and
58+
> **Note:** The `install_maxtext_tpu_github_deps`, `install_maxtext_cuda12_github_deps`, and
5959
> `install_maxtext_tpu_post_train_extra_deps` commands are temporarily required to install dependencies directly from GitHub
6060
> that are not yet available on PyPI. As shown above, choose the one that corresponds to your use case.
6161
62+
## Modern UV Project (Recommended for New Projects)
63+
64+
If you are starting a new project and want to use `uv`'s project management features (with a `pyproject.toml` and `uv.lock` in your own project), you can use `uv add`. MaxText's helper scripts will detect your `uv.lock` and correctly add their extra dependencies to your `pyproject.toml`.
65+
66+
```bash
67+
# 1. Initialize your project
68+
mkdir my-maxtext-project && cd my-maxtext-project
69+
uv init
70+
71+
# 2. Add MaxText as a dependency
72+
uv add maxtext[tpu] --resolution=lowest
73+
74+
# 3. Install MaxText's extra GitHub dependencies
75+
# These will be automatically added to your pyproject.toml
76+
install_maxtext_tpu_github_deps
77+
```
78+
6279
> **Note:** The maxtext package contains a comprehensive list of all direct and transitive dependencies, with lower bounds, generated by [seed-env](https://github.com/google-ml-infra/actions/tree/main/python_seed_env). We highly recommend the `--resolution=lowest` flag. It instructs `uv` to install the specific, tested versions of dependencies defined by MaxText, rather than the latest available ones. This ensures a consistent and reproducible environment, which is critical for stable performance and for running benchmarks.
6380
81+
## macOS Installation
82+
83+
Due to macOS's case-insensitive filesystem, special care is needed to avoid conflicts between the `maxtext` and legacy `MaxText` package names. We recommend installing it from source using the `.[runner]` configuration.
84+
6485
## From Source
6586

6687
If you plan to contribute to MaxText or need the latest unreleased features, install from source.
@@ -84,7 +105,7 @@ install_maxtext_tpu_github_deps
84105

85106
# Option 2: Installing .[cuda12]
86107
uv pip install -e .[cuda12] --resolution=lowest
87-
install_maxtext_cuda12_github_dep
108+
install_maxtext_cuda12_github_deps
88109

89110
# Option 3: Installing .[tpu-post-train]
90111
uv pip install -e .[tpu-post-train] --resolution=lowest
@@ -110,7 +131,7 @@ To update dependencies, you will follow these general steps:
110131

111132
1. **Modify Base Requirements**: Update the desired dependencies in `base_requirements/requirements.txt` or the hardware-specific files (`base_requirements/tpu-base-requirements.txt`, `base_requirements/gpu-base-requirements.txt`).
112133
2. **Generate New Files**: Run the `seed-env` CLI tool to generate new, fully-pinned requirements files based on your changes.
113-
3. **Update Project Files**: Copy the newly generated files into the `generated_requirements/` directory.
134+
3. **Update Project Files**: Copy the newly generated files into the `src/dependencies/requirements/generated_requirements/` directory.
114135
4. **Handle GitHub Dependencies**: Move any dependencies that are installed directly from GitHub from the generated files to `src/dependencies/github_deps/pre_train_deps.txt`.
115136
5. **Verify**: Test the new dependencies to ensure the project installs and runs correctly.
116137

@@ -166,8 +187,8 @@ After generating the new requirements, you need to update the files in the MaxTe
166187

167188
1. **Copy the generated files:**
168189

169-
- Move `generated_tpu_artifacts/tpu-requirements.txt` to `generated_requirements/tpu-requirements.txt`.
170-
- Move `generated_gpu_artifacts/cuda12-requirements.txt` to `generated_requirements/cuda12-requirements.txt`.
190+
- Move `generated_tpu_artifacts/tpu-requirements.txt` to `src/dependencies/requirements/generated_requirements/tpu-requirements.txt`.
191+
- Move `generated_gpu_artifacts/cuda12-requirements.txt` to `src/dependencies/requirements/generated_requirements/cuda12-requirements.txt`.
171192

172193
2. **Update `pre_train_deps.txt` (if necessary):**
173194
Currently, MaxText uses a few dependencies, such as `mlperf-logging` and `google-jetstream`, that are installed directly from GitHub source. These are defined in `base_requirements/requirements.txt`, and the `seed-env` tool will carry them over to the generated requirements files.

pyproject.toml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,10 @@ Repository = "https://github.com/AI-Hypercomputer/maxtext.git"
4040
allow-direct-references = true
4141

4242
[tool.hatch.build.targets.wheel]
43-
packages = ["src/MaxText", "src/maxtext", "src/dependencies"]
43+
packages = ["src/maxtext", "src/dependencies"]
4444

45-
# TODO: Add this hook back when it handles device-type parsing
46-
# [tool.hatch.build.targets.wheel.hooks.custom]
47-
# path = "build_hooks.py"
45+
[tool.hatch.build.targets.wheel.hooks.custom]
46+
path = "build_hooks.py"
4847

4948
[project.scripts]
5049
install_maxtext_tpu_github_deps = "dependencies.github_deps.install_pre_train_deps:main"

src/dependencies/github_deps/install_post_train_deps.py

Lines changed: 10 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,19 @@
2121
"""
2222

2323
import os
24-
import subprocess
25-
import sys
24+
25+
try:
26+
from . import uv_utils
27+
except ImportError:
28+
import uv_utils
2629

2730

2831
def main():
2932
"""
3033
Installs extra dependencies specified in post_train_deps.txt using uv.
3134
3235
This script looks for 'post_train_deps.txt' relative to its own location.
33-
It executes 'uv pip install -r <path_to_extra_deps.txt> --resolution=lowest'.
36+
It executes 'uv add' (if uv.lock is present) or 'uv pip install'.
3437
"""
3538
os.environ["VLLM_TARGET_DEVICE"] = "tpu"
3639

@@ -40,57 +43,10 @@ def main():
4043
if not os.path.exists(extra_deps_path):
4144
raise FileNotFoundError(f"Dependencies file not found at {extra_deps_path}")
4245

43-
# Check if 'uv' is available in the environment
44-
try:
45-
subprocess.run([sys.executable, "-m", "pip", "install", "uv"], check=True, capture_output=True)
46-
subprocess.run([sys.executable, "-m", "uv", "--version"], check=True, capture_output=True)
47-
except subprocess.CalledProcessError as e:
48-
print(f"Error checking uv version: {e}")
49-
print(f"Stderr: {e.stderr.decode()}")
50-
sys.exit(1)
51-
52-
command = [
53-
sys.executable, # Use the current Python executable's pip to ensure the correct environment
54-
"-m",
55-
"uv",
56-
"pip",
57-
"install",
58-
"-r",
59-
str(extra_deps_path),
60-
"--no-deps",
61-
]
62-
63-
local_vllm_install_command = [
64-
sys.executable, # Use the current Python executable's pip to ensure the correct environment
65-
"-m",
66-
"uv",
67-
"pip",
68-
"install",
69-
f"{repo_root}/maxtext/integration/vllm", # MaxText on vllm installations
70-
"--no-deps",
71-
]
72-
73-
try:
74-
# Run the command to install Github dependencies
75-
print(f"Installing extra dependencies: {' '.join(command)}")
76-
_ = subprocess.run(command, check=True, capture_output=True, text=True)
77-
print("Extra dependencies installed successfully!")
78-
79-
# Run the command to install the MaxText vLLM directory
80-
print(f"Installing MaxText vLLM dependency: {' '.join(local_vllm_install_command)}")
81-
_ = subprocess.run(local_vllm_install_command, check=True, capture_output=True, text=True)
82-
print("MaxText vLLM dependency installed successfully!")
83-
except subprocess.CalledProcessError as e:
84-
print("Failed to install extra dependencies.")
85-
print(f"Command '{' '.join(e.cmd)}' returned non-zero exit status {e.returncode}.")
86-
print("--- Stderr ---")
87-
print(e.stderr)
88-
print("--- Stdout ---")
89-
print(e.stdout)
90-
sys.exit(e.returncode)
91-
except (OSError, FileNotFoundError) as e:
92-
print(f"An OS-level error occurred while trying to run uv: {e}")
93-
sys.exit(1)
46+
# Install both requirements file and the local vLLM integration
47+
uv_utils.run_install(
48+
requirements_files=[extra_deps_path], paths=[f"{repo_root}/maxtext/integration/vllm"], is_editable=True
49+
)
9450

9551

9652
if __name__ == "__main__":

src/dependencies/github_deps/install_pre_train_deps.py

Lines changed: 7 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -21,58 +21,26 @@
2121
"""
2222

2323
import os
24-
import subprocess
25-
import sys
24+
25+
try:
26+
from . import uv_utils
27+
except ImportError:
28+
import uv_utils
2629

2730

2831
def main():
2932
"""
3033
Installs extra dependencies specified in pre_train_deps.txt using uv.
3134
3235
This script looks for 'pre_train_deps.txt' relative to its own location.
33-
It executes 'uv pip install -r <path_to_extra_deps.txt> --resolution=lowest'.
36+
It executes 'uv add' (if uv.lock is present) or 'uv pip install'.
3437
"""
3538
current_dir = os.path.dirname(os.path.abspath(__file__))
3639
extra_deps_path = os.path.join(current_dir, "pre_train_deps.txt")
3740
if not os.path.exists(extra_deps_path):
3841
raise FileNotFoundError(f"Dependencies file not found at {extra_deps_path}")
3942

40-
# Check if 'uv' is available in the environment
41-
try:
42-
subprocess.run([sys.executable, "-m", "pip", "install", "uv"], check=True, capture_output=True)
43-
subprocess.run([sys.executable, "-m", "uv", "--version"], check=True, capture_output=True)
44-
except subprocess.CalledProcessError as e:
45-
print(f"Error checking uv version: {e}")
46-
print(f"Stderr: {e.stderr.decode()}")
47-
sys.exit(1)
48-
49-
command = [
50-
sys.executable, # Use the current Python executable's pip to ensure the correct environment
51-
"-m",
52-
"uv",
53-
"pip",
54-
"install",
55-
"-r",
56-
str(extra_deps_path),
57-
"--no-deps",
58-
]
59-
60-
try:
61-
# Run the command
62-
print(f"Installing extra dependencies: {' '.join(command)}")
63-
_ = subprocess.run(command, check=True, capture_output=True, text=True)
64-
print("Extra dependencies installed successfully!")
65-
except subprocess.CalledProcessError as e:
66-
print("Failed to install extra dependencies.")
67-
print(f"Command '{' '.join(e.cmd)}' returned non-zero exit status {e.returncode}.")
68-
print("--- Stderr ---")
69-
print(e.stderr)
70-
print("--- Stdout ---")
71-
print(e.stdout)
72-
sys.exit(e.returncode)
73-
except (OSError, FileNotFoundError) as e:
74-
print(f"An OS-level error occurred while trying to run uv: {e}")
75-
sys.exit(1)
43+
uv_utils.run_install(requirements_files=[extra_deps_path])
7644

7745

7846
if __name__ == "__main__":
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Helper utilities for working with uv in installation scripts."""
16+
17+
import os
18+
import shutil
19+
import subprocess
20+
import sys
21+
22+
23+
def get_uv_command():
24+
"""
25+
Returns the command to run uv, either as a binary in PATH or as a module.
26+
Attempts to install uv via pip if not found.
27+
"""
28+
# 1. Try finding 'uv' in PATH
29+
uv_binary = shutil.which("uv")
30+
if uv_binary:
31+
return [uv_binary]
32+
33+
# 2. Try running it as a module
34+
try:
35+
subprocess.run([sys.executable, "-m", "uv", "--version"], check=True, capture_output=True)
36+
return [sys.executable, "-m", "uv"]
37+
except (subprocess.CalledProcessError, FileNotFoundError):
38+
pass
39+
40+
# 3. Fall back to installing via pip
41+
try:
42+
print("uv not found in PATH or as a module. Attempting to install it via pip...")
43+
subprocess.run([sys.executable, "-m", "pip", "install", "uv"], check=True, capture_output=True)
44+
# Check PATH again after installation
45+
uv_binary = shutil.which("uv")
46+
if uv_binary:
47+
return [uv_binary]
48+
return [sys.executable, "-m", "uv"]
49+
except subprocess.CalledProcessError as e:
50+
print(f"Error installing uv via pip: {e}")
51+
print(f"Stderr: {e.stderr.decode()}")
52+
sys.exit(1)
53+
54+
55+
def run_install(requirements_files=None, paths=None, editable_paths=None):
56+
"""
57+
Executes the appropriate uv install command (uv add or uv pip install).
58+
59+
Args:
60+
requirements_files: List of paths to requirements.txt files.
61+
paths: List of paths to local packages or directories (non-editable).
62+
editable_paths: List of paths to local packages or directories (editable).
63+
"""
64+
uv_command = get_uv_command()
65+
is_uv_project = os.path.exists("uv.lock")
66+
67+
# We run installations in two steps if we have both standard and editable items,
68+
# because 'uv add --editable' cannot be mixed with non-local requirements.
69+
70+
# Step 1: Standard installations
71+
if requirements_files or paths:
72+
if is_uv_project:
73+
cmd = uv_command + ["add", "--frozen"]
74+
else:
75+
cmd = uv_command + ["pip", "install", "--no-deps"]
76+
77+
if requirements_files:
78+
for req in requirements_files:
79+
cmd.extend(["-r", str(req)])
80+
if paths:
81+
cmd.extend(paths)
82+
83+
_execute_command(cmd)
84+
85+
# Step 2: Editable installations
86+
if editable_paths:
87+
if is_uv_project:
88+
cmd = uv_command + ["add", "--frozen", "--editable"]
89+
else:
90+
cmd = uv_command + ["pip", "install", "--no-deps", "-e"]
91+
92+
cmd.extend(editable_paths)
93+
_execute_command(cmd)
94+
95+
96+
def _execute_command(cmd):
97+
"""Helper to execute a command with logging and error handling."""
98+
try:
99+
print(f"Executing: {' '.join(cmd)}")
100+
subprocess.run(cmd, check=True, capture_output=True, text=True)
101+
print("Success!")
102+
except subprocess.CalledProcessError as e:
103+
print(f"Command failed with exit status {e.returncode}.")
104+
print("--- Stderr ---")
105+
print(e.stderr)
106+
print("--- Stdout ---")
107+
print(e.stdout)
108+
sys.exit(e.returncode)
109+
except (OSError, FileNotFoundError) as e:
110+
print(f"An OS-level error occurred: {e}")
111+
sys.exit(1)

0 commit comments

Comments
 (0)