chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
# Runtime Env Architecture
|
||||
|
||||
This document describes the architecture of Ray's runtime environment feature.
|
||||
|
||||
## Overview
|
||||
|
||||
For a high-level overview of runtime environments, see the blog post [here](https://www.anyscale.com/blog/handling-files-and-packages-on-your-cluster-with-ray-runtime-environments). The blog post also contains a simple example of how to use runtime environments.
|
||||
|
||||
For a more detailed explanation of how to use runtime environments, see the [documentation](https://docs.ray.io/en/latest/ray-core/handling-dependencies.html).
|
||||
|
||||
See also the relevant [design doc](https://docs.google.com/document/d/1x1JAHg7c0ewcOYwhhclbuW0B0UC7l92WFkF4Su0T-dk/edit#heading=h.j4mqiaz83o96) and runtime environment section of the Ray 2.0 whitepaper [here](https://docs.google.com/document/d/1tBw9A4j62ruI5omIJbMxly-la5w4q_TjyJgJL_jN2fI/preview#heading=h.ih8imml8oqbl).
|
||||
|
||||
## Architecture
|
||||
|
||||

|
||||
|
||||
|
||||
Runtime environment creation is handled by a "dashboard agent" process (`RuntimeEnvAgent`) that runs on each node of the cluster (python/ray/dashboard/modules/runtime_env/runtime_env_agent.py).
|
||||
|
||||
The dashboard agent fate-shares with the Raylet process (for more on the Raylet, see the [Ray whitepaper](https://docs.google.com/document/d/1tBw9A4j62ruI5omIJbMxly-la5w4q_TjyJgJL_jN2fI/preview)). The reason is that if the dashboard agent fails, then runtime env creation will fail, so the Raylet will no longer be able to set up the environment for its workers. The fate sharing simplifies the failure model and because it is a core component for scheduling tasks and actors.
|
||||
|
||||
The artifacts of a created runtime environment are a set of files on disk and a `RuntimeEnvContext` Python object (python/ray/_private/runtime_env/context.py) in memory.
|
||||
|
||||
The `RuntimeEnvContext` is serialized and passed to the Raylet, where it is used when starting a new worker process for this runtime env (see "Starting a Worker Process").
|
||||
|
||||
|
||||
|
||||
## Plugins
|
||||
|
||||
All options for runtime envs (e.g. `working_dir`, `pip`, etc) are implemented as plugins adhering to a Ray `RuntimeEnvPlugin` interface. This class contains public methods to be called during installation, deletion, and for updating the `RuntimeEnvContext` object.
|
||||
|
||||
For details, see the [design doc.](https://docs.google.com/document/d/1x1JAHg7c0ewcOYwhhclbuW0B0UC7l92WFkF4Su0T-dk/edit#heading=h.j4mqiaz83o96)
|
||||
|
||||
## The Worker Pool
|
||||
|
||||
The Raylet's worker pool (src/ray/raylet/worker_pool.cc) handles caching of worker processes and starting new worker processes. When scheduling a task, its runtime env spec is included in its `TaskSpec`. The worker pool then compares the hash of the runtime env spec against those of all running workers to determine whether the task can be scheduled on an existing worker process with that runtime env, or whether a new worker process needs to be started.
|
||||
|
||||
|
||||
## Creation and deletion
|
||||
|
||||
The `RuntimeEnvAgent` exposes gRPC endpoints for `runtime_env` creation and deletion to the Raylet.
|
||||
|
||||
The agent manager (src/ray/raylet/agent_manager.cc) runs in the Raylet and manages the connection to the agent, and calls the creation and deletion endpoints.
|
||||
|
||||
The worker pool holds a reference to the agent manager, and uses it to send `CreateRuntimeEnvIfNeeded` and `DeleteRuntimeEnvIfPossible` requests to the `RuntimeEnvAgent` as needed for new worker processes or when worker processes are removed.
|
||||
|
||||
## Starting a new worker process
|
||||
|
||||
Worker process are started by the Raylet in python/ray/_private/services.py. In this command, python/ray/_private/workers/setup_worker.py is run, which deserializes the `RuntimeEnvContext` object and calls its `exec_worker` method. The `exec_worker` method sets the appropriate environment variables, modifies the worker process startup command (e.g. prepending `conda activate some_env`) and then calls `execvp` to start the worker process (python/ray/_private/worker.py).
|
||||
|
||||
## Caching and garbage collection
|
||||
|
||||
For design details on caching and garbage collection, see the [design doc](https://docs.google.com/document/d/1x1JAHg7c0ewcOYwhhclbuW0B0UC7l92WFkF4Su0T-dk/edit#heading=h.j4mqiaz83o96).
|
||||
|
||||
The implementation is as follows:
|
||||
|
||||
### GCS internal KV garbage collection
|
||||
|
||||
This section deals with files that are stored in the head node in the internal KV, such as `working_dir` or `py_modules` packages uploaded by the user.
|
||||
|
||||
References are tracked per package (URI). The reference counting is managed in src/ray/common/runtime_env_manager.cc. References to these files are incremented when a driver is started that uses that URI, and decremented when a driver exits. Similarly, references are incremented when a detached actor is started that uses that URI, and decremented when that actor exits.
|
||||
|
||||
When a reference count reaches 0, the file is deleted.
|
||||
|
||||
#### The "temporary reference" for Ray Jobs API and Ray Client
|
||||
|
||||
When the user specifies a local directory in `working_dir` or `py_modules`, it is zipped and uploaded to the GCS as a URI by Ray. However, as described above, the reference count is ordinarily only incremented when a driver (or detached actor) is started that uses that URI. In the case of Ray Jobs API and Ray Client, this uploading happens before the driver starts.
|
||||
|
||||
To prevent the file from being garbage collected before the driver starts, a special "temporary reference" is added for the URI when it is uploaded. This reference is removed after a configurable timeout (controlled by the env var `RAY_RUNTIME_ENV_TEMPORARY_REFERENCE_EXPIRATION_S` on the head node, default 600 seconds).
|
||||
|
||||
### Local node garbage collection
|
||||
|
||||
This section deals with files that are stored on disk on all nodes, such as installed `pip` packages or `working_dir` files downloaded from the GCS or from a remote URI.
|
||||
|
||||
References for these files are tracked by the runtime env agent process on each node. Each agent keeps its own reference table in memory. References are incremented when a runtime env is created, and decremented when the runtime env is deleted. The files are not deleted until the reference count reaches 0 and the cache size exceeds the maximum cache size.
|
||||
|
||||
Each runtime_env field has its own cache size limit, which defaults to 10GB and can be configured via the env var `RAY_RUNTIME_ENV_<field>_CACHE_SIZE_GB` on each node (e.g. `RAY_RUNTIME_ENV_WORKING_DIR_CACHE_SIZE_GB`).
|
||||
|
||||
## Testing
|
||||
|
||||
The runtime environment feature is tested in files matching the pattern `test_runtime_env*`. The names of these files should be mostly self-explanatory.
|
||||
@@ -0,0 +1,6 @@
|
||||
load("//bazel:python.bzl", "doctest")
|
||||
|
||||
doctest(
|
||||
files = glob(["**/*.py"]),
|
||||
tags = ["team:core"],
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
from ray.runtime_env.runtime_env import RuntimeEnv, RuntimeEnvConfig # noqa: E402,F401
|
||||
|
||||
__all__ = [
|
||||
"RuntimeEnvConfig",
|
||||
"RuntimeEnv",
|
||||
]
|
||||
@@ -0,0 +1,685 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from copy import deepcopy
|
||||
from dataclasses import asdict, is_dataclass
|
||||
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
|
||||
|
||||
import ray
|
||||
from ray._private.ray_constants import DEFAULT_RUNTIME_ENV_TIMEOUT_SECONDS
|
||||
from ray._private.runtime_env.conda import get_uri as get_conda_uri
|
||||
from ray._private.runtime_env.default_impl import get_image_uri_plugin_cls
|
||||
from ray._private.runtime_env.pip import get_uri as get_pip_uri
|
||||
from ray._private.runtime_env.plugin_schema_manager import RuntimeEnvPluginSchemaManager
|
||||
from ray._private.runtime_env.uv import get_uri as get_uv_uri
|
||||
from ray._private.runtime_env.validation import (
|
||||
OPTION_TO_NO_PATH_VALIDATION_FN,
|
||||
OPTION_TO_VALIDATION_FN,
|
||||
)
|
||||
from ray._private.thirdparty.dacite import from_dict
|
||||
from ray.core.generated.runtime_environment_pb2 import (
|
||||
RuntimeEnvConfig as ProtoRuntimeEnvConfig,
|
||||
)
|
||||
from ray.util.annotations import PublicAPI
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@PublicAPI(stability="stable")
|
||||
class RuntimeEnvConfig(dict):
|
||||
"""Used to specify configuration options for a runtime environment.
|
||||
|
||||
The config is not included when calculating the runtime_env hash,
|
||||
which means that two runtime_envs with the same options but different
|
||||
configs are considered the same for caching purposes.
|
||||
|
||||
Args:
|
||||
setup_timeout_seconds: The timeout of runtime environment
|
||||
creation, timeout is in seconds. The value `-1` means disable
|
||||
timeout logic, except `-1`, `setup_timeout_seconds` cannot be
|
||||
less than or equal to 0. The default value of `setup_timeout_seconds`
|
||||
is 600 seconds.
|
||||
eager_install: Indicates whether to install the runtime environment
|
||||
on the cluster at `ray.init()` time, before the workers are leased.
|
||||
This flag is set to `True` by default.
|
||||
log_files: An optional list of log files to surface in the dashboard for
|
||||
this runtime environment.
|
||||
"""
|
||||
|
||||
known_fields: Set[str] = {"setup_timeout_seconds", "eager_install", "log_files"}
|
||||
|
||||
_default_config: Dict = {
|
||||
"setup_timeout_seconds": DEFAULT_RUNTIME_ENV_TIMEOUT_SECONDS,
|
||||
"eager_install": True,
|
||||
"log_files": [],
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
setup_timeout_seconds: int = DEFAULT_RUNTIME_ENV_TIMEOUT_SECONDS,
|
||||
eager_install: bool = True,
|
||||
log_files: Optional[List[str]] = None,
|
||||
):
|
||||
super().__init__()
|
||||
if not isinstance(setup_timeout_seconds, int):
|
||||
raise TypeError(
|
||||
"setup_timeout_seconds must be of type int, "
|
||||
f"got: {type(setup_timeout_seconds)}"
|
||||
)
|
||||
elif setup_timeout_seconds <= 0 and setup_timeout_seconds != -1:
|
||||
raise ValueError(
|
||||
"setup_timeout_seconds must be greater than zero "
|
||||
f"or equals to -1, got: {setup_timeout_seconds}"
|
||||
)
|
||||
self["setup_timeout_seconds"] = setup_timeout_seconds
|
||||
|
||||
if not isinstance(eager_install, bool):
|
||||
raise TypeError(
|
||||
f"eager_install must be a boolean. got {type(eager_install)}"
|
||||
)
|
||||
self["eager_install"] = eager_install
|
||||
|
||||
if log_files is not None:
|
||||
if not isinstance(log_files, list):
|
||||
raise TypeError(
|
||||
"log_files must be a list of strings or None, got "
|
||||
f"{log_files} with type {type(log_files)}."
|
||||
)
|
||||
for file_name in log_files:
|
||||
if not isinstance(file_name, str):
|
||||
raise TypeError("Each item in log_files must be a string.")
|
||||
else:
|
||||
log_files = self._default_config["log_files"]
|
||||
|
||||
self["log_files"] = log_files
|
||||
|
||||
@staticmethod
|
||||
def parse_and_validate_runtime_env_config(
|
||||
config: Union[Dict, "RuntimeEnvConfig"]
|
||||
) -> "RuntimeEnvConfig":
|
||||
if isinstance(config, RuntimeEnvConfig):
|
||||
return config
|
||||
elif isinstance(config, Dict):
|
||||
unknown_fields = set(config.keys()) - RuntimeEnvConfig.known_fields
|
||||
if len(unknown_fields):
|
||||
logger.warning(
|
||||
"The following unknown entries in the runtime_env_config "
|
||||
f"dictionary will be ignored: {unknown_fields}."
|
||||
)
|
||||
config_dict = dict()
|
||||
for field in RuntimeEnvConfig.known_fields:
|
||||
if field in config:
|
||||
config_dict[field] = config[field]
|
||||
return RuntimeEnvConfig(**config_dict)
|
||||
else:
|
||||
raise TypeError(
|
||||
"runtime_env['config'] must be of type dict or RuntimeEnvConfig, "
|
||||
f"got: {type(config)}"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def default_config(cls):
|
||||
return RuntimeEnvConfig(**cls._default_config)
|
||||
|
||||
def build_proto_runtime_env_config(self) -> ProtoRuntimeEnvConfig:
|
||||
runtime_env_config = ProtoRuntimeEnvConfig()
|
||||
runtime_env_config.setup_timeout_seconds = self["setup_timeout_seconds"]
|
||||
runtime_env_config.eager_install = self["eager_install"]
|
||||
if self["log_files"] is not None:
|
||||
runtime_env_config.log_files.extend(self["log_files"])
|
||||
return runtime_env_config
|
||||
|
||||
@classmethod
|
||||
def from_proto(cls, runtime_env_config: ProtoRuntimeEnvConfig):
|
||||
setup_timeout_seconds = runtime_env_config.setup_timeout_seconds
|
||||
# Cause python class RuntimeEnvConfig has validate to avoid
|
||||
# setup_timeout_seconds equals zero, so setup_timeout_seconds
|
||||
# on RuntimeEnvConfig is zero means other Language(except python)
|
||||
# dosn't assign value to setup_timeout_seconds. So runtime_env_agent
|
||||
# assign the default value to setup_timeout_seconds.
|
||||
if setup_timeout_seconds == 0:
|
||||
setup_timeout_seconds = cls._default_config["setup_timeout_seconds"]
|
||||
return cls(
|
||||
setup_timeout_seconds=setup_timeout_seconds,
|
||||
eager_install=runtime_env_config.eager_install,
|
||||
log_files=list(runtime_env_config.log_files),
|
||||
)
|
||||
|
||||
def to_dict(self) -> Dict:
|
||||
return dict(deepcopy(self))
|
||||
|
||||
|
||||
# Due to circular reference, field config can only be assigned a value here
|
||||
OPTION_TO_VALIDATION_FN[
|
||||
"config"
|
||||
] = RuntimeEnvConfig.parse_and_validate_runtime_env_config
|
||||
|
||||
|
||||
@PublicAPI
|
||||
class RuntimeEnv(dict):
|
||||
"""This class is used to define a runtime environment for a job, task,
|
||||
or actor.
|
||||
|
||||
See :ref:`runtime-environments` for detailed documentation.
|
||||
|
||||
This class can be used interchangeably with an unstructured dictionary
|
||||
in the relevant API calls.
|
||||
|
||||
Can specify a runtime environment whole job, whether running a script
|
||||
directly on the cluster, using Ray Job submission, or using Ray Client:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from ray.runtime_env import RuntimeEnv
|
||||
# Starting a single-node local Ray cluster
|
||||
ray.init(runtime_env=RuntimeEnv(...))
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from ray.runtime_env import RuntimeEnv
|
||||
# Connecting to remote cluster using Ray Client
|
||||
ray.init("ray://123.456.7.89:10001", runtime_env=RuntimeEnv(...))
|
||||
|
||||
Can specify different runtime environments per-actor or per-task using
|
||||
``.options()`` or the ``@ray.remote`` decorator:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from ray.runtime_env import RuntimeEnv
|
||||
# Invoke a remote task that runs in a specified runtime environment.
|
||||
f.options(runtime_env=RuntimeEnv(...)).remote()
|
||||
|
||||
# Instantiate an actor that runs in a specified runtime environment.
|
||||
actor = SomeClass.options(runtime_env=RuntimeEnv(...)).remote()
|
||||
|
||||
# Specify a runtime environment in the task definition. Future invocations via
|
||||
# `g.remote()` use this runtime environment unless overridden by using
|
||||
# `.options()` as above.
|
||||
@ray.remote(runtime_env=RuntimeEnv(...))
|
||||
def g():
|
||||
pass
|
||||
|
||||
# Specify a runtime environment in the actor definition. Future instantiations
|
||||
# via `MyClass.remote()` use this runtime environment unless overridden by
|
||||
# using `.options()` as above.
|
||||
@ray.remote(runtime_env=RuntimeEnv(...))
|
||||
class MyClass:
|
||||
pass
|
||||
|
||||
Here are some examples of RuntimeEnv initialization:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Example for using conda
|
||||
RuntimeEnv(conda={
|
||||
"channels": ["defaults"], "dependencies": ["codecov"]})
|
||||
RuntimeEnv(conda="pytorch_p36") # Found on DLAMIs
|
||||
|
||||
# Example for using container
|
||||
RuntimeEnv(
|
||||
container={"image": "anyscale/ray-ml:nightly-py38-cpu",
|
||||
"run_options": ["--cap-drop SYS_ADMIN","--log-level=debug"]})
|
||||
|
||||
# Example for set env_vars
|
||||
RuntimeEnv(env_vars={"OMP_NUM_THREADS": "32", "TF_WARNINGS": "none"})
|
||||
|
||||
# Example for set pip
|
||||
RuntimeEnv(
|
||||
pip={"packages":["tensorflow", "requests"], "pip_check": False,
|
||||
"pip_version": "==22.0.2;python_version=='3.8.11'"})
|
||||
|
||||
# Example for using image_uri
|
||||
RuntimeEnv(
|
||||
image_uri="rayproject/ray:2.39.0-py312-cu123")
|
||||
|
||||
Args:
|
||||
py_modules: List of local paths or remote URIs (either in the GCS or external
|
||||
storage), each of which is an archive that Ray unpacks and
|
||||
inserts into the PYTHONPATH of the workers. Supported formats for
|
||||
remote URIs: ``.zip``, ``.whl``, ``.tar.gz``, and ``.tgz``.
|
||||
py_executable: Path or command to the Python executable that Ray uses to
|
||||
launch worker processes. By default, Ray uses the same interpreter
|
||||
that is running the driver.
|
||||
working_dir: Local path or remote URI (either in the GCS or external storage) of an
|
||||
archive that Ray unpacks in the directory of each task/actor.
|
||||
Supported formats for remote URIs: ``.zip``, ``.tar.gz``, and ``.tgz``.
|
||||
pip: Either a list of pip packages, a string
|
||||
containing the path to a pip requirements.txt file, or a Python
|
||||
dictionary that has three fields: 1) ``packages`` (required, List[str]): a
|
||||
list of pip packages, 2) ``pip_check`` (optional, bool): whether enable
|
||||
pip check at the end of pip install, defaults to False.
|
||||
3) ``pip_version`` (optional, str): the version of pip, Ray prepends
|
||||
the package name "pip" in front of the ``pip_version`` to form the final
|
||||
requirement string, the syntax of a requirement specifier is defined in
|
||||
full in PEP 508.
|
||||
conda: Either the conda YAML config, the name of a
|
||||
local conda env (e.g., "pytorch_p36"), or the path to a conda
|
||||
environment.yaml file.
|
||||
Ray automatically injects the dependency into the conda
|
||||
env to ensure compatibility with the cluster Ray. Ray may automatically
|
||||
mangle the conda name to avoid conflicts between runtime envs.
|
||||
This field can't be specified at the same time as the 'pip' field.
|
||||
To use pip with conda, specify your pip dependencies within
|
||||
the conda YAML config:
|
||||
https://conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html#create-env-file-manually
|
||||
container: Require a given Docker container image,
|
||||
The Ray worker process runs in a container with this image.
|
||||
This parameter only works alone, or with the ``config`` or
|
||||
``env_vars`` parameters.
|
||||
The `run_options` list spec is here:
|
||||
https://docs.docker.com/engine/reference/run/
|
||||
env_vars: Environment variables to set.
|
||||
worker_process_setup_hook: Experimental. The setup hook that's
|
||||
called after workers start and before Tasks and Actors are scheduled.
|
||||
A module name (string type) or callable (function) can be passed.
|
||||
When a module name is passed, Ray worker should be able to access the
|
||||
module name. When a callable is passed, callable should be serializable.
|
||||
When a runtime env is specified by job submission API,
|
||||
only a module name (string) is allowed.
|
||||
nsight: Dictionary mapping nsight profile option name to it's value.
|
||||
rocprof_sys: Dictionary mapping rocprof-sys profile option name and environment
|
||||
variables to it's value.
|
||||
config: config for runtime environment. Either
|
||||
a dict or a RuntimeEnvConfig. Field: (1) setup_timeout_seconds, the
|
||||
timeout of runtime environment creation, timeout is in seconds.
|
||||
_validate: Whether to validate the runtime environment when the
|
||||
``RuntimeEnv`` is constructed. Disabling validation defers checks
|
||||
to the worker side.
|
||||
image_uri: URI to a container image. The Ray worker process runs
|
||||
in a container with this image. This parameter only works alone,
|
||||
or with the ``config`` or ``env_vars`` parameters.
|
||||
uv: Either a list of pip packages, or a Python dictionary that has one field:
|
||||
1) ``packages`` (required, List[str]).
|
||||
**kwargs: Additional runtime environment fields that are forwarded
|
||||
verbatim to the underlying runtime environment dictionary.
|
||||
"""
|
||||
|
||||
known_fields: Set[str] = {
|
||||
"py_modules",
|
||||
"py_executable",
|
||||
"java_jars",
|
||||
"working_dir",
|
||||
"conda",
|
||||
"pip",
|
||||
"uv",
|
||||
"container",
|
||||
"excludes",
|
||||
"env_vars",
|
||||
"_ray_release",
|
||||
"_ray_commit",
|
||||
"_inject_current_ray",
|
||||
"config",
|
||||
"worker_process_setup_hook",
|
||||
"_nsight",
|
||||
"_rocprof_sys",
|
||||
"image_uri",
|
||||
}
|
||||
|
||||
extensions_fields: Set[str] = {
|
||||
"_ray_release",
|
||||
"_ray_commit",
|
||||
"_inject_current_ray",
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
py_modules: Optional[List[str]] = None,
|
||||
py_executable: Optional[str] = None,
|
||||
working_dir: Optional[str] = None,
|
||||
pip: Optional[List[str]] = None,
|
||||
conda: Optional[Union[Dict[str, str], str]] = None,
|
||||
container: Optional[Dict[str, str]] = None,
|
||||
env_vars: Optional[Dict[str, str]] = None,
|
||||
worker_process_setup_hook: Optional[Union[Callable, str]] = None,
|
||||
nsight: Optional[Union[str, Dict[str, str]]] = None,
|
||||
rocprof_sys: Optional[Union[str, Dict[str, Dict[str, str]]]] = None,
|
||||
config: Optional[Union[Dict, RuntimeEnvConfig]] = None,
|
||||
_validate: bool = True,
|
||||
image_uri: Optional[str] = None,
|
||||
uv: Optional[List[str]] = None,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
runtime_env = kwargs
|
||||
if py_modules is not None:
|
||||
runtime_env["py_modules"] = py_modules
|
||||
if py_executable is not None:
|
||||
runtime_env["py_executable"] = py_executable
|
||||
if working_dir is not None:
|
||||
runtime_env["working_dir"] = working_dir
|
||||
if pip is not None:
|
||||
runtime_env["pip"] = pip
|
||||
if uv is not None:
|
||||
runtime_env["uv"] = uv
|
||||
if conda is not None:
|
||||
runtime_env["conda"] = conda
|
||||
if nsight is not None:
|
||||
runtime_env["_nsight"] = nsight
|
||||
if rocprof_sys is not None:
|
||||
runtime_env["_rocprof_sys"] = rocprof_sys
|
||||
if container is not None:
|
||||
runtime_env["container"] = container
|
||||
if env_vars is not None:
|
||||
runtime_env["env_vars"] = env_vars
|
||||
if config is not None:
|
||||
runtime_env["config"] = config
|
||||
if worker_process_setup_hook is not None:
|
||||
runtime_env["worker_process_setup_hook"] = worker_process_setup_hook
|
||||
if image_uri is not None:
|
||||
runtime_env["image_uri"] = image_uri
|
||||
|
||||
self.update(runtime_env)
|
||||
|
||||
# Blindly trust that the runtime_env has already been validated.
|
||||
# This is dangerous and should only be used internally (e.g., on the
|
||||
# deserialization codepath.
|
||||
if not _validate:
|
||||
return
|
||||
|
||||
if (self.get("conda") is not None) + (self.get("pip") is not None) + (
|
||||
self.get("uv") is not None
|
||||
) > 1:
|
||||
raise ValueError(
|
||||
"The 'pip' field, 'uv' field, and 'conda' field of "
|
||||
"runtime_env cannot be specified at the same time.\n"
|
||||
f"specified pip field: {self.get('pip')}\n"
|
||||
f"specified conda field: {self.get('conda')}\n"
|
||||
f"specified uv field: {self.get('uv')}\n"
|
||||
"To use pip with conda, please only set the 'conda'"
|
||||
"field, and specify your pip dependencies within the conda YAML "
|
||||
"config dict: see https://conda.io/projects/conda/en/latest/"
|
||||
"user-guide/tasks/manage-environments.html"
|
||||
"#create-env-file-manually"
|
||||
)
|
||||
|
||||
if self.get("container"):
|
||||
invalid_keys = set(runtime_env.keys()) - {"container", "config", "env_vars"}
|
||||
if len(invalid_keys):
|
||||
raise ValueError(
|
||||
"The 'container' field currently cannot be used "
|
||||
"together with other fields of runtime_env. "
|
||||
f"Specified fields: {invalid_keys}"
|
||||
)
|
||||
|
||||
if self.get("image_uri"):
|
||||
image_uri_plugin_cls = get_image_uri_plugin_cls()
|
||||
invalid_keys = (
|
||||
set(runtime_env.keys()) - image_uri_plugin_cls.get_compatible_keys()
|
||||
)
|
||||
if len(invalid_keys):
|
||||
raise ValueError(
|
||||
"The 'image_uri' field currently cannot be used "
|
||||
"together with other fields of runtime_env. "
|
||||
f"Specified fields: {invalid_keys}"
|
||||
)
|
||||
|
||||
for option, validate_fn in OPTION_TO_VALIDATION_FN.items():
|
||||
option_val = self.get(option)
|
||||
if option_val is not None:
|
||||
del self[option]
|
||||
self[option] = option_val
|
||||
|
||||
if "_ray_commit" not in self:
|
||||
if self.get("pip") or self.get("conda"):
|
||||
self["_ray_commit"] = ray.__commit__
|
||||
|
||||
# Used for testing wheels that have not yet been merged into master.
|
||||
# If this is set to True, then we do not inject Ray into the conda
|
||||
# or pip dependencies.
|
||||
if "_inject_current_ray" not in self:
|
||||
if "RAY_RUNTIME_ENV_LOCAL_DEV_MODE" in os.environ:
|
||||
self["_inject_current_ray"] = True
|
||||
|
||||
# NOTE(architkulkarni): This allows worker caching code in C++ to check
|
||||
# if a runtime env is empty without deserializing it. This is a catch-
|
||||
# all; for validated inputs we won't set the key if the value is None.
|
||||
if all(val is None for val in self.values()):
|
||||
self.clear()
|
||||
|
||||
def __setitem__(self, key: str, value: Any) -> None:
|
||||
if is_dataclass(value):
|
||||
jsonable_type = asdict(value)
|
||||
else:
|
||||
jsonable_type = value
|
||||
RuntimeEnvPluginSchemaManager.validate(key, jsonable_type)
|
||||
res_value = jsonable_type
|
||||
if key in RuntimeEnv.known_fields and key in OPTION_TO_VALIDATION_FN:
|
||||
res_value = OPTION_TO_VALIDATION_FN[key](jsonable_type)
|
||||
if res_value is None:
|
||||
return
|
||||
return super().__setitem__(key, res_value)
|
||||
|
||||
def set(self, name: str, value: Any) -> None:
|
||||
self.__setitem__(name, value)
|
||||
|
||||
def get(self, name, default=None, data_class=None):
|
||||
if name not in self:
|
||||
return default
|
||||
if not data_class:
|
||||
return self.__getitem__(name)
|
||||
else:
|
||||
return from_dict(data_class=data_class, data=self.__getitem__(name))
|
||||
|
||||
@classmethod
|
||||
def deserialize(cls, serialized_runtime_env: str) -> "RuntimeEnv": # noqa: F821
|
||||
return cls(_validate=False, **json.loads(serialized_runtime_env))
|
||||
|
||||
def serialize(self) -> str:
|
||||
# To ensure the accuracy of Proto, `__setitem__` can only guarantee the
|
||||
# accuracy of a certain field, not the overall accuracy
|
||||
runtime_env = type(self)(_validate=True, **self)
|
||||
return json.dumps(
|
||||
runtime_env,
|
||||
sort_keys=True,
|
||||
)
|
||||
|
||||
def to_dict(self) -> Dict:
|
||||
runtime_env_dict = dict(deepcopy(self))
|
||||
|
||||
# Replace strongly-typed RuntimeEnvConfig with a dict to allow the returned
|
||||
# dict to work properly as a field in a dataclass. Details in issue #26986
|
||||
if runtime_env_dict.get("config"):
|
||||
runtime_env_dict["config"] = runtime_env_dict["config"].to_dict()
|
||||
|
||||
return runtime_env_dict
|
||||
|
||||
def has_working_dir(self) -> bool:
|
||||
return self.get("working_dir") is not None
|
||||
|
||||
def working_dir_uri(self) -> Optional[str]:
|
||||
return self.get("working_dir")
|
||||
|
||||
def py_modules_uris(self) -> List[str]:
|
||||
if "py_modules" in self:
|
||||
return list(self["py_modules"])
|
||||
return []
|
||||
|
||||
def conda_uri(self) -> Optional[str]:
|
||||
if "conda" in self:
|
||||
return get_conda_uri(self)
|
||||
return None
|
||||
|
||||
def pip_uri(self) -> Optional[str]:
|
||||
if "pip" in self:
|
||||
return get_pip_uri(self)
|
||||
return None
|
||||
|
||||
def uv_uri(self) -> Optional[str]:
|
||||
if "uv" in self:
|
||||
return get_uv_uri(self)
|
||||
return None
|
||||
|
||||
def plugin_uris(self) -> List[str]:
|
||||
"""Not implemented yet, always return a empty list"""
|
||||
return []
|
||||
|
||||
def working_dir(self) -> str:
|
||||
return self.get("working_dir", "")
|
||||
|
||||
def py_modules(self) -> List[str]:
|
||||
if "py_modules" in self:
|
||||
return list(self["py_modules"])
|
||||
return []
|
||||
|
||||
def py_executable(self) -> Optional[str]:
|
||||
return self.get("py_executable", None)
|
||||
|
||||
def java_jars(self) -> List[str]:
|
||||
if "java_jars" in self:
|
||||
return list(self["java_jars"])
|
||||
return []
|
||||
|
||||
def nsight(self) -> Optional[Union[str, Dict[str, str]]]:
|
||||
return self.get("_nsight", None)
|
||||
|
||||
def rocprof_sys(self) -> Optional[Union[str, Dict[str, Dict[str, str]]]]:
|
||||
return self.get("_rocprof_sys", None)
|
||||
|
||||
def env_vars(self) -> Dict:
|
||||
return self.get("env_vars", {})
|
||||
|
||||
def has_conda(self) -> str:
|
||||
if self.get("conda"):
|
||||
return True
|
||||
return False
|
||||
|
||||
def conda_env_name(self) -> str:
|
||||
if not self.has_conda() or not isinstance(self["conda"], str):
|
||||
return None
|
||||
return self["conda"]
|
||||
|
||||
def conda_config(self) -> str:
|
||||
if not self.has_conda() or not isinstance(self["conda"], dict):
|
||||
return None
|
||||
return json.dumps(self["conda"], sort_keys=True)
|
||||
|
||||
def has_pip(self) -> bool:
|
||||
if self.get("pip"):
|
||||
return True
|
||||
return False
|
||||
|
||||
def has_uv(self) -> bool:
|
||||
if self.get("uv"):
|
||||
return True
|
||||
return False
|
||||
|
||||
def virtualenv_name(self) -> Optional[str]:
|
||||
if not self.has_pip() or not isinstance(self["pip"], str):
|
||||
return None
|
||||
return self["pip"]
|
||||
|
||||
def pip_config(self) -> Dict:
|
||||
if not self.has_pip() or isinstance(self["pip"], str):
|
||||
return {}
|
||||
# Parse and validate field pip on method `__setitem__`
|
||||
self["pip"] = self["pip"]
|
||||
return self["pip"]
|
||||
|
||||
def uv_config(self) -> Dict:
|
||||
if not self.has_uv() or isinstance(self["uv"], str):
|
||||
return {}
|
||||
# Parse and validate field pip on method `__setitem__`
|
||||
self["uv"] = self["uv"]
|
||||
return self["uv"]
|
||||
|
||||
def get_extension(self, key) -> Optional[str]:
|
||||
if key not in RuntimeEnv.extensions_fields:
|
||||
raise ValueError(
|
||||
f"Extension key must be one of {RuntimeEnv.extensions_fields}, "
|
||||
f"got: {key}"
|
||||
)
|
||||
return self.get(key)
|
||||
|
||||
def has_py_container(self) -> bool:
|
||||
if self.get("container"):
|
||||
return True
|
||||
return False
|
||||
|
||||
def py_container_image(self) -> Optional[str]:
|
||||
if not self.has_py_container():
|
||||
return None
|
||||
return self["container"].get("image", "")
|
||||
|
||||
def py_container_worker_path(self) -> Optional[str]:
|
||||
if not self.has_py_container():
|
||||
return None
|
||||
return self["container"].get("worker_path", "")
|
||||
|
||||
def py_container_run_options(self) -> List:
|
||||
if not self.has_py_container():
|
||||
return None
|
||||
return self["container"].get("run_options", [])
|
||||
|
||||
def image_uri(self) -> Optional[str]:
|
||||
return self.get("image_uri")
|
||||
|
||||
def plugins(self) -> List[Tuple[str, Any]]:
|
||||
result = list()
|
||||
for key, value in self.items():
|
||||
if key not in self.known_fields:
|
||||
result.append((key, value))
|
||||
return result
|
||||
|
||||
|
||||
def _validate_no_local_paths(runtime_env: RuntimeEnv):
|
||||
"""Checks that options such as working_dir and py_modules only contain URIs."""
|
||||
if not isinstance(runtime_env, RuntimeEnv):
|
||||
raise TypeError(
|
||||
f"Expected type to be RuntimeEnv but received {type(runtime_env)} instead."
|
||||
)
|
||||
for option, validate_fn in OPTION_TO_NO_PATH_VALIDATION_FN.items():
|
||||
option_val = runtime_env.get(option)
|
||||
if option_val:
|
||||
validate_fn(option_val)
|
||||
|
||||
|
||||
def _merge_runtime_env(
|
||||
parent: Optional[RuntimeEnv],
|
||||
child: Optional[RuntimeEnv],
|
||||
override: bool = False,
|
||||
) -> Optional[RuntimeEnv]:
|
||||
"""Merge the parent and child runtime environments.
|
||||
|
||||
If override = True, the child's runtime env overrides the parent's
|
||||
runtime env in the event of a conflict.
|
||||
|
||||
Merging happens per key (i.e., "conda", "pip", ...), but
|
||||
"env_vars" are merged per env var key.
|
||||
|
||||
It returns None if Ray fails to merge runtime environments because
|
||||
of a conflict and `override = False`.
|
||||
|
||||
Args:
|
||||
parent: Parent runtime env.
|
||||
child: Child runtime env.
|
||||
override: If True, the child's runtime env overrides
|
||||
conflicting fields.
|
||||
Returns:
|
||||
The merged runtime env's if Ray successfully merges them.
|
||||
None if the runtime env's conflict. Empty dict if
|
||||
parent and child are both None.
|
||||
"""
|
||||
if parent is None:
|
||||
parent = {}
|
||||
if child is None:
|
||||
child = {}
|
||||
|
||||
parent = deepcopy(parent)
|
||||
child = deepcopy(child)
|
||||
parent_env_vars = parent.pop("env_vars", {})
|
||||
child_env_vars = child.pop("env_vars", {})
|
||||
|
||||
if not override:
|
||||
if set(parent.keys()).intersection(set(child.keys())):
|
||||
return None
|
||||
if set(parent_env_vars.keys()).intersection(set(child_env_vars.keys())): # noqa
|
||||
return None
|
||||
|
||||
parent.update(child)
|
||||
parent_env_vars.update(child_env_vars)
|
||||
if parent_env_vars:
|
||||
parent["env_vars"] = parent_env_vars
|
||||
|
||||
return parent
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "http://github.com/ray-project/ray/runtime_env/pip_schema.json",
|
||||
"title": "pip",
|
||||
"description": "A pip environment specification.",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"packages": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "a list of pip packages"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "the path to a pip `requirements.txt` file"
|
||||
}
|
||||
]
|
||||
},
|
||||
"pip_check": {
|
||||
"type": "boolean",
|
||||
"description": "whether to enable pip check at the end of pip install"
|
||||
},
|
||||
"pip_version": {
|
||||
"type": "string",
|
||||
"description": "the version of pip"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"packages"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "the path to a pip `requirements.txt` file"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "a list of pip requirements specifiers"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "http://github.com/ray-project/ray/runtime_env/working_dir_schema.json",
|
||||
"title": "working_dir",
|
||||
"type": "string",
|
||||
"description": "Specifies the working directory for the Ray workers."
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import List
|
||||
|
||||
|
||||
@dataclass
|
||||
class Pip:
|
||||
packages: List[str]
|
||||
pip_check: bool = False
|
||||
Reference in New Issue
Block a user