From 970f92c2f2f091e7c48e3b12a54e8a58d834673e Mon Sep 17 00:00:00 2001 From: Dan Crankshaw Date: Thu, 1 Jun 2017 22:58:06 -0700 Subject: [PATCH 01/21] removed uid from prediction query schema --- src/frontends/src/query_frontend.hpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/frontends/src/query_frontend.hpp b/src/frontends/src/query_frontend.hpp index a35b487cc..7d1dbebaf 100644 --- a/src/frontends/src/query_frontend.hpp +++ b/src/frontends/src/query_frontend.hpp @@ -49,7 +49,6 @@ const std::string PREDICTION_ERROR_NAME_QUERY_PROCESSING = const std::string PREDICTION_JSON_SCHEMA = R"( { - "uid" := string, "input" := [double] | [int] | [string] | [byte] | [float], } )"; @@ -438,7 +437,6 @@ class RequestHandler { /* * JSON format for prediction query request: * { - * "uid" := string, * "input" := [double] | [int] | [string] | [byte] | [float] * } */ @@ -448,7 +446,10 @@ class RequestHandler { long latency_slo_micros, InputType input_type) { rapidjson::Document d; clipper::json::parse_json(json_content, d); - long uid = clipper::json::get_long(d, "uid"); + long uid = 0; + // NOTE: We will eventually support personalization again so this commented + // out code is intentionally left in as a placeholder. + // long uid = clipper::json::get_long(d, "uid"); std::shared_ptr input = clipper::json::parse_input(input_type, d); auto prediction = query_processor_.predict( Query{name, uid, input, latency_slo_micros, policy, models}); From 18584ccecd1a2b9b50c01d7d389e1a4749e3b122 Mon Sep 17 00:00:00 2001 From: Dan Crankshaw Date: Fri, 2 Jun 2017 00:01:57 -0700 Subject: [PATCH 02/21] added method to remove inactive containers --- clipper_admin/clipper_manager.py | 51 +++++++++++++++++++++++++++----- 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/clipper_admin/clipper_manager.py b/clipper_admin/clipper_manager.py index ea0036776..3f44e10f9 100644 --- a/clipper_admin/clipper_manager.py +++ b/clipper_admin/clipper_manager.py @@ -41,6 +41,7 @@ CLIPPER_LOGS_PATH = "/tmp/clipper-logs" CLIPPER_DOCKER_LABEL = "ai.clipper.container.label" +CLIPPER_MODEL_CONTAINER_LABEL = "ai.clipper.model_container.model_version" aws_cli_config = """ [default] @@ -126,7 +127,7 @@ def __init__(self, '--redis_port=%d' % self.redis_port ], 'image': - 'clipper/management_frontend:latest', + 'clipper/management_frontend:0.1', 'ports': [ '%d:%d' % (CLIPPER_MANAGEMENT_PORT, CLIPPER_MANAGEMENT_PORT) @@ -142,7 +143,7 @@ def __init__(self, ], 'depends_on': ['mgmt_frontend'], 'image': - 'clipper/query_frontend:latest', + 'clipper/query_frontend:0.1', 'ports': [ '%d:%d' % (CLIPPER_RPC_PORT, CLIPPER_RPC_PORT), '%d:%d' % (CLIPPER_QUERY_PORT, CLIPPER_QUERY_PORT) @@ -954,7 +955,7 @@ def add_container(self, model_name, model_version): add_container_cmd = ( "docker run -d --network={nw} --restart={restart_policy} -v {path}:/model:ro " "-e \"CLIPPER_MODEL_NAME={mn}\" -e \"CLIPPER_MODEL_VERSION={mv}\" " - "-e \"CLIPPER_IP=query_frontend\" -e \"CLIPPER_INPUT_TYPE={mip}\" -l \"{clipper_label}\" " + "-e \"CLIPPER_IP=query_frontend\" -e \"CLIPPER_INPUT_TYPE={mip}\" -l \"{clipper_label}\" -l \"{mv_label}\" " "{image}".format( path=model_data_path, nw=DOCKER_NW, @@ -963,6 +964,8 @@ def add_container(self, model_name, model_version): mv=model_version, mip=model_input_type, clipper_label=CLIPPER_DOCKER_LABEL, + mv_label="%s=%s:%d" % (CLIPPER_MODEL_CONTAINER_LABEL, + model_name, model_version), restart_policy=restart_policy)) result = self._execute_root(add_container_cmd) return result.return_code == 0 @@ -1058,6 +1061,44 @@ def set_model_version(self, model_name, model_version, num_containers=0): for r in range(num_containers): self.add_container(model_name, model_version) + def remove_inactive_containers(self, model_name): + # TODO: Test this function + """Removes all containers serving stale versions of the specified model. + + Parameters + ---------- + model_name : str + The name of the model whose old containers you want to clean. + + """ + # Get all Docker containers tagged as model containers + num_containers_removed = 0 + with hide("output", "warnings", "running"): + containers = self._execute_root( + "docker ps -aq --filter label={model_container_label}".format( + model_container_label=CLIPPER_MODEL_CONTAINER_LABEL)) + container_ids = [l.strip() for l in containers.split("\n")] + for container in container_ids: + # returns a string formatted as ":" + container_model_name_and_version = self._execute_root( + "docker inspect --format \"{{ index .Config.Labels \\\"%s\\\"}}\" %s" + % (CLIPPER_MODEL_CONTAINER_LABEL, container)) + splits = container_model_name_and_version.split(":") + container_model_name = splits[0] + container_model_version = int(splits[1]) + if container_model_name == model_name: + # check if container_model_version is the currently deployed version + model_info = self.get_model_info(container_model_name, + container_model_version) + if model_info == None or not model_info["is_current_version"]: + self._execute_root("docker stop {container}".format( + container=container)) + self._execute_root("docker rm {container}".format( + container=container)) + num_containers_removed += 1 + print("Removed %d inactive containers for model %s" % + (num_containers_removed, model_name)) + def stop_all(self): """Stops and removes all Clipper Docker containers on the host. @@ -1130,10 +1171,6 @@ def _put_container_on_host(self, container_name): "docker images -q {cn}".format(cn=container_name)) if len(local_result.stdout) > 0: - saved_fname = container_name.replace("/", "_") - subprocess.call("docker save -o /tmp/{fn}.tar {cn}".format( - fn=saved_fname, cn=container_name)) - tar_loc = "/tmp/{fn}.tar".format(fn=saved_fname) self._execute_put(tar_loc, tar_loc) self._execute_root("docker load -i {loc}".format(loc=tar_loc)) # self._execute_root("docker tag {image_id} {cn}".format( From 6964b85176b8f741bf65734756fb8497d3b4bbaf Mon Sep 17 00:00:00 2001 From: Dan Crankshaw Date: Fri, 2 Jun 2017 00:20:43 -0700 Subject: [PATCH 03/21] add Clipper class to clipper_admin module --- README.md | 4 ++-- clipper_admin/__init__.py | 1 + docs/index.rst | 2 +- examples/basic_query/README.md | 17 ----------------- examples/basic_query/example_client.py | 6 ++---- examples/tutorial/tutorial_part_one.ipynb | 18 ++++++++++++------ examples/tutorial/tutorial_part_two.ipynb | 5 ++--- 7 files changed, 20 insertions(+), 33 deletions(-) delete mode 100644 examples/basic_query/README.md diff --git a/README.md b/README.md index bdb0ea9d4..886ee9858 100644 --- a/README.md +++ b/README.md @@ -35,9 +35,9 @@ Clipper is a prediction serving system that sits between user-facing application ``` $ pip install clipper_admin $ python ->>> import clipper_admin.clipper_manager as cm, numpy as np +>>> from clipper_admin import Clipper, numpy as np # Start a Clipper instance on localhost ->>> clipper = cm.Clipper("localhost") +>>> clipper = Clipper("localhost") Checking if Docker is running... # Start Clipper. Running this command for the first time will diff --git a/clipper_admin/__init__.py b/clipper_admin/__init__.py index e69de29bb..de5151cc0 100644 --- a/clipper_admin/__init__.py +++ b/clipper_admin/__init__.py @@ -0,0 +1 @@ +from clipper_manager import Clipper diff --git a/docs/index.rst b/docs/index.rst index 36818c2de..400050937 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2,7 +2,7 @@ Clipper Manager API Reference ============================== -.. automodule:: clipper_admin.clipper_manager +.. autoclass:: clipper_admin.Clipper :members: :undoc-members: :show-inheritance: diff --git a/examples/basic_query/README.md b/examples/basic_query/README.md deleted file mode 100644 index e9686f0d7..000000000 --- a/examples/basic_query/README.md +++ /dev/null @@ -1,17 +0,0 @@ - -# Basic Query Example Requirements - -The examples in this directory depend on a few Python packages. -We recommend using [Anaconda](https://www.continuum.io/downloads) -to install Python packages. - -+ [`requests`](http://docs.python-requests.org/en/master/) -+ [`numpy`](http://www.numpy.org/) - -# Running the example query - -1. Start Clipper locally - + With Docker `cd /docker && docker-compose up -d query_frontend` - + Without Docker `/bin/start_clipper.sh` -2. Run the example: `python example_client.py` -3. Connect a container: `cd /containers/python && CLIPPER_MODEL_NAME=example_model CLIPPER_MODEL_VERSION=1 CLIPPER_INPUT_TYPE=doubles python noop_container.py` diff --git a/examples/basic_query/example_client.py b/examples/basic_query/example_client.py index a7a4c61ce..e04669313 100644 --- a/examples/basic_query/example_client.py +++ b/examples/basic_query/example_client.py @@ -1,9 +1,7 @@ from __future__ import print_function import sys import os -cur_dir = os.path.dirname(os.path.abspath(__file__)) -sys.path.append(os.path.abspath('%s/../../management/' % cur_dir)) -import clipper_manager as cm +from clipper_admin import Clipper import json import requests from datetime import datetime @@ -24,7 +22,7 @@ def predict(host, uid, x): if __name__ == '__main__': host = "localhost" - clipper = cm.Clipper(host, check_for_docker=False) + clipper = Clipper(host, check_for_docker=False) clipper.register_application("example_app", "example_model", "doubles", "-1.0", 40000) clipper.register_external_model("example_model", 1, ["l1", "l2"], diff --git a/examples/tutorial/tutorial_part_one.ipynb b/examples/tutorial/tutorial_part_one.ipynb index 8741405db..1a4ab3259 100644 --- a/examples/tutorial/tutorial_part_one.ipynb +++ b/examples/tutorial/tutorial_part_one.ipynb @@ -121,9 +121,16 @@ "source": [ "# Start Clipper\n", "\n", - "Now you're ready to start Clipper! You will be using the `clipper_manager` client library to perform admninistrative commands.\n", + "Now you're ready to start Clipper! You will be using the `clipper_admin` client library to perform administrative commands.\n", "\n", - "> *Remember, Docker and Docker-Compose must be installed before deploying Clipper. Visit https://docs.docker.com/compose/install/ for instructions on how to do so.*\n", + "\n", + "> *Remember, Docker and Docker-Compose must be installed before deploying Clipper. Visit https://docs.docker.com/compose/install/ for instructions on how to do so. In addition, we recommend using [Anaconda](https://www.continuum.io/downloads) and Anaconda environments to manage Python.*\n", + "\n", + "Start by installing the library with `pip`:\n", + "\n", + "```sh\n", + "pip install clipper_admin\n", + "```\n", "\n", "Clipper uses Docker to manage application configurations and to deploy machine-learning models. Make sure your Docker daemon, local or remote, is up and running. You can check this by running `docker ps` in your command line – if your Docker daemon is not running, you will be told explicitly.\n", "\n", @@ -131,7 +138,7 @@ "\n", "If you'd like to deploy Clipper locally, you can leave the `user` and `key` variables blank and set `host=\"localhost\"`. Otherwise, you can deploy Clipper remotely to a machine that you have SSH access to. Set the `user` variable to your SSH username, the `key` variable to the path to your SSH key, and the `host` variable to the remote hostname or IP address.\n", "\n", - "> If your SSH server is running on a non-standard port, you can specify the SSH port to use as another argument to the Clipper constructor. For example, `clipper = cm.Clipper(host, user, key, ssh_port=9999)`." + "> If your SSH server is running on a non-standard port, you can specify the SSH port to use as another argument to the Clipper constructor. For example, `clipper = Clipper(host, user, key, ssh_port=9999)`." ] }, { @@ -145,17 +152,16 @@ }, "outputs": [], "source": [ - "# clipper_manager must be on your path:\n", "import sys\n", "import os\n", - "import clipper_admin.clipper_manager as cm\n", + "from clipper_admin import Clipper\n", "# Change the username if necessary\n", "user = \"\"\n", "# Set the path to the SSH key\n", "key = \"\"\n", "# Set the SSH host\n", "host = \"\"\n", - "clipper = cm.Clipper(host, user, key)\n", + "clipper = Clipper(host, user, key)\n", "\n", "clipper.start()" ] diff --git a/examples/tutorial/tutorial_part_two.ipynb b/examples/tutorial/tutorial_part_two.ipynb index 9d8900f3f..8176fdbe2 100644 --- a/examples/tutorial/tutorial_part_two.ipynb +++ b/examples/tutorial/tutorial_part_two.ipynb @@ -22,17 +22,16 @@ }, "outputs": [], "source": [ - "# clipper_manager must be on your path:\n", "import sys\n", "import os\n", - "import clipper_admin.clipper_manager as cm\n", + "from clipper_admin import Clipper\n", "# Change the username if necessary\n", "user = \"\"\n", "# Set the path to the SSH key\n", "key = \"\"\n", "# Set the SSH host\n", "host = \"\"\n", - "clipper = cm.Clipper(host, user, key)" + "clipper = Clipper(host, user, key)" ] }, { From e2e54e5949c576b583e5ae8d011cdef17e857ad8 Mon Sep 17 00:00:00 2001 From: Dan Crankshaw Date: Fri, 2 Jun 2017 00:41:08 -0700 Subject: [PATCH 04/21] Make model labels optional in clipper_admin --- README.md | 2 +- clipper_admin/clipper_manager.py | 22 +++++++++++++--------- examples/basic_query/README.md | 18 ++++++++++++++++++ examples/basic_query/example_client.py | 3 +-- examples/tutorial/tutorial_part_two.ipynb | 2 -- 5 files changed, 33 insertions(+), 14 deletions(-) create mode 100644 examples/basic_query/README.md diff --git a/README.md b/README.md index 886ee9858..3fde65b31 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ Success! return [str(np.sum(x)) for x in xs] # Deploy the model, naming it "feature_sum_model" and giving it version 1 ->>> clipper.deploy_predict_function("feature_sum_model", 1, feature_sum_function, ["quickstart"], "doubles") +>>> clipper.deploy_predict_function("feature_sum_model", 1, feature_sum_function, "doubles") ``` diff --git a/clipper_admin/clipper_manager.py b/clipper_admin/clipper_manager.py index 3f44e10f9..b507a6c91 100644 --- a/clipper_admin/clipper_manager.py +++ b/clipper_admin/clipper_manager.py @@ -43,6 +43,8 @@ CLIPPER_DOCKER_LABEL = "ai.clipper.container.label" CLIPPER_MODEL_CONTAINER_LABEL = "ai.clipper.model_container.model_version" +DEFAULT_LABEL = "default" + aws_cli_config = """ [default] region = us-east-1 @@ -442,8 +444,8 @@ def deploy_model(self, version, model_data, container_name, - labels, input_type, + labels=[DEFAULT_LABEL], num_containers=1): """Registers a model with Clipper and deploys instances of it in containers. @@ -464,10 +466,10 @@ def deploy_model(self, be the path you provided to the serialize method call. container_name : str The Docker container image to use to run this model container. - labels : list of str - A set of strings annotating the model input_type : str One of "integers", "floats", "doubles", "bytes", or "strings". + labels : list of str, optional + A list of strings annotating the model num_containers : int, optional The number of replicas of the model to create. More replicas can be created later as well. Defaults to 1. @@ -546,7 +548,7 @@ def deploy_model(self, for r in range(num_containers) ]) - def register_external_model(self, name, version, labels, input_type): + def register_external_model(self, name, version, input_type, labels = [DEFAULT_LABEL]): """Registers a model with Clipper without deploying it in any containers. Parameters @@ -555,10 +557,10 @@ def register_external_model(self, name, version, labels, input_type): The name to assign this model. version : int The version to assign this model. - labels : list of str - A set of strings annotating the model input_type : str One of "integers", "floats", "doubles", "bytes", or "strings". + labels : list of str, optional + A list of strings annotating the model. """ return self._publish_new_model(name, version, labels, input_type, EXTERNALLY_MANAGED_MODEL, @@ -568,8 +570,8 @@ def deploy_predict_function(self, name, version, predict_function, - labels, input_type, + labels = [DEFAULT_LABEL], num_containers=1): """Deploy an arbitrary Python function to Clipper. @@ -587,16 +589,18 @@ def deploy_predict_function(self, predict_function : function The prediction function. Any state associated with the function should be captured via closure capture. - labels : list of str - A set of strings annotating the model input_type : str One of "integers", "floats", "doubles", "bytes", or "strings". + labels : list of str, optional + A list of strings annotating the model num_containers : int, optional The number of replicas of the model to create. More replicas can be created later as well. Defaults to 1. Example ------- + Define a feature function ``center()`` and train a model on the featurized input:: + def center(xs): means = np.mean(xs, axis=0) return xs - means diff --git a/examples/basic_query/README.md b/examples/basic_query/README.md new file mode 100644 index 000000000..e67b0ee17 --- /dev/null +++ b/examples/basic_query/README.md @@ -0,0 +1,18 @@ + +# Basic Query Example Requirements + +The examples in this directory assume you have the `clipper_admin` pip package installed: + +```sh +pip install clipper_admin +``` +We recommend using [Anaconda](https://www.continuum.io/downloads) +to install Python packages. + +# Running the example query + +1. Start Clipper locally + + With Docker `cd /docker && docker-compose up -d query_frontend` + + Without Docker `/bin/start_clipper.sh` +2. Run the example: `python example_client.py` +3. Connect a container: `cd /containers/python && CLIPPER_MODEL_NAME=example_model CLIPPER_MODEL_VERSION=1 CLIPPER_INPUT_TYPE=doubles python noop_container.py` diff --git a/examples/basic_query/example_client.py b/examples/basic_query/example_client.py index e04669313..4150f8d61 100644 --- a/examples/basic_query/example_client.py +++ b/examples/basic_query/example_client.py @@ -25,8 +25,7 @@ def predict(host, uid, x): clipper = Clipper(host, check_for_docker=False) clipper.register_application("example_app", "example_model", "doubles", "-1.0", 40000) - clipper.register_external_model("example_model", 1, ["l1", "l2"], - "doubles") + clipper.register_external_model("example_model", 1, "doubles") time.sleep(1.0) uid = 0 while True: diff --git a/examples/tutorial/tutorial_part_two.ipynb b/examples/tutorial/tutorial_part_two.ipynb index 8176fdbe2..7b7dbd44c 100644 --- a/examples/tutorial/tutorial_part_two.ipynb +++ b/examples/tutorial/tutorial_part_two.ipynb @@ -119,7 +119,6 @@ " 1,\n", " lr_model,\n", " \"clipper/sklearn_cifar_container:latest\",\n", - " [\"cifar\", \"sklearn\"],\n", " \"doubles\",\n", " num_containers=1\n", ")\n", @@ -200,7 +199,6 @@ " 2,\n", " os.path.abspath(\"tf_cifar_model\"),\n", " \"clipper/tf_cifar_container:latest\",\n", - " [\"cifar\", \"tf\"],\n", " \"doubles\",\n", " num_containers=1\n", ")\n", From ca5a9d5a917b77674813cc1a7249e025a4c43374 Mon Sep 17 00:00:00 2001 From: Dan Crankshaw Date: Fri, 2 Jun 2017 00:43:32 -0700 Subject: [PATCH 05/21] make inspect_selection_policy private --- clipper_admin/clipper_manager.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/clipper_admin/clipper_manager.py b/clipper_admin/clipper_manager.py index b507a6c91..08524b294 100644 --- a/clipper_admin/clipper_manager.py +++ b/clipper_admin/clipper_manager.py @@ -802,7 +802,9 @@ def get_container_info(self, model_name, model_version, replica_id): print(r.text) return None - def inspect_selection_policy(self, app_name, uid): + def _inspect_selection_policy(self, app_name, uid): + # NOTE: This method is private (it's still functional, but it won't be documented) + # until Clipper supports different selection policies """Fetches a human-readable string with the current selection policy state. Parameters From cdbeb856cedc429cb1b06f8c7851c2a9288b2ab4 Mon Sep 17 00:00:00 2001 From: Dan Crankshaw Date: Fri, 2 Jun 2017 00:56:11 -0700 Subject: [PATCH 06/21] added check for python3 and note about downloading docker images --- clipper_admin/__init__.py | 6 ++++++ clipper_admin/clipper_manager.py | 11 +++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/clipper_admin/__init__.py b/clipper_admin/__init__.py index de5151cc0..1ee613389 100644 --- a/clipper_admin/__init__.py +++ b/clipper_admin/__init__.py @@ -1 +1,7 @@ +import sys +if sys.version_info >= (3, 0): + sys.stdout.write( + "Sorry, clipper_admin requires Python 2.x, not Python 3.x\n") + sys.exit(1) + from clipper_manager import Clipper diff --git a/clipper_admin/clipper_manager.py b/clipper_admin/clipper_manager.py index 08524b294..81674b4da 100644 --- a/clipper_admin/clipper_manager.py +++ b/clipper_admin/clipper_manager.py @@ -338,6 +338,9 @@ def start(self): yaml.dump( self.docker_compost_dict, default_flow_style=False)) + print( + "Note: Docker must download the Clipper Docker images if they are not already cached. This may take awhile." + ) self._execute_root("docker-compose up -d query_frontend") print("Clipper is running") @@ -548,7 +551,11 @@ def deploy_model(self, for r in range(num_containers) ]) - def register_external_model(self, name, version, input_type, labels = [DEFAULT_LABEL]): + def register_external_model(self, + name, + version, + input_type, + labels=[DEFAULT_LABEL]): """Registers a model with Clipper without deploying it in any containers. Parameters @@ -571,7 +578,7 @@ def deploy_predict_function(self, version, predict_function, input_type, - labels = [DEFAULT_LABEL], + labels=[DEFAULT_LABEL], num_containers=1): """Deploy an arbitrary Python function to Clipper. From ee90331e48798d4d0713771b5ef18d57d9ea6b06 Mon Sep 17 00:00:00 2001 From: Dan Crankshaw Date: Fri, 2 Jun 2017 09:38:57 -0700 Subject: [PATCH 07/21] addressed review comments. Still need a test for remove_inactive_containers --- clipper_admin/__init__.py | 2 +- clipper_admin/clipper_manager.py | 43 ++++++++++++++++++-------------- 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/clipper_admin/__init__.py b/clipper_admin/__init__.py index 1ee613389..311265c89 100644 --- a/clipper_admin/__init__.py +++ b/clipper_admin/__init__.py @@ -1,7 +1,7 @@ import sys if sys.version_info >= (3, 0): sys.stdout.write( - "Sorry, clipper_admin requires Python 2.x, not Python 3.x\n") + "Sorry, clipper_admin requires Python 2.x, but you are running Python 3.x\n") sys.exit(1) from clipper_manager import Clipper diff --git a/clipper_admin/clipper_manager.py b/clipper_admin/clipper_manager.py index 81674b4da..105c5adb4 100644 --- a/clipper_admin/clipper_manager.py +++ b/clipper_admin/clipper_manager.py @@ -1090,25 +1090,26 @@ def remove_inactive_containers(self, model_name): containers = self._execute_root( "docker ps -aq --filter label={model_container_label}".format( model_container_label=CLIPPER_MODEL_CONTAINER_LABEL)) - container_ids = [l.strip() for l in containers.split("\n")] - for container in container_ids: - # returns a string formatted as ":" - container_model_name_and_version = self._execute_root( - "docker inspect --format \"{{ index .Config.Labels \\\"%s\\\"}}\" %s" - % (CLIPPER_MODEL_CONTAINER_LABEL, container)) - splits = container_model_name_and_version.split(":") - container_model_name = splits[0] - container_model_version = int(splits[1]) - if container_model_name == model_name: - # check if container_model_version is the currently deployed version - model_info = self.get_model_info(container_model_name, - container_model_version) - if model_info == None or not model_info["is_current_version"]: - self._execute_root("docker stop {container}".format( - container=container)) - self._execute_root("docker rm {container}".format( - container=container)) - num_containers_removed += 1 + if len(containers) > 0: + container_ids = [l.strip() for l in containers.split("\n")] + for container in container_ids: + # returns a string formatted as ":" + container_model_name_and_version = self._execute_root( + "docker inspect --format \"{{ index .Config.Labels \\\"%s\\\"}}\" %s" + % (CLIPPER_MODEL_CONTAINER_LABEL, container)) + splits = container_model_name_and_version.split(":") + container_model_name = splits[0] + container_model_version = int(splits[1]) + if container_model_name == model_name: + # check if container_model_version is the currently deployed version + model_info = self.get_model_info(container_model_name, + container_model_version) + if model_info == None or not model_info["is_current_version"]: + self._execute_root("docker stop {container}".format( + container=container)) + self._execute_root("docker rm {container}".format( + container=container)) + num_containers_removed += 1 print("Removed %d inactive containers for model %s" % (num_containers_removed, model_name)) @@ -1184,6 +1185,10 @@ def _put_container_on_host(self, container_name): "docker images -q {cn}".format(cn=container_name)) if len(local_result.stdout) > 0: + saved_fname = container_name.replace("/", "_") + subprocess.call("docker save -o /tmp/{fn}.tar {cn}".format( + fn=saved_fname, cn=container_name)) + tar_loc = "/tmp/{fn}.tar".format(fn=saved_fname) self._execute_put(tar_loc, tar_loc) self._execute_root("docker load -i {loc}".format(loc=tar_loc)) # self._execute_root("docker tag {image_id} {cn}".format( From 06cd6ce4210ce96affef6eca5b924664893488e2 Mon Sep 17 00:00:00 2001 From: Dan Crankshaw Date: Fri, 2 Jun 2017 09:41:44 -0700 Subject: [PATCH 08/21] format code --- clipper_admin/__init__.py | 3 ++- clipper_admin/clipper_manager.py | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/clipper_admin/__init__.py b/clipper_admin/__init__.py index 311265c89..259d6f87f 100644 --- a/clipper_admin/__init__.py +++ b/clipper_admin/__init__.py @@ -1,7 +1,8 @@ import sys if sys.version_info >= (3, 0): sys.stdout.write( - "Sorry, clipper_admin requires Python 2.x, but you are running Python 3.x\n") + "Sorry, clipper_admin requires Python 2.x, but you are running Python 3.x\n" + ) sys.exit(1) from clipper_manager import Clipper diff --git a/clipper_admin/clipper_manager.py b/clipper_admin/clipper_manager.py index 105c5adb4..250077fc0 100644 --- a/clipper_admin/clipper_manager.py +++ b/clipper_admin/clipper_manager.py @@ -1102,11 +1102,11 @@ def remove_inactive_containers(self, model_name): container_model_version = int(splits[1]) if container_model_name == model_name: # check if container_model_version is the currently deployed version - model_info = self.get_model_info(container_model_name, - container_model_version) + model_info = self.get_model_info( + container_model_name, container_model_version) if model_info == None or not model_info["is_current_version"]: - self._execute_root("docker stop {container}".format( - container=container)) + self._execute_root("docker stop {container}". + format(container=container)) self._execute_root("docker rm {container}".format( container=container)) num_containers_removed += 1 From 60bb9dd70e1088a29d17209f083f6077301f6e2d Mon Sep 17 00:00:00 2001 From: Dan Crankshaw Date: Fri, 2 Jun 2017 10:03:05 -0700 Subject: [PATCH 09/21] Fix unit tests --- src/frontends/src/query_frontend_tests.cpp | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/frontends/src/query_frontend_tests.cpp b/src/frontends/src/query_frontend_tests.cpp index 17e8927d9..cb0e141aa 100644 --- a/src/frontends/src/query_frontend_tests.cpp +++ b/src/frontends/src/query_frontend_tests.cpp @@ -61,7 +61,7 @@ class QueryFrontendTest : public ::testing::Test { }; TEST_F(QueryFrontendTest, TestDecodeCorrectInputInts) { - std::string test_json_ints = "{\"uid\": 23, \"input\": [1,2,3,4]}"; + std::string test_json_ints = "{\"input\": [1,2,3,4]}"; Response response = rh_.decode_and_handle_predict(test_json_ints, "test", {}, "test_policy", 30000, InputType::Ints) @@ -69,7 +69,6 @@ TEST_F(QueryFrontendTest, TestDecodeCorrectInputInts) { Query parsed_query = response.query_; - EXPECT_EQ(parsed_query.user_id_, 23); const std::vector& parsed_input = std::static_pointer_cast(parsed_query.input_)->get_data(); std::vector expected_input{1, 2, 3, 4}; @@ -80,8 +79,7 @@ TEST_F(QueryFrontendTest, TestDecodeCorrectInputInts) { } TEST_F(QueryFrontendTest, TestDecodeCorrectInputDoubles) { - std::string test_json_doubles = - "{\"uid\": 23, \"input\": [1.4,2.23,3.243242,0.3223424]}"; + std::string test_json_doubles = "{\"input\": [1.4,2.23,3.243242,0.3223424]}"; Response response = rh_.decode_and_handle_predict(test_json_doubles, "test", {}, "test_policy", 30000, InputType::Doubles) @@ -89,7 +87,6 @@ TEST_F(QueryFrontendTest, TestDecodeCorrectInputDoubles) { Query parsed_query = response.query_; - EXPECT_EQ(parsed_query.user_id_, 23); const std::vector& parsed_input = std::static_pointer_cast(parsed_query.input_)->get_data(); std::vector expected_input{1.4, 2.23, 3.243242, 0.3223424}; @@ -101,7 +98,7 @@ TEST_F(QueryFrontendTest, TestDecodeCorrectInputDoubles) { TEST_F(QueryFrontendTest, TestDecodeCorrectInputString) { std::string test_json_string = - "{\"uid\": 23, \"input\": \"hello world. This is a test string with " + "{\"input\": \"hello world. This is a test string with " "punctionation!@#$Y#;}#\"}"; Response response = rh_.decode_and_handle_predict(test_json_string, "test", {}, "test_policy", @@ -110,7 +107,6 @@ TEST_F(QueryFrontendTest, TestDecodeCorrectInputString) { Query parsed_query = response.query_; - EXPECT_EQ(parsed_query.user_id_, 23); const std::string& parsed_input = std::static_pointer_cast(parsed_query.input_) ->get_data(); @@ -140,7 +136,8 @@ TEST_F(QueryFrontendTest, TestDecodeMalformedJSON) { } TEST_F(QueryFrontendTest, TestDecodeMissingJsonField) { - std::string json_missing_field = "{\"input\": [1.4,2.23,3.243242,0.3223424]}"; + std::string json_missing_field = + "{\"other_field\": [1.4,2.23,3.243242,0.3223424]}"; ASSERT_THROW( rh_.decode_and_handle_predict(json_missing_field, "test", {}, "test_policy", 30000, InputType::Doubles), From 5826a19f1023f3b6f18040517cb13de2a2174589 Mon Sep 17 00:00:00 2001 From: Dan Crankshaw Date: Fri, 2 Jun 2017 11:48:43 -0700 Subject: [PATCH 10/21] fix integration test --- integration-tests/light_load_all_functionality.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration-tests/light_load_all_functionality.py b/integration-tests/light_load_all_functionality.py index 000f0f847..f06e353d7 100644 --- a/integration-tests/light_load_all_functionality.py +++ b/integration-tests/light_load_all_functionality.py @@ -71,7 +71,7 @@ def deploy_model(clipper, name, version): model_name, version, fake_model_data, - "clipper/noop-container", [name], + "clipper/noop-container", "doubles", num_containers=1) time.sleep(10) From 54bbb804c12b147e09010370674a12425bc7076e Mon Sep 17 00:00:00 2001 From: Dan Crankshaw Date: Sat, 3 Jun 2017 12:15:24 -0700 Subject: [PATCH 11/21] fixed some tests --- clipper_admin/clipper_manager.py | 2 +- clipper_admin/tests/clipper_manager_test.py | 64 +++++++++++++++---- .../light_load_all_functionality.py | 2 - 3 files changed, 51 insertions(+), 17 deletions(-) diff --git a/clipper_admin/clipper_manager.py b/clipper_admin/clipper_manager.py index 250077fc0..da1aa3aec 100644 --- a/clipper_admin/clipper_manager.py +++ b/clipper_admin/clipper_manager.py @@ -1075,7 +1075,6 @@ def set_model_version(self, model_name, model_version, num_containers=0): self.add_container(model_name, model_version) def remove_inactive_containers(self, model_name): - # TODO: Test this function """Removes all containers serving stale versions of the specified model. Parameters @@ -1112,6 +1111,7 @@ def remove_inactive_containers(self, model_name): num_containers_removed += 1 print("Removed %d inactive containers for model %s" % (num_containers_removed, model_name)) + return num_containers_removed def stop_all(self): """Stops and removes all Clipper Docker containers on the host. diff --git a/clipper_admin/tests/clipper_manager_test.py b/clipper_admin/tests/clipper_manager_test.py index da0cb4493..8bd2828da 100644 --- a/clipper_admin/tests/clipper_manager_test.py +++ b/clipper_admin/tests/clipper_manager_test.py @@ -128,11 +128,10 @@ def test_model_deploys_successfully(self): # that will be deployed to a no-op container model_data = svm.SVC() container_name = "clipper/noop-container" - labels = ["test"] input_type = "doubles" result = self.clipper_inst.deploy_model( self.deploy_model_name, self.deploy_model_version, model_data, - container_name, labels, input_type) + container_name, input_type) self.assertTrue(result) model_info = self.clipper_inst.get_model_info( self.deploy_model_name, self.deploy_model_version) @@ -152,14 +151,53 @@ def test_add_container_for_deployed_model_succeeds(self): split_output = running_containers_output.split("\n") self.assertGreaterEqual(len(split_output), 2) + def test_remove_inactive_containers_succeeds(self): + # Initialize a support vector classifier + # that will be deployed to a no-op container + model_data = svm.SVC() + container_name = "clipper/noop-container" + input_type = "doubles" + result = self.clipper_inst.deploy_model( + self.deploy_model_name, + 1, + model_data, + container_name, + input_type, + num_containers=2) + self.assertTrue(result) + running_containers_output = self.clipper_inst._execute_standard( + "docker ps -q --filter \"ancestor=clipper/noop-container\"") + self.assertIsNotNone(running_containers_output) + self.assertEqual(len(running_containers_output), 2) + + result = self.clipper_inst.deploy_model( + self.deploy_model_name, + 2, + model_data, + container_name, + input_type, + num_containers=3) + self.assertTrue(result) + running_containers_output = self.clipper_inst._execute_standard( + "docker ps -q --filter \"ancestor=clipper/noop-container\"") + self.assertIsNotNone(running_containers_output) + self.assertEqual(len(running_containers_output), 5) + + num_containers_removed = self.clipper_inst.remove_inactive_containers( + self.deploy_model_name) + self.assertEqual(num_containers_removed, 2) + running_containers_output = self.clipper_inst._execute_standard( + "docker ps -q --filter \"ancestor=clipper/noop-container\"") + self.assertIsNotNone(running_containers_output) + self.assertEqual(len(running_containers_output), 3) + def test_predict_function_deploys_successfully(self): model_name = "m2" model_version = 1 predict_func = lambda inputs: ["0" for x in inputs] - labels = ["test"] input_type = "doubles" result = self.clipper_inst.deploy_predict_function( - model_name, model_version, predict_func, labels, input_type) + model_name, model_version, predict_func, input_type) self.assertTrue(result) model_info = self.clipper_inst.get_model_info(model_name, model_version) @@ -200,17 +238,16 @@ def test_deployed_model_queried_successfully(self): # that will be deployed to a no-op container model_data = svm.SVC() container_name = "clipper/noop-container" - labels = ["test"] result = self.clipper_inst.deploy_model( self.model_name_2, model_version, model_data, container_name, - labels, self.input_type) + self.input_type) self.assertTrue(result) time.sleep(30) url = "http://localhost:1337/{}/predict".format(self.app_name_2) test_input = [99.3, 18.9, 67.2, 34.2] - req_json = json.dumps({'uid': 0, 'input': test_input}) + req_json = json.dumps({'input': test_input}) headers = {'Content-type': 'application/json'} response = requests.post(url, headers=headers, data=req_json) parsed_response = json.loads(response.text) @@ -220,10 +257,9 @@ def test_deployed_model_queried_successfully(self): def test_deployed_predict_function_queried_successfully(self): model_version = 1 predict_func = lambda inputs: [str(len(x)) for x in inputs] - labels = ["test"] input_type = "doubles" result = self.clipper_inst.deploy_predict_function( - self.model_name_1, model_version, predict_func, labels, input_type) + self.model_name_1, model_version, predict_func, input_type) self.assertTrue(result) time.sleep(60) @@ -231,7 +267,7 @@ def test_deployed_predict_function_queried_successfully(self): received_non_default_prediction = False url = "http://localhost:1337/{}/predict".format(self.app_name_1) test_input = [101.1, 99.5, 107.2] - req_json = json.dumps({'uid': 0, 'input': test_input}) + req_json = json.dumps({'input': test_input}) headers = {'Content-type': 'application/json'} for i in range(0, 40): response = requests.post(url, headers=headers, data=req_json) @@ -253,17 +289,17 @@ def test_deployed_predict_function_queried_successfully(self): 'get_app_info_for_registered_app_returns_info_dictionary', 'get_app_info_for_nonexistent_app_returns_none', 'test_add_container_for_external_model_fails', - 'test_model_version_sets_correctly', - 'test_get_logs_creates_log_files', + 'test_model_version_sets_correctly', 'test_get_logs_creates_log_files', 'test_inspect_instance_returns_json_dict', 'test_model_deploys_successfully', 'test_add_container_for_deployed_model_succeeds', - # 'test_predict_function_deploys_successfully' + 'test_remove_inactive_containers_succeeds', + 'test_predict_function_deploys_successfully' ] LONG_TEST_ORDERING = [ 'test_deployed_model_queried_successfully', - # 'test_deployed_predict_function_queried_successfully' + 'test_deployed_predict_function_queried_successfully' ] if __name__ == '__main__': diff --git a/integration-tests/light_load_all_functionality.py b/integration-tests/light_load_all_functionality.py index f06e353d7..363918888 100644 --- a/integration-tests/light_load_all_functionality.py +++ b/integration-tests/light_load_all_functionality.py @@ -82,7 +82,6 @@ def deploy_model(clipper, name, version): "http://localhost:1337/%s/predict" % app_name, headers=headers, data=json.dumps({ - 'uid': 0, 'input': list(np.random.random(30)) })) result = response.json() @@ -106,7 +105,6 @@ def create_and_test_app(clipper, name, num_models): "http://localhost:1337/%s/predict" % app_name, headers=headers, data=json.dumps({ - 'uid': 0, 'input': list(np.random.random(30)) })) result = response.json() From 614b51a5face1e6aaf2f69ea1f77bcf1fefe1457 Mon Sep 17 00:00:00 2001 From: Dan Crankshaw Date: Sat, 3 Jun 2017 12:45:45 -0700 Subject: [PATCH 12/21] tried to fix more tests --- clipper_admin/tests/clipper_manager_test.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/clipper_admin/tests/clipper_manager_test.py b/clipper_admin/tests/clipper_manager_test.py index 8bd2828da..be0228e6d 100644 --- a/clipper_admin/tests/clipper_manager_test.py +++ b/clipper_admin/tests/clipper_manager_test.py @@ -56,10 +56,9 @@ def tearDownClass(self): def test_external_models_register_correctly(self): name = "m1" version1 = 1 - tags = ["test"] input_type = "doubles" result = self.clipper_inst.register_external_model( - self.model_name, self.model_version_1, tags, input_type) + self.model_name, self.model_version_1, input_type) self.assertTrue(result) registered_model_info = self.clipper_inst.get_model_info( self.model_name, self.model_version_1) @@ -67,7 +66,7 @@ def test_external_models_register_correctly(self): version2 = 2 result = self.clipper_inst.register_external_model( - self.model_name, self.model_version_2, tags, input_type) + self.model_name, self.model_version_2, input_type) self.assertTrue(result) registered_model_info = self.clipper_inst.get_model_info( self.model_name, self.model_version_2) From 38f4fe4fae747794b4741072e66077f50793db78 Mon Sep 17 00:00:00 2001 From: Dan Crankshaw Date: Sat, 3 Jun 2017 14:08:33 -0700 Subject: [PATCH 13/21] I think I fixed unit tests --- bin/run_unittests.sh | 13 +-------- clipper_admin/clipper_manager.py | 9 +++--- clipper_admin/tests/__init__.py | 0 .../clipper_manager_tests.py | 29 +++++++++++++------ .../light_load_all_functionality.py | 4 +-- 5 files changed, 27 insertions(+), 28 deletions(-) delete mode 100644 clipper_admin/tests/__init__.py rename clipper_admin/tests/clipper_manager_test.py => integration-tests/clipper_manager_tests.py (93%) diff --git a/bin/run_unittests.sh b/bin/run_unittests.sh index b019657b1..f8f1f36e7 100755 --- a/bin/run_unittests.sh +++ b/bin/run_unittests.sh @@ -109,12 +109,6 @@ function run_management_tests { ./src/management/managementtests --redis_port $REDIS_PORT } -function run_clipper_admin_tests { - echo -e "Running clipper admin tests" - cd $DIR - python ../clipper_admin/tests/clipper_manager_test.py -} - function run_frontend_tests { echo -e "\nRunning frontend tests\n\n" ./src/frontends/frontendtests --redis_port $REDIS_PORT @@ -123,6 +117,7 @@ function run_frontend_tests { function run_integration_tests { echo -e "\nRunning integration tests\n\n" cd $DIR + python ../integration-tests/clipper_manager_tests.py python ../integration-tests/light_load_all_functionality.py 2 3 } @@ -133,9 +128,6 @@ function run_all_tests { redis-cli -p $REDIS_PORT "flushall" run_management_tests redis-cli -p $REDIS_PORT "flushall" - sleep 5 - run_clipper_admin_tests - redis-cli -p $REDIS_PORT "flushall" run_jvm_container_tests redis-cli -p $REDIS_PORT "flushall" run_rpc_container_tests @@ -160,9 +152,6 @@ case $args in -m | --management ) set_test_environment run_management_tests ;; - -c | --clipperadmin ) set_test_environment - run_clipper_admin_tests - ;; -f | --frontend ) set_test_environment run_frontend_tests ;; diff --git a/clipper_admin/clipper_manager.py b/clipper_admin/clipper_manager.py index da1aa3aec..1d7436cae 100644 --- a/clipper_admin/clipper_manager.py +++ b/clipper_admin/clipper_manager.py @@ -129,7 +129,7 @@ def __init__(self, '--redis_port=%d' % self.redis_port ], 'image': - 'clipper/management_frontend:0.1', + 'clipper/management_frontend:latest', 'ports': [ '%d:%d' % (CLIPPER_MANAGEMENT_PORT, CLIPPER_MANAGEMENT_PORT) @@ -145,7 +145,7 @@ def __init__(self, ], 'depends_on': ['mgmt_frontend'], 'image': - 'clipper/query_frontend:0.1', + 'clipper/query_frontend:latest', 'ports': [ '%d:%d' % (CLIPPER_RPC_PORT, CLIPPER_RPC_PORT), '%d:%d' % (CLIPPER_QUERY_PORT, CLIPPER_QUERY_PORT) @@ -624,7 +624,6 @@ def centered_predict(inputs): "example_model", 1, centered_predict, - ["example"], "doubles", num_containers=1) """ @@ -680,8 +679,8 @@ def centered_predict(inputs): # Deploy function deploy_result = self.deploy_model(name, version, serialization_dir, - default_python_container, labels, - input_type, num_containers) + default_python_container, + input_type, labels, num_containers) # Remove temp files shutil.rmtree(serialization_dir) diff --git a/clipper_admin/tests/__init__.py b/clipper_admin/tests/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/clipper_admin/tests/clipper_manager_test.py b/integration-tests/clipper_manager_tests.py similarity index 93% rename from clipper_admin/tests/clipper_manager_test.py rename to integration-tests/clipper_manager_tests.py index be0228e6d..b6aed2352 100644 --- a/clipper_admin/tests/clipper_manager_test.py +++ b/integration-tests/clipper_manager_tests.py @@ -8,7 +8,7 @@ from argparse import ArgumentParser cur_dir = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, os.path.abspath('%s/../' % cur_dir)) -import clipper_manager +from clipper_admin import clipper_manager import random import socket """ @@ -41,6 +41,7 @@ class ClipperManagerTestCaseShort(unittest.TestCase): def setUpClass(self): self.clipper_inst = clipper_manager.Clipper( "localhost", redis_port=find_unbound_port()) + self.clipper_inst.stop_all() self.clipper_inst.start() self.app_name = "app1" self.model_name = "m1" @@ -153,11 +154,14 @@ def test_add_container_for_deployed_model_succeeds(self): def test_remove_inactive_containers_succeeds(self): # Initialize a support vector classifier # that will be deployed to a no-op container + self.clipper_inst.stop_all() + self.clipper_inst.start() model_data = svm.SVC() container_name = "clipper/noop-container" input_type = "doubles" + model_name = "remove_inactive_test_model" result = self.clipper_inst.deploy_model( - self.deploy_model_name, + model_name, 1, model_data, container_name, @@ -167,10 +171,12 @@ def test_remove_inactive_containers_succeeds(self): running_containers_output = self.clipper_inst._execute_standard( "docker ps -q --filter \"ancestor=clipper/noop-container\"") self.assertIsNotNone(running_containers_output) - self.assertEqual(len(running_containers_output), 2) + num_running_containers = running_containers_output.split("\n") + print("RUNNING CONTAINERS: %s" % str(num_running_containers)) + self.assertEqual(len(num_running_containers), 2) result = self.clipper_inst.deploy_model( - self.deploy_model_name, + model_name, 2, model_data, container_name, @@ -180,15 +186,17 @@ def test_remove_inactive_containers_succeeds(self): running_containers_output = self.clipper_inst._execute_standard( "docker ps -q --filter \"ancestor=clipper/noop-container\"") self.assertIsNotNone(running_containers_output) - self.assertEqual(len(running_containers_output), 5) + num_running_containers = running_containers_output.split("\n") + self.assertEqual(len(num_running_containers), 5) num_containers_removed = self.clipper_inst.remove_inactive_containers( - self.deploy_model_name) + model_name) self.assertEqual(num_containers_removed, 2) running_containers_output = self.clipper_inst._execute_standard( "docker ps -q --filter \"ancestor=clipper/noop-container\"") self.assertIsNotNone(running_containers_output) - self.assertEqual(len(running_containers_output), 3) + num_running_containers = running_containers_output.split("\n") + self.assertEqual(len(num_running_containers), 3) def test_predict_function_deploys_successfully(self): model_name = "m2" @@ -212,6 +220,7 @@ class ClipperManagerTestCaseLong(unittest.TestCase): def setUpClass(self): self.clipper_inst = clipper_manager.Clipper( "localhost", redis_port=find_unbound_port()) + self.clipper_inst.stop_all() self.clipper_inst.start() self.app_name_1 = "app3" self.app_name_2 = "app4" @@ -249,7 +258,8 @@ def test_deployed_model_queried_successfully(self): req_json = json.dumps({'input': test_input}) headers = {'Content-type': 'application/json'} response = requests.post(url, headers=headers, data=req_json) - parsed_response = json.loads(response.text) + parsed_response = response.json() + print(parsed_response) self.assertNotEqual(parsed_response["output"], self.default_output) self.assertFalse(parsed_response["default"]) @@ -270,7 +280,8 @@ def test_deployed_predict_function_queried_successfully(self): headers = {'Content-type': 'application/json'} for i in range(0, 40): response = requests.post(url, headers=headers, data=req_json) - parsed_response = json.loads(response.text) + parsed_response = response.json() + print(parsed_response) output = parsed_response["output"] if output == self.default_output: time.sleep(20) diff --git a/integration-tests/light_load_all_functionality.py b/integration-tests/light_load_all_functionality.py index 363918888..079dea828 100644 --- a/integration-tests/light_load_all_functionality.py +++ b/integration-tests/light_load_all_functionality.py @@ -5,8 +5,8 @@ import json import numpy as np cur_dir = os.path.dirname(os.path.abspath(__file__)) -sys.path.insert(0, os.path.abspath('%s/../clipper_admin/' % cur_dir)) -import clipper_manager as cm +sys.path.insert(0, os.path.abspath('%s/../' % cur_dir)) +from clipper_admin import clipper_manager as cm import time import subprocess32 as subprocess import pprint From f561c5c954efef24f7470f5cbb1e26ef025a20da Mon Sep 17 00:00:00 2001 From: Dan Crankshaw Date: Sat, 3 Jun 2017 14:12:29 -0700 Subject: [PATCH 14/21] format code --- clipper_admin/clipper_manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/clipper_admin/clipper_manager.py b/clipper_admin/clipper_manager.py index 1d7436cae..5606d2594 100644 --- a/clipper_admin/clipper_manager.py +++ b/clipper_admin/clipper_manager.py @@ -679,8 +679,8 @@ def centered_predict(inputs): # Deploy function deploy_result = self.deploy_model(name, version, serialization_dir, - default_python_container, - input_type, labels, num_containers) + default_python_container, input_type, + labels, num_containers) # Remove temp files shutil.rmtree(serialization_dir) From 872e0af51feb70614dd3b693d7adb162dd6b0cfa Mon Sep 17 00:00:00 2001 From: Dan Crankshaw Date: Sun, 4 Jun 2017 16:44:03 -0700 Subject: [PATCH 15/21] addressed review comments --- bin/run_unittests.sh | 2 +- clipper_admin/clipper_manager.py | 8 +++----- integration-tests/clipper_manager_tests.py | 8 ++++---- ...load_all_functionality.py => many_apps_many_models.py} | 6 +++--- 4 files changed, 11 insertions(+), 13 deletions(-) rename integration-tests/{light_load_all_functionality.py => many_apps_many_models.py} (96%) diff --git a/bin/run_unittests.sh b/bin/run_unittests.sh index f8f1f36e7..c638261bf 100755 --- a/bin/run_unittests.sh +++ b/bin/run_unittests.sh @@ -118,7 +118,7 @@ function run_integration_tests { echo -e "\nRunning integration tests\n\n" cd $DIR python ../integration-tests/clipper_manager_tests.py - python ../integration-tests/light_load_all_functionality.py 2 3 + python ../integration-tests/many_apps_many_models.py 2 3 } function run_all_tests { diff --git a/clipper_admin/clipper_manager.py b/clipper_admin/clipper_manager.py index 5606d2594..169332c0e 100644 --- a/clipper_admin/clipper_manager.py +++ b/clipper_admin/clipper_manager.py @@ -43,8 +43,6 @@ CLIPPER_DOCKER_LABEL = "ai.clipper.container.label" CLIPPER_MODEL_CONTAINER_LABEL = "ai.clipper.model_container.model_version" -DEFAULT_LABEL = "default" - aws_cli_config = """ [default] region = us-east-1 @@ -448,7 +446,7 @@ def deploy_model(self, model_data, container_name, input_type, - labels=[DEFAULT_LABEL], + labels=[], num_containers=1): """Registers a model with Clipper and deploys instances of it in containers. @@ -555,7 +553,7 @@ def register_external_model(self, name, version, input_type, - labels=[DEFAULT_LABEL]): + labels=[]): """Registers a model with Clipper without deploying it in any containers. Parameters @@ -578,7 +576,7 @@ def deploy_predict_function(self, version, predict_function, input_type, - labels=[DEFAULT_LABEL], + labels=[], num_containers=1): """Deploy an arbitrary Python function to Clipper. diff --git a/integration-tests/clipper_manager_tests.py b/integration-tests/clipper_manager_tests.py index b6aed2352..12fc73729 100644 --- a/integration-tests/clipper_manager_tests.py +++ b/integration-tests/clipper_manager_tests.py @@ -8,12 +8,12 @@ from argparse import ArgumentParser cur_dir = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, os.path.abspath('%s/../' % cur_dir)) -from clipper_admin import clipper_manager +from clipper_admin.clipper_manager import Clipper import random import socket """ Executes a test suite consisting of two separate cases: short tests and long tests. -Before each case, an instance of clipper_manager.Clipper is created. Tests +Before each case, an instance of Clipper is created. Tests are then performed by invoking methods on this instance, often resulting in the execution of docker commands. """ @@ -39,7 +39,7 @@ def find_unbound_port(): class ClipperManagerTestCaseShort(unittest.TestCase): @classmethod def setUpClass(self): - self.clipper_inst = clipper_manager.Clipper( + self.clipper_inst = Clipper( "localhost", redis_port=find_unbound_port()) self.clipper_inst.stop_all() self.clipper_inst.start() @@ -218,7 +218,7 @@ def test_predict_function_deploys_successfully(self): class ClipperManagerTestCaseLong(unittest.TestCase): @classmethod def setUpClass(self): - self.clipper_inst = clipper_manager.Clipper( + self.clipper_inst = Clipper( "localhost", redis_port=find_unbound_port()) self.clipper_inst.stop_all() self.clipper_inst.start() diff --git a/integration-tests/light_load_all_functionality.py b/integration-tests/many_apps_many_models.py similarity index 96% rename from integration-tests/light_load_all_functionality.py rename to integration-tests/many_apps_many_models.py index 079dea828..6a1d9fed3 100644 --- a/integration-tests/light_load_all_functionality.py +++ b/integration-tests/many_apps_many_models.py @@ -6,7 +6,7 @@ import numpy as np cur_dir = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, os.path.abspath('%s/../' % cur_dir)) -from clipper_admin import clipper_manager as cm +from clipper_admin.clipper_manager import Clipper import time import subprocess32 as subprocess import pprint @@ -48,7 +48,7 @@ def find_unbound_port(): def init_clipper(): - clipper = cm.Clipper("localhost", redis_port=find_unbound_port()) + clipper = Clipper("localhost", redis_port=find_unbound_port()) clipper.start() time.sleep(1) return clipper @@ -146,6 +146,6 @@ def create_and_test_app(clipper, name, num_models): else: clipper.stop_all() except: - clipper = cm.Clipper("localhost") + clipper = Clipper("localhost") clipper.stop_all() sys.exit(1) From f52dbe469ba6b64180df4ec2ad632dc5775a2ee0 Mon Sep 17 00:00:00 2001 From: Dan Crankshaw Date: Sun, 4 Jun 2017 16:44:26 -0700 Subject: [PATCH 16/21] format code --- clipper_admin/clipper_manager.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/clipper_admin/clipper_manager.py b/clipper_admin/clipper_manager.py index 169332c0e..230cf4dc5 100644 --- a/clipper_admin/clipper_manager.py +++ b/clipper_admin/clipper_manager.py @@ -549,11 +549,7 @@ def deploy_model(self, for r in range(num_containers) ]) - def register_external_model(self, - name, - version, - input_type, - labels=[]): + def register_external_model(self, name, version, input_type, labels=[]): """Registers a model with Clipper without deploying it in any containers. Parameters From 8d624db600c2c0cd6c514c5e104a16bbee3fdd8f Mon Sep 17 00:00:00 2001 From: Dan Crankshaw Date: Sun, 4 Jun 2017 17:24:01 -0700 Subject: [PATCH 17/21] fixed check for missing container info --- clipper_admin/clipper_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clipper_admin/clipper_manager.py b/clipper_admin/clipper_manager.py index 230cf4dc5..077781c40 100644 --- a/clipper_admin/clipper_manager.py +++ b/clipper_admin/clipper_manager.py @@ -942,7 +942,7 @@ def add_container(self, model_name, model_version): db=REDIS_MODEL_DB_NUM), capture=True) - if "nil" in result.stdout: + if "empty list or set" in result.stdout: # Model not found warn("Trying to add container but model {mn}:{mv} not in " "Redis".format(mn=model_name, mv=model_version)) From f60fcff1d1ee4abb37409bb42d08d345824f8a6b Mon Sep 17 00:00:00 2001 From: Dan Crankshaw Date: Sun, 4 Jun 2017 17:50:00 -0700 Subject: [PATCH 18/21] debugging --- bin/run_unittests.sh | 2 +- clipper_admin/clipper_manager.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/bin/run_unittests.sh b/bin/run_unittests.sh index c638261bf..7fade2a85 100755 --- a/bin/run_unittests.sh +++ b/bin/run_unittests.sh @@ -122,6 +122,7 @@ function run_integration_tests { } function run_all_tests { + run_integration_tests run_libclipper_tests redis-cli -p $REDIS_PORT "flushall" run_frontend_tests @@ -132,7 +133,6 @@ function run_all_tests { redis-cli -p $REDIS_PORT "flushall" run_rpc_container_tests redis-cli -p $REDIS_PORT "flushall" - run_integration_tests } if [ "$#" == 0 ] diff --git a/clipper_admin/clipper_manager.py b/clipper_admin/clipper_manager.py index 077781c40..b19d1b046 100644 --- a/clipper_admin/clipper_manager.py +++ b/clipper_admin/clipper_manager.py @@ -941,6 +941,7 @@ def add_container(self, model_name, model_version): key=model_key, db=REDIS_MODEL_DB_NUM), capture=True) + print(result) if "empty list or set" in result.stdout: # Model not found From acf9c1160ccb2eadcc3367d1cc55f27ca108d0e2 Mon Sep 17 00:00:00 2001 From: Dan Crankshaw Date: Sun, 4 Jun 2017 18:09:55 -0700 Subject: [PATCH 19/21] fixed unittest script --- bin/run_unittests.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bin/run_unittests.sh b/bin/run_unittests.sh index 7fade2a85..215cfa4e6 100755 --- a/bin/run_unittests.sh +++ b/bin/run_unittests.sh @@ -100,16 +100,19 @@ function run_rpc_container_tests { } function run_libclipper_tests { + cd $DIR/../debug echo -e "\nRunning libclipper tests\n\n" ./src/libclipper/libclippertests --redis_port $REDIS_PORT } function run_management_tests { + cd $DIR/../debug echo -e "\nRunning management tests\n\n" ./src/management/managementtests --redis_port $REDIS_PORT } function run_frontend_tests { + cd $DIR/../debug echo -e "\nRunning frontend tests\n\n" ./src/frontends/frontendtests --redis_port $REDIS_PORT } @@ -122,7 +125,6 @@ function run_integration_tests { } function run_all_tests { - run_integration_tests run_libclipper_tests redis-cli -p $REDIS_PORT "flushall" run_frontend_tests @@ -133,6 +135,7 @@ function run_all_tests { redis-cli -p $REDIS_PORT "flushall" run_rpc_container_tests redis-cli -p $REDIS_PORT "flushall" + run_integration_tests } if [ "$#" == 0 ] From ab0d0e2c172e394a0eadee7864b1553d96b56a01 Mon Sep 17 00:00:00 2001 From: Dan Crankshaw Date: Sun, 4 Jun 2017 18:48:16 -0700 Subject: [PATCH 20/21] added default label --- clipper_admin/clipper_manager.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/clipper_admin/clipper_manager.py b/clipper_admin/clipper_manager.py index b19d1b046..b9c792a29 100644 --- a/clipper_admin/clipper_manager.py +++ b/clipper_admin/clipper_manager.py @@ -43,6 +43,8 @@ CLIPPER_DOCKER_LABEL = "ai.clipper.container.label" CLIPPER_MODEL_CONTAINER_LABEL = "ai.clipper.model_container.model_version" +DEFAULT_LABEL=["DEFAULT"] + aws_cli_config = """ [default] region = us-east-1 @@ -446,7 +448,7 @@ def deploy_model(self, model_data, container_name, input_type, - labels=[], + labels=DEFAULT_LABEL, num_containers=1): """Registers a model with Clipper and deploys instances of it in containers. @@ -549,7 +551,7 @@ def deploy_model(self, for r in range(num_containers) ]) - def register_external_model(self, name, version, input_type, labels=[]): + def register_external_model(self, name, version, input_type, labels=DEFAULT_LABEL): """Registers a model with Clipper without deploying it in any containers. Parameters @@ -572,7 +574,7 @@ def deploy_predict_function(self, version, predict_function, input_type, - labels=[], + labels=DEFAULT_LABEL, num_containers=1): """Deploy an arbitrary Python function to Clipper. From 0bcd4ca5af0b7ac490408b56d3c0f4f00eeafd3b Mon Sep 17 00:00:00 2001 From: Dan Crankshaw Date: Sun, 4 Jun 2017 18:51:00 -0700 Subject: [PATCH 21/21] format code --- clipper_admin/clipper_manager.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/clipper_admin/clipper_manager.py b/clipper_admin/clipper_manager.py index b9c792a29..ff9581c87 100644 --- a/clipper_admin/clipper_manager.py +++ b/clipper_admin/clipper_manager.py @@ -43,7 +43,7 @@ CLIPPER_DOCKER_LABEL = "ai.clipper.container.label" CLIPPER_MODEL_CONTAINER_LABEL = "ai.clipper.model_container.model_version" -DEFAULT_LABEL=["DEFAULT"] +DEFAULT_LABEL = ["DEFAULT"] aws_cli_config = """ [default] @@ -551,7 +551,11 @@ def deploy_model(self, for r in range(num_containers) ]) - def register_external_model(self, name, version, input_type, labels=DEFAULT_LABEL): + def register_external_model(self, + name, + version, + input_type, + labels=DEFAULT_LABEL): """Registers a model with Clipper without deploying it in any containers. Parameters