Skip to content

Commit f14b80a

Browse files
authored
⭐ 💥 support RayJob via Dagster Pipes; rework of everything (#10)
⭐ 💥 support RayJob via Dagster Pipes; small breaking API changes
1 parent 47d3a64 commit f14b80a

30 files changed

Lines changed: 1830 additions & 1000 deletions

.github/workflows/CI.yml

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,24 @@ concurrency:
1616

1717
jobs:
1818
test:
19-
name: test ${{ matrix.py }} - ${{ matrix.os }}
19+
name: Test Python ${{ matrix.py }} - Kubernetes ${{ matrix.kubernetes }}
2020
runs-on: ${{ matrix.os }}-latest
2121
strategy:
2222
fail-fast: false
2323
matrix:
2424
os:
2525
- Ubuntu
2626
py:
27-
# - "3.12"
27+
#- "3.12"
2828
- "3.11"
2929
- "3.10"
3030
- "3.9"
31-
# - "3.8"
31+
kubernetes:
32+
- "1.31.0"
33+
- "1.30.0"
34+
- "1.29.0"
3235
steps:
33-
- name: Setup python for test ${{ matrix.py }}
36+
- name: Setup Python
3437
uses: actions/setup-python@v4
3538
with:
3639
python-version: ${{ matrix.py }}
@@ -54,7 +57,8 @@ jobs:
5457
run: poetry install --all-extras --sync
5558
- name: Run tests
5659
env:
57-
PYTEST_KUBERAY_VERSIONS: "1.0.0,1.1.0" # will run tests for all these KubeRay versions
60+
PYTEST_KUBERAY_VERSIONS: "1.2.0" # will run tests for all these KubeRay versions
61+
PYTEST_KUBERNETES_VERSION: ${{ matrix.kubernetes }}
5862
run: pytest -v .
5963

6064
lint:
@@ -66,11 +70,10 @@ jobs:
6670
os:
6771
- Ubuntu
6872
py:
69-
# - "3.12"
73+
#- "3.12"
7074
- "3.11"
7175
- "3.10"
7276
- "3.9"
73-
# - "3.8"
7477
steps:
7578
- name: Setup python for test ${{ matrix.py }}
7679
uses: actions/setup-python@v4

Dockerfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ ENV DEBIAN_FRONTEND=noninteractive
1010
RUN --mount=type=cache,target=/var/cache/apt \
1111
apt-get update && apt-get install -y git jq curl gcc python3-dev libpq-dev wget
1212

13+
COPY --from=bitnami/kubectl:1.30.3 /opt/bitnami/kubectl/bin/kubectl /usr/local/bin/
14+
1315
# install poetry
1416
ENV PYTHONUNBUFFERED=1 \
1517
PYTHONDONTWRITEBYTECODE=1 \

README.md

Lines changed: 81 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ The following backends are implemented:
2525
Documentation can be found below.
2626

2727
> [!NOTE]
28-
> This project is in early development. Contributions are very welcome! See the [Development](#development) section below.
28+
> This project is in early development. APIs are unstable and can change at any time. Contributions are very welcome! See the [Development](#development) section below.
2929
3030
# Installation
3131

@@ -128,10 +128,83 @@ definitions = Definitions(
128128

129129
This backend requires a Kubernetes cluster with the `KubeRay Operator` installed.
130130

131-
Integrates with [Dagster+](https://dagster.io/plus) by injecting environment variables such as `DAGSTER_CLOUD_DEPLOYMENT_NAME` and tags such as `dagster/user` into default configuration values and `RayCluster` labels.
131+
Integrates with [Dagster+](https://dagster.io/plus) by injecting environment variables such as `DAGSTER_CLOUD_DEPLOYMENT_NAME` and tags such as `dagster/user` into default configuration values and Kubernetes labels.
132+
133+
To run `ray` code in client mode (from the Dagster Python process directly), use the `KubeRayClient` resource (see the [KubeRayCluster](#KubeRayCluster) section).
134+
To run `ray` code in job mode, use the `PipesRayJobClient` with Dagster Pipes (see the [Pipes](#pipes) section).
132135

133136
The public objects can be imported from `dagster_ray.kuberay` module.
134137

138+
### Pipes
139+
140+
`dagster-ray` provides the `PipesRayJobClient` which can be used to execute remote Ray jobs on Kubernetes and receive Dagster events and logs from them.
141+
[RayJob](https://docs.ray.io/en/latest/cluster/kubernetes/getting-started/rayjob-quick-start.html) will manage the lifecycle of the underlying `RayCluster`, which will be cleaned up after the specified entrypoint exits.
142+
143+
Examples:
144+
145+
On the orchestration side, import the `PipesRayJobClient` and invoke it inside an `@op` or an `@asset`:
146+
147+
```python
148+
from dagster import AssetExecutionContext, Definitions, asset
149+
150+
from dagster_ray.kuberay import PipesRayJobClient
151+
152+
153+
@asset
154+
def my_asset(context: AssetExecutionContext, pipes_rayjob_client: PipesRayJobClient):
155+
pipes_rayjob_client.run(
156+
context=context,
157+
ray_job={
158+
# RayJob manifest goes here
159+
# .metadata.name is not required and will be generated if not provided
160+
# *.container.image is not required and will be set to the current `dagster/image` tag if not provided
161+
# full reference: https://ray-project.github.io/kuberay/reference/api/#rayjob
162+
...
163+
},
164+
extra={"foo": "bar"},
165+
)
166+
167+
168+
definitions = Definitions(
169+
resources={"pipes_rayjob_client": PipesRayJobClient()}, assets=[my_asset]
170+
)
171+
```
172+
173+
In the Ray job, import `dagster_pipes` (must be provided as a dependency) and emit regular Dagster events such as logs or asset materializations:
174+
175+
```python
176+
from dagster_pipes import open_dagster_pipes
177+
178+
179+
with open_dagster_pipes() as pipes:
180+
pipes.log.info("Hello from Ray Pipes!")
181+
pipes.report_asset_materialization(
182+
metadata={"some_metric": {"raw_value": 0, "type": "int"}},
183+
data_version="alpha",
184+
)
185+
```
186+
187+
A convenient way to provide `dagster-pipes` to the Ray job is with `runtimeEnvYaml` field:
188+
189+
```python
190+
import yaml
191+
192+
ray_job = {"spec": {"runtimeEnvYaml": yaml.safe_dump({"pip": ["dagster-pipes"]})}}
193+
```
194+
195+
The logs and events emitted by the Ray job will be captured by the `PipesRayJobClient` and will become available in the Dagster event log. Standard output and standard error streams will be forwarded to the standard output of the Dagster process.
196+
197+
198+
**Running locally**
199+
200+
When running locally, the `port_forward` option has to be set to `True` in the `PipesRayJobClient` resource in order to interact with the Ray job. For convenience, it can be set automatically with:
201+
202+
```python
203+
from dagster_ray.kuberay.configs import in_k8s
204+
205+
pipes_rayjob_client = PipesRayJobClient(..., port_forward=not in_k8s)
206+
```
207+
135208
### Resources
136209

137210
#### `KubeRayCluster`
@@ -188,7 +261,7 @@ ray_cluster = KubeRayCluster(
188261
)
189262
)
190263
```
191-
#### `KubeRayAPI`
264+
#### `KubeRayClient`
192265

193266
This resource can be used to interact with the Kubernetes API Server.
194267

@@ -198,14 +271,14 @@ Listing currently running `RayClusters`:
198271

199272
```python
200273
from dagster import op, Definitions
201-
from dagster_ray.kuberay import KubeRayAPI
274+
from dagster_ray.kuberay import KubeRayClient
202275

203276

204277
@op
205278
def list_ray_clusters(
206-
kube_ray_api: KubeRayAPI,
279+
kube_ray_client: KubeRayClient,
207280
):
208-
return kube_ray_api.kuberay.list_ray_clusters(k8s_namespace="kuberay")
281+
return kube_ray_client.client.list(namespace="kuberay")
209282
```
210283

211284
### Jobs
@@ -252,13 +325,13 @@ Running `pytest` will **automatically**:
252325
- build an image with the local `dagster-ray` code
253326
- start a `minikube` Kubernetes cluster
254327
- load the built `dagster-ray` and loaded `kuberay-operator` images into the cluster
255-
- install the `KubeRay Operator` in the cluster with `helm`
328+
- install `KubeRay Operator` into the cluster with `helm`
256329
- run the tests
257330

258331
Thus, no manual setup is required, just the presence of the tools listed above. This makes testing a breeze!
259332

260333
> [!NOTE]
261-
> Specifying a comma-separated list of `KubeRay Operator` versions in the `KUBE_RAY_OPERATOR_VERSIONS` environment variable will spawn a new test for each version.
334+
> Specifying a comma-separated list of `KubeRay Operator` versions in the `PYTEST_KUBERAY_VERSIONS` environment variable will spawn a new test for each version.
262335
263336
> [!NOTE]
264337
> it may take a while to download `minikube` and `kuberay-operator` images and build the local `dagster-ray` image during the first tests invocation

dagster_ray/_base/constants.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import os
2+
3+
DEFAULT_DEPLOYMENT_NAME = (
4+
os.getenv("DAGSTER_CLOUD_DEPLOYMENT_NAME")
5+
if os.getenv("DAGSTER_CLOUD_IS_BRANCH_DEPLOYMENT") == "0"
6+
else os.getenv("DAGSTER_CLOUD_GIT_BRANCH")
7+
) or "dev"
8+
9+
IS_PROD = DEFAULT_DEPLOYMENT_NAME == "prod"

dagster_ray/_base/resources.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import sys
21
import uuid
32
from abc import ABC, abstractmethod
4-
from typing import TYPE_CHECKING, Optional, Union, cast
3+
from typing import TYPE_CHECKING, Dict, Optional, Union, cast
54

65
from dagster import ConfigurableResource, InitResourceContext, OpExecutionContext
76
from pydantic import Field, PrivateAttr
@@ -11,14 +10,9 @@
1110
from requests.exceptions import ConnectionError
1211
from tenacity import retry, retry_if_exception_type, stop_after_delay
1312

13+
from dagster_ray._base.utils import get_dagster_tags
1414
from dagster_ray.config import RayDataExecutionOptions
1515

16-
if sys.version_info >= (3, 11):
17-
pass
18-
else:
19-
pass
20-
21-
2216
if TYPE_CHECKING:
2317
from ray._private.worker import BaseContext as RayBaseContext # noqa
2418

@@ -86,6 +80,10 @@ def init_ray(self, context: Union[OpExecutionContext, InitResourceContext]) -> "
8680
context.log.info("Initialized Ray!")
8781
return cast("RayBaseContext", self._context)
8882

83+
def get_dagster_tags(self, context: InitResourceContext) -> Dict[str, str]:
84+
tags = get_dagster_tags(context)
85+
return tags
86+
8987
def _get_step_key(self, context: InitResourceContext) -> str:
9088
# just return a random string
9189
# since we want a fresh cluster every time

dagster_ray/_base/utils.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import os
2+
from typing import Dict, Union, cast
3+
4+
from dagster import InitResourceContext, OpExecutionContext
5+
6+
from dagster_ray._base.constants import DEFAULT_DEPLOYMENT_NAME
7+
8+
9+
def get_dagster_tags(context: Union[InitResourceContext, OpExecutionContext]) -> Dict[str, str]:
10+
"""
11+
Returns a dictionary with common Dagster tags.
12+
"""
13+
assert context.dagster_run is not None
14+
15+
labels = {
16+
"dagster.io/run_id": cast(str, context.run_id),
17+
"dagster.io/deployment": DEFAULT_DEPLOYMENT_NAME,
18+
# TODO: add more labels
19+
}
20+
21+
if context.dagster_run.tags.get("user"):
22+
labels["dagster.io/user"] = context.dagster_run.tags["user"]
23+
24+
if os.getenv("DAGSTER_CLOUD_GIT_BRANCH"):
25+
labels["dagster.io/git-branch"] = os.environ["DAGSTER_CLOUD_GIT_BRANCH"]
26+
27+
if os.getenv("DAGSTER_CLOUD_GIT_SHA"):
28+
labels["dagster.io/git-sha"] = os.environ["DAGSTER_CLOUD_GIT_SHA"]
29+
30+
return labels

dagster_ray/config.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,8 @@
22

33
from typing import Optional
44

5-
import ray
65
from dagster import Config
76
from pydantic import Field
8-
from ray.data import ExecutionResources
97

108

119
class ExecutionOptionsConfig(Config):
@@ -23,6 +21,9 @@ class RayDataExecutionOptions(Config):
2321
use_polars: bool = True
2422

2523
def apply(self):
24+
import ray
25+
from ray.data import ExecutionResources
26+
2627
ctx = ray.data.DatasetContext.get_current()
2728

2829
ctx.execution_options.resource_limits = ExecutionResources.for_limits(
@@ -35,6 +36,8 @@ def apply(self):
3536
ctx.use_polars = self.use_polars
3637

3738
def apply_remote(self):
39+
import ray
40+
3841
@ray.remote
3942
def apply():
4043
self.apply()

dagster_ray/kuberay/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
from dagster_ray.kuberay.configs import RayClusterConfig
22
from dagster_ray.kuberay.jobs import cleanup_kuberay_clusters, delete_kuberay_clusters
33
from dagster_ray.kuberay.ops import cleanup_kuberay_clusters_op, delete_kuberay_clusters_op
4-
from dagster_ray.kuberay.resources import KubeRayAPI, KubeRayCluster
4+
from dagster_ray.kuberay.resources import KubeRayCluster, RayClusterClientResource
55
from dagster_ray.kuberay.schedules import cleanup_kuberay_clusters_daily
66

77
__all__ = [
88
"KubeRayCluster",
99
"RayClusterConfig",
10-
"KubeRayAPI",
10+
"RayClusterClientResource",
1111
"cleanup_kuberay_clusters",
1212
"delete_kuberay_clusters",
1313
"cleanup_kuberay_clusters_op",
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from dagster_ray.kuberay.client.raycluster import RayClusterClient
2+
from dagster_ray.kuberay.client.rayjob import RayJobClient
3+
4+
__all__ = ["RayClusterClient", "RayJobClient"]

0 commit comments

Comments
 (0)