mirror of
https://github.com/vale981/ray
synced 2025-03-06 02:21:39 -05:00
[Serve] Add experimental CLI for serve deploy
(#20371)
This commit is contained in:
parent
454db6902c
commit
18d605fa7c
7 changed files with 111 additions and 12 deletions
|
@ -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
|
||||
|
|
|
@ -78,6 +78,7 @@ In general, **Option 2 is recommended for most users** because it allows you to
|
|||
|
||||
my_func.deploy()
|
||||
|
||||
|
||||
Deploying on Kubernetes
|
||||
=======================
|
||||
|
||||
|
|
|
@ -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.")
|
||||
|
|
|
@ -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()
|
||||
|
|
0
python/ray/serve/tests/__init__.py
Normal file
0
python/ray/serve/tests/__init__.py
Normal 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__]))
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue