Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use rapids-dependency-file-generator #17

Merged
merged 25 commits into from
May 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
5596991
Use rapids-dependency-file-generator
KyleFromNVIDIA Mar 25, 2024
d3ca171
Fix parsing of nvcc output
KyleFromNVIDIA Mar 25, 2024
69e6cf2
Escape first line of files
KyleFromNVIDIA Mar 26, 2024
e813c72
Allow no nvcc in path
KyleFromNVIDIA Mar 26, 2024
d0becef
Update package tests
KyleFromNVIDIA Mar 26, 2024
8581b49
Add matrix configuration option
KyleFromNVIDIA Mar 26, 2024
2df3de0
Bump version
KyleFromNVIDIA Mar 26, 2024
6608caa
Ensure that different directories work
KyleFromNVIDIA Mar 26, 2024
a298749
Use correct default pyproject dir
KyleFromNVIDIA Mar 26, 2024
f2ad679
Minimum DFG version
KyleFromNVIDIA Mar 27, 2024
dd33d0e
Update DFG in dependencies.yaml
KyleFromNVIDIA Mar 27, 2024
3252bf8
Remove version bump
KyleFromNVIDIA Apr 1, 2024
afc92fb
Use match.groups()
KyleFromNVIDIA Apr 1, 2024
63e1a23
Use new DFG API
KyleFromNVIDIA Apr 23, 2024
1151b73
Merge branch 'main' into use-dfg
KyleFromNVIDIA Apr 23, 2024
6cb0e6b
Update DFG version
KyleFromNVIDIA Apr 23, 2024
041e642
Use kwargs
KyleFromNVIDIA Apr 23, 2024
9ab5366
Bump DFG version
KyleFromNVIDIA Apr 24, 2024
262fcfd
Review feedback
KyleFromNVIDIA Apr 30, 2024
55a4b5f
Use lambda for mutable default values
KyleFromNVIDIA Apr 30, 2024
ca7240b
Use textrap.dedent()
KyleFromNVIDIA Apr 30, 2024
9e07618
Make DFG optional based on presence of dependencies.yaml
KyleFromNVIDIA Apr 30, 2024
ff0d741
Use callable()
KyleFromNVIDIA Apr 30, 2024
f0accaa
Review feedback
KyleFromNVIDIA Apr 30, 2024
5948d22
Update README
KyleFromNVIDIA May 1, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 10 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@
It currently support `scikit-build-core` and `setuptools` as the wrapped builder.
The package's primary purpose is to automate the various bits of preprocessing that are typically done to RAPIDS package metadata prior to publishing packages.
This includes the following notable changes:
- Running [`rapids-dependency-file-generator`](https://github.com/rapidsai/dependency-file-generator) to get the dependencies for the CUDA version and architecture.
- Modifying the package name to include CUDA suffixes.
- Updating the git commit embedded in the importable package.
- Modifying the package's (build, runtime, etc) dependencies to include CUDA suffixes.
- Filtering out dependencies based on the CUDA version at build time.
- Updating dependency specifiers to include an alpha specifier to allow pulling nightly dependencies in nightly builds.

Since some of these modifications are only desirable in certain scenarios (wheel vs conda builds vs editable installs), all of these functions are customizable via the project's configuration in pyproject.toml.
In cases where more dynamic customization is sensible, suitable environment variables and `config_settings` are supported during builds of distributions.
Expand All @@ -17,14 +15,15 @@ In cases where more dynamic customization is sensible, suitable environment vari

Any option without a default is required.

| Option | Definition | Type | Default | Supports dynamic modification |
|-----------------------|---------------------------------------------------------------------|-----------|--------------|-------------------------------|
| `allow-nightly-deps` | If true, append alpha specifiers to dependencies | bool | true | Y |
| `build-backend` | The wrapped build backend (e.g. `setuptools.build_meta`) | string | | N |
| `commit-file` | The file in which to write the git commit hash | string | "" (No file) | N |
| `disable-cuda-suffix` | If true, don't try to write CUDA suffixes | bool | false | Y |
| `require-cuda` | If false, builds will succeed even if nvcc is not available | bool | true | Y |
| `requires` | List of build requirements (in addition to `build-system.requires`) | list[str] | [] | N |
| Option | Definition | Type | Default | Supports dynamic modification |
|-----------------------|---------------------------------------------------------------------|----------------|---------------------|-------------------------------|
| `build-backend` | The wrapped build backend (e.g. `setuptools.build_meta`) | string | | N |
| `commit-file` | The file in which to write the git commit hash | string | "" (No file) | N |
| `dependencies-file` | The path to the `dependencies.yaml` file to use | string | "dependencies.yaml" | Y |
| `disable-cuda-suffix` | If true, don't try to write CUDA suffixes | bool | false | Y |
| `matrix-entry` | A `;`-separated list of `=`-delimited key/value pairs | string | "" | Y |
| `require-cuda` | If false, builds will succeed even if nvcc is not available | bool | true | Y |
| `requires` | List of build requirements (in addition to `build-system.requires`) | list[str] | [] | N |


## Outstanding questions
Expand Down
2 changes: 2 additions & 0 deletions dependencies.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,9 @@ dependencies:
common:
- output_types: [conda, requirements, pyproject]
packages:
- PyYAML
- packaging
- rapids-dependency-file-generator>=1.13.3,<2.0.dev0
- tomli
- tomli-w
test:
Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ name = "rapids-build-backend"
version = "0.0.1"
description = "Custom PEP517 builder for RAPIDS"
dependencies = [
"PyYAML",
"packaging",
"rapids-dependency-file-generator>=1.13.3,<2.0.dev0",
"tomli",
"tomli-w",
] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit dependencies.yaml and run `rapids-dependency-file-generator`.
Expand Down
7 changes: 5 additions & 2 deletions rapids_build_backend/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ class Config:
# required) and whether it may be overridden by an environment variable or a config
# setting.
config_options = {
"allow-nightly-deps": (True, True),
"build-backend": (None, False),
"commit-file": ("", False),
"dependencies-file": ("dependencies.yaml", True),
vyasr marked this conversation as resolved.
Show resolved Hide resolved
"disable-cuda-suffix": (False, True),
"matrix-entry": ("", True),
"require-cuda": (True, True),
"requires": ([], False),
"requires": (lambda: [], False),
}

def __init__(self, dirname=".", config_settings=None):
Expand All @@ -32,6 +33,8 @@ def __getattr__(self, name):
config_name = name.replace("_", "-")
if config_name in Config.config_options:
default_value, allows_override = Config.config_options[config_name]
if callable(default_value):
default_value = default_value()

# If overrides are allowed environment variables take precedence over the
# config_settings dict.
Expand Down
211 changes: 65 additions & 146 deletions rapids_build_backend/impls.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,19 @@
from functools import lru_cache
from importlib import import_module

import rapids_dependency_file_generator
import tomli_w
from packaging.requirements import Requirement
from packaging.specifiers import SpecifierSet

from . import utils
from .config import Config
from .utils import _get_pyproject


def _parse_matrix(matrix):
if not matrix:
return None
return {
key: [value] for key, value in (item.split("=") for item in matrix.split(";"))
vyasr marked this conversation as resolved.
Show resolved Hide resolved
}


@lru_cache
Expand All @@ -31,7 +38,7 @@ def _get_backend(build_backend):


@lru_cache
def _get_cuda_major(require_cuda=False):
def _get_cuda_version(require_cuda=False):
"""Get the CUDA suffix based on nvcc.

Parameters
Expand Down Expand Up @@ -60,10 +67,10 @@ def _get_cuda_major(require_cuda=False):

output_lines = process_output.stdout.decode().splitlines()

match = re.search(r"release (\d+)", output_lines[3])
match = re.search(r"release (\d+)\.(\d+)", output_lines[3])
if match is None:
raise ValueError("Failed to parse CUDA version from nvcc output.")
return match.group(1)
return match.groups()
except Exception:
if not require_cuda:
return None
Expand All @@ -86,126 +93,9 @@ def _get_cuda_suffix(require_cuda=False):
The CUDA suffix (e.g., "-cu11") or an empty string if CUDA could not be
detected.
"""
if (major := _get_cuda_major(require_cuda)) is None:
if (version := _get_cuda_version(require_cuda)) is None:
return ""
return f"-cu{major}"


# Wheels with a CUDA suffix.
_VERSIONED_RAPIDS_WHEELS = [
"rmm",
"pylibcugraphops",
"pylibcugraph",
"nx-cugraph",
"dask-cudf",
"cuspatial",
"cuproj",
"cuml",
"cugraph",
"cudf",
"ptxcompiler",
"cubinlinker",
"cugraph-dgl",
"cugraph-pyg",
"cugraph-equivariant",
"raft-dask",
"pylibwholegraph",
"pylibraft",
"cuxfilter",
"cucim",
"ucx-py",
"ucxx",
"pynvjitlink",
"distributed-ucxx",
]

# Wheels without a CUDA suffix.
_UNVERSIONED_RAPIDS_WHEELS = [
"dask-cuda",
"rapids-dask-dependency",
]

# Wheels that don't release regular alpha versions
_CUDA_11_ONLY_WHEELS = (
"ptxcompiler",
"cubinlinker",
)


def _add_cuda_suffix(req, cuda_suffix, cuda_major):
req = Requirement(req)
if req.name == "cupy" and cuda_major is not None:
req.name += f"-cuda{cuda_major}x"
elif req.name in _VERSIONED_RAPIDS_WHEELS:
req.name += cuda_suffix

return str(req)


def _add_alpha_specifier(req):
req = Requirement(req)
if (
req.name in _VERSIONED_RAPIDS_WHEELS or req.name in _UNVERSIONED_RAPIDS_WHEELS
) and req.name not in _CUDA_11_ONLY_WHEELS:
req.specifier &= SpecifierSet(">=0.0.0a0")
return str(req)
vyasr marked this conversation as resolved.
Show resolved Hide resolved


def _process_dependencies(config, dependencies=None):
"""Add the CUDA suffix to any versioned RAPIDS wheels in dependencies.

If dependencies is None, then config.requires is used.

Parameters
----------
config : Config
The project's configuration.
dependencies : list of str, optional
The dependencies to suffix. If None, then config.requires is used.

Returns
-------
list of str
The dependencies with the CUDA suffix added to any versioned RAPIDS wheels.
"""
# Note that this implementation is currently suboptimal, in each step to allow the
# steps to be more freely composable based on options. We could optimize by using a
# single loop with multiple nested conditionals, but that would make the logic
# harder to understand and modify. The performance of this code should be negligible
# in the context of a build anyway.
dependencies = dependencies or config.requires

# Step 1: Filter out CUDA 11-only wheels if we're not in a CUDA 11 build. Skip this
# step if if we were unable to detect a CUDA version.
major = _get_cuda_major(config.require_cuda)
if major is not None and major != "11":
dependencies = filter(
lambda dep: dep not in _CUDA_11_ONLY_WHEELS,
dependencies,
)

# Step 2: Allow nightlies of RAPIDS packages except in release builds. Do this
# before suffixing the names so that lookups in _add_alpha_specifier are accurate.
if config.allow_nightly_deps:
dependencies = map(
_add_alpha_specifier,
dependencies,
)

# Step 3: Add the CUDA suffix to any versioned RAPIDS wheels. This step may be
# explicitly skipped by setting the disable_cuda_suffix option to True, or
# implicitly skipped if we were unable to detect a CUDA version and require_cuda was
# False.
if not config.disable_cuda_suffix:
suffix = _get_cuda_suffix(config.require_cuda)
# If we can't determine the local CUDA version then we can skip this step
if suffix:
dependencies = map(
lambda dep: _add_cuda_suffix(dep, suffix, major),
dependencies,
)

return list(dependencies)
return f"-cu{version[0]}"


@lru_cache
Expand Down Expand Up @@ -271,27 +161,50 @@ def _edit_pyproject(config):
being built. This is useful for projects that want to build wheels
with a different name than the package name.
"""
pyproject = _get_pyproject()
project_data = pyproject["project"]
project_data["name"] += _get_cuda_suffix(config.require_cuda)

dependencies = pyproject["project"].get("dependencies")
if dependencies is not None:
project_data["dependencies"] = _process_dependencies(
config, project_data["dependencies"]
)

optional_dependencies = pyproject["project"].get("optional-dependencies")
if optional_dependencies is not None:
project_data["optional-dependencies"] = {
extra: _process_dependencies(config, deps)
for extra, deps in optional_dependencies.items()
}

pyproject_file = "pyproject.toml"
bkp_pyproject_file = ".pyproject.toml.rapids-build-backend.bak"

cuda_version = _get_cuda_version(config.require_cuda)

try:
parsed_config = rapids_dependency_file_generator.load_config_from_file(
config.dependencies_file
)
except FileNotFoundError:
parsed_config = None

try:
shutil.move(pyproject_file, bkp_pyproject_file)
shutil.copyfile(pyproject_file, bkp_pyproject_file)
if parsed_config:
for file_key, file_config in parsed_config.files.items():
if (
rapids_dependency_file_generator.Output.PYPROJECT
not in file_config.output
):
continue
pyproject_dir = os.path.join(
os.path.dirname(config.dependencies_file),
file_config.pyproject_dir,
)
if not (
os.path.exists(pyproject_dir)
and os.path.samefile(pyproject_dir, ".")
):
continue
matrix = _parse_matrix(config.matrix_entry) or dict(file_config.matrix)
if cuda_version is not None:
matrix["cuda"] = [f"{cuda_version[0]}.{cuda_version[1]}"]
rapids_dependency_file_generator.make_dependency_files(
parsed_config=parsed_config,
file_keys=[file_key],
output={rapids_dependency_file_generator.Output.PYPROJECT},
matrix=matrix,
prepend_channels=[],
to_stdout=False,
)
pyproject = utils._get_pyproject()
project_data = pyproject["project"]
project_data["name"] += _get_cuda_suffix(config.require_cuda)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still needs to obey the disable_cuda_suffix config.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The old version did not obey disable_cuda_suffix - that was only when transforming the dependencies.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we are going to want the ability to turn on and off the suffix for the package as well. However, this PR is pretty well-scoped right now so let's put a pin in this question, get the PR merged, and then come back to address it. The use-case where I think this will matter is devcontainers (or more generally in integrated dev environments where things are being built from source). We'll just need to ensure consistency.

with open(pyproject_file, "wb") as f:
tomli_w.dump(pyproject, f)
yield
Expand All @@ -314,7 +227,9 @@ def _edit_pyproject(config):
def get_requires_for_build_wheel(config_settings):
config = Config(config_settings=config_settings)
with _edit_pyproject(config), _edit_git_commit(config):
requires = _process_dependencies(config)
# Reload the config for a possibly updated tool.rapids-build-backend.requires
reloaded_config = Config(config_settings=config_settings)
requires = list(reloaded_config.requires)

if hasattr(
backend := _get_backend(config.build_backend),
Expand All @@ -328,7 +243,9 @@ def get_requires_for_build_wheel(config_settings):
def get_requires_for_build_sdist(config_settings):
config = Config(config_settings=config_settings)
with _edit_pyproject(config), _edit_git_commit(config):
requires = _process_dependencies(config)
# Reload the config for a possibly updated tool.rapids-build-backend.requires
reloaded_config = Config(config_settings=config_settings)
requires = list(reloaded_config.requires)

if hasattr(
backend := _get_backend(config.build_backend),
Expand All @@ -342,7 +259,9 @@ def get_requires_for_build_sdist(config_settings):
def get_requires_for_build_editable(config_settings):
config = Config(config_settings=config_settings)
with _edit_pyproject(config):
requires = _process_dependencies(config)
# Reload the config for a possibly updated tool.rapids-build-backend.requires
reloaded_config = Config(config_settings=config_settings)
requires = list(reloaded_config.requires)

if hasattr(
backend := _get_backend(config.build_backend),
Expand Down
3 changes: 0 additions & 3 deletions rapids_build_backend/utils.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
# Copyright (c) 2024, NVIDIA CORPORATION.

import os
from functools import lru_cache

import tomli


# Avoid unnecessary I/O by caching.
@lru_cache
KyleFromNVIDIA marked this conversation as resolved.
Show resolved Hide resolved
def _get_pyproject(dirname="."):
"""Parse and return the pyproject.toml file."""
with open(os.path.join(dirname, "pyproject.toml"), "rb") as f:
Expand Down
Loading