chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:17:40 +08:00
commit f1825c8ceb
10096 changed files with 2364182 additions and 0 deletions
+1069
View File
File diff suppressed because it is too large Load Diff
View File
@@ -0,0 +1,17 @@
cloud: {{env["ANYSCALE_CLOUD_NAME"]}}
head_node:
instance_type: m5.2xlarge
worker_nodes:
- instance_type: g4dn.12xlarge
min_nodes: 4
max_nodes: 4
market_type: ON_DEMAND
advanced_instance_config:
TagSpecifications:
- ResourceType: "instance"
Tags:
- Key: ttl-hours
Value: '24'
@@ -0,0 +1,11 @@
base_image: {{ env["RAY_IMAGE_ML_NIGHTLY_GPU"] | default("anyscale/ray:nightly-py38-cu118") }}
env_vars: {}
debian_packages:
- curl
post_build_cmds:
- pip uninstall -y ray || true && pip3 install -U {{ env["RAY_WHEELS"] | default("ray") }}
- {{ env["RAY_WHEELS_SANITY_CHECK"] | default("echo No Ray wheels sanity check") }}
- pip3 uninstall -y pytorch-lightning pytorch_lightning
- pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118
- pip3 install "lightning==2.0.2" "transformers==4.29.2" "accelerate==0.19.0"
@@ -0,0 +1 @@
../../../doc/source/train/examples/lightning/dolly_lightning_fsdp_finetuning.ipynb
@@ -0,0 +1 @@
../../../doc/test_myst_doc.py
+1
View File
@@ -0,0 +1 @@
../../../doc/source/templates/05_dreambooth_finetuning/dreambooth
@@ -0,0 +1,6 @@
cloud: {{env["ANYSCALE_CLOUD_NAME"]}}
head_node:
instance_type: g5.12xlarge
worker_nodes: []
@@ -0,0 +1,16 @@
# NOTE:
# - This test runs with py38 (see the entry in release_tests.yaml)
# - This test installs dependencies on top of a base ray image
# instead of using the default ray-ml image. See dreambooth/requirements.txt.
base_image: "anyscale/ray:nightly-py38-cu118"
env_vars: {}
debian_packages:
- curl
python:
pip_packages: []
conda_packages: []
post_build_cmds:
- pip uninstall -y ray || true && pip3 install -U {{ env["RAY_WHEELS"] | default("ray") }}
- {{ env["RAY_WHEELS_SANITY_CHECK"] | default("echo No Ray wheels sanity check") }}
+1
View File
@@ -0,0 +1 @@
../../../doc/source/templates/05_dreambooth_finetuning/dreambooth_run.sh
@@ -0,0 +1,17 @@
cloud: {{env["ANYSCALE_CLOUD_NAME"]}}
head_node:
instance_type: m5.2xlarge
worker_nodes:
- instance_type: g4dn.4xlarge
min_nodes: 8
max_nodes: 8
market_type: ON_DEMAND
advanced_instance_config:
TagSpecifications:
- ResourceType: "instance"
Tags:
- Key: ttl-hours
Value: '24'
@@ -0,0 +1,22 @@
cloud_id: {{env["ANYSCALE_CLOUD_ID"]}}
region: us-west1
allowed_azs:
- us-west1-b
head_node_type:
name: head_node
instance_type: n2-standard-8
worker_node_types:
- name: worker_node
instance_type: n1-standard-16-nvidia-tesla-t4-1
min_workers: 8
max_workers: 8
use_spot: false
#advanced_configurations_json:
# TagSpecifications:
# - ResourceType: "instance"
# Tags:
# - Key: ttl-hours
# Value: '24'
@@ -0,0 +1,8 @@
base_image: {{ env["RAY_IMAGE_ML_NIGHTLY_GPU"] }}
env_vars: {}
debian_packages:
- curl
post_build_cmds:
- pip uninstall -y ray || true && pip3 install -U {{ env["RAY_WHEELS"] | default("ray") }}
- {{ env["RAY_WHEELS_SANITY_CHECK"] | default("echo No Ray wheels sanity check") }}
@@ -0,0 +1 @@
../../../doc/source/train/examples/deepspeed/gptj_deepspeed_fine_tuning.ipynb
@@ -0,0 +1,76 @@
"""Convert a jupytext-compliant format in to a python script
and execute it with parsed arguments.
Any cell with 'remove-cell-ci' tag in metadata will not be included
in the converted python script.
"""
import argparse
import subprocess
import sys
import tempfile
from pathlib import Path
import jupytext
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--path",
help="path to the jupytext-compatible file",
)
parser.add_argument(
"--find-recursively",
action="store_true",
help="if true, will attempt to find path recursively in cwd",
)
parser.add_argument(
"--no-postprocess",
action="store_true",
help="if true, will not postprocess the notebook",
)
def filter_out_cells_with_remove_cell_ci_tag(cells: list):
"""Filters out cells which contain the 'remove-cell-ci' tag in metadata"""
def should_keep_cell(cell):
tags = cell.metadata.get("tags")
if tags:
# Both - and _ for consistent behavior with built-in tags
return "remove_cell_ci" not in tags and "remove-cell-ci" not in tags
return True
return [cell for cell in cells if should_keep_cell(cell)]
def postprocess_notebook(notebook):
notebook.cells = filter_out_cells_with_remove_cell_ci_tag(notebook.cells)
return notebook
if __name__ == "__main__":
args, remainder = parser.parse_known_args()
path = Path(args.path)
cwd = Path.cwd()
if args.find_recursively and not path.exists():
path = next((p for p in cwd.rglob("*") if str(p).endswith(args.path)), None)
assert path and path.exists()
with open(path, "r") as f:
notebook = jupytext.read(f)
if not args.no_postprocess:
notebook = postprocess_notebook(notebook)
name = ""
with tempfile.NamedTemporaryFile("w", delete=False) as f:
jupytext.write(notebook, f, fmt="py:percent")
name = f.name
remainder.insert(0, name)
remainder.insert(0, sys.executable)
# Run the notebook
subprocess.run(remainder, check=True)
@@ -0,0 +1,15 @@
cloud_id: {{env["ANYSCALE_CLOUD_ID"]}}
region: us-west-2
head_node_type:
name: head_node
instance_type: p3.16xlarge
worker_node_types: []
advanced_configurations_json:
BlockDeviceMappings:
- DeviceName: /dev/sda1
Ebs:
DeleteOnTermination: true
VolumeSize: 500
@@ -0,0 +1,8 @@
base_image: {{ env["RAY_IMAGE_ML_NIGHTLY_GPU"] }}
env_vars: {}
debian_packages:
- curl
post_build_cmds:
- pip uninstall -y ray || true && pip3 install -U {{ env["RAY_WHEELS"] | default("ray") }}
- {{ env["RAY_WHEELS_SANITY_CHECK"] | default("echo No Ray wheels sanity check") }}
@@ -0,0 +1 @@
../../../doc/test_myst_doc.py
@@ -0,0 +1 @@
../../../doc/test_myst_doc.py
@@ -0,0 +1,17 @@
cloud: {{env["ANYSCALE_CLOUD_NAME"]}}
head_node:
instance_type: g5.16xlarge
worker_nodes:
- instance_type: g5.4xlarge
min_nodes: 15
max_nodes: 15
market_type: ON_DEMAND
advanced_instance_config:
TagSpecifications:
- ResourceType: "instance"
Tags:
- Key: ttl-hours
Value: '24'
@@ -0,0 +1 @@
../../../doc/source/train/examples/lightning/vicuna_13b_lightning_deepspeed_finetune.ipynb
@@ -0,0 +1,10 @@
cloud_id: {{env["ANYSCALE_CLOUD_ID"]}}
region: us-west-2
max_workers: 0
head_node_type:
name: head_node
instance_type: m5.2xlarge
worker_node_types: []
@@ -0,0 +1,12 @@
cloud_id: {{env["ANYSCALE_CLOUD_ID"]}}
region: us-west1
allowed_azs:
- us-west1-b
max_workers: 0
head_node_type:
name: head_node
instance_type: n1-standard-8
worker_node_types: []
@@ -0,0 +1,10 @@
cloud: {{env["ANYSCALE_CLOUD_NAME"]}}
head_node:
instance_type: m5.2xlarge
worker_nodes:
- instance_type: m5.2xlarge
min_nodes: 3
max_nodes: 3
market_type: ON_DEMAND
@@ -0,0 +1,12 @@
cloud: {{env["ANYSCALE_CLOUD_NAME"]}}
zones:
- us-west1-b
head_node:
instance_type: n1-standard-8
worker_nodes:
- instance_type: n1-standard-8
min_nodes: 3
max_nodes: 3
market_type: ON_DEMAND
@@ -0,0 +1,15 @@
cloud_id: {{env["ANYSCALE_CLOUD_ID"]}}
region: us-west-2
max_workers: 7
head_node_type:
name: head_node
instance_type: m5.2xlarge
worker_node_types:
- name: worker_node
instance_type: m5.2xlarge
max_workers: 7
min_workers: 7
use_spot: false
@@ -0,0 +1,17 @@
cloud_id: {{env["ANYSCALE_CLOUD_ID"]}}
region: us-west1
allowed_azs:
- us-west1-b
max_workers: 7
head_node_type:
name: head_node
instance_type: n1-standard-8
worker_node_types:
- name: worker_node
instance_type: n1-standard-8
max_workers: 7
min_workers: 7
use_spot: false
@@ -0,0 +1,20 @@
cloud_id: {{env["ANYSCALE_CLOUD_ID"]}}
region: us-west-2
max_workers: 0
head_node_type:
name: head_node
instance_type: g4dn.8xlarge
worker_node_types: []
advanced_configurations_json:
BlockDeviceMappings:
- DeviceName: /dev/sda1
Ebs:
DeleteOnTermination: true
Iops: 5000
Throughput: 1000
VolumeSize: 1000
VolumeType: gp3
@@ -0,0 +1,20 @@
cloud_id: {{env["ANYSCALE_CLOUD_ID"]}}
region: us-west1
allowed_azs:
- us-west1-b
max_workers: 0
head_node_type:
name: head_node
instance_type: n1-standard-32-nvidia-tesla-t4-2
worker_node_types: []
gcp_advanced_configurations_json:
instance_properties:
disks:
- boot: true
auto_delete: true
initialize_params:
disk_size_gb: 1000
@@ -0,0 +1,13 @@
cloud: {{env["ANYSCALE_CLOUD_NAME"]}}
head_node:
instance_type: g4dn.8xlarge
resources:
CPU: 32
GPU: 1
worker_nodes:
- instance_type: g4dn.8xlarge
min_nodes: 1
max_nodes: 1
market_type: ON_DEMAND
@@ -0,0 +1,17 @@
cloud_id: {{env["ANYSCALE_CLOUD_ID"]}}
region: us-west1
allowed_azs:
- us-west1-b
max_workers: 1
head_node_type:
name: head_node
instance_type: n1-standard-32-nvidia-tesla-t4-2
worker_node_types:
- name: worker_node
instance_type: n1-standard-32-nvidia-tesla-t4-2
max_workers: 1
min_workers: 1
use_spot: false
@@ -0,0 +1,23 @@
cloud: {{env["ANYSCALE_CLOUD_NAME"]}}
head_node:
instance_type: g4dn.12xlarge
resources:
CPU: 48
GPU: 4
worker_nodes:
- instance_type: g4dn.12xlarge
min_nodes: 3
max_nodes: 3
market_type: ON_DEMAND
advanced_instance_config:
BlockDeviceMappings:
- DeviceName: /dev/sda1
Ebs:
DeleteOnTermination: true
VolumeSize: 800
Iops: 5000
Throughput: 1000
VolumeType: gp3
@@ -0,0 +1,23 @@
cloud: {{env["ANYSCALE_CLOUD_NAME"]}}
zones:
- us-west1-b
head_node:
instance_type: n1-standard-64-nvidia-tesla-t4-4
resources:
CPU: 64
GPU: 4
worker_nodes:
- instance_type: n1-standard-64-nvidia-tesla-t4-4
min_nodes: 3
max_nodes: 3
market_type: ON_DEMAND
advanced_instance_config:
instance_properties:
disks:
- boot: true
auto_delete: true
initialize_params:
disk_size_gb: 800
@@ -0,0 +1,3 @@
import tensorflow as tf
tf.keras.datasets.fashion_mnist.load_data()
@@ -0,0 +1,3 @@
import torchvision
torchvision.datasets.FashionMNIST("/tmp/data_fashion_mnist", download=True)
@@ -0,0 +1,155 @@
import os
import socket
import subprocess
from collections import defaultdict
from contextlib import closing
from pathlib import Path
from ray.air.util.node import _force_on_node
import ray
from typing import List, Dict, Union, Callable
def schedule_remote_fn_on_all_nodes(
remote_fn, exclude_head: bool = False, *args, **kwargs
):
head_ip = ray.util.get_node_ip_address()
futures = []
for node in ray.nodes():
if not node["Alive"]:
continue
node_ip = node["NodeManagerAddress"]
if exclude_head and node_ip == head_ip:
continue
node_id = node["NodeID"]
future = _force_on_node(node_id, remote_fn).remote(
*args,
**kwargs,
)
futures.append(future)
return futures
@ray.remote
def _write(stream: bytes, path: str):
Path(path).parent.mkdir(parents=True, exist_ok=True)
with open(path, "wb") as f:
f.write(stream)
def upload_file_to_all_nodes(path: str):
path = os.path.abspath(path)
with open(path, "rb") as f:
stream = f.read()
futures = schedule_remote_fn_on_all_nodes(
_write, exclude_head=True, stream=stream, path=path
)
return ray.get(futures)
@ray.remote
def _run_command(cmd: str):
return subprocess.check_call(cmd)
def run_command_on_all_nodes(cmd: List[str]):
futures = schedule_remote_fn_on_all_nodes(_run_command, cmd=cmd)
return ray.get(futures)
@ray.remote
class CommandRunner:
def run_command(self, cmd: str):
return subprocess.check_call(cmd)
def run_fn(self, fn: Callable, *args, **kwargs):
return fn(*args, **kwargs)
def create_actors_with_options(
num_actors: int,
resources: Dict[str, Union[float, int]],
) -> List[ray.actor.ActorHandle]:
num_cpus = resources.pop("CPU", 1)
num_gpus = resources.pop("GPU", 0)
options = {"num_cpus": num_cpus, "num_gpus": num_gpus, "resources": resources}
return [CommandRunner.options(**options).remote() for _ in range(num_actors)]
def run_commands_on_actors(actors: List[ray.actor.ActorHandle], cmds: List[List[str]]):
assert len(actors) == len(cmds)
futures = []
for actor, cmd in zip(actors, cmds):
futures.append(actor.run_command.remote(cmd))
return ray.get(futures)
def run_fn_on_actors(
actors: List[ray.actor.ActorHandle], fn: Callable, *args, **kwargs
):
futures = []
for actor in actors:
futures.append(actor.run_fn.remote(fn, *args, **kwargs))
return ray.get(futures)
def get_ip_port_actors(actors: List[ray.actor.ActorHandle]) -> List[str]:
# We need this wrapper to avoid deserialization issues with benchmark_util.py
def get_ip_port():
ip = ray.util.get_node_ip_address()
with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
s.bind(("localhost", 0))
port = s.getsockname()[1]
return ip, port
return run_fn_on_actors(actors=actors, fn=get_ip_port)
def get_gpu_ids_actors(actors: List[ray.actor.ActorHandle]) -> List[List[int]]:
# We need this wrapper to avoid deserialization issues with benchmark_util.py
def get_gpu_ids():
return ray.get_gpu_ids()
return run_fn_on_actors(actors=actors, fn=get_gpu_ids)
def map_ips_to_gpus(ips: List[str], gpus: List[List[int]]):
assert len(ips) == len(gpus)
map = defaultdict(set)
for ip, gpu in zip(ips, gpus):
map[ip].update(set(gpu))
return {ip: sorted(gpus) for ip, gpus in map.items()}
def set_cuda_visible_devices(
actors: List[ray.actor.ActorHandle],
actor_ips: List[str],
ip_to_gpus: Dict[str, set],
):
assert len(actors) == len(actor_ips)
def set_env(key: str, val: str):
os.environ[key] = val
futures = []
for actor, ip in zip(actors, actor_ips):
assert ip in ip_to_gpus
gpu_str = ",".join([str(device) for device in sorted(ip_to_gpus[ip])])
future = actor.run_fn.remote(set_env, "CUDA_VISIBLE_DEVICES", gpu_str)
futures.append(future)
ray.get(futures)
@@ -0,0 +1,149 @@
import click
import time
import json
import os
import tempfile
from typing import Dict
import numpy as np
from torchvision import transforms
from torchvision.models import resnet18
import torch
import torch.nn as nn
import torch.optim as optim
import ray
from ray import train
from ray.train import Checkpoint, RunConfig, ScalingConfig
from ray.train.torch import TorchTrainer
def add_fake_labels(batch: Dict[str, np.ndarray]) -> Dict[str, np.ndarray]:
batch_size = len(batch["image"])
batch["label"] = np.zeros([batch_size], dtype=int)
return batch
def transform_image(
batch: Dict[str, np.ndarray], transform: torch.nn.Module
) -> Dict[str, np.ndarray]:
transformed_tensors = [transform(image).numpy() for image in batch["image"]]
batch["image"] = transformed_tensors
return batch
def train_loop_per_worker(config):
raw_model = resnet18(pretrained=True)
model = train.torch.prepare_model(raw_model)
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9)
train_dataset_shard = train.get_dataset_shard("train")
for epoch in range(config["num_epochs"]):
running_loss = 0.0
for i, data in enumerate(
train_dataset_shard.iter_torch_batches(batch_size=config["batch_size"])
):
# get the inputs; data is a list of [inputs, labels]
inputs = data["image"].to(device=train.torch.get_device())
labels = data["label"].to(device=train.torch.get_device())
# zero the parameter gradients
optimizer.zero_grad()
# forward + backward + optimize
outputs = model(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
# print statistics
running_loss += loss.item()
if i % 2000 == 1999: # print every 2000 mini-batches
print(f"[{epoch + 1}, {i + 1:5d}] loss: {running_loss / 2000:.3f}")
running_loss = 0.0
with tempfile.TemporaryDirectory() as tmpdir:
torch.save(model.state_dict(), os.path.join(tmpdir, "model.pt"))
train.report(
dict(running_loss=running_loss),
checkpoint=Checkpoint.from_directory(tmpdir),
)
@click.command(help="Run Batch prediction on Pytorch ResNet models.")
@click.option("--data-size-gb", type=int, default=1)
@click.option("--num-epochs", type=int, default=2)
@click.option("--num-workers", type=int, default=1)
@click.option("--smoke-test", is_flag=True, default=False)
def main(data_size_gb: int, num_epochs=2, num_workers=1, smoke_test: bool = False):
data_url = (
f"s3://anonymous@air-example-data-2/{data_size_gb}G-image-data-synthetic-raw"
)
print(
"Running Pytorch image model training with "
f"{data_size_gb}GB data from {data_url}"
)
print(f"Training for {num_epochs} epochs with {num_workers} workers.")
start = time.time()
if smoke_test:
# Only read one image
data_url = [data_url + "/dog.jpg"]
print("Running smoke test on CPU with a single example")
else:
print(f"Running GPU training with {data_size_gb}GB data from {data_url}")
dataset = ray.data.read_images(data_url, size=(256, 256))
transform = transforms.Compose(
[
transforms.ToTensor(),
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
]
)
dataset = dataset.map_batches(add_fake_labels)
dataset = dataset.map_batches(transform_image, fn_kwargs={"transform": transform})
trainer = TorchTrainer(
train_loop_per_worker=train_loop_per_worker,
train_loop_config={"batch_size": 64, "num_epochs": num_epochs},
datasets={"train": dataset},
scaling_config=ScalingConfig(
num_workers=num_workers, use_gpu=int(not smoke_test)
),
run_config=RunConfig(storage_path="/mnt/cluster_storage"),
)
trainer.fit()
total_time_s = round(time.time() - start, 2)
# For structured output integration with internal tooling
results = {"data_size_gb": data_size_gb, "num_epochs": num_epochs}
results["perf_metrics"] = [
{
"perf_metric_name": "total_time_s",
"perf_metric_value": total_time_s,
"perf_metric_type": "LATENCY",
},
{
"perf_metric_name": "throughout_MB_s",
"perf_metric_value": round(
num_epochs * data_size_gb * 1024 / total_time_s, 2
),
"perf_metric_type": "THROUGHPUT",
},
]
test_output_json = os.environ.get("TEST_OUTPUT_JSON", "/tmp/release_test_out.json")
with open(test_output_json, "wt") as f:
json.dump(results, f)
print(results)
if __name__ == "__main__":
main()
@@ -0,0 +1,440 @@
import json
import os
import time
from pathlib import Path
import click
import numpy as np
import tensorflow as tf
from typing import List, Tuple
from ray._common.network_utils import build_address
CONFIG = {"lr": 1e-3, "batch_size": 64}
VANILLA_RESULT_JSON = "/tmp/vanilla_out.json"
def mnist_dataset(batch_size: int) -> tf.data.Dataset:
(x_train, y_train), _ = tf.keras.datasets.fashion_mnist.load_data()
# The `x` arrays are in uint8 and have values in the [0, 255] range.
# You need to convert them to float32 with values in the [0, 1] range.
x_train = x_train / np.float32(255)
y_train = y_train.astype(np.int64)
train_dataset = (
tf.data.Dataset.from_tensor_slices((x_train, y_train))
.shuffle(60000, seed=1234)
.batch(batch_size)
)
return train_dataset
def build_cnn_model() -> tf.keras.Model:
model = tf.keras.Sequential(
[
tf.keras.Input(shape=(28, 28)),
tf.keras.layers.Flatten(),
tf.keras.layers.Dense(512, activation="relu"),
tf.keras.layers.Dense(512, activation="relu"),
tf.keras.layers.Dense(10),
]
)
return model
def train_func(use_ray: bool, config: dict):
local_start_time = time.monotonic()
per_worker_batch_size = config.get("batch_size", 64)
epochs = config.get("epochs", 3)
steps_per_epoch = config.get("steps_per_epoch", None)
learning_rate = config.get("lr", 0.001)
tf_config = json.loads(os.environ["TF_CONFIG"])
num_workers = len(tf_config["cluster"]["worker"])
local_rank = tf_config["task"]["index"]
strategy = tf.distribute.MultiWorkerMirroredStrategy()
global_batch_size = per_worker_batch_size * num_workers
multi_worker_dataset = mnist_dataset(global_batch_size)
with strategy.scope():
# Model building/compiling need to be within `strategy.scope()`.
multi_worker_model = build_cnn_model()
multi_worker_model.compile(
loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
optimizer=tf.keras.optimizers.SGD(learning_rate=learning_rate),
metrics=["accuracy"],
)
if use_ray:
from ray.air.integrations.keras import ReportCheckpointCallback
class CustomReportCallback(ReportCheckpointCallback):
def _handle(self, logs: dict, when: str = None):
logs["local_time_taken"] = time.monotonic() - local_start_time
super()._handle(logs, when)
# NOTE: We shouldn't checkpoint to be identical to the vanilla TF run.
callbacks = [CustomReportCallback()]
else:
callbacks = []
history = multi_worker_model.fit(
multi_worker_dataset,
epochs=epochs,
steps_per_epoch=steps_per_epoch,
callbacks=callbacks,
verbose=2, # Disables progress bar in remote actors.
)
results = history.history
loss = results["loss"][-1]
if not use_ray:
local_time_taken = time.monotonic() - local_start_time
print(f"Reporting loss: {loss:.4f}")
if local_rank == 0:
with open(VANILLA_RESULT_JSON, "w") as f:
json.dump({"loss": loss, "local_time_taken": local_time_taken}, f)
return results
def train_tf_ray_air(
*,
config: dict,
num_workers: int = 4,
cpus_per_worker: int = 8,
use_gpu: bool = False,
) -> Tuple[float, float, float]:
from ray.train.tensorflow import TensorflowTrainer
from ray.train import ScalingConfig
def train_loop(config):
train_func(use_ray=True, config=config)
start_time = time.monotonic()
trainer = TensorflowTrainer(
train_loop_per_worker=train_loop,
train_loop_config=config,
scaling_config=ScalingConfig(
num_workers=num_workers,
resources_per_worker={"CPU": cpus_per_worker},
use_gpu=use_gpu,
),
)
result = trainer.fit()
time_taken = time.monotonic() - start_time
print(f"Last result: {result.metrics}")
return time_taken, result.metrics["local_time_taken"], result.metrics["loss"]
def train_tf_vanilla_worker(
*,
config: dict,
rank: int,
world_size: int,
worker_ip_port_list: List[str],
use_gpu: bool = False,
):
# This function is kicked off by the main() function and runs the vanilla
# training script on a single worker.
assert world_size == len(worker_ip_port_list)
tf_config = {
"cluster": {"worker": worker_ip_port_list},
"task": {"type": "worker", "index": rank},
}
os.environ["TF_CONFIG"] = json.dumps(tf_config)
train_func(use_ray=False, config=config)
def train_tf_vanilla(
*,
config: dict,
num_workers: int = 4,
cpus_per_worker: int = 8,
use_gpu: bool = False,
) -> Tuple[float, float, float]:
# This function is kicked off by the main() function and subsequently kicks
# off tasks that run train_tf_vanilla_worker() on the worker nodes.
from benchmark_util import (
upload_file_to_all_nodes,
create_actors_with_options,
run_commands_on_actors,
run_fn_on_actors,
get_ip_port_actors,
)
path = os.path.abspath(__file__)
upload_file_to_all_nodes(path)
num_epochs = config["epochs"]
actors = create_actors_with_options(
num_actors=num_workers,
resources={
"CPU": cpus_per_worker,
"GPU": int(use_gpu),
},
)
run_fn_on_actors(actors=actors, fn=lambda: os.environ.pop("OMP_NUM_THREADS", None))
ips_ports = get_ip_port_actors(actors=actors)
ip_port_list = [build_address(ip, port) for ip, port in ips_ports]
ip_port_str = ",".join(ip_port_list)
cmds = [
[
"python",
path,
"worker",
"--num-epochs",
str(num_epochs),
"--num-workers",
str(num_workers),
"--rank",
str(rank),
"--worker-ip-ports",
ip_port_str,
"--batch-size",
str(config["batch_size"]),
]
+ (["--use-gpu"] if use_gpu else [])
for rank in range(num_workers)
]
run_fn_on_actors(
actors=actors, fn=lambda: os.environ.setdefault("OMP_NUM_THREADS", "1")
)
start_time = time.monotonic()
run_commands_on_actors(actors=actors, cmds=cmds)
time_taken = time.monotonic() - start_time
loss = local_time_taken = 0.0
if os.path.exists(VANILLA_RESULT_JSON):
with open(VANILLA_RESULT_JSON, "r") as f:
result = json.load(f)
loss = result["loss"]
local_time_taken = result["local_time_taken"]
return time_taken, local_time_taken, loss
@click.group(help="Run Tensorflow benchmarks")
def cli():
pass
@cli.command(help="Kick off Ray and vanilla benchmarks")
@click.option("--num-runs", type=int, default=1)
@click.option("--num-epochs", type=int, default=4)
@click.option("--num-workers", type=int, default=4)
@click.option("--cpus-per-worker", type=int, default=8)
@click.option("--use-gpu", is_flag=True, default=False)
@click.option("--batch-size", type=int, default=64)
@click.option("--smoke-test", is_flag=True, default=False)
@click.option("--local", is_flag=True, default=False)
def run(
num_runs: int = 1,
num_epochs: int = 4,
num_workers: int = 4,
cpus_per_worker: int = 8,
use_gpu: bool = False,
batch_size: int = 64,
smoke_test: bool = False,
local: bool = False,
):
# Note: smoke_test is ignored as we just adjust the batch size.
# The parameter is passed by the release test pipeline.
import ray
from benchmark_util import upload_file_to_all_nodes, run_command_on_all_nodes
config = CONFIG.copy()
config["epochs"] = num_epochs
config["batch_size"] = batch_size
if local:
ray.init(num_cpus=4)
else:
ray.init("auto")
print("Preparing Tensorflow benchmark: Downloading MNIST")
path = str((Path(__file__).parent / "_tensorflow_prepare.py").absolute())
upload_file_to_all_nodes(path)
run_command_on_all_nodes(["python", path])
times_ray = []
times_local_ray = []
losses_ray = []
times_vanilla = []
times_local_vanilla = []
losses_vanilla = []
for run in range(1, num_runs + 1):
time.sleep(2)
print(f"[Run {run}/{num_runs}] Running Tensorflow Ray benchmark")
time_ray, time_local_ray, loss_ray = train_tf_ray_air(
num_workers=num_workers,
cpus_per_worker=cpus_per_worker,
use_gpu=use_gpu,
config=config,
)
print(
f"[Run {run}/{num_runs}] Finished Ray training ({num_epochs} epochs) in "
f"{time_ray:.2f} seconds (local training time: {time_local_ray:.2f}s). "
f"Observed loss = {loss_ray:.4f}"
)
time.sleep(2)
print(f"[Run {run}/{num_runs}] Running Tensorflow vanilla benchmark")
# Todo: Vanilla runs are sometimes failing. We just retry here, but we should
# get to the bottom of it.
time_vanilla = time_local_vanilla = loss_vanilla = 0.0
for i in range(3):
try:
time_vanilla, time_local_vanilla, loss_vanilla = train_tf_vanilla(
num_workers=num_workers,
cpus_per_worker=cpus_per_worker,
use_gpu=use_gpu,
config=config,
)
except Exception as e:
if i >= 2:
raise RuntimeError("Vanilla TF run failed 3 times") from e
print("Vanilla TF run failed:", e)
continue
break
print(
f"[Run {run}/{num_runs}] Finished vanilla training ({num_epochs} epochs) "
f"in {time_vanilla:.2f} seconds "
f"(local training time: {time_local_vanilla:.2f}s). "
f"Observed loss = {loss_vanilla:.4f}"
)
print(
f"[Run {run}/{num_runs}] Observed results: ",
{
"tensorflow_mnist_ray_time_s": time_ray,
"tensorflow_mnist_ray_local_time_s": time_local_ray,
"tensorflow_mnist_ray_loss": loss_ray,
"tensorflow_mnist_vanilla_time_s": time_vanilla,
"tensorflow_mnist_vanilla_local_time_s": time_local_vanilla,
"tensorflow_mnist_vanilla_loss": loss_vanilla,
},
)
times_ray.append(time_ray)
times_local_ray.append(time_local_ray)
losses_ray.append(loss_ray)
times_vanilla.append(time_vanilla)
times_local_vanilla.append(time_local_vanilla)
losses_vanilla.append(loss_vanilla)
times_ray_mean = np.mean(times_ray)
times_ray_sd = np.std(times_ray)
times_local_ray_mean = np.mean(times_local_ray)
times_local_ray_sd = np.std(times_local_ray)
times_vanilla_mean = np.mean(times_vanilla)
times_vanilla_sd = np.std(times_vanilla)
times_local_vanilla_mean = np.mean(times_local_vanilla)
times_local_vanilla_sd = np.std(times_local_vanilla)
result = {
"tensorflow_mnist_ray_num_runs": num_runs,
"tensorflow_mnist_ray_time_s_all": times_ray,
"tensorflow_mnist_ray_time_s_mean": times_ray_mean,
"tensorflow_mnist_ray_time_s_sd": times_ray_sd,
"tensorflow_mnist_ray_time_local_s_all": times_local_ray,
"tensorflow_mnist_ray_time_local_s_mean": times_local_ray_mean,
"tensorflow_mnist_ray_time_local_s_sd": times_local_ray_sd,
"tensorflow_mnist_ray_loss_mean": np.mean(losses_ray),
"tensorflow_mnist_ray_loss_sd": np.std(losses_ray),
"tensorflow_mnist_vanilla_time_s_all": times_vanilla,
"tensorflow_mnist_vanilla_time_s_mean": times_vanilla_mean,
"tensorflow_mnist_vanilla_time_s_sd": times_vanilla_sd,
"tensorflow_mnist_vanilla_local_time_s_all": times_local_vanilla,
"tensorflow_mnist_vanilla_local_time_s_mean": times_local_vanilla_mean,
"tensorflow_mnist_vanilla_local_time_s_sd": times_local_vanilla_sd,
"tensorflow_mnist_vanilla_loss_mean": np.mean(losses_vanilla),
"tensorflow_mnist_vanilla_loss_std": np.std(losses_vanilla),
}
print("Results:", result)
test_output_json = os.environ.get("TEST_OUTPUT_JSON", "/tmp/result.json")
with open(test_output_json, "wt") as f:
json.dump(result, f)
target_ratio = 1.2
ratio = (
(times_local_ray_mean / times_local_vanilla_mean)
if times_local_vanilla_mean != 0.0
else 1.0
)
if ratio > target_ratio:
raise RuntimeError(
f"Training on Ray took an average of {times_local_ray_mean:.2f} seconds, "
f"which is more than {target_ratio:.2f}x of the average vanilla training "
f"time of {times_local_vanilla_mean:.2f} seconds ({ratio:.2f}x). FAILED"
)
print(
f"Training on Ray took an average of {times_local_ray_mean:.2f} seconds, "
f"which is less than {target_ratio:.2f}x of the average vanilla training "
f"time of {times_local_vanilla_mean:.2f} seconds ({ratio:.2f}x). PASSED"
)
@cli.command(help="Run Tensorflow vanilla worker")
@click.option("--num-epochs", type=int, default=4)
@click.option("--num-workers", type=int, default=4)
@click.option("--rank", type=int, default=0)
@click.option("--worker-ip-ports", type=str, default="")
@click.option("--batch-size", type=int, default=64)
@click.option("--use-gpu", is_flag=True, default=False)
def worker(
num_epochs: int = 4,
num_workers: int = 4,
rank: int = 0,
worker_ip_ports: str = "",
batch_size: int = 64,
use_gpu: bool = False,
):
config = CONFIG.copy()
config["epochs"] = num_epochs
config["batch_size"] = batch_size
# Parse worker ip ports
worker_ip_port_list = worker_ip_ports.split(",")
# Then we kick off the training function on every worker.
return train_tf_vanilla_worker(
config=config,
rank=rank,
world_size=num_workers,
worker_ip_port_list=worker_ip_port_list,
use_gpu=use_gpu,
)
def main():
return cli()
if __name__ == "__main__":
main()
@@ -0,0 +1,592 @@
import json
import os
import time
from pathlib import Path
from typing import Dict, Tuple
import tempfile
import click
import numpy as np
import torch
from torch import nn, distributed
from torch.utils.data import DataLoader, DistributedSampler
from torch.utils.data.dataloader import default_collate
from torchvision import datasets
from torchvision.transforms import ToTensor
CONFIG = {"lr": 1e-3, "batch_size": 64}
VANILLA_RESULT_JSON = "/tmp/vanilla_out.json"
# Define model
class NeuralNetwork(nn.Module):
def __init__(self):
super(NeuralNetwork, self).__init__()
self.flatten = nn.Flatten()
self.linear_relu_stack = nn.Sequential(
nn.Linear(28 * 28, 512),
nn.ReLU(),
nn.Linear(512, 512),
nn.ReLU(),
nn.Linear(512, 10),
nn.ReLU(),
)
def forward(self, x):
x = self.flatten(x)
logits = self.linear_relu_stack(x)
return logits
def train_epoch(
dataloader, model, loss_fn, optimizer, world_size: int, local_rank: int
):
size = len(dataloader.dataset) // world_size
model.train()
for batch, (X, y) in enumerate(dataloader):
# Compute prediction error
pred = model(X)
loss = loss_fn(pred, y)
# Backpropagation
optimizer.zero_grad()
loss.backward()
optimizer.step()
if batch % 100 == 0:
loss, current = loss.item(), batch * len(X)
print(f"[rank={local_rank}] loss: {loss:>7f} [{current:>5d}/{size:>5d}]")
def validate_epoch(dataloader, model, loss_fn, world_size: int, local_rank: int):
size = len(dataloader.dataset) // world_size
num_batches = len(dataloader)
model.eval()
test_loss, correct = 0, 0
with torch.no_grad():
for X, y in dataloader:
pred = model(X)
test_loss += loss_fn(pred, y).item()
correct += (pred.argmax(1) == y).type(torch.float).sum().item()
test_loss /= num_batches
correct /= size
print(
f"[rank={local_rank}] Test Error: \n "
f"Accuracy: {(100 * correct):>0.1f}%, "
f"Avg loss: {test_loss:>8f} \n"
)
return test_loss
def train_func(use_ray: bool, config: Dict):
local_start_time = time.monotonic()
if use_ray:
import ray.train as train
batch_size = config["batch_size"]
lr = config["lr"]
epochs = config["epochs"]
shuffle = config.get("shuffle", False)
if use_ray:
world_size = train.get_context().get_world_size()
local_rank = distributed.get_rank()
else:
world_size = distributed.get_world_size()
local_rank = distributed.get_rank()
worker_batch_size = batch_size // world_size
# Load datasets. Use download=False to catch errors in preparation, as the
# data should have already been downloaded.
training_data = datasets.FashionMNIST(
root="/tmp/data_fashion_mnist",
train=True,
download=False,
transform=ToTensor(),
)
test_data = datasets.FashionMNIST(
root="/tmp/data_fashion_mnist",
train=False,
download=False,
transform=ToTensor(),
)
if use_ray:
# Ray adds DistributedSampler in train.torch.prepare_data_loader below
training_sampler = None
test_sampler = None
else:
# In vanilla PyTorch we create the distributed sampler here
training_sampler = DistributedSampler(training_data, shuffle=shuffle)
test_sampler = DistributedSampler(test_data, shuffle=shuffle)
if not use_ray and config.get("use_gpu", False):
assert torch.cuda.is_available(), "No GPUs available"
gpu_id = config.get("gpu_id", 0)
vanilla_device = torch.device(f"cuda:{gpu_id}")
torch.cuda.set_device(vanilla_device)
print(
"Setting GPU ID to",
gpu_id,
"with visible devices",
os.environ.get("CUDA_VISIBLE_DEVICES"),
)
def collate_fn(x):
return tuple(x_.to(vanilla_device) for x_ in default_collate(x))
else:
vanilla_device = torch.device("cpu")
collate_fn = None
# Create data loaders and potentially pass distributed sampler
train_dataloader = DataLoader(
training_data,
shuffle=shuffle,
batch_size=worker_batch_size,
sampler=training_sampler,
collate_fn=collate_fn,
)
test_dataloader = DataLoader(
test_data,
shuffle=shuffle,
batch_size=worker_batch_size,
sampler=test_sampler,
collate_fn=collate_fn,
)
if use_ray:
# In Ray, we now retrofit the DistributedSampler
train_dataloader = train.torch.prepare_data_loader(train_dataloader)
test_dataloader = train.torch.prepare_data_loader(test_dataloader)
# Create model.
model = NeuralNetwork()
# Prepare model
if use_ray:
model = train.torch.prepare_model(model)
else:
model = model.to(vanilla_device)
if config.get("use_gpu", False):
model = nn.parallel.DistributedDataParallel(
model, device_ids=[gpu_id], output_device=gpu_id
)
else:
model = nn.parallel.DistributedDataParallel(model)
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=lr)
for epoch in range(epochs):
if world_size > 1:
train_dataloader.sampler.set_epoch(epoch)
train_epoch(
train_dataloader,
model,
loss_fn,
optimizer,
world_size=world_size,
local_rank=local_rank,
)
loss = validate_epoch(
test_dataloader,
model,
loss_fn,
world_size=world_size,
local_rank=local_rank,
)
local_time_taken = time.monotonic() - local_start_time
if use_ray:
with tempfile.TemporaryDirectory() as temp_checkpoint_dir:
if train.get_context().get_world_rank() == 0:
torch.save(
model.state_dict(),
os.path.join(temp_checkpoint_dir, "model.pt"),
)
train.report(
dict(loss=loss, local_time_taken=local_time_taken),
checkpoint=train.Checkpoint.from_directory(temp_checkpoint_dir),
)
else:
print(f"Reporting loss: {loss:.4f}")
if local_rank == 0:
with open(VANILLA_RESULT_JSON, "w") as f:
json.dump({"loss": loss, "local_time_taken": local_time_taken}, f)
def train_torch_ray_air(
*,
config: dict,
num_workers: int = 4,
cpus_per_worker: int = 8,
use_gpu: bool = False,
) -> Tuple[float, float, float]:
# This function is kicked off by the main() function and runs a full training
# run using Ray Train.
from ray.train import ScalingConfig, RunConfig
from ray.train.torch import TorchTrainer
def train_loop(config):
train_func(use_ray=True, config=config)
start_time = time.monotonic()
trainer = TorchTrainer(
train_loop_per_worker=train_loop,
train_loop_config=config,
scaling_config=ScalingConfig(
num_workers=num_workers,
resources_per_worker={"CPU": cpus_per_worker},
use_gpu=use_gpu,
),
run_config=RunConfig(storage_path="/mnt/cluster_storage"),
)
result = trainer.fit()
time_taken = time.monotonic() - start_time
print(f"Last result: {result.metrics}")
return time_taken, result.metrics["local_time_taken"], result.metrics["loss"]
def train_torch_vanilla_worker(
*,
config: dict,
rank: int,
world_size: int,
master_addr: str,
master_port: int,
use_gpu: bool = False,
gpu_id: int = 0,
):
# This function is kicked off by the main() function and runs the vanilla
# training script on a single worker.
backend = "nccl" if use_gpu else "gloo"
os.environ["MASTER_ADDR"] = master_addr
os.environ["MASTER_PORT"] = str(master_port)
os.environ["NCCL_BLOCKING_WAIT"] = "1"
distributed.init_process_group(
backend=backend, rank=rank, world_size=world_size, init_method="env://"
)
config["use_gpu"] = use_gpu
config["gpu_id"] = gpu_id
train_func(use_ray=False, config=config)
distributed.destroy_process_group()
def train_torch_vanilla(
*,
config: dict,
num_workers: int = 4,
cpus_per_worker: int = 8,
use_gpu: bool = False,
) -> Tuple[float, float, float]:
# This function is kicked off by the main() function and subsequently kicks
# off tasks that run train_torch_vanilla_worker() on the worker nodes.
from benchmark_util import (
upload_file_to_all_nodes,
create_actors_with_options,
run_commands_on_actors,
run_fn_on_actors,
get_ip_port_actors,
get_gpu_ids_actors,
map_ips_to_gpus,
set_cuda_visible_devices,
)
path = os.path.abspath(__file__)
upload_file_to_all_nodes(path)
num_epochs = config["epochs"]
actors = create_actors_with_options(
num_actors=num_workers,
resources={
"CPU": cpus_per_worker,
"GPU": int(use_gpu),
},
)
run_fn_on_actors(actors=actors, fn=lambda: os.environ.pop("OMP_NUM_THREADS", None))
# Get IPs and ports for all actors
ip_ports = get_ip_port_actors(actors=actors)
# Rank 0 is the master addr/port
master_addr, master_port = ip_ports[0]
if use_gpu:
# Extract IPs
actor_ips = [ipp[0] for ipp in ip_ports]
# Get allocated GPU IDs for all actors
gpu_ids = get_gpu_ids_actors(actors=actors)
# Build a map of IP to all allocated GPUs on that machine
ip_to_gpu_map = map_ips_to_gpus(ips=actor_ips, gpus=gpu_ids)
# Set the environment variables on the workers
set_cuda_visible_devices(
actors=actors, actor_ips=actor_ips, ip_to_gpus=ip_to_gpu_map
)
use_gpu_ids = [gi[0] for gi in gpu_ids]
else:
use_gpu_ids = [0] * num_workers
cmds = [
[
"python",
path,
"worker",
"--num-epochs",
str(num_epochs),
"--num-workers",
str(num_workers),
"--rank",
str(rank),
"--master-addr",
master_addr,
"--master-port",
str(master_port),
"--batch-size",
str(config["batch_size"]),
]
+ (["--use-gpu"] if use_gpu else [])
+ (["--gpu-id", str(use_gpu_ids[rank])] if use_gpu else [])
for rank in range(num_workers)
]
run_fn_on_actors(
actors=actors, fn=lambda: os.environ.setdefault("OMP_NUM_THREADS", "1")
)
start_time = time.monotonic()
run_commands_on_actors(actors=actors, cmds=cmds)
time_taken = time.monotonic() - start_time
loss = 0.0
if os.path.exists(VANILLA_RESULT_JSON):
with open(VANILLA_RESULT_JSON, "r") as f:
result = json.load(f)
loss = result["loss"]
local_time_taken = result["local_time_taken"]
return time_taken, local_time_taken, loss
@click.group(help="Run Torch benchmarks")
def cli():
pass
@cli.command(help="Kick off Ray and vanilla benchmarks")
@click.option("--num-runs", type=int, default=1)
@click.option("--num-epochs", type=int, default=4)
@click.option("--num-workers", type=int, default=4)
@click.option("--cpus-per-worker", type=int, default=8)
@click.option("--use-gpu", is_flag=True, default=False)
@click.option("--batch-size", type=int, default=64)
@click.option("--smoke-test", is_flag=True, default=False)
@click.option("--local", is_flag=True, default=False)
def run(
num_runs: int = 1,
num_epochs: int = 4,
num_workers: int = 4,
cpus_per_worker: int = 8,
use_gpu: bool = False,
batch_size: int = 64,
smoke_test: bool = False,
local: bool = False,
):
# Note: smoke_test is ignored as we just adjust the batch size.
# The parameter is passed by the release test pipeline.
import ray
from benchmark_util import upload_file_to_all_nodes, run_command_on_all_nodes
config = CONFIG.copy()
config["epochs"] = num_epochs
config["batch_size"] = batch_size
if local:
ray.init(num_cpus=4)
else:
ray.init("auto")
print("Preparing Torch benchmark: Downloading MNIST")
path = str((Path(__file__).parent / "_torch_prepare.py").absolute())
upload_file_to_all_nodes(path)
run_command_on_all_nodes(["python", path])
times_ray = []
times_local_ray = []
losses_ray = []
times_vanilla = []
times_local_vanilla = []
losses_vanilla = []
for run in range(1, num_runs + 1):
time.sleep(2)
print(f"[Run {run}/{num_runs}] Running Torch Ray benchmark")
time_ray, time_local_ray, loss_ray = train_torch_ray_air(
num_workers=num_workers,
cpus_per_worker=cpus_per_worker,
use_gpu=use_gpu,
config=config,
)
print(
f"[Run {run}/{num_runs}] Finished Ray training ({num_epochs} epochs) in "
f"{time_ray:.2f} seconds (local training time: {time_local_ray:.2f}s). "
f"Observed loss = {loss_ray:.4f}"
)
time.sleep(2)
print(f"[Run {run}/{num_runs}] Running Torch vanilla benchmark")
time_vanilla, time_local_vanilla, loss_vanilla = train_torch_vanilla(
num_workers=num_workers,
cpus_per_worker=cpus_per_worker,
use_gpu=use_gpu,
config=config,
)
print(
f"[Run {run}/{num_runs}] Finished vanilla training ({num_epochs} epochs) "
f"in {time_vanilla:.2f} seconds "
f"(local training time: {time_local_vanilla:.2f}s). "
f"Observed loss = {loss_vanilla:.4f}"
)
print(
f"[Run {run}/{num_runs}] Observed results: ",
{
"tensorflow_mnist_ray_time_s": time_ray,
"tensorflow_mnist_ray_local_time_s": time_local_ray,
"tensorflow_mnist_ray_loss": loss_ray,
"tensorflow_mnist_vanilla_time_s": time_vanilla,
"tensorflow_mnist_vanilla_local_time_s": time_local_vanilla,
"tensorflow_mnist_vanilla_loss": loss_vanilla,
},
)
times_ray.append(time_ray)
times_local_ray.append(time_local_ray)
losses_ray.append(loss_ray)
times_vanilla.append(time_vanilla)
times_local_vanilla.append(time_local_vanilla)
losses_vanilla.append(loss_vanilla)
times_ray_mean = np.mean(times_ray)
times_ray_sd = np.std(times_ray)
times_local_ray_mean = np.mean(times_local_ray)
times_local_ray_sd = np.std(times_local_ray)
times_vanilla_mean = np.mean(times_vanilla)
times_vanilla_sd = np.std(times_vanilla)
times_local_vanilla_mean = np.mean(times_local_vanilla)
times_local_vanilla_sd = np.std(times_local_vanilla)
result = {
"torch_mnist_ray_num_runs": num_runs,
"torch_mnist_ray_time_s_all": times_ray,
"torch_mnist_ray_time_s_mean": times_ray_mean,
"torch_mnist_ray_time_s_sd": times_ray_sd,
"torch_mnist_ray_time_local_s_all": times_local_ray,
"torch_mnist_ray_time_local_s_mean": times_local_ray_mean,
"torch_mnist_ray_time_local_s_sd": times_local_ray_sd,
"torch_mnist_ray_loss_mean": np.mean(losses_ray),
"torch_mnist_ray_loss_sd": np.std(losses_ray),
"torch_mnist_vanilla_time_s_all": times_vanilla,
"torch_mnist_vanilla_time_s_mean": times_vanilla_mean,
"torch_mnist_vanilla_time_s_sd": times_vanilla_sd,
"torch_mnist_vanilla_local_time_s_all": times_local_vanilla,
"torch_mnist_vanilla_local_time_s_mean": times_local_vanilla_mean,
"torch_mnist_vanilla_local_time_s_sd": times_local_vanilla_sd,
"torch_mnist_vanilla_loss_mean": np.mean(losses_vanilla),
"torch_mnist_vanilla_loss_std": np.std(losses_vanilla),
}
print("Results:", result)
test_output_json = os.environ.get("TEST_OUTPUT_JSON", "/tmp/result.json")
with open(test_output_json, "wt") as f:
json.dump(result, f)
target_ratio = 1.15
ratio = (
(times_local_ray_mean / times_local_vanilla_mean)
if times_local_vanilla_mean != 0.0
else 1.0
)
if ratio > target_ratio:
raise RuntimeError(
f"Training on Ray took an average of {times_local_ray_mean:.2f} seconds, "
f"which is more than {target_ratio:.2f}x of the average vanilla training "
f"time of {times_local_vanilla_mean:.2f} seconds ({ratio:.2f}x). FAILED"
)
print(
f"Training on Ray took an average of {times_local_ray_mean:.2f} seconds, "
f"which is less than {target_ratio:.2f}x of the average vanilla training "
f"time of {times_local_vanilla_mean:.2f} seconds ({ratio:.2f}x). PASSED"
)
@cli.command(help="Run PyTorch vanilla worker")
@click.option("--num-epochs", type=int, default=4)
@click.option("--num-workers", type=int, default=4)
@click.option("--rank", type=int, default=0)
@click.option("--master-addr", type=str, default="")
@click.option("--master-port", type=int, default=0)
@click.option("--batch-size", type=int, default=64)
@click.option("--use-gpu", is_flag=True, default=False)
@click.option("--gpu-id", type=int, default=0)
def worker(
num_epochs: int = 4,
num_workers: int = 4,
rank: int = 0,
master_addr: str = "",
master_port: int = 0,
batch_size: int = 64,
use_gpu: bool = False,
gpu_id: int = 0,
):
config = CONFIG.copy()
config["epochs"] = num_epochs
config["batch_size"] = batch_size
# Then we kick off the training function on every worker.
return train_torch_vanilla_worker(
config=config,
rank=rank,
world_size=num_workers,
master_addr=master_addr,
master_port=master_port,
use_gpu=use_gpu,
gpu_id=gpu_id,
)
def main():
return cli()
if __name__ == "__main__":
main()
@@ -0,0 +1,232 @@
import json
import os
import time
from typing import Optional, Dict, List
import click
import numpy as np
import ray
from ray.train import ScalingConfig, RunConfig
from ray.train.torch import TorchTrainer
from ray.tune.integration.ray_train import TuneReportCallback
CONFIG = {"lr": 1e-3, "batch_size": 64, "epochs": 20}
def prepare_mnist():
# Pre-download the data onto each node.
from benchmark_util import schedule_remote_fn_on_all_nodes
print("Preparing Torch benchmark: Downloading MNIST")
@ray.remote(num_cpus=0)
def _download_data():
import torchvision
torchvision.datasets.FashionMNIST("/tmp/data_fashion_mnist", download=True)
return True
ray.get(schedule_remote_fn_on_all_nodes(_download_data))
def train_loop(config: Dict):
from torch_benchmark import train_func
train_func(use_ray=True, config=config)
def get_trainer(
num_workers: int = 4,
use_gpu: bool = False,
config: Optional[Dict] = None,
):
"""Get the trainer to be used across train and tune to ensure consistency."""
# We are using STRICT_PACK here to do an apples to apples comparison.
# PyTorch defaults to using multithreading, so if the workers are spread,
# they are able to utilize more resources. We would effectively be comparing
# X tune runs with 2 CPUs per worker vs. 1 tune run with up to 8 CPUs per
# worker. Using STRICT_PACK avoids this by forcing all workers to be
# co-located.
config = config or CONFIG
trainer = TorchTrainer(
train_loop_per_worker=train_loop,
train_loop_config=config,
scaling_config=ScalingConfig(
num_workers=num_workers,
resources_per_worker={"CPU": 2},
use_gpu=use_gpu,
placement_strategy="STRICT_PACK",
),
run_config=RunConfig(
name="train_torch_benchmark",
storage_path="/mnt/cluster_storage/ray-train-results",
),
)
return trainer
def train_torch(
num_workers: int, use_gpu: bool = False, config: Optional[Dict] = None
) -> float:
trainer = get_trainer(num_workers=num_workers, use_gpu=use_gpu, config=config)
result = trainer.fit()
return result.metrics["local_time_taken"]
def train_driver_fn(config: Dict):
trainer = TorchTrainer(
train_loop_per_worker=train_loop,
train_loop_config=config["train_loop_config"],
run_config=RunConfig(
name="tune_torch_benchmark",
storage_path="/mnt/cluster_storage/ray-tune-results",
callbacks=[TuneReportCallback()],
),
scaling_config=ScalingConfig(
num_workers=config["num_workers"],
resources_per_worker={"CPU": 2},
use_gpu=config["use_gpu"],
placement_strategy="STRICT_PACK",
),
)
trainer.fit()
def tune_torch(
num_workers: int = 4,
num_trials: int = 8,
use_gpu: bool = False,
config: Optional[Dict] = None,
) -> List[float]:
"""Making sure that tuning multiple trials in parallel is not
taking significantly longer than training each one individually.
Some overhead is expected.
"""
from ray import tune
from ray.tune.tuner import Tuner
from ray.tune.tune_config import TuneConfig
param_space = {
"train_loop_config": {
"lr": tune.loguniform(1e-4, 1e-1),
},
"num_workers": num_workers,
"use_gpu": use_gpu,
}
param_space["train_loop_config"].update(config or {})
tuner = Tuner(
trainable=train_driver_fn,
param_space=param_space,
tune_config=TuneConfig(mode="min", metric="loss", num_samples=num_trials),
)
results = tuner.fit()
return [result.metrics["local_time_taken"] for result in results]
@click.command(help="Run Benchmark comparing Train to Tune.")
@click.option("--num-runs", type=int, default=1)
@click.option("--num-trials", type=int, default=8)
@click.option("--num-workers", type=int, default=4)
@click.option("--use-gpu", is_flag=True)
@click.option("--smoke-test", is_flag=True, default=False)
def main(
num_runs: int = 1,
num_trials: int = 8,
num_workers: int = 4,
use_gpu: bool = False,
smoke_test: bool = False,
):
ray.init(
runtime_env={
"working_dir": os.path.dirname(__file__),
}
)
prepare_mnist()
config = CONFIG.copy()
if smoke_test:
config["epochs"] = 1
train_times = []
tune_times = []
train_computes = []
tune_trial_computes = []
for i in range(num_runs):
print(f"Run {i+1} / {num_runs}")
time.sleep(2)
train_start = time.monotonic()
train_compute = train_torch(
num_workers=num_workers, use_gpu=use_gpu, config=config
)
train_time = time.monotonic() - train_start
train_times.append(train_time)
train_computes.append(train_compute)
time.sleep(2)
tune_start = time.monotonic()
trial_computes = tune_torch(
num_workers=num_workers,
num_trials=num_trials,
use_gpu=use_gpu,
config=config,
)
tune_time = time.monotonic() - tune_start
tune_times.append(tune_time)
tune_trial_computes.append(trial_computes)
result = {
"train_time": train_time,
"train_compute": train_compute,
"train_overhead": train_time - train_compute,
"tune_time": tune_time,
"tune_overhead": tune_time - max(trial_computes),
}
print(f"Results run {i+1}: {result}")
mean_train_time = np.mean(train_times)
mean_tune_time = np.mean(tune_times)
full_results = {
"train_times": train_times,
"train_mean": mean_train_time,
"train_sd": np.std(train_times),
"tune_times": tune_times,
"tune_mean": mean_tune_time,
"tune_sd": np.std(tune_times),
"train_computes": train_computes,
"tune_trial_computes": tune_trial_computes,
}
print("Full results:", full_results)
# NOTE: The value of `factor` is mostly arbitrary. It was previously `1.2`, but
# that value turned out to be too low. For more context, see #29682.
factor = 1.35
threshold = mean_train_time * factor
test_output_json = os.environ.get("TEST_OUTPUT_JSON", "/tmp/result.json")
with open(test_output_json, "wt") as f:
json.dump(full_results, f)
assert (
mean_tune_time <= threshold
), f"{mean_tune_time:.2f} > {threshold:.2f} = {factor:.1f} * {mean_train_time:.2f}"
if __name__ == "__main__":
main()
+17
View File
@@ -0,0 +1,17 @@
cloud_id: {{env["ANYSCALE_CLOUD_ID"]}}
region: us-west-2
max_workers: 20
head_node_type:
name: head_node
instance_type: m5.16xlarge
resources:
cpu: 0
worker_node_types:
- name: worker_node
instance_type: m5.xlarge
min_workers: 0
max_workers: 20
+18
View File
@@ -0,0 +1,18 @@
import logging
from rich.logging import RichHandler
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def add_handlers(logger: logging.Logger):
handler = RichHandler()
formatter = logging.Formatter(
fmt="[%(levelname)s %(asctime)s] %(filename)s:%(lineno)d - %(message)s"
)
handler.setFormatter(formatter)
logger.addHandler(handler)
if not logger.hasHandlers():
add_handlers(logger)
+82
View File
@@ -0,0 +1,82 @@
import subprocess
import click
import json
import os
import time
from logger import logger
WORKLOAD_SCRIPTS = [
"test_core.py",
]
def setup_cluster():
from ray.cluster_utils import AutoscalingCluster
cluster = AutoscalingCluster(
head_resources={"CPU": 0},
worker_node_types={
"type-1": {
"resources": {"CPU": 4},
"node_config": {},
"min_workers": 0,
"max_workers": 10,
},
},
idle_timeout_minutes=1 * 0.1,
)
cluster.start(_system_config={"enable_autoscaler_v2": True})
return cluster
def run_test():
failed_workloads = []
for workload in WORKLOAD_SCRIPTS:
# Run the python script.
logger.info(f"Running workload {workload}:")
try:
subprocess.check_call(["python", workload])
except subprocess.CalledProcessError as e:
failed_workloads.append((workload, e))
if failed_workloads:
for workload, e in failed_workloads:
logger.error(f"Workload {workload} failed with {e}")
raise RuntimeError(f"{len(failed_workloads)} workloads failed.")
else:
logger.info("All workloads passed!")
@click.command()
@click.option("--local", is_flag=True, help="Run locally.", default=False)
def run(local):
start_time = time.time()
cluster = None
try:
if local:
cluster = setup_cluster()
run_test()
cluster.shutdown()
else:
run_test()
except Exception as e:
logger.error(f"Test failed with {e}")
raise e
finally:
if cluster:
cluster.shutdown()
results = {
"time": time.time() - start_time,
}
if "TEST_OUTPUT_JSON" in os.environ:
with open(os.environ["TEST_OUTPUT_JSON"], "w") as out_file:
json.dump(results, out_file)
print(json.dumps(results, indent=2))
if __name__ == "__main__":
run()
+131
View File
@@ -0,0 +1,131 @@
import ray
from ray._common.test_utils import wait_for_condition
from ray.autoscaler.v2.sdk import get_cluster_status
import time
from logger import logger
from typing import Dict
ray.init("auto")
# Sync with the compute config.
HEAD_NODE_CPU = 0
WORKER_NODE_CPU = 4
IDLE_TERMINATION_S = 60 * 5 # 5 min
DEFAULT_RETRY_INTERVAL_MS = 15 * 1000 # 15 sec
def check_cluster(target_num_nodes: int, target_resources: Dict[str, float]):
gcs_address = ray.get_runtime_context().gcs_address
cluster_status = get_cluster_status(gcs_address)
assert (
len(cluster_status.active_nodes) + len(cluster_status.idle_nodes)
) == target_num_nodes
for k, v in target_resources.items():
assert cluster_status.total_resources().get(k, 0) == v
return True
ctx = {
"num_cpus": 0,
"num_nodes": 1,
}
logger.info(f"Starting cluster with {ctx['num_nodes']} nodes, {ctx['num_cpus']} cpus")
check_cluster(
target_num_nodes=ctx["num_nodes"], target_resources={"CPU": ctx["num_cpus"]}
)
# Request for cluster resources
def test_request_cluster_resources(ctx: dict):
from ray.autoscaler._private.commands import request_resources
request_resources(num_cpus=8)
ctx["num_cpus"] += 8
ctx["num_nodes"] += 8 // WORKER_NODE_CPU
# Assert on number of worker nodes.
logger.info(
f"Requesting cluster constraints: {ctx['num_nodes']} nodes, "
f"{ctx['num_cpus']} cpus"
)
wait_for_condition(
check_cluster,
timeout=60 * 5, # 5min
retry_interval_ms=DEFAULT_RETRY_INTERVAL_MS,
target_num_nodes=ctx["num_nodes"],
target_resources={"CPU": ctx["num_cpus"]},
)
# Reset the cluster constraints.
request_resources(num_cpus=0)
ctx["num_cpus"] -= 8
ctx["num_nodes"] -= 8 // WORKER_NODE_CPU
logger.info(
f"Waiting for cluster go idle after constraint cleared: {ctx['num_nodes']} "
f"nodes, {ctx['num_cpus']} cpus"
)
wait_for_condition(
check_cluster,
timeout=60 + IDLE_TERMINATION_S, # 1min + idle timeout
retry_interval_ms=DEFAULT_RETRY_INTERVAL_MS,
target_num_nodes=ctx["num_nodes"],
target_resources={"CPU": ctx["num_cpus"]},
)
# Run actors/tasks that exceed the cluster resources should upscale the cluster
def test_run_tasks_concurrent(ctx: dict):
num_tasks = 2
num_actors = 2
@ray.remote(num_cpus=WORKER_NODE_CPU)
def f():
while True:
time.sleep(1)
@ray.remote(num_cpus=WORKER_NODE_CPU)
class Actor:
def __init__(self):
pass
tasks = [f.remote() for _ in range(num_tasks)]
actors = [Actor.remote() for _ in range(num_actors)]
ctx["num_cpus"] += (num_tasks + num_actors) * WORKER_NODE_CPU
ctx["num_nodes"] += num_tasks + num_actors
logger.info(f"Waiting for {ctx['num_nodes']} nodes, {ctx['num_cpus']} cpus")
wait_for_condition(
check_cluster,
timeout=60 * 5, # 5min
retry_interval_ms=DEFAULT_RETRY_INTERVAL_MS,
target_num_nodes=ctx["num_nodes"],
target_resources={"CPU": ctx["num_cpus"]},
)
[ray.cancel(task) for task in tasks]
[ray.kill(actor) for actor in actors]
ctx["num_cpus"] -= (num_actors + num_tasks) * WORKER_NODE_CPU
ctx["num_nodes"] -= num_actors + num_tasks
logger.info(
f"Waiting for cluster to scale down to {ctx['num_nodes']} nodes, "
f"{ctx['num_cpus']} cpus"
)
wait_for_condition(
check_cluster,
timeout=60 + IDLE_TERMINATION_S,
retry_interval_ms=DEFAULT_RETRY_INTERVAL_MS,
target_num_nodes=ctx["num_nodes"],
target_resources={"CPU": ctx["num_cpus"]},
)
test_request_cluster_resources(ctx)
test_run_tasks_concurrent(ctx)
+13
View File
@@ -0,0 +1,13 @@
{
"type": "external_account",
"audience": "//iam.googleapis.com/projects/498773744730/locations/global/workloadIdentityPools/anyscale-ci-aws/providers/aws-anyscale",
"subject_token_type": "urn:ietf:params:aws:token-type:aws4_request",
"service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/498773744730-compute@developer.gserviceaccount.com:generateAccessToken",
"token_url": "https://sts.googleapis.com/v1/token",
"credential_source": {
"environment_id": "aws1",
"region_url": "http://169.254.169.254/latest/meta-data/placement/availability-zone",
"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials",
"regional_cred_verification_url": "https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15"
}
}
+25
View File
@@ -0,0 +1,25 @@
#!/bin/bash
# This script is used to login to azure docker registry using azure cli
set -euo pipefail
# Retrieve credentials from Secrets Manager
SECRET="$(aws secretsmanager get-secret-value \
--secret-id azure-service-principal-oss-release \
--query SecretString \
--region us-west-2 \
--output text)"
CLIENT_ID="$(echo "$SECRET" | jq -r '.client_id')"
TENANT_ID="$(echo "$SECRET" | jq -r '.tenant_id')"
temp_dir=$(mktemp -d)
aws secretsmanager get-secret-value \
--secret-id azure-service-principal-certificate \
--query SecretString \
--region us-west-2 \
--output text > "${temp_dir}/azure_cert.pem"
# Login to azure
az login --service-principal --username "$CLIENT_ID" --certificate "${temp_dir}/azure_cert.pem" --tenant "$TENANT_ID"
@@ -0,0 +1,10 @@
base_image: {{ env["RAY_IMAGE_ML_NIGHTLY_GPU"] | default("anyscale/ray-ml:nightly-py39-gpu") }}
env_vars: {}
python:
pip_packages: []
conda_packages: []
post_build_cmds:
- pip uninstall -y ray || true && pip3 install -U {{ env["RAY_WHEELS"] | default("ray") }}
- {{ env["RAY_WHEELS_SANITY_CHECK"] | default("echo No Ray wheels sanity check") }}
@@ -0,0 +1,365 @@
#!/usr/bin/env python3
"""
$ ./benchmark_worker_startup.py --help
usage: benchmark_worker_startup.py [-h] --num_gpus_in_cluster
NUM_GPUS_IN_CLUSTER
--num_cpus_in_cluster
NUM_CPUS_IN_CLUSTER
--num_tasks_or_actors_per_run
NUM_TASKS_OR_ACTORS_PER_RUN
--num_measurements_per_configuration
NUM_MEASUREMENTS_PER_CONFIGURATION
This release test measures Ray worker startup time. Specifically, it
measures the time to start N different tasks or actors, where each task or
actor imports a large library (currently PyTorch). N is configurable. The
test runs under a few different configurations: {task, actor} x {runtime
env, no runtime env} x {GPU, no GPU} x {cold start, warm start} x {import
torch, no imports}.
options:
-h, --help show this help message and exit
--num_gpus_in_cluster NUM_GPUS_IN_CLUSTER
The number of GPUs in the cluster. This determines
how many GPU resources each actor/task requests.
--num_cpus_in_cluster NUM_CPUS_IN_CLUSTER
The number of CPUs in the cluster. This determines
how many CPU resources each actor/task requests.
--num_tasks_or_actors_per_run NUM_TASKS_OR_ACTORS_PER_RUN
The number of tasks or actors per 'run'. A run
starts this many tasks/actors and consitutes a
single measurement. Several runs can be composed
within a single job for measure warm start, or
spread across different jobs to measure cold start.
--num_measurements_per_configuration NUM_MEASUREMENTS_PER_CONFIGURATION
The number of measurements to record per
configuration.
This script uses test_single_configuration.py to run the actual
measurements.
"""
import argparse
import asyncio
import random
import statistics
import subprocess
import sys
from collections import defaultdict
from dataclasses import dataclass
import ray
from ray._private.test_utils import safe_write_to_results_json
from ray.job_submission import JobStatus, JobSubmissionClient
def main(
num_cpus_in_cluster: int,
num_gpus_in_cluster: int,
num_tasks_or_actors_per_run: int,
num_measurements_per_configuration: int,
):
"""
Generate test cases, then run them in random order via run_and_stream_logs.
"""
metrics_actor_name = "metrics_actor"
metrics_actor_namespace = "metrics_actor_namespace"
metrics_actor = MetricsActor.options( # noqa: F841
name=metrics_actor_name,
namespace=metrics_actor_namespace,
).remote(
expected_measurements_per_test=num_measurements_per_configuration,
)
print_disk_config()
run_matrix = generate_test_matrix(
num_cpus_in_cluster,
num_gpus_in_cluster,
num_tasks_or_actors_per_run,
num_measurements_per_configuration,
)
print(f"List of tests: {run_matrix}")
for test in random.sample(list(run_matrix), k=len(run_matrix)):
print(f"Running test {test}")
asyncio.run(
run_and_stream_logs(
metrics_actor_name,
metrics_actor_namespace,
test,
)
)
@ray.remote(num_cpus=0)
class MetricsActor:
"""
Actor which tests will report metrics to.
"""
def __init__(self, expected_measurements_per_test: int):
self.measurements = defaultdict(list)
self.expected_measurements_per_test = expected_measurements_per_test
def submit(self, test_name: str, latency: float):
print(f"got latency {latency} s for test {test_name}")
self.measurements[test_name].append(latency)
results = self.create_results_dict_from_measurements(
self.measurements, self.expected_measurements_per_test
)
safe_write_to_results_json(results)
assert (
len(self.measurements[test_name]) <= self.expected_measurements_per_test
), (
f"Expected {self.measurements[test_name]} to not have more elements than "
f"{self.expected_measurements_per_test}"
)
@staticmethod
def create_results_dict_from_measurements(
all_measurements, expected_measurements_per_test
):
results = {}
perf_metrics = []
for test_name, measurements in all_measurements.items():
test_summary = {
"measurements": measurements,
}
if len(measurements) == expected_measurements_per_test:
median = statistics.median(measurements)
test_summary["p50"] = median
perf_metrics.append(
{
"perf_metric_name": f"p50.{test_name}",
"perf_metric_value": median,
"perf_metric_type": "LATENCY",
}
)
results[test_name] = test_summary
results["perf_metrics"] = perf_metrics
return results
def print_disk_config():
print("Getting disk sizes via df -h")
subprocess.check_call("df -h", shell=True)
def generate_test_matrix(
num_cpus_in_cluster: int,
num_gpus_in_cluster: int,
num_tasks_or_actors_per_run: int,
num_measurements_per_test: int,
):
num_repeated_jobs_or_runs = num_measurements_per_test
total_num_tasks_or_actors = num_tasks_or_actors_per_run * num_repeated_jobs_or_runs
num_jobs_per_type = {
"cold_start": num_repeated_jobs_or_runs,
"warm_start": 1,
}
imports_to_try = ["torch", "none"]
tests = set()
for with_tasks in [True, False]:
for with_gpu in [True, False]:
# Do not run without runtime env. TODO(cade) Infra team added cgroups to
# default runtime env, need to find some way around that if we want
# "pure" (non-runtime-env) measurements.
for with_runtime_env in [True]:
for import_to_try in imports_to_try:
for num_jobs in num_jobs_per_type.values():
num_tasks_or_actors_per_job = (
total_num_tasks_or_actors // num_jobs
)
num_runs_per_job = (
num_tasks_or_actors_per_job // num_tasks_or_actors_per_run
)
test = TestConfiguration(
num_jobs=num_jobs,
num_runs_per_job=num_runs_per_job,
num_tasks_or_actors_per_run=num_tasks_or_actors_per_run,
with_tasks=with_tasks,
with_gpu=with_gpu,
with_runtime_env=with_runtime_env,
import_to_try=import_to_try,
num_cpus_in_cluster=num_cpus_in_cluster,
num_gpus_in_cluster=num_gpus_in_cluster,
num_nodes_in_cluster=1,
)
tests.add(test)
return tests
@dataclass(eq=True, frozen=True)
class TestConfiguration:
num_jobs: int
num_runs_per_job: int
num_tasks_or_actors_per_run: int
with_gpu: bool
with_tasks: bool
with_runtime_env: bool
import_to_try: str
num_cpus_in_cluster: int
num_gpus_in_cluster: int
num_nodes_in_cluster: int
def __repr__(self):
with_gpu_str = "with_gpu" if self.with_gpu else "without_gpu"
executable_unit = "tasks" if self.with_tasks else "actors"
cold_or_warm_start = "cold" if self.num_jobs > 1 else "warm"
with_runtime_env_str = (
"with_runtime_env" if self.with_runtime_env else "without_runtime_env"
)
single_node_or_multi_node = (
"single_node" if self.num_nodes_in_cluster == 1 else "multi_node"
)
import_torch_or_none = (
"import_torch" if self.import_to_try == "torch" else "no_import"
)
return "-".join(
[
f"seconds_to_{cold_or_warm_start}_start_"
f"{self.num_tasks_or_actors_per_run}_{executable_unit}",
import_torch_or_none,
with_gpu_str,
single_node_or_multi_node,
with_runtime_env_str,
f"{self.num_cpus_in_cluster}_CPU_{self.num_gpus_in_cluster}"
"_GPU_cluster",
]
)
async def run_and_stream_logs(
metrics_actor_name, metrics_actor_namespace, test: TestConfiguration
):
"""
Run a particular test configuration by invoking ./test_single_configuration.py.
"""
client = JobSubmissionClient("http://127.0.0.1:8265")
entrypoint = generate_entrypoint(metrics_actor_name, metrics_actor_namespace, test)
for _ in range(test.num_jobs):
print(f"Running {entrypoint}")
if not test.with_runtime_env:
# On non-workspaces, this will run as a job but without a runtime env.
subprocess.check_call(entrypoint, shell=True)
else:
job_id = client.submit_job(
entrypoint=entrypoint,
runtime_env={"working_dir": "./"},
)
try:
async for lines in client.tail_job_logs(job_id):
print(lines, end="")
except KeyboardInterrupt:
print(f"Stopping job {job_id}")
client.stop_job(job_id)
raise
job_status = client.get_job_status(job_id)
if job_status != JobStatus.SUCCEEDED:
raise ValueError(
f"Job {job_id} was not successful; status is {job_status}"
)
def generate_entrypoint(
metrics_actor_name: str, metrics_actor_namespace: str, test: TestConfiguration
):
task_or_actor_arg = "--with_tasks" if test.with_tasks else "--with_actors"
with_gpu_arg = "--with_gpu" if test.with_gpu else "--without_gpu"
with_runtime_env_arg = (
"--with_runtime_env" if test.with_runtime_env else "--without_runtime_env"
)
return " ".join(
[
"python ./test_single_configuration.py",
f"--metrics_actor_name {metrics_actor_name}",
f"--metrics_actor_namespace {metrics_actor_namespace}",
f"--test_name {test}",
f"--num_runs {test.num_runs_per_job} ",
f"--num_tasks_or_actors_per_run {test.num_tasks_or_actors_per_run}",
f"--num_cpus_in_cluster {test.num_cpus_in_cluster}",
f"--num_gpus_in_cluster {test.num_gpus_in_cluster}",
task_or_actor_arg,
with_gpu_arg,
with_runtime_env_arg,
f"--library_to_import {test.import_to_try}",
]
)
def parse_args():
parser = argparse.ArgumentParser(
description="This release test measures Ray worker startup time. "
"Specifically, it measures the time to start N different tasks or"
" actors, where each task or actor imports a large library ("
"currently PyTorch). N is configurable.\nThe test runs under a "
"few different configurations: {task, actor} x {runtime env, "
"no runtime env} x {GPU, no GPU} x {cold start, warm start} x "
"{import torch, no imports}.",
epilog="This script uses test_single_configuration.py to run the "
"actual measurements.",
)
parser.add_argument(
"--num_gpus_in_cluster",
type=int,
required=True,
help="The number of GPUs in the cluster. This determines how many "
"GPU resources each actor/task requests.",
)
parser.add_argument(
"--num_cpus_in_cluster",
type=int,
required=True,
help="The number of CPUs in the cluster. This determines how many "
"CPU resources each actor/task requests.",
)
parser.add_argument(
"--num_tasks_or_actors_per_run",
type=int,
required=True,
help="The number of tasks or actors per 'run'. A run starts this "
"many tasks/actors and consitutes a single measurement. Several "
"runs can be composed within a single job for measure warm start, "
"or spread across different jobs to measure cold start.",
)
parser.add_argument(
"--num_measurements_per_configuration",
type=int,
required=True,
help="The number of measurements to record per configuration.",
)
return parser.parse_args()
if __name__ == "__main__":
args = parse_args()
sys.exit(
main(
args.num_cpus_in_cluster,
args.num_gpus_in_cluster,
args.num_tasks_or_actors_per_run,
args.num_measurements_per_configuration,
)
)
@@ -0,0 +1,21 @@
cloud: {{env["ANYSCALE_CLOUD_NAME"]}}
advanced_instance_config:
# Fix the volume size so that IOPS is constant even if the default changes.
BlockDeviceMappings:
- DeviceName: /dev/sda1
Ebs:
DeleteOnTermination: true
# 150GB is the default in Anyscale.
VolumeSize: 150
head_node:
instance_type: g5.16xlarge
# GPU: 64 is an intentional override (instance has 1 physical GPU);
# the benchmark script uses --num_gpus_in_cluster 64 to test
# resource scheduling with 64 logical GPU slots.
resources:
CPU: 64
GPU: 64
worker_nodes: []
@@ -0,0 +1,21 @@
cloud: {{env["ANYSCALE_CLOUD_NAME"]}}
zones: [us-west1-b]
advanced_instance_config:
instance_properties:
disks:
- boot: true
auto_delete: true
initialize_params:
disk_size_gb: 150
head_node:
instance_type: n2-standard-64
# GPU: 64 is an intentional override (instance has no physical GPU);
# the benchmark script uses --num_gpus_in_cluster 64 to test
# resource scheduling with 64 logical GPU slots.
resources:
CPU: 64
GPU: 64
worker_nodes: []
@@ -0,0 +1,139 @@
#!/usr/bin/env python3
"""
Helper file for benchmark_worker_startup.py. This file runs a particular test
configuration.
"""
import argparse
import sys
import time
import ray
@ray.remote
class Actor:
def run_code(self, should_import_torch: bool):
if should_import_torch:
import torch # noqa: F401
@ray.remote
def task(should_import_torch: bool):
if should_import_torch:
import torch # noqa: F401
def main(
metrics_actor,
test_name: str,
num_runs: int,
num_tasks_or_actors_per_run: int,
num_cpus_in_cluster: int,
num_gpus_in_cluster: int,
library_to_import: str,
use_actors: bool,
with_gpu: bool,
with_runtime_env: bool,
):
num_gpus = (num_gpus_in_cluster / num_tasks_or_actors_per_run) if with_gpu else 0
num_cpus = num_cpus_in_cluster / num_tasks_or_actors_per_run
print(f"Assigning each task/actor {num_cpus} num_cpus and {num_gpus} num_gpus")
actor_with_resources = Actor.options(num_gpus=num_gpus, num_cpus=num_cpus)
task_with_resources = task.options(num_gpus=num_gpus, num_cpus=num_cpus)
should_import_torch = library_to_import == "torch"
print(f"should_import_torch: {should_import_torch}")
fail_if_incorrect_runtime_env(expect_runtime_env=with_runtime_env)
def with_actors():
actors = [
actor_with_resources.remote() for _ in range(num_tasks_or_actors_per_run)
]
ray.get([actor.run_code.remote(should_import_torch) for actor in actors])
def with_tasks():
ray.get(
[
task_with_resources.remote(should_import_torch)
for _ in range(num_tasks_or_actors_per_run)
]
)
func_to_measure = with_actors if use_actors else with_tasks
for run in range(num_runs):
print(f"Starting measurement for run {run}")
start = time.time()
func_to_measure()
dur_s = time.time() - start
ray.get(metrics_actor.submit.remote(test_name, dur_s))
def fail_if_incorrect_runtime_env(expect_runtime_env: bool):
ctx = ray.runtime_context.get_runtime_context()
print(f"Found runtime_env={ctx.runtime_env}")
if expect_runtime_env and ctx.runtime_env == {}:
raise AssertionError(
f"Expected a runtime environment but found runtime_env={ctx.runtime_env}"
)
if not expect_runtime_env and ctx.runtime_env != {}:
raise AssertionError(
f"Expected no runtime environment but found runtime_env={ctx.runtime_env}"
)
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument("--metrics_actor_name", type=str, required=True)
parser.add_argument("--metrics_actor_namespace", type=str, required=True)
parser.add_argument("--test_name", type=str, required=True)
parser.add_argument("--num_runs", type=int, required=True)
parser.add_argument("--num_tasks_or_actors_per_run", type=int, required=True)
parser.add_argument("--num_cpus_in_cluster", type=int, required=True)
parser.add_argument("--num_gpus_in_cluster", type=int, required=True)
parser.add_argument(
"--library_to_import", type=str, required=True, choices=["torch", "none"]
)
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument("--with_actors", action="store_true")
group.add_argument("--with_tasks", action="store_true")
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument("--with_gpu", action="store_true")
group.add_argument("--without_gpu", action="store_true")
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument("--with_runtime_env", action="store_true")
group.add_argument("--without_runtime_env", action="store_true")
return parser.parse_args()
if __name__ == "__main__":
args = parse_args()
metrics_actor = ray.get_actor(
args.metrics_actor_name,
args.metrics_actor_namespace,
)
sys.exit(
main(
metrics_actor,
args.test_name,
args.num_runs,
args.num_tasks_or_actors_per_run,
args.num_cpus_in_cluster,
args.num_gpus_in_cluster,
args.library_to_import,
args.with_actors,
args.with_gpu,
args.with_runtime_env,
)
)
+33
View File
@@ -0,0 +1,33 @@
# Ray Scalability Envelope
**NOTE**: the Ray scalability benchmarks are in the process of being refreshed. If you have questions about a specific workload or limit, please get in touch by filing a [GitHub issue](https://github.com/ray-project/ray/issues).
## Distributed Benchmarks
All distributed tests are run on 64 nodes with 64 cores/node. Maximum number of nodes is achieved by adding 4 core nodes.
| Dimension | Quantity |
| --------- | -------- |
| # nodes in cluster (with trivial task workload) | 2k+ |
| # actors in cluster (with trivial workload) | 40k+ |
| # simultaneously running tasks | 10k+ |
| # simultaneously running placement groups | 1k+ |
## Object Store Benchmarks
| Dimension | Quantity |
| --------- | -------- |
| 1 GiB object broadcast (# of nodes) | 50+ |
## Single Node Benchmarks.
All single node benchmarks are run on a single m4.16xlarge.
| Dimension | Quantity |
| --------- | -------- |
| # of object arguments to a single task | 10000+ |
| # of objects returned from a single task | 3000+ |
| # of plasma objects in a single `ray.get` call | 10000+ |
| # of tasks queued on a single node | 1,000,000+ |
| Maximum `ray.get` numpy object size | 100GiB+ |
+22
View File
@@ -0,0 +1,22 @@
cloud: {{env["ANYSCALE_CLOUD_NAME"]}}
head_node:
instance_type: r5dn.16xlarge # Network optimized.
resources:
CPU: 0
node: 1
small: 1
worker_nodes:
- instance_type: m5.16xlarge
min_nodes: 32
max_nodes: 32
market_type: ON_DEMAND
resources:
node: 1
- instance_type: r5.16xlarge
min_nodes: 32
max_nodes: 32
market_type: ON_DEMAND
resources:
node: 1
@@ -0,0 +1,145 @@
import argparse
import os
import math
from time import sleep, perf_counter
import json
import ray
from dashboard_test import DashboardTestAtScale
def test_max_actors_launch(cpus_per_actor, total_actors):
@ray.remote(num_cpus=cpus_per_actor)
class Actor:
def foo(self):
pass
print("Start launch actors")
actors = [Actor.options(max_restarts=-1).remote() for _ in range(total_actors)]
return actors
def parse_script_args():
parser = argparse.ArgumentParser()
parser.add_argument("--cpus-per-actor", type=float, default=0.2)
parser.add_argument("--total-actors", nargs="+", type=int, required=True)
parser.add_argument("--no-report", default=False, action="store_true")
parser.add_argument("--no-wait", default=False, action="store_true")
return parser.parse_known_args()
def scale_cluster_up(num_cpus):
print(f"Start to scale up to {num_cpus} cpus")
def get_curr_cpus():
return int(sum([r.get("Resources", {}).get("CPU", 0) for r in ray.nodes()]))
step = 1000
curr_cpus = get_curr_cpus()
target_cpus = curr_cpus
while curr_cpus < num_cpus:
curr_cpus = get_curr_cpus()
new_target_cpus = min(curr_cpus + step, num_cpus)
if new_target_cpus != target_cpus:
target_cpus = new_target_cpus
ray.autoscaler.sdk.request_resources(num_cpus=target_cpus)
print(f"Waiting for cluster to be up: {curr_cpus}->{target_cpus}->{num_cpus}")
sleep(10)
def get_head_node_cpus():
head_ip = ray.util.get_node_ip_address()
for node in ray.nodes():
if node["Alive"] and node["NodeManagerAddress"] == head_ip:
return int(node.get("Resources", {}).get("CPU", 0))
return 0
def run_one(total_actors, cpus_per_actor, no_wait):
total_cpus = cpus_per_actor * total_actors + get_head_node_cpus()
total_cpus = int(math.ceil(total_cpus))
scale_cluster_up(total_cpus)
actor_launch_start = perf_counter()
actors = test_max_actors_launch(cpus_per_actor, total_actors)
actor_launch_end = perf_counter()
actor_launch_time = actor_launch_end - actor_launch_start
actor_ready_start = perf_counter()
total_actors = len(actors)
objs = [actor.foo.remote() for actor in actors]
while len(objs) != 0:
timeout = None if no_wait else 30
objs_ready, objs = ray.wait(objs, num_returns=len(objs), timeout=timeout)
print(
f"Status: {total_actors - len(objs)}/{total_actors}, "
f"{perf_counter() - actor_ready_start}"
)
actor_ready_end = perf_counter()
actor_ready_time = actor_ready_end - actor_ready_start
throughput = total_actors / (actor_ready_time + actor_launch_time)
print(f"Actor launch time: {actor_launch_time} ({total_actors} actors)")
print(f"Actor ready time: {actor_ready_time} ({total_actors} actors)")
print(
f"Total time: {actor_launch_time + actor_ready_time}"
f" ({total_actors} actors)"
)
print(f"Through put: {throughput}")
return {
"actor_launch_time": actor_launch_time,
"actor_ready_time": actor_ready_time,
"total_time": actor_launch_time + actor_ready_time,
"num_actors": total_actors,
"throughput": throughput,
}
def main():
args, unknown = parse_script_args()
args.total_actors.sort()
addr = ray.init(address="auto")
dashboard_test = DashboardTestAtScale(addr)
result = {}
for i in args.total_actors:
result[f"many_nodes_actor_tests_{i}"] = run_one(
i, args.cpus_per_actor, args.no_wait
)
# Print the results early so if failed in the future, we still
# can see it in the log.
print(f"Result: {json.dumps(result, indent=2)}")
if "TEST_OUTPUT_JSON" in os.environ and not args.no_report:
with open(os.environ["TEST_OUTPUT_JSON"], "w") as out_file:
perf = [
{
"perf_metric_name": name,
"perf_metric_value": r["throughput"],
"perf_metric_type": "THROUGHPUT",
}
for (name, r) in result.items()
]
result["perf_metrics"] = perf
dashboard_test.update_release_test_result(result)
print(f"Writing data into file: {os.environ['TEST_OUTPUT_JSON']}")
json.dump(result, out_file)
print("Test finished successfully!")
ray.shutdown()
# We need to make sure GCS cool down otherwise, testing infra
# might get timeout when fetching the result because when the driver
# got shutdown, many actors needs to be terminated which will
# overload GCS.
print("Sleep for 60s, waiting for the cluster to cool down.")
sleep(60)
if __name__ == "__main__":
main()
@@ -0,0 +1,25 @@
cloud: {{env["ANYSCALE_CLOUD_NAME"]}}
# NFS needs to be disabled for this test, since the test spawns too many nodes
# and may hit the limit on the # of clients.
advanced_instance_config:
TagSpecifications:
- ResourceType: "instance"
Tags:
- Key: as-feature-disable-nfs-mount
Value: "true"
BlockDeviceMappings:
- DeviceName: /dev/sda1
Ebs:
DeleteOnTermination: true
VolumeSize: 30
head_node:
instance_type: m5.16xlarge
worker_nodes:
- instance_type: m6i.large
min_nodes: 500
max_nodes: 2000
market_type: ON_DEMAND
@@ -0,0 +1,25 @@
cloud: {{env["ANYSCALE_CLOUD_NAME"]}}
zones:
- us-west1-c
advanced_instance_config:
instance_properties:
disks:
- boot: true
auto_delete: true
initialize_params:
disk_size_gb: 30
# NFS needs to be disabled for this test, since the test spawns too many nodes
# and may hit the limit on the # of clients.
labels:
as-feature-disable-nfs-mount: "true"
head_node:
instance_type: n2-standard-64
worker_nodes:
- instance_type: n2-standard-2
min_nodes: 500
max_nodes: 2000
market_type: ON_DEMAND
@@ -0,0 +1,185 @@
import asyncio
import time
import urllib
from typing import Dict, Optional, List
from pprint import pprint
import requests
import ray
import logging
import os
from collections import defaultdict
from ray.util.state import list_nodes
from ray._private.test_utils import get_system_metric_for_component
from pydantic import BaseModel
from ray.dashboard.utils import get_address_for_submission_client
from ray.dashboard.modules.metrics.metrics_head import (
DEFAULT_PROMETHEUS_HOST,
PROMETHEUS_HOST_ENV_VAR,
)
logger = logging.getLogger(__name__)
def calc_p(latencies, percent):
if len(latencies) == 0:
return 0
return round(sorted(latencies)[int(len(latencies) / 100 * percent)] * 1000, 3)
class Result(BaseModel):
success: bool
# endpoints -> list of latencies
result: Dict[str, List[float]]
# Dashboard memory usage in MB.
memory_mb: Optional[float]
# Currently every endpoint is GET endpoints.
endpoints = [
"/logical/actors",
"/nodes?view=summary",
"/",
"/api/cluster_status",
"/events",
"/api/jobs/",
"/api/v0/logs",
"/api/prometheus_health",
]
@ray.remote(num_cpus=0)
class DashboardTester:
def __init__(self, interval_s: int = 1):
self.dashboard_url = get_address_for_submission_client(None)
# Ping interval for all endpoints.
self.interval_s = interval_s
# endpoint -> a list of latencies
self.result = defaultdict(list)
async def run(self):
await asyncio.gather(*[self.ping(endpoint) for endpoint in endpoints])
async def ping(self, endpoint):
"""Synchronously call an endpoint."""
node_id = ray.get_runtime_context().get_node_id()
while True:
start = time.monotonic()
# for logs API, we should append node ID and glob.
if "/api/v0/logs" in endpoint:
glob_filter = "*"
options_dict = {"node_id": node_id, "glob": glob_filter}
url = (
f"{self.dashboard_url}{endpoint}?"
f"{urllib.parse.urlencode(options_dict)}"
)
else:
url = f"{self.dashboard_url}{endpoint}"
resp = requests.get(url, timeout=30)
elapsed = time.monotonic() - start
if resp.status_code == 200:
self.result[endpoint].append(time.monotonic() - start)
else:
try:
resp.raise_for_status()
except Exception as e:
logger.exception(e)
await asyncio.sleep(max(0, self.interval_s, elapsed))
def get_result(self):
return self.result
class DashboardTestAtScale:
"""This is piggybacked into existing scalability tests."""
def __init__(self, addr: ray._private.worker.RayContext):
self.addr = addr
# Schedule the actor on the current node (which is a head node).
current_node_ip = ray._private.worker.global_worker.node_ip_address
nodes = list_nodes(filters=[("node_ip", "=", current_node_ip)])
assert len(nodes) > 0, f"{current_node_ip} not found in the cluster"
node = nodes[0]
# Schedule on a head node.
self.tester = DashboardTester.options(
label_selector={ray._raylet.RAY_NODE_ID_KEY: node["node_id"]}
).remote()
self.tester.run.remote()
def get_result(self):
"""Get the result from the test.
Returns:
A tuple of success, and the result (Result object).
"""
try:
result = ray.get(self.tester.get_result.remote(), timeout=60)
except ray.exceptions.GetTimeoutError:
return Result(success=False)
# Get the memory usage.
memories = get_system_metric_for_component(
"ray_component_uss_bytes",
"dashboard",
os.environ.get(PROMETHEUS_HOST_ENV_VAR, DEFAULT_PROMETHEUS_HOST),
)
return Result(
success=True,
result=result,
memory_mb=max(memories) / 1.0e6 if memories else None,
)
def update_release_test_result(self, release_result: dict):
test_result = self.get_result()
def calc_endpoints_p(result, percent):
return {
# sort -> get PX -> convert second to ms -> round up.
endpoint: calc_p(latencies, percent)
for endpoint, latencies in result.items()
}
print("======Print per dashboard endpoint latencies======")
print("=====================P50==========================")
pprint(calc_endpoints_p(test_result.result, 50))
print("=====================P95==========================")
pprint(calc_endpoints_p(test_result.result, 95))
print("=====================P99==========================")
pprint(calc_endpoints_p(test_result.result, 99))
latencies = []
for per_endpoint_latencies in test_result.result.values():
latencies.extend(per_endpoint_latencies)
aggregated_metrics = {
"p50": calc_p(latencies, 50),
"p95": calc_p(latencies, 95),
"p99": calc_p(latencies, 99),
}
print("=====================Aggregated====================")
pprint(aggregated_metrics)
release_result["_dashboard_test_success"] = test_result.success
if test_result.success:
if "perf_metrics" not in release_result:
release_result["perf_metrics"] = []
release_result["perf_metrics"].extend(
[
{
"perf_metric_name": f"dashboard_{p}_latency_ms",
"perf_metric_value": value,
"perf_metric_type": "LATENCY",
}
for p, value in aggregated_metrics.items()
]
)
release_result["_dashboard_memory_usage_mb"] = test_result.memory_mb
@@ -0,0 +1,90 @@
import argparse
import os
from time import sleep, perf_counter
import json
import ray
def test_max_actors_launch(cpus_per_actor, total_actors, num_masters):
# By default, there are 50 groups, each group has 1 master and 99 slaves.
num_slaves_per_master = total_actors / num_masters - 1
@ray.remote(num_cpus=cpus_per_actor)
class Actor:
def foo(self):
pass
def create(self):
return [
Actor.options(max_restarts=-1).remote()
for _ in range(num_slaves_per_master)
]
print("Start launch actors")
# The 50 masters are spreaded.
actors = [
Actor.options(max_restarts=-1, scheduling_strategy="SPREAD").remote()
for _ in range(num_masters)
]
slaves_per_master = []
for master in actors:
slaves_per_master.append(master.create.remote())
for slaves in slaves_per_master:
actors.extend(ray.get(slaves))
return actors
def test_actor_ready(actors):
remaining = [actor.foo.remote() for actor in actors]
ray.get(remaining)
def parse_script_args():
parser = argparse.ArgumentParser()
parser.add_argument("--cpus-per-actor", type=float, default=0.2)
parser.add_argument("--total-actors", type=int, default=5000)
parser.add_argument("--num-masters", type=int, default=50)
parser.add_argument("--no-report", default=False, action="store_true")
parser.add_argument("--fail", default=False, action="store_true")
return parser.parse_known_args()
def main():
args, unknown = parse_script_args()
ray.init(address="auto")
actor_launch_start = perf_counter()
actors = test_max_actors_launch(
args.cpus_per_actor, args.total_actors, args.num_masters
)
actor_launch_end = perf_counter()
actor_launch_time = actor_launch_end - actor_launch_start
if args.fail:
sleep(10)
return
actor_ready_start = perf_counter()
test_actor_ready(actors)
actor_ready_end = perf_counter()
actor_ready_time = actor_ready_end - actor_ready_start
print(f"Actor launch time: {actor_launch_time} ({args.total_actors} actors)")
print(f"Actor ready time: {actor_ready_time} ({args.total_actors} actors)")
print(
f"Total time: {actor_launch_time + actor_ready_time}"
f" ({args.total_actors} actors)"
)
if "TEST_OUTPUT_JSON" in os.environ and not args.no_report:
with open(os.environ["TEST_OUTPUT_JSON"], "w") as out_file:
results = {
"actor_launch_time": actor_launch_time,
"actor_ready_time": actor_ready_time,
"total_time": actor_launch_time + actor_ready_time,
"num_actors": args.total_actors,
}
json.dump(results, out_file)
if __name__ == "__main__":
main()
@@ -0,0 +1,90 @@
import os
import time
import tqdm
from many_nodes_tests.dashboard_test import DashboardTestAtScale
import ray
import ray._common.test_utils
import ray._private.test_utils as test_utils
from ray._private.state_api_test_utils import summarize_worker_startup_time
is_smoke_test = True
if "SMOKE_TEST" in os.environ:
MAX_ACTORS_IN_CLUSTER = 100
else:
MAX_ACTORS_IN_CLUSTER = 10000
is_smoke_test = False
def test_max_actors():
# TODO (Alex): Dynamically set this based on number of cores
cpus_per_actor = 0.25
@ray.remote(num_cpus=cpus_per_actor)
class Actor:
def foo(self):
pass
actors = [
Actor.remote()
for _ in tqdm.trange(MAX_ACTORS_IN_CLUSTER, desc="Launching actors")
]
done = ray.get([actor.foo.remote() for actor in actors])
for result in done:
assert result is None
def no_resource_leaks():
return test_utils.no_resource_leaks_excluding_node_resources()
addr = ray.init(address="auto")
ray._common.test_utils.wait_for_condition(no_resource_leaks)
monitor_actor = test_utils.monitor_memory_usage()
dashboard_test = DashboardTestAtScale(addr)
start_time = time.time()
test_max_actors()
end_time = time.time()
ray.get(monitor_actor.stop_run.remote())
used_gb, usage = ray.get(monitor_actor.get_peak_memory_info.remote())
print(f"Peak memory usage: {round(used_gb, 2)}GB")
print(f"Peak memory usage per processes:\n {usage}")
del monitor_actor
# Get the dashboard result
ray._common.test_utils.wait_for_condition(no_resource_leaks)
rate = MAX_ACTORS_IN_CLUSTER / (end_time - start_time)
try:
summarize_worker_startup_time()
except Exception as e:
print("Failed to summarize worker startup time.")
print(e)
print(
f"Success! Started {MAX_ACTORS_IN_CLUSTER} actors in "
f"{end_time - start_time}s. ({rate} actors/s)"
)
results = {
"actors_per_second": rate,
"num_actors": MAX_ACTORS_IN_CLUSTER,
"time": end_time - start_time,
"_peak_memory": round(used_gb, 2),
"_peak_process_memory": usage,
}
if not is_smoke_test:
results["perf_metrics"] = [
{
"perf_metric_name": "actors_per_second",
"perf_metric_value": rate,
"perf_metric_type": "THROUGHPUT",
}
]
dashboard_test.update_release_test_result(results)
test_utils.safe_write_to_results_json(results)
@@ -0,0 +1,121 @@
import os
import time
import tqdm
from many_nodes_tests.dashboard_test import DashboardTestAtScale
import ray
import ray._common.test_utils
import ray._private.test_utils as test_utils
from ray.util.placement_group import placement_group, remove_placement_group
from ray.util.scheduling_strategies import PlacementGroupSchedulingStrategy
is_smoke_test = True
if "SMOKE_TEST" in os.environ:
MAX_PLACEMENT_GROUPS = 20
else:
MAX_PLACEMENT_GROUPS = 1000
is_smoke_test = False
def test_many_placement_groups():
# @ray.remote(num_cpus=1, resources={"node": 0.02})
@ray.remote
class C1:
def ping(self):
return "pong"
# @ray.remote(num_cpus=1)
@ray.remote
class C2:
def ping(self):
return "pong"
# @ray.remote(resources={"node": 0.02})
@ray.remote
class C3:
def ping(self):
return "pong"
bundle1 = {"node": 0.02, "CPU": 1}
bundle2 = {"CPU": 1}
bundle3 = {"node": 0.02}
pgs = []
for _ in tqdm.trange(MAX_PLACEMENT_GROUPS, desc="Creating pgs"):
pg = placement_group(bundles=[bundle1, bundle2, bundle3])
pgs.append(pg)
for pg in tqdm.tqdm(pgs, desc="Waiting for pgs to be ready"):
ray.get(pg.ready())
actors = []
for pg in tqdm.tqdm(pgs, desc="Scheduling tasks"):
actors.append(
C1.options(
scheduling_strategy=PlacementGroupSchedulingStrategy(placement_group=pg)
).remote()
)
actors.append(
C2.options(
scheduling_strategy=PlacementGroupSchedulingStrategy(placement_group=pg)
).remote()
)
actors.append(
C3.options(
scheduling_strategy=PlacementGroupSchedulingStrategy(placement_group=pg)
).remote()
)
not_ready = [actor.ping.remote() for actor in actors]
for _ in tqdm.trange(len(actors)):
ready, not_ready = ray.wait(not_ready)
assert ray.get(*ready) == "pong"
for pg in tqdm.tqdm(pgs, desc="Cleaning up pgs"):
remove_placement_group(pg)
def no_resource_leaks():
return test_utils.no_resource_leaks_excluding_node_resources()
addr = ray.init(address="auto")
ray._common.test_utils.wait_for_condition(no_resource_leaks)
monitor_actor = test_utils.monitor_memory_usage()
dashboard_test = DashboardTestAtScale(addr)
start_time = time.time()
test_many_placement_groups()
end_time = time.time()
ray.get(monitor_actor.stop_run.remote())
used_gb, usage = ray.get(monitor_actor.get_peak_memory_info.remote())
print(f"Peak memory usage: {round(used_gb, 2)}GB")
print(f"Peak memory usage per processes:\n {usage}")
del monitor_actor
ray._common.test_utils.wait_for_condition(no_resource_leaks)
rate = MAX_PLACEMENT_GROUPS / (end_time - start_time)
print(
f"Success! Started {MAX_PLACEMENT_GROUPS} pgs in "
f"{end_time - start_time}s. ({rate} pgs/s)"
)
results = {
"pgs_per_second": rate,
"num_pgs": MAX_PLACEMENT_GROUPS,
"time": end_time - start_time,
"_peak_memory": round(used_gb, 2),
"_peak_process_memory": usage,
}
if not is_smoke_test:
results["perf_metrics"] = [
{
"perf_metric_name": "pgs_per_second",
"perf_metric_value": rate,
"perf_metric_type": "THROUGHPUT",
}
]
dashboard_test.update_release_test_result(results)
test_utils.safe_write_to_results_json(results)
@@ -0,0 +1,138 @@
import time
import click
import tqdm
from many_nodes_tests.dashboard_test import DashboardTestAtScale
import ray
import ray._common.test_utils
import ray._private.test_utils as test_utils
from ray._private.state_api_test_utils import (
StateAPICallSpec,
periodic_invoke_state_apis_with_actor,
summarize_worker_startup_time,
)
from ray.util.state import summarize_tasks
sleep_time = 300
def test_max_running_tasks(num_tasks):
cpus_per_task = 0.25
@ray.remote(num_cpus=cpus_per_task)
def task():
time.sleep(sleep_time)
def time_up(start_time):
return time.time() - start_time >= sleep_time
refs = [task.remote() for _ in tqdm.trange(num_tasks, desc="Launching tasks")]
max_cpus = ray.cluster_resources()["CPU"]
min_cpus_available = max_cpus
start_time = time.time()
for _ in tqdm.trange(int(sleep_time / 0.1), desc="Waiting"):
try:
cur_cpus = ray.available_resources().get("CPU", 0)
min_cpus_available = min(min_cpus_available, cur_cpus)
except Exception:
# There are race conditions `.get` can fail if a new heartbeat
# comes at the same time.
pass
if time_up(start_time):
print(f"Time up for sleeping {sleep_time} seconds")
break
time.sleep(0.1)
# There are some relevant magic numbers in this check. 10k tasks each
# require 1/4 cpus. Therefore, ideally 2.5k cpus will be used.
used_cpus = max_cpus - min_cpus_available
err_str = f"Only {used_cpus}/{max_cpus} cpus used."
# 1500 tasks. Note that it is a pretty low threshold, and the
# performance should be tracked via perf dashboard.
threshold = num_tasks * cpus_per_task * 0.60
print(f"{used_cpus}/{max_cpus} used.")
assert used_cpus > threshold, err_str
for _ in tqdm.trange(num_tasks, desc="Ensuring all tasks have finished"):
done, refs = ray.wait(refs)
assert ray.get(done[0]) is None
return used_cpus
def no_resource_leaks():
return test_utils.no_resource_leaks_excluding_node_resources()
@click.command()
@click.option("--num-tasks", required=True, type=int, help="Number of tasks to launch.")
def test(num_tasks):
addr = ray.init(address="auto")
ray._common.test_utils.wait_for_condition(no_resource_leaks)
monitor_actor = test_utils.monitor_memory_usage()
dashboard_test = DashboardTestAtScale(addr)
def not_none(res):
return res is not None
api_caller = periodic_invoke_state_apis_with_actor(
apis=[StateAPICallSpec(summarize_tasks, not_none)],
call_interval_s=4,
print_result=True,
)
start_time = time.time()
used_cpus = test_max_running_tasks(num_tasks)
end_time = time.time()
ray.get(monitor_actor.stop_run.remote())
used_gb, usage = ray.get(monitor_actor.get_peak_memory_info.remote())
print(f"Peak memory usage: {round(used_gb, 2)}GB")
print(f"Peak memory usage per processes:\n {usage}")
ray.get(api_caller.stop.remote())
del api_caller
del monitor_actor
ray._common.test_utils.wait_for_condition(no_resource_leaks)
try:
summarize_worker_startup_time()
except Exception as e:
print("Failed to summarize worker startup time.")
print(e)
rate = num_tasks / (end_time - start_time - sleep_time)
print(
f"Success! Started {num_tasks} tasks in {end_time - start_time}s. "
f"({rate} tasks/s)"
)
results = {
"tasks_per_second": rate,
"num_tasks": num_tasks,
"time": end_time - start_time,
"used_cpus": used_cpus,
"_peak_memory": round(used_gb, 2),
"_peak_process_memory": usage,
"perf_metrics": [
{
"perf_metric_name": "tasks_per_second",
"perf_metric_value": rate,
"perf_metric_type": "THROUGHPUT",
},
{
"perf_metric_name": "used_cpus_by_deadline",
"perf_metric_value": used_cpus,
"perf_metric_type": "THROUGHPUT",
},
],
}
dashboard_test.update_release_test_result(results)
test_utils.safe_write_to_results_json(results)
if __name__ == "__main__":
test()
@@ -0,0 +1,134 @@
import argparse
from math import floor
from time import sleep, time
import ray
import ray._private.test_utils as test_utils
from ray._private.test_utils import safe_write_to_results_json
@ray.remote
def simple_task(t):
sleep(t)
@ray.remote
class SimpleActor:
def __init__(self, job=None):
self._job = job
def ready(self):
return
def do_job(self):
if self._job is not None:
self._job()
def start_tasks(num_task, num_cpu_per_task, task_duration):
ray.get(
[
simple_task.options(num_cpus=num_cpu_per_task).remote(task_duration)
for _ in range(num_task)
]
)
def measure(f):
start = time()
ret = f()
end = time()
return (end - start, ret)
def start_actor(num_actors, num_actors_per_nodes, job):
resources = {"node": floor(1.0 / num_actors_per_nodes)}
submission_cost, actors = measure(
lambda: [
SimpleActor.options(resources=resources, num_cpus=0).remote(job)
for _ in range(num_actors)
]
)
ready_cost, _ = measure(lambda: ray.get([actor.ready.remote() for actor in actors]))
actor_job_cost, _ = measure(
lambda: ray.get([actor.do_job.remote() for actor in actors])
)
return (submission_cost, ready_cost, actor_job_cost)
if __name__ == "__main__":
parser = argparse.ArgumentParser(prog="Test Scheduling")
# Task workloads
parser.add_argument(
"--total-num-task", type=int, help="Total number of tasks.", required=False
)
parser.add_argument(
"--num-cpu-per-task",
type=int,
help="Resources needed for tasks.",
required=False,
)
parser.add_argument(
"--task-duration-s",
type=int,
help="How long does each task execute.",
required=False,
default=1,
)
# Actor workloads
parser.add_argument(
"--total-num-actors", type=int, help="Total number of actors.", required=True
)
parser.add_argument(
"--num-actors-per-nodes",
type=int,
help="How many actors to allocate for each nodes.",
required=True,
)
ray.init(address="auto")
monitor_actor = test_utils.monitor_memory_usage()
total_cpus_per_node = [node["Resources"].get("CPU", 0) for node in ray.nodes()]
num_nodes = len(total_cpus_per_node)
total_cpus = sum(total_cpus_per_node)
args = parser.parse_args()
job = None
if args.total_num_task is not None:
if args.num_cpu_per_task is None:
args.num_cpu_per_task = floor(1.0 * total_cpus / args.total_num_task)
job = lambda: start_tasks( # noqa: E731
args.total_num_task, args.num_cpu_per_task, args.task_duration_s
)
submission_cost, ready_cost, actor_job_cost = start_actor(
args.total_num_actors, args.num_actors_per_nodes, job
)
ray.get(monitor_actor.stop_run.remote())
used_gb, usage = ray.get(monitor_actor.get_peak_memory_info.remote())
print(f"Peak memory usage: {round(used_gb, 2)}GB")
print(f"Peak memory usage per processes:\n {usage}")
del monitor_actor
result = {
"total_num_task": args.total_num_task,
"num_cpu_per_task": args.num_cpu_per_task,
"task_duration_s": args.task_duration_s,
"total_num_actors": args.total_num_actors,
"num_actors_per_nodes": args.num_actors_per_nodes,
"num_nodes": num_nodes,
"total_cpus": total_cpus,
"submission_cost": submission_cost,
"ready_cost": ready_cost,
"actor_job_cost": actor_job_cost,
"_peak_memory": round(used_gb, 2),
"_peak_process_memory": usage,
"_runtime": submission_cost + ready_cost + actor_job_cost,
}
safe_write_to_results_json(result)
print(result)
+26
View File
@@ -0,0 +1,26 @@
cloud: {{env["ANYSCALE_CLOUD_NAME"]}}
zones:
- us-west1-c
head_node:
instance_type: n2-standard-64 # Network optimized.
resources:
CPU: 0
node: 1
small: 1
worker_nodes:
- name: worker_node_m
instance_type: n2-standard-64
min_nodes: 32
max_nodes: 32
market_type: ON_DEMAND
resources:
node: 1
- name: worker_node_r
instance_type: n2-standard-64
min_nodes: 32
max_nodes: 32
market_type: ON_DEMAND
resources:
node: 1
@@ -0,0 +1,16 @@
cloud: {{env["ANYSCALE_CLOUD_NAME"]}}
head_node:
instance_type: r5dn.16xlarge # Network optimized.
resources:
CPU: 0
node: 1
small: 1
worker_nodes:
- instance_type: m5.16xlarge
min_nodes: 1
max_nodes: 1
market_type: ON_DEMAND
resources:
node: 1
+15
View File
@@ -0,0 +1,15 @@
cloud: {{env["ANYSCALE_CLOUD_NAME"]}}
head_node:
instance_type: r6idn.16xlarge # Network optimized.
resources:
node: 1
small: 1
worker_nodes:
- instance_type: m6i.2xlarge
min_nodes: 249
max_nodes: 249
market_type: ON_DEMAND
resources:
node: 1
+17
View File
@@ -0,0 +1,17 @@
cloud: {{env["ANYSCALE_CLOUD_NAME"]}}
zones:
- us-west1-c
head_node:
instance_type: n2-standard-64 # Network optimized.
resources:
node: 1
small: 1
worker_nodes:
- instance_type: n2-standard-8
min_nodes: 249
max_nodes: 249
market_type: ON_DEMAND
resources:
node: 1
+14
View File
@@ -0,0 +1,14 @@
cloud: {{env["ANYSCALE_CLOUD_NAME"]}}
head_node:
instance_type: m6i.16xlarge
resources:
node: 1
worker_nodes:
- instance_type: m6i.2xlarge
min_nodes: 49
max_nodes: 49
market_type: ON_DEMAND
resources:
node: 1
@@ -0,0 +1,14 @@
cloud: {{env["ANYSCALE_CLOUD_NAME"]}}
head_node:
instance_type: m6i.2xlarge
resources:
node: 1
worker_nodes:
- instance_type: m6i.2xlarge
min_nodes: 10
max_nodes: 10
market_type: ON_DEMAND
resources:
node: 1
@@ -0,0 +1,14 @@
cloud: {{env["ANYSCALE_CLOUD_NAME"]}}
head_node:
instance_type: m6i.16xlarge
resources:
node: 1
worker_nodes:
- instance_type: m6i.4xlarge
min_nodes: 9
max_nodes: 9
market_type: ON_DEMAND
resources:
node: 1
@@ -0,0 +1,10 @@
cloud: {{env["ANYSCALE_CLOUD_NAME"]}}
head_node:
instance_type: m6i.16xlarge
worker_nodes:
- instance_type: m6i.4xlarge
min_nodes: 4
max_nodes: 4
market_type: ON_DEMAND
@@ -0,0 +1,108 @@
import json
import os
import threading
import time
import numpy as np
import ray
import ray._private.worker
NUM_WORKERS = 10
OBJECT_SIZE = 1024 * 1024 # 1 MiB, above the 100 KB inlining threshold
@ray.remote(num_cpus=1)
def produce_block():
return np.zeros(OBJECT_SIZE, dtype=np.uint8)
@ray.remote(num_cpus=1)
def consume_block(block_ref):
return len(block_ref)
def test_callback_pipeline(num_blocks, timeout_s=60):
core_worker = ray._private.worker.global_worker.core_worker
latencies = []
drop_times = {}
lock = threading.Lock()
done = threading.Event()
def on_freed(id_bytes):
with lock:
latencies.append(time.perf_counter() - drop_times[id_bytes])
if len(latencies) == num_blocks:
done.set()
refs = [
produce_block.options(scheduling_strategy="SPREAD").remote()
for _ in range(num_blocks)
]
ray.wait(refs, num_returns=len(refs))
# live_refs keeps each block ref alive until its consumer completes.
live_refs = {}
for ref in refs:
assert core_worker.add_object_out_of_scope_callback(ref, on_freed)
consumer = consume_block.remote(ref)
live_refs[consumer] = ref
del refs
# Release each ref as its consumer completes.
pending = list(live_refs.keys())
while pending:
done_list, pending = ray.wait(pending, num_returns=1)
for consumer in done_list:
ref = live_refs.pop(consumer)
drop_times[ref.binary()] = time.perf_counter()
del ref
if not done.wait(timeout=timeout_s):
raise TimeoutError(
f"Only {len(latencies)}/{num_blocks} callbacks fired within {timeout_s}s"
)
latencies.sort()
p95 = latencies[int(len(latencies) * 0.95)]
print(f" {num_blocks} blocks: p95={p95:.4f}s")
return p95
ray.init(address="auto")
# Warm up gRPC connections and worker pools.
ray.get(
[
produce_block.options(scheduling_strategy="SPREAD").remote()
for _ in range(NUM_WORKERS)
]
)
p95_100 = test_callback_pipeline(100)
p95_1k = test_callback_pipeline(1000)
print("\nSummary:")
print(f" 100 blocks: p95={p95_100:.4f}s")
print(f" 1k blocks: p95={p95_1k:.4f}s")
if "TEST_OUTPUT_JSON" in os.environ:
with open(os.environ["TEST_OUTPUT_JSON"], "w") as out_file:
results = {
"p95_100": p95_100,
"p95_1k": p95_1k,
"perf_metrics": [
{
"perf_metric_name": "callback_p95_latency_100_blocks_s",
"perf_metric_value": p95_100,
"perf_metric_type": "LATENCY",
},
{
"perf_metric_name": "callback_p95_latency_1k_blocks_s",
"perf_metric_value": p95_1k,
"perf_metric_type": "LATENCY",
},
],
}
json.dump(results, out_file)
@@ -0,0 +1,101 @@
import json
import os
from time import perf_counter
import numpy as np
from tqdm import tqdm
import ray
NUM_NODES = 9
OBJECT_SIZE = 2**32
def test_object_many_to_one():
@ray.remote(num_cpus=1, resources={"node": 1})
class Actor:
def foo(self):
pass
def send_objects(self):
return np.ones(OBJECT_SIZE, dtype=np.uint8)
actors = [Actor.remote() for _ in range(NUM_NODES)]
for actor in tqdm(actors, desc="Ensure all actors have started."):
ray.get(actor.foo.remote())
start = perf_counter()
result_refs = []
for actor in tqdm(actors, desc="Tasks kickoff"):
result_refs.append(actor.send_objects.remote())
results = ray.get(result_refs)
end = perf_counter()
for result in results:
assert len(result) == OBJECT_SIZE
return end - start
def test_object_one_to_many():
@ray.remote(num_cpus=1, resources={"node": 1})
class Actor:
def foo(self):
pass
def data_len(self, arr):
return len(arr)
actors = [Actor.remote() for _ in range(NUM_NODES)]
arr = np.ones(OBJECT_SIZE, dtype=np.uint8)
ref = ray.put(arr)
for actor in tqdm(actors, desc="Ensure all actors have started."):
ray.get(actor.foo.remote())
start = perf_counter()
result_refs = []
for actor in tqdm(actors, desc="Tasks kickoff"):
result_refs.append(actor.data_len.remote(ref))
results = ray.get(result_refs)
end = perf_counter()
for result in results:
assert result == OBJECT_SIZE
return end - start
ray.init(address="auto")
many_to_one_duration = test_object_many_to_one()
print(f"many_to_one time: {many_to_one_duration} ({OBJECT_SIZE} B x {NUM_NODES} nodes)")
one_to_many_duration = test_object_one_to_many()
print(f"one_to_many time: {one_to_many_duration} ({OBJECT_SIZE} B x {NUM_NODES} nodes)")
if "TEST_OUTPUT_JSON" in os.environ:
with open(os.environ["TEST_OUTPUT_JSON"], "w") as out_file:
results = {
"many_to_one_time": many_to_one_duration,
"one_to_many_time": one_to_many_duration,
"object_size": OBJECT_SIZE,
"num_nodes": NUM_NODES,
}
results["perf_metrics"] = [
{
"perf_metric_name": f"time_many_to_one_{OBJECT_SIZE}_bytes_from_{NUM_NODES}_nodes",
"perf_metric_value": many_to_one_duration,
"perf_metric_type": "LATENCY",
},
{
"perf_metric_name": f"time_one_to_many_{OBJECT_SIZE}_bytes_to_{NUM_NODES}_nodes",
"perf_metric_value": one_to_many_duration,
"perf_metric_type": "LATENCY",
},
]
json.dump(results, out_file)
@@ -0,0 +1,75 @@
import json
import os
from time import perf_counter
import numpy as np
from tqdm import tqdm
import ray
import ray.autoscaler.sdk
NUM_NODES = 50
OBJECT_SIZE = 2**30
def num_alive_nodes():
n = 0
for node in ray.nodes():
if node["Alive"]:
n += 1
return n
def test_object_broadcast():
assert num_alive_nodes() == NUM_NODES
@ray.remote(num_cpus=1, resources={"node": 1})
class Actor:
def foo(self):
pass
def data_len(self, arr):
return len(arr)
actors = [Actor.remote() for _ in range(NUM_NODES)]
arr = np.ones(OBJECT_SIZE, dtype=np.uint8)
ref = ray.put(arr)
for actor in tqdm(actors, desc="Ensure all actors have started."):
ray.get(actor.foo.remote())
start = perf_counter()
result_refs = []
for actor in tqdm(actors, desc="Broadcasting objects"):
result_refs.append(actor.data_len.remote(ref))
results = ray.get(result_refs)
end = perf_counter()
for result in results:
assert result == OBJECT_SIZE
return end - start
ray.init(address="auto")
duration = test_object_broadcast()
print(f"Broadcast time: {duration} ({OBJECT_SIZE} B x {NUM_NODES} nodes)")
if "TEST_OUTPUT_JSON" in os.environ:
with open(os.environ["TEST_OUTPUT_JSON"], "w") as out_file:
results = {
"broadcast_time": duration,
"object_size": OBJECT_SIZE,
"num_nodes": NUM_NODES,
}
perf_metric_name = f"time_to_broadcast_{OBJECT_SIZE}_bytes_to_{NUM_NODES}_nodes"
results["perf_metrics"] = [
{
"perf_metric_name": perf_metric_name,
"perf_metric_value": duration,
"perf_metric_type": "LATENCY",
}
]
json.dump(results, out_file)
@@ -0,0 +1,81 @@
import json
import os
import time
import numpy as np
import ray
def test_small_objects_many_to_one():
@ray.remote(num_cpus=1)
class Actor:
def send(self, _, actor_idx):
# this size is chosen because it's >100kb so big enough to be stored in plasma
numpy_arr = np.ones((20, 1024))
return (numpy_arr, actor_idx)
actors = [Actor.remote() for _ in range(64)]
not_ready = []
for index, actor in enumerate(actors):
not_ready.append(actor.send.remote(0, index))
num_messages = 0
start_time = time.time()
while time.time() - start_time < 60:
ready, not_ready = ray.wait(not_ready, num_returns=10)
for ready_ref in ready:
_, actor_idx = ray.get(ready_ref)
not_ready.append(actors[actor_idx].send.remote(0, actor_idx))
num_messages += 10
return num_messages / 60
def test_small_objects_one_to_many():
@ray.remote(num_cpus=1)
class Actor:
def receive(self, numpy_arr, actor_idx):
return actor_idx
actors = [Actor.remote() for _ in range(64)]
numpy_arr_ref = ray.put(np.ones((20, 1024)))
not_ready = []
num_messages = 0
start_time = time.time()
for idx, actor in enumerate(actors):
not_ready.append(actor.receive.remote(numpy_arr_ref, idx))
while time.time() - start_time < 60:
ready, not_ready = ray.wait(not_ready, num_returns=10)
actor_idxs = ray.get(ready)
for actor_idx in actor_idxs:
not_ready.append(actors[actor_idx].receive.remote(numpy_arr_ref, actor_idx))
num_messages += 10
return num_messages / 60
ray.init(address="auto")
many_to_one_throughput = test_small_objects_many_to_one()
print(f"Number of messages per second many_to_one: {many_to_one_throughput}")
one_to_many_throughput = test_small_objects_one_to_many()
print(f"Number of messages per second one_to_many: {one_to_many_throughput}")
if "TEST_OUTPUT_JSON" in os.environ:
with open(os.environ["TEST_OUTPUT_JSON"], "w") as out_file:
results = {
"num_messages_many_to_one": many_to_one_throughput,
"num_messages_one_to_many": one_to_many_throughput,
}
results["perf_metrics"] = [
{
"perf_metric_name": "num_small_objects_many_to_one",
"perf_metric_value": many_to_one_throughput,
"perf_metric_type": "THROUGHPUT",
},
{
"perf_metric_name": "num_small_objects_one_to_many_per_second",
"perf_metric_value": one_to_many_throughput,
"perf_metric_type": "THROUGHPUT",
},
]
json.dump(results, out_file)
+16
View File
@@ -0,0 +1,16 @@
cloud: {{env["ANYSCALE_CLOUD_NAME"]}}
zones:
- us-west1-c
head_node:
instance_type: n2-standard-64
resources:
node: 1
worker_nodes:
- instance_type: n2-standard-8
min_nodes: 49
max_nodes: 49
market_type: ON_DEMAND
resources:
node: 1
+19
View File
@@ -0,0 +1,19 @@
cloud: {{env["ANYSCALE_CLOUD_NAME"]}}
head_node:
instance_type: m5.4xlarge
resources:
# Assume the node has 64 CPU instead of 16.
# This should be fine since each task has little
# computation in scheduling tests.
CPU: 64
node: 1
worker_nodes:
- instance_type: m5.4xlarge
min_nodes: 31
max_nodes: 31
market_type: ON_DEMAND
resources:
CPU: 64
node: 1
+21
View File
@@ -0,0 +1,21 @@
cloud: {{env["ANYSCALE_CLOUD_NAME"]}}
zones:
- us-west1-c
head_node:
instance_type: n2-standard-16
resources:
# Assume the node has 64 CPU instead of 16.
# This should be fine since each task has little
# computation in scheduling tests.
CPU: 64
node: 1
worker_nodes:
- instance_type: n2-standard-16
min_nodes: 31
max_nodes: 31
market_type: ON_DEMAND
resources:
CPU: 64
node: 1
+16
View File
@@ -0,0 +1,16 @@
cloud: {{env["ANYSCALE_CLOUD_NAME"]}}
advanced_instance_config:
BlockDeviceMappings:
- DeviceName: /dev/sda1
Ebs:
DeleteOnTermination: true
VolumeSize: 500
head_node:
instance_type: m4.16xlarge
resources:
# 128 GB
object_store_memory: 128000000000
worker_nodes: []
@@ -0,0 +1,237 @@
import json
import os
import time
from time import perf_counter
import numpy as np
from tqdm import tqdm, trange
import ray
import ray.autoscaler.sdk
from ray._common.test_utils import Semaphore
MAX_ARGS = 10000
MAX_RETURNS = 3000
MAX_RAY_GET_ARGS = 10000
MAX_QUEUED_TASKS = 1_000_000
MAX_RAY_GET_SIZE = 100 * 2**30
def assert_no_leaks():
total = ray.cluster_resources()
current = ray.available_resources()
total.pop("memory")
total.pop("object_store_memory")
current.pop("memory")
current.pop("object_store_memory")
assert total == current, (total, current)
def test_many_args():
@ray.remote
def sum_args(*args):
return sum(sum(arg) for arg in args)
args = [[1 for _ in range(10000)] for _ in range(MAX_ARGS)]
result = ray.get(sum_args.remote(*args))
assert result == MAX_ARGS * 10000
def test_many_returns():
@ray.remote(num_returns=MAX_RETURNS)
def f():
to_return = []
for _ in range(MAX_RETURNS):
obj = list(range(10000))
to_return.append(obj)
return tuple(to_return)
returned_refs = f.remote()
assert len(returned_refs) == MAX_RETURNS
for ref in returned_refs:
expected = list(range(10000))
obj = ray.get(ref)
assert obj == expected
def test_ray_get_args():
def with_dese():
print("Putting test objects:")
refs = []
for _ in trange(MAX_RAY_GET_ARGS):
obj = list(range(10000))
refs.append(ray.put(obj))
print("Getting objects")
results = ray.get(refs)
assert len(results) == MAX_RAY_GET_ARGS
print("Asserting correctness")
for obj in tqdm(results):
expected = list(range(10000))
assert obj == expected
def with_zero_copy():
print("Putting test objects:")
refs = []
for _ in trange(MAX_RAY_GET_ARGS):
obj = np.arange(10000)
refs.append(ray.put(obj))
print("Getting objects")
results = ray.get(refs)
assert len(results) == MAX_RAY_GET_ARGS
print("Asserting correctness")
for obj in tqdm(results):
expected = np.arange(10000)
assert (obj == expected).all()
with_dese()
print("Done with dese")
with_zero_copy()
print("Done with zero copy")
def test_many_queued_tasks():
sema = Semaphore.remote(0)
@ray.remote(num_cpus=1)
def block():
ray.get(sema.acquire.remote())
@ray.remote(num_cpus=1)
def f():
pass
num_cpus = int(ray.cluster_resources()["CPU"])
blocked_tasks = []
for _ in range(num_cpus):
blocked_tasks.append(block.remote())
print("Submitting many tasks")
pending_tasks = []
for _ in trange(MAX_QUEUED_TASKS):
pending_tasks.append(f.remote())
# Make sure all the tasks can actually run.
for _ in range(num_cpus):
sema.release.remote()
print("Unblocking tasks")
for ref in tqdm(pending_tasks):
assert ray.get(ref) is None
def test_large_object():
print("Generating object")
obj = np.zeros(MAX_RAY_GET_SIZE, dtype=np.int8)
print("Putting object")
ref = ray.put(obj)
del obj
print("Getting object")
big_obj = ray.get(ref)
assert big_obj[0] == 0
assert big_obj[-1] == 0
ray.init(address="auto")
args_start = perf_counter()
test_many_args()
args_end = perf_counter()
time.sleep(5)
assert_no_leaks()
print("Finished many args")
returns_start = perf_counter()
test_many_returns()
returns_end = perf_counter()
time.sleep(5)
assert_no_leaks()
print("Finished many returns")
get_start = perf_counter()
test_ray_get_args()
get_end = perf_counter()
time.sleep(5)
assert_no_leaks()
print("Finished ray.get on many objects")
queued_start = perf_counter()
test_many_queued_tasks()
queued_end = perf_counter()
time.sleep(5)
assert_no_leaks()
print("Finished queueing many tasks")
large_object_start = perf_counter()
test_large_object()
large_object_end = perf_counter()
time.sleep(5)
assert_no_leaks()
print("Done")
args_time = args_end - args_start
returns_time = returns_end - returns_start
get_time = get_end - get_start
queued_time = queued_end - queued_start
large_object_time = large_object_end - large_object_start
print(f"Many args time: {args_time} ({MAX_ARGS} args)")
print(f"Many returns time: {returns_time} ({MAX_RETURNS} returns)")
print(f"Ray.get time: {get_time} ({MAX_RAY_GET_ARGS} args)")
print(f"Queued task time: {queued_time} ({MAX_QUEUED_TASKS} tasks)")
print(f"Ray.get large object time: {large_object_time} " f"({MAX_RAY_GET_SIZE} bytes)")
if "TEST_OUTPUT_JSON" in os.environ:
with open(os.environ["TEST_OUTPUT_JSON"], "w") as out_file:
results = {
"args_time": args_time,
"num_args": MAX_ARGS,
"returns_time": returns_time,
"num_returns": MAX_RETURNS,
"get_time": get_time,
"num_get_args": MAX_RAY_GET_ARGS,
"queued_time": queued_time,
"num_queued": MAX_QUEUED_TASKS,
"large_object_time": large_object_time,
"large_object_size": MAX_RAY_GET_SIZE,
"success": "1",
}
results["perf_metrics"] = [
{
"perf_metric_name": f"{MAX_ARGS}_args_time",
"perf_metric_value": args_time,
"perf_metric_type": "LATENCY",
},
{
"perf_metric_name": f"{MAX_RETURNS}_returns_time",
"perf_metric_value": returns_time,
"perf_metric_type": "LATENCY",
},
{
"perf_metric_name": f"{MAX_RAY_GET_ARGS}_get_time",
"perf_metric_value": get_time,
"perf_metric_type": "LATENCY",
},
{
"perf_metric_name": f"{MAX_QUEUED_TASKS}_queued_time",
"perf_metric_value": queued_time,
"perf_metric_type": "LATENCY",
},
{
"perf_metric_name": f"{MAX_RAY_GET_SIZE}_large_object_time",
"perf_metric_value": large_object_time,
"perf_metric_type": "LATENCY",
},
]
json.dump(results, out_file)
+19
View File
@@ -0,0 +1,19 @@
cloud: {{env["ANYSCALE_CLOUD_NAME"]}}
zones:
- us-west1-c
advanced_instance_config:
instance_properties:
disks:
- boot: true
auto_delete: true
initialize_params:
disk_size_gb: 500
head_node:
instance_type: n2-standard-64
resources:
# 128 GB
object_store_memory: 128000000000
worker_nodes: []
@@ -0,0 +1,12 @@
cloud: {{env["ANYSCALE_CLOUD_NAME"]}}
head_node:
instance_type: m5.xlarge # 4 CPUs
resources:
CPU: 4
worker_nodes:
- instance_type: m5.xlarge
min_nodes: 0
max_nodes: 2
market_type: ON_DEMAND
@@ -0,0 +1,14 @@
cloud: {{env["ANYSCALE_CLOUD_NAME"]}}
zones:
- us-west1-b
head_node:
instance_type: n2-standard-4 # 4 CPUs
resources:
CPU: 4
worker_nodes:
- instance_type: n2-standard-4
min_nodes: 0
max_nodes: 2
market_type: ON_DEMAND
@@ -0,0 +1,26 @@
head_node_type:
name: head_node
instance_type: n2-standard-4 # 4 CPUs
resources:
limits:
cpu: "4"
memory: "16Gi"
requests:
cpu: "4"
memory: "16Gi"
worker_node_types:
- name: worker_node
instance_type: n2-standard-4
resources:
limits:
cpu: "4"
memory: "16Gi"
requests:
cpu: "4"
memory: "16Gi"
min_workers: 0
max_workers: 2
use_spot: false
autoscaler_version: v2
@@ -0,0 +1,81 @@
"""Test cluster up/down scaling behavior.
This test should run on a cluster with autoscaling enabled. It assumes 1-3 nodes
with 4 CPUs each.
We start a Ray Tune run with 3 trials. Each trial uses 4 CPUs, so fills up a node
completely. This means we will trigger autoscaling after starting up.
The trial on the head node will run for 30 minutes. This is to make sure that
we have enough time that the nodes for the other two trials come up, complete
training, and come down before the first trial finishes.
The other two trials will run once their nodes are up, and take 3 minutes each
to finish. The three minutes have been chosen to make sure that both trials
run in parallel for some time, i.e. to avoid that both additional trials run on
only one node.
We keep track of the number of nodes we observe at any point during the run.
Test owner: krfricke
Acceptance criteria: Should have scaled to 3 nodes at some point during the run.
Should have scaled down to 1 node at the end.
"""
from collections import Counter
import time
import ray
from ray import tune
def train_fn(config):
this_node_ip = ray.util.get_node_ip_address()
if config["head_node_ip"] == this_node_ip:
# On the head node, run for 30 minutes
for i in range(30):
tune.report({"metric": i})
time.sleep(60)
else:
# On worker nodes, run for 3 minutes
for i in range(3):
tune.report({"metric": i})
time.sleep(60)
class NodeCountCallback(tune.Callback):
def __init__(self):
self.node_counts = []
def on_step_begin(self, iteration, trials, **info):
node_count = len([n for n in ray.nodes() if n["Alive"]])
self.node_counts.append(node_count)
def main():
ray.init()
head_node_ip = ray.util.get_node_ip_address()
assert (
len([n for n in ray.nodes() if n["Alive"]]) == 1
), "Too many nodes available at start of script"
node_counter = NodeCountCallback()
tune.run(
train_fn,
num_samples=3,
config={"head_node_ip": head_node_ip},
callbacks=[node_counter],
resources_per_trial={"cpu": 4},
)
node_counts = Counter(node_counter.node_counts)
assert node_counts[3] > 0, "Cluster never scaled to 3 nodes"
assert node_counter.node_counts[-1] == 1, "Cluster didn't scale down to 1 node."
if __name__ == "__main__":
main()
@@ -0,0 +1,15 @@
cloud: {{env["ANYSCALE_CLOUD_NAME"]}}
advanced_instance_config:
TagSpecifications:
- ResourceType: "instance"
Tags:
- Key: ttl-hours
Value: '24'
head_node:
instance_type: m5.16xlarge
resources:
CPU: 85
worker_nodes: []
@@ -0,0 +1,9 @@
cloud: {{env["ANYSCALE_CLOUD_NAME"]}}
zones: [us-west1-c]
head_node:
instance_type: n2-standard-64
resources:
CPU: 85
worker_nodes: []
+93
View File
@@ -0,0 +1,93 @@
import argparse
import json
import os
import time
import ray
from ray._private.memory_monitor import MemoryMonitor, get_top_n_memory_usage
from ray._private.test_utils import get_system_metric_for_component
from ray.dashboard.modules.metrics.metrics_head import (
DEFAULT_PROMETHEUS_HOST,
PROMETHEUS_HOST_ENV_VAR,
)
from ray.job_submission import JobStatus, JobSubmissionClient
# Initialize ray to avoid autosuspend.
addr = ray.init()
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument(
"--working-dir",
required=True,
help="working_dir to use for the job within this test.",
)
args = parser.parse_args()
client = JobSubmissionClient("http://127.0.0.1:8265")
job_id = client.submit_job(
# Entrypoint shell command to execute
entrypoint="python workload.py",
runtime_env={"working_dir": args.working_dir},
)
print(job_id)
# If using a remote cluster, replace 127.0.0.1 with the head node's IP address.
client = JobSubmissionClient("http://127.0.0.1:8265")
m = MemoryMonitor()
start = time.time()
# Run for 3 hours
initial_used_gb = m.get_memory_usage()[0]
terminal_states = {JobStatus.SUCCEEDED, JobStatus.STOPPED, JobStatus.FAILED}
while time.time() - start < 3600 * 3:
print(f"{round((time.time() - start) / 60, 2)}m passed...")
m.raise_if_low_memory()
used_gb = m.get_memory_usage()[0]
print("Used GB: ", used_gb)
print(get_top_n_memory_usage())
print("\n\n")
# Terminate the test if the job is failed.
status = client.get_job_status(job_id)
print(f"Job status: {status}")
if status in terminal_states:
break
time.sleep(15)
ending_used_gb = m.get_memory_usage()[0]
mem_growth = ending_used_gb - initial_used_gb
top_n_mem_usage = get_top_n_memory_usage()
print(top_n_mem_usage)
print(f"Memory growth: {mem_growth} GB")
if status == JobStatus.FAILED or status == JobStatus.STOPPED:
print(client.get_job_logs(job_id))
assert False, "Job has failed."
uss_bytes_for_agent_component = get_system_metric_for_component(
"ray_component_uss_bytes",
"agent",
os.environ.get(PROMETHEUS_HOST_ENV_VAR, DEFAULT_PROMETHEUS_HOST),
)
assert (
len(uss_bytes_for_agent_component) > 0
), "Agent component memory metrics are not found."
for bytes in uss_bytes_for_agent_component:
print(f"Agent component memory usage: {bytes} bytes")
assert bytes < 500 * 1024 * 1024, "Agent component memory usage is too high."
with open(os.environ["TEST_OUTPUT_JSON"], "w") as f:
results = {
"memory_growth_gb": mem_growth,
}
results["perf_metrics"] = [
{
"perf_metric_name": "memory_growth_gb",
"perf_metric_value": mem_growth,
"perf_metric_type": "LATENCY",
}
]
f.write(json.dumps(results))
+19
View File
@@ -0,0 +1,19 @@
import time
import ray
ray.init("auto")
@ray.remote(num_cpus=1)
class A:
def f(self):
return 1
actors = [A.remote() for _ in range(85)]
# Keep calling actor methods which will generate lots of metrics.
while True:
time.sleep(0.1)
ray.get([actor.f.remote() for actor in actors])
+34
View File
@@ -0,0 +1,34 @@
#!/bin/bash
# This script is used to login to gcloud docker registry using GCP workload identity
# federation service account
set -euo pipefail
# Only install gcloud if not already available
if ! command -v gcloud &> /dev/null; then
ARCH=$(uname -m)
case "$ARCH" in
x86_64)
GCLOUD_ARCH="x86_64"
;;
aarch64|arm64)
GCLOUD_ARCH="arm"
;;
*)
echo "Unsupported architecture: $ARCH"
exit 1
;;
esac
GCLOUD_VERSION="550.0.0"
GCLOUD_TARBALL="google-cloud-cli-${GCLOUD_VERSION}-linux-${GCLOUD_ARCH}.tar.gz"
curl -fO "https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/${GCLOUD_TARBALL}"
tar -xf "$GCLOUD_TARBALL"
./google-cloud-sdk/install.sh -q
PATH="$(pwd)/google-cloud-sdk/bin:$PATH"
export PATH
fi
gcloud auth login --cred-file="$1" --quiet
gcloud auth configure-docker us-west1-docker.pkg.dev --quiet
@@ -0,0 +1,15 @@
cloud_id: {{env["ANYSCALE_CLOUD_ID"]}}
region: us-west-2
max_workers: 3
head_node_type:
name: head_node
instance_type: m5.4xlarge
worker_node_types:
- name: worker_node
instance_type: m5.4xlarge
min_workers: 3
max_workers: 3
use_spot: false
@@ -0,0 +1,18 @@
cloud_id: {{env["ANYSCALE_CLOUD_ID"]}}
region: us-west-2
max_workers: 2
head_node_type:
name: head_node
instance_type: m5.xlarge
worker_node_types:
- name: worker_node
instance_type: g4dn.12xlarge
min_workers: 2
max_workers: 2
use_spot: true
resources:
custom_resources:
worker: 1
@@ -0,0 +1,17 @@
cloud_id: {{env["ANYSCALE_CLOUD_ID"]}}
region: us-west1
allowed_azs:
- us-west1-b
max_workers: 2
head_node_type:
name: head_node
instance_type: n1-standard-4
worker_node_types:
- name: worker_node
instance_type: n1-standard-32-nvidia-tesla-t4-2
min_workers: 2
max_workers: 2
use_spot: true
@@ -0,0 +1,302 @@
import argparse
import atexit
import json
import os
import tempfile
import time
import subprocess
import ray
from ray.train import Checkpoint, ScalingConfig, RunConfig
from ray.tune.tune_config import TuneConfig
import requests
import torch
import torch.nn as nn
import torchvision.transforms as transforms
from filelock import FileLock
from ray import serve, tune, train
from ray.train.torch import TorchTrainer
from ray.tune import Tuner
from torch.utils.data import DataLoader, Subset
from torchvision.datasets import MNIST
from torchvision.models import resnet18
def load_mnist_data(train: bool, download: bool):
transform = transforms.Compose(
[transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,))]
)
with FileLock(os.path.expanduser("~/.ray.lock")):
return MNIST(
root=os.path.expanduser("~/data"),
train=train,
download=download,
transform=transform,
)
def train_epoch(epoch, dataloader, model, loss_fn, optimizer):
if ray.train.get_context().get_world_size() > 1:
dataloader.sampler.set_epoch(epoch)
for X, y in dataloader:
# Compute prediction error
pred = model(X)
loss = loss_fn(pred, y)
# Backpropagation
optimizer.zero_grad()
loss.backward()
optimizer.step()
def validate_epoch(dataloader, model, loss_fn):
num_batches = len(dataloader)
model.eval()
loss = 0
with torch.no_grad():
for X, y in dataloader:
pred = model(X)
loss += loss_fn(pred, y).item()
loss /= num_batches
result = {"val_loss": loss}
return result
def training_loop(config):
# Create model.
model = resnet18()
model.conv1 = nn.Conv2d(1, 64, kernel_size=7, stride=1, padding=3, bias=False)
model = train.torch.prepare_model(model)
# Create optimizer.
optimizer = torch.optim.SGD(
model.parameters(),
lr=config.get("lr", 0.1),
momentum=config.get("momentum", 0.9),
)
# Load in training and validation data.
train_dataset = load_mnist_data(True, True)
validation_dataset = load_mnist_data(False, False)
if config["test_mode"]:
train_dataset = Subset(train_dataset, list(range(64)))
validation_dataset = Subset(validation_dataset, list(range(64)))
train_loader = DataLoader(
train_dataset, batch_size=config["batch_size"], num_workers=2, shuffle=True
)
validation_loader = DataLoader(
validation_dataset, batch_size=config["batch_size"], num_workers=2
)
train_loader = train.torch.prepare_data_loader(train_loader)
validation_loader = train.torch.prepare_data_loader(validation_loader)
# Create loss.
criterion = nn.CrossEntropyLoss()
for epoch_idx in range(2):
train_epoch(epoch_idx, train_loader, model, criterion, optimizer)
validation_loss = validate_epoch(validation_loader, model, criterion)
with tempfile.TemporaryDirectory() as tmpdir:
torch.save(model.module.state_dict(), os.path.join(tmpdir, "model.pt"))
train.report(validation_loss, checkpoint=Checkpoint.from_directory(tmpdir))
def train_mnist(test_mode=False, num_workers=1, use_gpu=False):
trainer = TorchTrainer(
training_loop,
scaling_config=ScalingConfig(num_workers=num_workers, use_gpu=use_gpu),
)
tuner = Tuner(
trainer,
param_space={
"train_loop_config": {
"lr": tune.grid_search([1e-4, 1e-3]),
"test_mode": test_mode,
"batch_size": 128,
}
},
tune_config=TuneConfig(
metric="val_loss",
mode="min",
num_samples=1,
),
run_config=RunConfig(
verbose=1,
storage_path=(
"/mnt/cluster_storage"
if os.path.exists("/mnt/cluster_storage")
else None
),
),
)
return tuner.fit()
def get_model(checkpoint_dir: str):
model = resnet18()
model.conv1 = nn.Conv2d(1, 64, kernel_size=7, stride=1, padding=3, bias=False)
model_state_dict = torch.load(
os.path.join(checkpoint_dir, "model.pt"), map_location="cpu"
)
model.load_state_dict(model_state_dict)
return model
@serve.deployment(name="mnist")
class MnistDeployment:
def __init__(self, model):
use_cuda = torch.cuda.is_available()
self.device = torch.device("cuda" if use_cuda else "cpu")
model = model.to(self.device)
self.model = model
async def __call__(self, request):
json_input = await request.json()
prediction = await self.my_batch_handler(json_input["image"])
return {"result": prediction.cpu().numpy().tolist()}
@serve.batch(max_batch_size=4, batch_wait_timeout_s=1)
async def my_batch_handler(self, images):
print(f"Processing request with batch size {len(images)}.")
image_tensors = torch.tensor(images)
image_tensors = image_tensors.to(self.device)
outputs = self.model(image_tensors)
predictions = torch.max(outputs.data, 1)[1]
return predictions
def setup_serve(model, use_gpu: bool = False):
serve.start(
http_options={"location": "EveryNode"}
) # Start on every node so `predict` can hit localhost.
serve.run(
MnistDeployment.options(
num_replicas=2,
ray_actor_options={"num_gpus": 1, "resources": {"worker": 1}}
if use_gpu
else {},
).bind(model),
route_prefix="/mnist",
)
@ray.remote
def predict_and_validate(index, image, label):
def predict(image):
response = requests.post(
"http://localhost:8000/mnist", json={"image": image.numpy().tolist()}
)
try:
return response.json()["result"]
except: # noqa: E722
return -1
prediction = predict(image)
print(
"Querying model with example #{}. "
"Label = {}, Prediction = {}, Correct = {}".format(
index, label, prediction, label == prediction
)
)
return prediction
def test_predictions(test_mode=False):
# Load in data
dataset = load_mnist_data(False, True)
num_to_test = 10 if test_mode else 1000
filtered_dataset = [dataset[i] for i in range(num_to_test)]
images, labels = zip(*filtered_dataset)
# Remote function calls are done here for parallelism.
# As a byproduct `predict` can hit localhost.
predictions = ray.get(
[
predict_and_validate.remote(i, images[i], labels[i])
for i in range(num_to_test)
]
)
correct = 0
for label, prediction in zip(labels, predictions):
if label == prediction:
correct += 1
print(
"Labels = {}. Predictions = {}. {} out of {} are correct.".format(
list(labels), predictions, correct, num_to_test
)
)
return correct / float(num_to_test)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument(
"--smoke-test",
action="store_true",
default=False,
help="Finish quickly for testing.",
)
args = parser.parse_args()
if os.environ.get("IS_SMOKE_TEST"):
args.smoke_test = True
proc = subprocess.Popen(["ray", "start", "--head"])
proc.wait()
os.environ["RAY_ADDRESS"] = "ray://localhost:10001"
def stop_ray():
subprocess.Popen(["ray", "stop", "--force"]).wait()
atexit.register(stop_ray)
start = time.time()
addr = os.environ.get("RAY_ADDRESS")
job_name = os.environ.get("RAY_JOB_NAME", "torch_tune_serve_test")
if addr is not None and addr.startswith("anyscale://"):
client = ray.init(address=addr, job_name=job_name)
else:
client = ray.init()
num_workers = 2
use_gpu = not args.smoke_test
print("Training model.")
result_grid = train_mnist(args.smoke_test, num_workers, use_gpu)
print("Retrieving best model.")
best_result = result_grid.get_best_result()
best_checkpoint = best_result.get_best_checkpoint(metric="val_loss", mode="min")
with best_checkpoint.as_directory() as checkpoint_dir:
model = get_model(checkpoint_dir)
print("Setting up Serve.")
setup_serve(model, use_gpu)
print("Testing Prediction Service.")
accuracy = test_predictions(args.smoke_test)
taken = time.time() - start
result = {
"time_taken": taken,
"accuracy": accuracy,
}
test_output_json = os.environ.get(
"TEST_OUTPUT_JSON", "/tmp/torch_tune_serve_test.json"
)
with open(test_output_json, "wt") as f:
json.dump(result, f)
print("Test Successful!")
@@ -0,0 +1,17 @@
import os.path
from pathlib import Path
import importlib.util
def import_and_execute_test_script(relative_path_to_test_script: str):
"""Imports and executes a module from a path relative to Ray repo root."""
# get the ray folder
ray_path = Path(
os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
)
notebook_path = ray_path.joinpath(relative_path_to_test_script)
assert notebook_path.exists()
spec = importlib.util.spec_from_file_location("notebook_test", notebook_path)
notebook_test_module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(notebook_test_module)
+14
View File
@@ -0,0 +1,14 @@
import ray
@ray.remote
def hello_world():
return "Hello, world!"
def main():
print(ray.get(hello_world.remote()))
if __name__ == "__main__":
main()
@@ -0,0 +1,6 @@
cloud: {{env["ANYSCALE_CLOUD_NAME"]}}
head_node:
instance_type: m5.xlarge
worker_nodes: []
@@ -0,0 +1,6 @@
cloud: {{env["ANYSCALE_CLOUD_NAME"]}}
head_node:
instance_type: 4CPU-16GB
worker_nodes: []
@@ -0,0 +1,8 @@
cloud: {{env["ANYSCALE_CLOUD_NAME"]}}
zones:
- us-west1-c
head_node:
instance_type: n2-standard-4
worker_nodes: []
@@ -0,0 +1,15 @@
import ray
import emoji
@ray.remote
def hello_world_emoji():
return emoji.emojize(":globe_showing_Americas:")
def main():
print(ray.get(hello_world_emoji.remote()))
if __name__ == "__main__":
main()

Some files were not shown because too many files have changed in this diff Show More