[Serve] Add experimental CLI for serve deploy (#20371)

This commit is contained in:
Simon Mo 2021-11-16 20:22:09 -08:00 committed by GitHub
parent 454db6902c
commit 18d605fa7c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 111 additions and 12 deletions

View file

@ -141,6 +141,7 @@ test_python() {
python/ray/tests/...
-python/ray/serve:conda_env # runtime_env unsupported on Windows
-python/ray/serve:test_api # segfault on windows? https://github.com/ray-project/ray/issues/12541
-python/ray/serve:test_cli # cli
-python/ray/serve:test_router # timeout
-python/ray/serve:test_handle # "fatal error" (?) https://github.com/ray-project/ray/pull/13695
-python/ray/serve:test_controller_crashes # timeout

View file

@ -78,6 +78,7 @@ In general, **Option 2 is recommended for most users** because it allows you to
my_func.deploy()
Deploying on Kubernetes
=======================

View file

@ -658,7 +658,7 @@ class Deployment:
raise TypeError("version must be a string.")
if not (prev_version is None or isinstance(prev_version, str)):
raise TypeError("prev_version must be a string.")
if not (init_args is None or isinstance(init_args, tuple)):
if not (init_args is None or isinstance(init_args, (tuple, list))):
raise TypeError("init_args must be a tuple.")
if not (init_kwargs is None or isinstance(init_kwargs, dict)):
raise TypeError("init_kwargs must be a dict.")

View file

@ -1,9 +1,13 @@
#!/usr/bin/env python
import json
import os
import click
from ray.serve.config import DeploymentMode
import ray
from ray.serve.api import Deployment
from ray.serve.config import DeploymentMode
from ray._private.utils import import_attr
from ray import serve
from ray.serve.constants import (DEFAULT_CHECKPOINT_PATH, DEFAULT_HTTP_HOST,
DEFAULT_HTTP_PORT)
@ -14,7 +18,7 @@ from ray.serve.constants import (DEFAULT_CHECKPOINT_PATH, DEFAULT_HTTP_HOST,
@click.option(
"--address",
"-a",
default="auto",
default=os.environ.get("RAY_ADDRESS", "auto"),
required=False,
type=str,
help="Address of the running Ray cluster to connect to. "
@ -26,8 +30,19 @@ from ray.serve.constants import (DEFAULT_CHECKPOINT_PATH, DEFAULT_HTTP_HOST,
required=False,
type=str,
help="Ray namespace to connect to. Defaults to \"serve\".")
def cli(address, namespace):
ray.init(address=address, namespace=namespace)
@click.option(
"--runtime-env-json",
default=r"{}",
required=False,
type=str,
help=("Runtime environment dictionary to pass into ray.init. "
"Defaults to empty."))
def cli(address, namespace, runtime_env_json):
ray.init(
address=address,
namespace=namespace,
runtime_env=json.loads(runtime_env_json),
)
@cli.command(help="Start a detached Serve instance on the Ray cluster.")
@ -73,3 +88,27 @@ def start(http_host, http_port, http_location, checkpoint_path):
def shutdown():
serve.api._connect()
serve.shutdown()
@cli.command(
help="""
[Experimental]
Create a deployment in running Serve instance. The required argument is the
import path for the deployment: ``my_module.sub_module.file.MyClass``. The
class may or may not be decorated with ``@serve.deployment``.
""",
hidden=True,
)
@click.argument("deployment")
@click.option(
"--options-json",
default=r"{}",
required=False,
type=str,
help="JSON string for the deployments options")
def deploy(deployment: str, options_json: str):
deployment_cls = import_attr(deployment)
if not isinstance(deployment_cls, Deployment):
deployment_cls = serve.deployment(deployment_cls)
options = json.loads(options_json)
deployment_cls.options(**options).deploy()

View file

View file

@ -1,7 +1,14 @@
import json
import os
from pathlib import Path
import subprocess
import sys
import pytest
import requests
from ray import serve
from ray.tests.test_runtime_env_working_dir import tmp_working_dir # noqa: F401, E501
@pytest.fixture
@ -27,5 +34,62 @@ def test_start_shutdown_in_namespace(ray_start_stop):
subprocess.check_output(["serve", "-n", "test", "shutdown"])
class A:
def __init__(self, value, increment=1):
self.value = value
self.increment = increment
self.decrement = 0
self.multiplier = int(os.environ["SERVE_TEST_MULTIPLIER"])
p = Path("hello")
assert p.exists()
with open(p) as f:
assert f.read() == "world"
def reconfigure(self, config):
self.decrement = config["decrement"]
def __call__(self, inp):
return (self.value + self.increment - self.decrement) * self.multiplier
@serve.deployment
class DecoratedA(A):
pass
@pytest.mark.parametrize("class_name", ["A", "DecoratedA"])
def test_deploy(ray_start_stop, tmp_working_dir, class_name): # noqa: F811
subprocess.check_output(["serve", "start"])
subprocess.check_output([
"serve", "--runtime-env-json",
json.dumps({
"working_dir": tmp_working_dir,
}), "deploy", f"ray.serve.tests.test_cli.{class_name}",
"--options-json",
json.dumps({
"name": "B",
"init_args": [42],
"init_kwargs": {
"increment": 10
},
"num_replicas": 2,
"user_config": {
"decrement": 5
},
"ray_actor_options": {
"runtime_env": {
"env_vars": {
"SERVE_TEST_MULTIPLIER": "2",
},
}
}
})
])
resp = requests.get("http://127.0.0.1:8000/B")
resp.raise_for_status()
assert resp.text == "94", resp.text
if __name__ == "__main__":
sys.exit(pytest.main(["-v", "-s", __file__]))

View file

@ -688,12 +688,6 @@ def test_deploy_handle_validation(serve_instance):
def test_init_args(serve_instance):
with pytest.raises(TypeError):
@serve.deployment(init_args=[1, 2, 3])
class BadInitArgs:
pass
@serve.deployment(init_args=(1, 2, 3))
class D:
def __init__(self, *args):
@ -840,7 +834,7 @@ def test_input_validation():
with pytest.raises(TypeError):
@serve.deployment(init_args=[1, 2, 3])
@serve.deployment(init_args={1, 2, 3})
class BadInitArgs:
pass