Skip to content

Commit 74a1120

Browse files
authored
🐛 fir working_dir and 📖 add docker example (#42)
1 parent dc5a726 commit 74a1120

30 files changed

Lines changed: 581 additions & 48 deletions

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ There are different options available for running Dagster code on Ray. The follo
4545
| Enabled per-asset |||||
4646
| Configurable per-asset |||||
4747
| Runs asset/op body on Ray |||||
48+
| Requires configuring Dagster in the Ray cluster |||||
4849

4950
# Examples
5051

@@ -78,8 +79,9 @@ run_launcher:
7879
module: dagster_ray
7980
class: RayRunLauncher
8081
config:
81-
num_cpus: 1
82-
num_gpus: 0
82+
ray:
83+
num_cpus: 1
84+
num_gpus: 0
8385
```
8486
8587
Individual Runs can **override** Ray configuration:

dagster_ray/executor.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,17 @@
2525
from dagster_ray.config import RayExecutionConfig, RayJobSubmissionClientConfig
2626
from dagster_ray.kuberay.resources import get_k8s_object_name
2727
from dagster_ray.run_launcher import RayRunLauncher
28+
from dagster_ray.utils import resolve_env_vars_list
2829

2930
if TYPE_CHECKING:
3031
from ray.job_submission import JobSubmissionClient
3132

3233

3334
class RayExecutorConfig(RayExecutionConfig, RayJobSubmissionClientConfig):
35+
env_vars: Optional[List[str]] = Field(
36+
default=None,
37+
description="A list of environment variables to inject into the Job. Each can be of the form KEY=VALUE or just KEY (in which case the value will be pulled from the current process).",
38+
)
3439
address: Optional[str] = Field(default=None, description="The address of the Ray cluster to connect to.") # type: ignore
3540
# sorry for the long name, but it has to be very clear what this is doing
3641
inherit_job_submission_client_from_ray_run_launcher: bool = True
@@ -74,6 +79,7 @@ def ray_executor(init_context: InitExecutorContext) -> Executor:
7479
return StepDelegatingExecutor(
7580
RayStepHandler(
7681
client=client,
82+
env_vars=ray_cfg.env_vars,
7783
runtime_env=ray_cfg.runtime_env,
7884
num_cpus=ray_cfg.num_cpus,
7985
num_gpus=ray_cfg.num_gpus,
@@ -95,6 +101,7 @@ def name(self):
95101
def __init__(
96102
self,
97103
client: "JobSubmissionClient",
104+
env_vars: Optional[List[str]],
98105
runtime_env: Optional[Dict[str, Any]],
99106
num_cpus: Optional[float],
100107
num_gpus: Optional[float],
@@ -104,6 +111,7 @@ def __init__(
104111
super().__init__()
105112

106113
self.client = client
114+
self.env_vars = env_vars or []
107115
self.runtime_env = runtime_env or {}
108116
self.num_cpus = num_cpus
109117
self.num_gpus = num_gpus
@@ -148,6 +156,7 @@ def launch_step(self, step_handler_context: StepHandlerContext) -> Iterator[Dags
148156

149157
user_provided_config = RayExecutionConfig.from_tags({**step_handler_context.step_tags[step_key]})
150158

159+
# note! ray modifies the user-provided runtime_env, so we copy it
151160
runtime_env = (user_provided_config.runtime_env or self.runtime_env).copy()
152161

153162
dagster_env_vars = {
@@ -158,6 +167,8 @@ def launch_step(self, step_handler_context: StepHandlerContext) -> Iterator[Dags
158167

159168
runtime_env["env_vars"] = {**dagster_env_vars, **runtime_env.get("env_vars", {})} # type: ignore
160169

170+
runtime_env["env_vars"].update(resolve_env_vars_list(self.env_vars))
171+
161172
num_cpus = self.num_cpus or user_provided_config.num_cpus
162173
num_gpus = self.num_gpus or user_provided_config.num_gpus
163174
memory = self.memory or user_provided_config.memory

dagster_ray/io_manager.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ def handle_output(self, context: OutputContext, obj):
7878

7979
object_map.set.remote(storage_key, ref)
8080

81-
context.log.debug(f"Stored object with key {storage_key} as {ref}")
81+
context.log.debug(f"[RayIOManager] Stored object with key {storage_key} as {ref}")
8282

8383
def load_input(self, context: InputContext):
8484
import ray
@@ -100,11 +100,11 @@ def load_input(self, context: InputContext):
100100
else:
101101
storage_key = self._get_single_key(context)
102102

103-
context.log.debug(f"Loading object with key {storage_key}")
103+
context.log.debug(f"[RayIOManager] Loading object with key {storage_key}")
104104

105105
ref = object_map.get.remote(storage_key)
106106

107-
assert ref is not None, f"Object with key {storage_key} not found in RayObjectMap"
107+
assert ref is not None, f"[RayIOManager] Object with key {storage_key} not found in RayObjectMap"
108108

109109
return ray.get(ref)
110110

@@ -120,4 +120,6 @@ def _get_multiple_keys(self, context: InputContext) -> Dict[str, str]:
120120
partition_key: "/".join(asset_path + [partition_key]) for partition_key in context.asset_partition_keys
121121
}
122122
else:
123-
raise RuntimeError("This method can only be called with an InputContext that has multiple partitions")
123+
raise RuntimeError(
124+
"[RayIOManager] This method can only be called with an InputContext that has multiple partitions"
125+
)

dagster_ray/kuberay/client/base.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
import time
22
from typing import TYPE_CHECKING, Any, Dict, Optional
33

4-
from kubernetes import client
5-
from kubernetes.client import ApiException
6-
74
if TYPE_CHECKING:
5+
from kubernetes import client
86
from kubernetes.client.models.v1_endpoints import V1Endpoints
97

108

@@ -27,8 +25,10 @@ def __init__(
2725
version: str,
2826
kind: str,
2927
plural: str,
30-
api_client: Optional[client.ApiClient] = None,
28+
api_client: Optional["client.ApiClient"] = None,
3129
):
30+
from kubernetes import client
31+
3232
self.group = group
3333
self.version = version
3434
self.kind = kind
@@ -38,6 +38,8 @@ def __init__(
3838
self._core_v1_api = client.CoreV1Api(api_client=api_client)
3939

4040
def wait_for_service_endpoints(self, service_name: str, namespace: str, poll_interval: int = 5, timeout: int = 60):
41+
from kubernetes.client import ApiException
42+
4143
start_time = time.time()
4244

4345
while True:
@@ -62,6 +64,8 @@ def wait_for_service_endpoints(self, service_name: str, namespace: str, poll_int
6264
time.sleep(poll_interval)
6365

6466
def get_status(self, name: str, namespace: str, timeout: int = 60, poll_interval: int = 5) -> Dict[str, Any]:
67+
from kubernetes.client import ApiException
68+
6569
while timeout > 0:
6670
try:
6771
resource: Any = self._api.get_namespaced_custom_object_status(
@@ -83,6 +87,8 @@ def get_status(self, name: str, namespace: str, timeout: int = 60, poll_interval
8387
raise TimeoutError(f"Timed out waiting for status of {self.kind} {name} in namespace {namespace}")
8488

8589
def list(self, namespace: str, label_selector: str = "", async_req: bool = False) -> Dict[str, Any]:
90+
from kubernetes.client import ApiException
91+
8692
try:
8793
resource: Any = self._api.list_namespaced_custom_object(
8894
group=self.group,
@@ -103,6 +109,8 @@ def list(self, namespace: str, label_selector: str = "", async_req: bool = False
103109
raise
104110

105111
def get(self, name: str, namespace: str) -> Dict[str, Any]:
112+
from kubernetes.client import ApiException
113+
106114
try:
107115
resource: Any = self._api.get_namespaced_custom_object(
108116
group=self.group,

dagster_ray/kuberay/client/raycluster/client.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
cast,
1818
)
1919

20-
from kubernetes import watch
2120
from typing_extensions import NotRequired
2221

2322
from dagster_ray.kuberay.client.base import BaseKubeRayClient
@@ -104,6 +103,8 @@ def wait_until_ready(
104103
timeout: int,
105104
image: Optional[str] = None,
106105
) -> Tuple[str, Dict[str, str]]:
106+
from kubernetes import watch
107+
107108
"""
108109
If ready, returns service ip address and a dictionary of ports.
109110
Dictionary keys: ["client", "dashboard", "metrics", "redis", "serve"]

dagster_ray/kuberay/resources.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,9 @@
44
import re
55
import string
66
import sys
7-
from typing import Any, Dict, Generator, Optional, cast
7+
from typing import TYPE_CHECKING, Any, Dict, Generator, Optional, cast
88

99
import dagster._check as check
10-
import kubernetes
1110
from dagster import ConfigurableResource, InitResourceContext
1211
from dagster._annotations import experimental
1312
from pydantic import Field, PrivateAttr
@@ -30,15 +29,18 @@
3029
from dagster_ray._base.resources import BaseRayResource
3130
from dagster_ray.kuberay.client.base import load_kubeconfig
3231

32+
if TYPE_CHECKING:
33+
import kubernetes
34+
3335

3436
@experimental
3537
class RayClusterClientResource(ConfigurableResource):
3638
kube_context: Optional[str] = None
3739
kubeconfig_file: Optional[str] = None
3840

3941
_raycluster_client: RayClusterClient = PrivateAttr()
40-
_k8s_api: kubernetes.client.CustomObjectsApi = PrivateAttr()
41-
_k8s_core_api: kubernetes.client.CoreV1Api = PrivateAttr()
42+
_k8s_api: "kubernetes.client.CustomObjectsApi" = PrivateAttr()
43+
_k8s_core_api: "kubernetes.client.CoreV1Api" = PrivateAttr()
4244

4345
@property
4446
def client(self) -> RayClusterClient:
@@ -47,18 +49,20 @@ def client(self) -> RayClusterClient:
4749
return self._raycluster_client
4850

4951
@property
50-
def k8s(self) -> kubernetes.client.CustomObjectsApi:
52+
def k8s(self) -> "kubernetes.client.CustomObjectsApi":
5153
if self._k8s_api is None:
5254
raise ValueError(f"{self.__class__.__name__} not initialized")
5355
return self._k8s_api
5456

5557
@property
56-
def k8s_core(self) -> kubernetes.client.CoreV1Api:
58+
def k8s_core(self) -> "kubernetes.client.CoreV1Api":
5759
if self._k8s_core_api is None:
5860
raise ValueError(f"{self.__class__.__name__} not initialized")
5961
return self._k8s_core_api
6062

6163
def setup_for_execution(self, context: InitResourceContext) -> None:
64+
import kubernetes
65+
6266
load_kubeconfig(context=self.kube_context, config_file=self.kubeconfig_file)
6367

6468
self._raycluster_client = RayClusterClient(context=self.kube_context, config_file=self.kubeconfig_file)
@@ -218,6 +222,8 @@ def update_group_spec(group_spec: Dict[str, Any]):
218222
}
219223

220224
def _wait_raycluster_ready(self):
225+
import kubernetes
226+
221227
self.client.client.wait_until_ready(self.cluster_name, namespace=self.namespace, timeout=self.timeout)
222228

223229
# the above code only checks for RayCluster creation

dagster_ray/run_launcher.py

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import logging
22
import sys
3-
from typing import Any, Dict, Optional
3+
from typing import TYPE_CHECKING, Any, Dict, List, Optional
44

55
from dagster import _check as check
66
from dagster._cli.api import ExecuteRunArgs # type: ignore
@@ -12,15 +12,24 @@
1212
from dagster._grpc.types import ResumeRunArgs
1313
from dagster._serdes import ConfigurableClass, ConfigurableClassData
1414
from dagster._utils.error import serializable_error_info_from_exc_info
15+
from pydantic import Field
1516

1617
from dagster_ray.config import RayExecutionConfig, RayJobSubmissionClientConfig
18+
from dagster_ray.utils import resolve_env_vars_list
19+
20+
if TYPE_CHECKING:
21+
from ray.job_submission import JobSubmissionClient
1722

1823

1924
def get_job_submission_id_from_run_id(run_id: str, resume_attempt_number=None):
2025
return f"dagster-run-{run_id}" + ("" if not resume_attempt_number else f"-{resume_attempt_number}")
2126

2227

23-
class RayLauncherConfig(RayExecutionConfig, RayJobSubmissionClientConfig): ...
28+
class RayLauncherConfig(RayExecutionConfig, RayJobSubmissionClientConfig):
29+
env_vars: Optional[List[str]] = Field(
30+
default=None,
31+
description="A list of environment variables to inject into the Job. Each can be of the form KEY=VALUE or just KEY (in which case the value will be pulled from the current process).",
32+
)
2433

2534

2635
class RayRunLauncher(RunLauncher, ConfigurableClass):
@@ -30,6 +39,7 @@ def __init__(
3039
metadata: Optional[Dict[str, Any]] = None,
3140
headers: Optional[Dict[str, Any]] = None,
3241
cookies: Optional[Dict[str, Any]] = None,
42+
env_vars: Optional[List[str]] = None,
3343
runtime_env: Optional[Dict[str, Any]] = None,
3444
num_cpus: Optional[int] = None,
3545
num_gpus: Optional[int] = None,
@@ -55,17 +65,13 @@ def __init__(
5565
Fields such as `num_cpus` set via `dagster-ray/config` Run tag will override the yaml configuration.
5666
5767
"""
58-
59-
from ray.job_submission import JobSubmissionClient
60-
6168
self._inst_data = check.opt_inst_param(inst_data, "inst_data", ConfigurableClassData)
6269

63-
self.client = JobSubmissionClient(address, metadata=metadata, headers=headers, cookies=cookies)
64-
6570
self.address = address
6671
self.metadata = metadata
6772
self.headers = headers
6873
self.cookies = cookies
74+
self.env_vars = env_vars
6975
self.runtime_env = runtime_env
7076
self.num_cpus = num_cpus
7177
self.num_gpus = num_gpus
@@ -74,17 +80,23 @@ def __init__(
7480

7581
super().__init__()
7682

83+
@property
84+
def client(self) -> "JobSubmissionClient": # note: this must be a property
85+
from ray.job_submission import JobSubmissionClient
86+
87+
return JobSubmissionClient(self.address, metadata=self.metadata, headers=self.headers, cookies=self.cookies)
88+
7789
@property
7890
def inst_data(self) -> Optional[ConfigurableClassData]:
7991
return self._inst_data
8092

8193
@classmethod
8294
def config_type(cls) -> UserConfigSchema:
83-
return RayLauncherConfig.to_fields_dict()
95+
return {"ray": RayLauncherConfig.to_config_schema().as_field()}
8496

8597
@classmethod
8698
def from_config_value(cls, inst_data, config_value):
87-
return cls(inst_data=inst_data, **config_value)
99+
return cls(inst_data=inst_data, **config_value["ray"])
88100

89101
@property
90102
def supports_resume_run(self):
@@ -132,13 +144,16 @@ def _launch_ray_job(self, submission_id: str, entrypoint: str, run: DagsterRun):
132144

133145
cfg_from_tags = RayLauncherConfig.from_tags(run.tags)
134146

135-
runtime_env = cfg_from_tags.runtime_env or self.runtime_env or {}
147+
env_vars = cfg_from_tags.env_vars or self.env_vars or []
148+
# note! ray modifies the user-provided runtime_env, so we copy it
149+
runtime_env = (cfg_from_tags.runtime_env or self.runtime_env or {}).copy()
136150
num_cpus = cfg_from_tags.num_cpus or self.num_cpus
137151
num_gpus = cfg_from_tags.num_gpus or self.num_gpus
138152
memory = cfg_from_tags.memory or self.memory
139153
resources = cfg_from_tags.resources or self.resources
140154

141155
runtime_env["env_vars"] = runtime_env.get("env_vars", {})
156+
runtime_env["env_vars"].update(resolve_env_vars_list(env_vars))
142157
runtime_env["env_vars"].update(
143158
{
144159
"DAGSTER_RUN_JOB_NAME": job_origin.job_name,

dagster_ray/utils.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import os
2+
from typing import Dict, List, Optional
3+
4+
5+
def resolve_env_vars_list(env_vars: Optional[List[str]]) -> Dict[str, str]:
6+
res = {}
7+
8+
if env_vars is not None:
9+
for env_var in env_vars:
10+
if "=" in env_var:
11+
var, value = env_var.split("=", 1)
12+
res[var] = value
13+
else:
14+
if value := os.getenv(env_var):
15+
res[env_var] = value
16+
17+
return res

0 commit comments

Comments
 (0)