Skip to content

Commit b98bd5b

Browse files
committed
updated to Ray 2.7
1 parent 71b3d50 commit b98bd5b

File tree

15 files changed

+3345
-1947
lines changed

15 files changed

+3345
-1947
lines changed

README.md

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ touch .env
108108
```bash
109109
# Inside .env
110110
GITHUB_USERNAME="CHANGE_THIS_TO_YOUR_USERNAME" # ← CHANGE THIS
111+
```
111112
```bash
112113
source .env
113114
```
@@ -120,8 +121,6 @@ Now we're ready to clone the repository that has all of our code:
120121

121122
```bash
122123
git clone https://github.com/GokuMohandas/Made-With-ML.git .
123-
git remote set-url origin https://github.com/$GITHUB_USERNAME/Made-With-ML.git # <-- CHANGE THIS to your username
124-
git checkout -b dev
125124
```
126125

127126
### Virtual environment
@@ -289,7 +288,6 @@ python madewithml/evaluate.py \
289288

290289
### Inference
291290
```bash
292-
# Get run ID
293291
export EXPERIMENT_NAME="llm"
294292
export RUN_ID=$(python madewithml/predict.py get-best-run-id --experiment-name $EXPERIMENT_NAME --metric val_loss --mode ASC)
295293
python madewithml/predict.py predict \
@@ -485,17 +483,23 @@ We're not going to manually deploy our application every time we make a change.
485483
<img src="https://madewithml.com/static/images/mlops/cicd/cicd.png">
486484
</div>
487485

488-
1. We'll start by adding the necessary credentials to the [`/settings/secrets/actions`](https://github.com/GokuMohandas/Made-With-ML/settings/secrets/actions) page of our GitHub repository.
486+
1. Create a new github branch to save our changes to and execute CI/CD workloads:
487+
```bash
488+
git remote set-url origin https://github.com/$GITHUB_USERNAME/Made-With-ML.git # <-- CHANGE THIS to your username
489+
git checkout -b dev
490+
```
491+
492+
2. We'll start by adding the necessary credentials to the [`/settings/secrets/actions`](https://github.com/GokuMohandas/Made-With-ML/settings/secrets/actions) page of our GitHub repository.
489493

490494
``` bash
491495
export ANYSCALE_HOST=https://console.anyscale.com
492496
export ANYSCALE_CLI_TOKEN=$YOUR_CLI_TOKEN # retrieved from https://console.anyscale.com/o/madewithml/credentials
493497
```
494498

495-
2. Now we can make changes to our code (not on `main` branch) and push them to GitHub. But in order to push our code to GitHub, we'll need to first authenticate with our credentials before pushing to our repository:
499+
3. Now we can make changes to our code (not on `main` branch) and push them to GitHub. But in order to push our code to GitHub, we'll need to first authenticate with our credentials before pushing to our repository:
496500

497501
```bash
498-
git config --global user.name "Your Name" # <-- CHANGE THIS to your name
502+
git config --global user.name $GITHUB_USERNAME # <-- CHANGE THIS to your username
499503
git config --global user.email you@example.com # <-- CHANGE THIS to your email
500504
git add .
501505
git commit -m "" # <-- CHANGE THIS to your message
@@ -504,13 +508,13 @@ git push origin dev
504508

505509
Now you will be prompted to enter your username and password (personal access token). Follow these steps to get personal access token: [New GitHub personal access token](https://github.com/settings/tokens/new) → Add a name → Toggle `repo` and `workflow` → Click `Generate token` (scroll down) → Copy the token and paste it when prompted for your password.
506510

507-
3. Now we can start a PR from this branch to our `main` branch and this will trigger the [workloads workflow](/.github/workflows/workloads.yaml). If the workflow (Anyscale Jobs) succeeds, this will produce comments with the training and evaluation results directly on the PR.
511+
4. Now we can start a PR from this branch to our `main` branch and this will trigger the [workloads workflow](/.github/workflows/workloads.yaml). If the workflow (Anyscale Jobs) succeeds, this will produce comments with the training and evaluation results directly on the PR.
508512

509513
<div align="center">
510514
<img src="https://madewithml.com/static/images/mlops/cicd/comments.png">
511515
</div>
512516

513-
4. If we like the results, we can merge the PR into the `main` branch. This will trigger the [serve workflow](/.github/workflows/serve.yaml) which will rollout our new service to production!
517+
5. If we like the results, we can merge the PR into the `main` branch. This will trigger the [serve workflow](/.github/workflows/serve.yaml) which will rollout our new service to production!
514518

515519
### Continual learning
516520

madewithml/config.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
from pathlib import Path
66

77
import mlflow
8-
import pretty_errors # NOQA: F401 (imported but unused)
98

109
# Directories
1110
ROOT_DIR = Path(__file__).parent.parent.absolute()

madewithml/data.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import pandas as pd
66
import ray
77
from ray.data import Dataset
8-
from ray.data.preprocessor import Preprocessor
98
from sklearn.model_selection import train_test_split
109
from transformers import BertTokenizer
1110

@@ -135,13 +134,18 @@ def preprocess(df: pd.DataFrame, class_to_index: Dict) -> Dict:
135134
return outputs
136135

137136

138-
class CustomPreprocessor(Preprocessor):
137+
class CustomPreprocessor:
139138
"""Custom preprocessor class."""
140139

141-
def _fit(self, ds):
140+
def __init__(self, class_to_index={}):
141+
self.class_to_index = class_to_index or {} # mutable defaults
142+
self.index_to_class = {v: k for k, v in self.class_to_index.items()}
143+
144+
def fit(self, ds):
142145
tags = ds.unique(column="tag")
143146
self.class_to_index = {tag: i for i, tag in enumerate(tags)}
144147
self.index_to_class = {v: k for k, v in self.class_to_index.items()}
148+
return self
145149

146-
def _transform_pandas(self, batch): # could also do _transform_numpy
147-
return preprocess(batch, class_to_index=self.class_to_index)
150+
def transform(self, ds):
151+
return ds.map_batches(preprocess, fn_kwargs={"class_to_index": self.class_to_index}, batch_format="pandas")

madewithml/evaluate.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@
88
import ray.train.torch # NOQA: F401 (imported but unused)
99
import typer
1010
from ray.data import Dataset
11-
from ray.train.torch.torch_predictor import TorchPredictor
1211
from sklearn.metrics import precision_recall_fscore_support
1312
from snorkel.slicing import PandasSFApplier, slicing_function
1413
from typing_extensions import Annotated
1514

1615
from madewithml import predict, utils
1716
from madewithml.config import logger
17+
from madewithml.predict import TorchPredictor
1818

1919
# Initialize Typer CLI app
2020
app = typer.Typer()
@@ -133,8 +133,8 @@ def evaluate(
133133
y_true = np.stack([item["targets"] for item in values])
134134

135135
# y_pred
136-
z = predictor.predict(data=ds.to_pandas())["predictions"]
137-
y_pred = np.stack(z).argmax(1)
136+
predictions = preprocessed_ds.map_batches(predictor).take_all()
137+
y_pred = np.array([d["output"] for d in predictions])
138138

139139
# Metrics
140140
metrics = {

madewithml/models.py

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
1+
import json
2+
import os
3+
from pathlib import Path
4+
15
import torch
26
import torch.nn as nn
7+
import torch.nn.functional as F
8+
from transformers import BertModel
39

410

5-
class FinetunedLLM(nn.Module): # pragma: no cover, torch model
6-
"""Model architecture for a Large Language Model (LLM) that we will fine-tune."""
7-
11+
class FinetunedLLM(nn.Module):
812
def __init__(self, llm, dropout_p, embedding_dim, num_classes):
913
super(FinetunedLLM, self).__init__()
1014
self.llm = llm
15+
self.dropout_p = dropout_p
16+
self.embedding_dim = embedding_dim
17+
self.num_classes = num_classes
1118
self.dropout = torch.nn.Dropout(dropout_p)
1219
self.fc1 = torch.nn.Linear(embedding_dim, num_classes)
1320

@@ -17,3 +24,36 @@ def forward(self, batch):
1724
z = self.dropout(pool)
1825
z = self.fc1(z)
1926
return z
27+
28+
@torch.inference_mode()
29+
def predict(self, batch):
30+
self.eval()
31+
z = self(batch)
32+
y_pred = torch.argmax(z, dim=1).cpu().numpy()
33+
return y_pred
34+
35+
@torch.inference_mode()
36+
def predict_proba(self, batch):
37+
self.eval()
38+
z = self(batch)
39+
y_probs = F.softmax(z, dim=1).cpu().numpy()
40+
return y_probs
41+
42+
def save(self, dp):
43+
with open(Path(dp, "args.json"), "w") as fp:
44+
contents = {
45+
"dropout_p": self.dropout_p,
46+
"embedding_dim": self.embedding_dim,
47+
"num_classes": self.num_classes,
48+
}
49+
json.dump(contents, fp, indent=4, sort_keys=False)
50+
torch.save(self.state_dict(), os.path.join(dp, "model.pt"))
51+
52+
@classmethod
53+
def load(cls, args_fp, state_dict_fp):
54+
with open(args_fp, "r") as fp:
55+
kwargs = json.load(fp=fp)
56+
llm = BertModel.from_pretrained("allenai/scibert_scivocab_uncased", return_dict=False)
57+
model = cls(llm=llm, **kwargs)
58+
model.load_state_dict(torch.load(state_dict_fp, map_location=torch.device("cpu")))
59+
return model

madewithml/predict.py

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
11
import json
2+
from pathlib import Path
23
from typing import Any, Dict, Iterable, List
34
from urllib.parse import urlparse
45

56
import numpy as np
6-
import pandas as pd
77
import ray
8-
import torch
98
import typer
109
from numpyencoder import NumpyEncoder
1110
from ray.air import Result
12-
from ray.train.torch import TorchPredictor
1311
from ray.train.torch.torch_checkpoint import TorchCheckpoint
1412
from typing_extensions import Annotated
1513

1614
from madewithml.config import logger, mlflow
15+
from madewithml.data import CustomPreprocessor
16+
from madewithml.models import FinetunedLLM
17+
from madewithml.utils import collate_fn
1718

1819
# Initialize Typer CLI app
1920
app = typer.Typer()
@@ -48,25 +49,51 @@ def format_prob(prob: Iterable, index_to_class: Dict) -> Dict:
4849
return d
4950

5051

51-
def predict_with_proba(
52-
df: pd.DataFrame,
53-
predictor: ray.train.torch.torch_predictor.TorchPredictor,
52+
class TorchPredictor:
53+
def __init__(self, preprocessor, model):
54+
self.preprocessor = preprocessor
55+
self.model = model
56+
self.model.eval()
57+
58+
def __call__(self, batch):
59+
results = self.model.predict(collate_fn(batch))
60+
return {"output": results}
61+
62+
def predict_proba(self, batch):
63+
results = self.model.predict_proba(collate_fn(batch))
64+
return {"output": results}
65+
66+
def get_preprocessor(self):
67+
return self.preprocessor
68+
69+
@classmethod
70+
def from_checkpoint(cls, checkpoint):
71+
metadata = checkpoint.get_metadata()
72+
preprocessor = CustomPreprocessor(class_to_index=metadata["class_to_index"])
73+
model = FinetunedLLM.load(Path(checkpoint.path, "args.json"), Path(checkpoint.path, "model.pt"))
74+
return cls(preprocessor=preprocessor, model=model)
75+
76+
77+
def predict_proba(
78+
ds: ray.data.dataset.Dataset,
79+
predictor: TorchPredictor,
5480
) -> List: # pragma: no cover, tested with inference workload
5581
"""Predict tags (with probabilities) for input data from a dataframe.
5682
5783
Args:
5884
df (pd.DataFrame): dataframe with input features.
59-
predictor (ray.train.torch.torch_predictor.TorchPredictor): loaded predictor from a checkpoint.
85+
predictor (TorchPredictor): loaded predictor from a checkpoint.
6086
6187
Returns:
6288
List: list of predicted labels.
6389
"""
6490
preprocessor = predictor.get_preprocessor()
65-
z = predictor.predict(data=df)["predictions"]
66-
y_prob = torch.tensor(np.stack(z)).softmax(dim=1).numpy()
91+
preprocessed_ds = preprocessor.transform(ds)
92+
outputs = preprocessed_ds.map_batches(predictor.predict_proba)
93+
y_prob = np.array([d["output"] for d in outputs.take_all()])
6794
results = []
6895
for i, prob in enumerate(y_prob):
69-
tag = decode([z[i].argmax()], preprocessor.index_to_class)[0]
96+
tag = preprocessor.index_to_class[prob.argmax()]
7097
results.append({"prediction": tag, "probabilities": format_prob(prob, preprocessor.index_to_class)})
7198
return results
7299

@@ -125,11 +152,10 @@ def predict(
125152
# Load components
126153
best_checkpoint = get_best_checkpoint(run_id=run_id)
127154
predictor = TorchPredictor.from_checkpoint(best_checkpoint)
128-
# preprocessor = predictor.get_preprocessor()
129155

130156
# Predict
131-
sample_df = pd.DataFrame([{"title": title, "description": description, "tag": "other"}])
132-
results = predict_with_proba(df=sample_df, predictor=predictor)
157+
sample_ds = ray.data.from_items([{"title": title, "description": description, "tag": "other"}])
158+
results = predict_proba(ds=sample_ds, predictor=predictor)
133159
logger.info(json.dumps(results, cls=NumpyEncoder, indent=2))
134160
return results
135161

madewithml/serve.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,9 @@
33
from http import HTTPStatus
44
from typing import Dict
55

6-
import pandas as pd
76
import ray
87
from fastapi import FastAPI
98
from ray import serve
10-
from ray.train.torch import TorchPredictor
119
from starlette.requests import Request
1210

1311
from madewithml import evaluate, predict
@@ -21,7 +19,7 @@
2119
)
2220

2321

24-
@serve.deployment(route_prefix="/", num_replicas="1", ray_actor_options={"num_cpus": 8, "num_gpus": 0})
22+
@serve.deployment(num_replicas="1", ray_actor_options={"num_cpus": 8, "num_gpus": 0})
2523
@serve.ingress(app)
2624
class ModelDeployment:
2725
def __init__(self, run_id: str, threshold: int = 0.9):
@@ -30,8 +28,7 @@ def __init__(self, run_id: str, threshold: int = 0.9):
3028
self.threshold = threshold
3129
mlflow.set_tracking_uri(MLFLOW_TRACKING_URI) # so workers have access to model registry
3230
best_checkpoint = predict.get_best_checkpoint(run_id=run_id)
33-
self.predictor = TorchPredictor.from_checkpoint(best_checkpoint)
34-
self.preprocessor = self.predictor.get_preprocessor()
31+
self.predictor = predict.TorchPredictor.from_checkpoint(best_checkpoint)
3532

3633
@app.get("/")
3734
def _index(self) -> Dict:
@@ -55,11 +52,10 @@ async def _evaluate(self, request: Request) -> Dict:
5552
return {"results": results}
5653

5754
@app.post("/predict/")
58-
async def _predict(self, request: Request) -> Dict:
59-
# Get prediction
55+
async def _predict(self, request: Request):
6056
data = await request.json()
61-
df = pd.DataFrame([{"title": data.get("title", ""), "description": data.get("description", ""), "tag": ""}])
62-
results = predict.predict_with_proba(df=df, predictor=self.predictor)
57+
sample_ds = ray.data.from_items([{"title": data.get("title", ""), "description": data.get("description", ""), "tag": ""}])
58+
results = predict.predict_proba(ds=sample_ds, predictor=self.predictor)
6359

6460
# Apply custom logic
6561
for i, result in enumerate(results):

0 commit comments

Comments
 (0)