Files
2026-07-13 13:17:40 +08:00

255 lines
9.4 KiB
Markdown

# raydepsets
A dependency lock file management tool for Ray CI pipelines. It
maintains consistency relationships among lock files — ensuring that
when a dependency is updated, all related lock files are regenerated
together in the correct order. Built on top of
[`uv pip compile`](https://docs.astral.sh/uv/pip/compile/), it
generates reproducible, hash-verified lock files across multiple
Python versions, platforms, and CUDA variants.
## Why
Ray's CI builds containers and test environments for many
combinations of Python version, platform (Linux x86_64, macOS ARM),
and GPU support (CPU, e.g. CUDA 12.8). Each combination needs a
locked, reproducible set of dependencies, and many of these lock
files have consistency relationships with each other — for example,
a test environment's lock file must be a strict superset of the base
image it runs on, and all CUDA variants for a given Python version
must agree on common package versions. A single `uv pip compile`
call can produce one lock file, but it has no awareness of these
cross-file constraints. When a dependency is updated, all downstream
lock files need to be regenerated in the right order to stay
consistent.
raydepsets solves this by:
- Modeling the relationships between lock files as a dependency
graph, so that updating one file automatically propagates to all
dependents
- Supporting four composable operations (**compile**, **subset**,
**expand**, **relax**) to express how lock files derive from each other
- Defining dependency sets declaratively in YAML with template
variables for matrix builds
- Automatically resolving execution order via topological sort
- Validating that committed lock files are up-to-date and mutually
consistent in CI (`--check` mode)
## Directory Structure
```
ci/raydepsets/
├── raydepsets.py # Entry point
├── cli.py # CLI and DependencySetManager
├── workspace.py # Config parsing and data models
├── BUILD.bazel # Bazel build targets
├── configs/ # Production YAML configs
│ ├── rayimg.depsets.yaml
│ ├── rayllm.depsets.yaml
│ ├── data_test.depsets.yaml
│ ├── docs.depsets.yaml
│ └── ...
├── pre_hooks/ # Shell scripts run before compilation
│ ├── build-placeholder-wheel.sh
│ └── remove-compiled-headers.sh
└── tests/
├── test_cli.py
├── test_workspace.py
├── utils.py
└── test_data/
```
## Usage
raydepsets is built and run via Bazel:
```bash
# Build all depsets in a config
bazelisk run //ci/raydepsets:raydepsets -- build ci/raydepsets/configs/rayimg.depsets.yaml
# Build a single named depset (and its dependencies)
bazelisk run //ci/raydepsets:raydepsets -- build ci/raydepsets/configs/rayimg.depsets.yaml --name ray_img_depset_313
# Build all configs at once
bazelisk run //ci/raydepsets:raydepsets -- build --all-configs
# Validate that lock files are up-to-date (used in CI)
bazelisk run //ci/raydepsets:raydepsets -- build ci/raydepsets/configs/rayimg.depsets.yaml --check
```
### CLI Options
| Option | Description |
|---|---|
| `CONFIG_PATH` | Path to a `.depsets.yaml` config file (default: `ci/raydepsets/configs/*.depsets.yaml`) |
| `--workspace-dir` | Workspace root directory (default: `$BUILD_WORKSPACE_DIRECTORY`) |
| `--name` | Build only this depset and its dependencies |
| `--uv-cache-dir` | Cache directory for uv |
| `--check` | Validate lock files match what would be generated; exit non-zero on diff |
| `--all-configs` | Build depsets from all config files, not just the specified one |
## Configuration Format
Config files use the `.depsets.yaml` extension and contain two top-level keys:
### `build_arg_sets` (optional)
Defines template variable sets for matrix expansion. Each key maps to a dictionary of variable substitutions:
```yaml
build_arg_sets:
py311:
PYTHON_VERSION: "3.11"
PYTHON_SHORT: "311"
py312:
PYTHON_VERSION: "3.12"
PYTHON_SHORT: "312"
```
Variables are referenced in depset fields using `${VARIABLE_NAME}` syntax. When a depset lists multiple `build_arg_sets`, it is expanded into one depset per set.
### `depsets`
A list of dependency set definitions. Each depset has these common fields:
| Field | Type | Description |
|---|---|---|
| `name` | string | Unique identifier (supports `${VAR}` substitution) |
| `operation` | string | One of `compile`, `subset`, `expand`, `relax` |
| `output` | string | Output lock file path relative to workspace root |
| `build_arg_sets` | list | Which build arg sets to expand this depset with |
| `append_flags` | list | Additional flags passed to `uv pip compile` |
| `override_flags` | list | Flags that replace matching defaults |
| `pre_hooks` | list | Shell commands to run before this depset executes |
| `include_setuptools` | bool | Allow setuptools in output (default: `false`) |
#### Operation: `compile`
Runs `uv pip compile` to resolve and lock dependencies from requirements files.
```yaml
- name: ray_img_depset_${PYTHON_SHORT}
operation: compile
requirements:
- python/deplocks/ray_img/ray_dev.in
constraints:
- /tmp/ray-deps/requirements_compiled_py${PYTHON_VERSION}.txt
output: python/deplocks/ray_img/ray_img_py${PYTHON_SHORT}.lock
append_flags:
- --python-version=${PYTHON_VERSION}
build_arg_sets:
- py310
- py311
- py312
- py313
```
Additional fields: `requirements` (input requirement files), `constraints` (version constraint files), `packages` (inline package specs passed via stdin).
#### Operation: `subset`
Extracts a subset of already-resolved dependencies from another depset's lock file. Validates that all requested requirements exist in the source.
```yaml
- name: ray_base_deps_${PYTHON_SHORT}
operation: subset
source_depset: ray_base_extra_testdeps_${PYTHON_SHORT}
requirements:
- docker/base-deps/requirements.in
output: python/deplocks/base_deps/ray_base_deps_py${PYTHON_VERSION}.lock
```
Additional fields: `source_depset` (name of the depset to subset from).
#### Operation: `expand`
Combines multiple depsets into one, optionally adding new requirements. Recursively collects all transitive requirements from referenced depsets.
```yaml
- name: compiled_ray_llm_test_depset_${PYTHON_VERSION}_${CUDA_CODE}
operation: expand
depsets:
- ray_base_test_depset_${PYTHON_VERSION}_${CUDA_CODE}
requirements:
- python/requirements/llm/llm-requirements.txt
- python/requirements/llm/llm-test-requirements.txt
constraints:
- python/deplocks/llm/ray_test_${PYTHON_VERSION}_${CUDA_CODE}.lock
output: python/deplocks/llm/rayllm_test_${PYTHON_VERSION}_${CUDA_CODE}.lock
```
Additional fields: `depsets` (list of depset names to combine), `requirements` (extra requirements to include), `constraints` (constraint files).
#### Operation: `relax`
Removes specified packages from another depset's lock file. Validates that all specified packages exist in the source before removing them.
```yaml
- name: relaxed_depset_${PYTHON_SHORT}
operation: relax
source_depset: ray_img_depset_${PYTHON_SHORT}
packages:
- some-unwanted-package
- another-package
output: python/deplocks/ray_img/ray_img_relaxed_py${PYTHON_SHORT}.lock
```
Additional fields: `source_depset` (name of the depset to relax from), `packages` (list of package names to remove from the lock file).
> **Warning:** This operation performs a simple removal of packages from the lock file and does not re-evaluate the dependency graph. Removing a package that is required by another package in the lock file may result in an inconsistent environment. Use with caution.
### YAML Anchors
Configs support standard YAML anchors for DRY definitions:
```yaml
.common_settings: &common_settings
append_flags:
- --python-version=3.11
- --unsafe-package ray
build_arg_sets:
- cpu
- cu128
depsets:
- name: my_depset
<<: *common_settings
operation: compile
requirements:
- requirements.txt
output: output.lock
```
## Pre-Hooks
Pre-hooks are shell scripts that run before a depset is executed. They are useful for preparing the build environment (e.g., building placeholder wheels, stripping GPU index URLs from constraint files).
```yaml
pre_hooks:
- ci/raydepsets/pre_hooks/build-placeholder-wheel.sh
- ci/raydepsets/pre_hooks/remove-compiled-headers.sh ${PYTHON_VERSION}
```
Pre-hooks support template variable substitution and are modeled as nodes in the dependency graph, so they execute in the correct order.
## How It Works
1. **Config loading** -- YAML configs are parsed into `Depset` dataclasses. Template variables from `build_arg_sets` are substituted, expanding one depset definition into N concrete depsets.
2. **Graph construction** -- A directed acyclic graph (NetworkX `DiGraph`) is built from depset dependencies and pre-hooks.
3. **Topological execution** -- Depsets are executed in topological order so dependencies are resolved before dependents.
4. **Lock file generation** -- Each depset calls `uv pip compile` with the appropriate flags, constraints, and requirements.
5. **Validation** (`--check` mode) -- Lock files are generated to a temp directory and compared against committed versions. Any diff causes a non-zero exit.
### Default `uv pip compile` Flags
```
--no-header
--generate-hashes
--index-strategy unsafe-best-match
--no-strip-markers
--emit-index-url
--emit-find-links
--quiet
--unsafe-package setuptools (unless include_setuptools: true)
```