Skip to content

Commit

Permalink
Assorted usability fixes (#192)
Browse files Browse the repository at this point in the history
* removed uid from prediction query schema

* added method to remove inactive containers

* add Clipper class to clipper_admin module

* Make model labels optional in clipper_admin

* make inspect_selection_policy private

* added check for python3 and note about downloading docker images

* addressed review comments. Still need a test for remove_inactive_containers

* format code

* Fix unit tests

* fix integration test

* fixed some tests

* tried to fix more tests

* I think I fixed unit tests

* format code

* addressed review comments

* format code

* fixed check for missing container info

* debugging

* fixed unittest script

* added default label

* format code
  • Loading branch information
dcrankshaw authored and Corey-Zumar committed Jun 5, 2017
1 parent 818d8b4 commit fc7e2be
Show file tree
Hide file tree
Showing 14 changed files with 192 additions and 94 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")
```

Expand Down
18 changes: 5 additions & 13 deletions bin/run_unittests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -100,30 +100,28 @@ 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_clipper_admin_tests {
echo -e "Running clipper admin tests"
cd $DIR
python ../clipper_admin/tests/clipper_manager_test.py
}

function run_frontend_tests {
cd $DIR/../debug
echo -e "\nRunning frontend tests\n\n"
./src/frontends/frontendtests --redis_port $REDIS_PORT
}

function run_integration_tests {
echo -e "\nRunning integration tests\n\n"
cd $DIR
python ../integration-tests/light_load_all_functionality.py 2 3
python ../integration-tests/clipper_manager_tests.py
python ../integration-tests/many_apps_many_models.py 2 3
}

function run_all_tests {
Expand All @@ -133,9 +131,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
Expand All @@ -160,9 +155,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
;;
Expand Down
8 changes: 8 additions & 0 deletions clipper_admin/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +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"
)
sys.exit(1)

from clipper_manager import Clipper
85 changes: 70 additions & 15 deletions clipper_admin/clipper_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@
CLIPPER_LOGS_PATH = "/tmp/clipper-logs"

CLIPPER_DOCKER_LABEL = "ai.clipper.container.label"
CLIPPER_MODEL_CONTAINER_LABEL = "ai.clipper.model_container.model_version"

DEFAULT_LABEL = ["DEFAULT"]

aws_cli_config = """
[default]
Expand Down Expand Up @@ -335,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")

Expand Down Expand Up @@ -441,8 +447,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.
Expand All @@ -463,10 +469,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.
Expand Down Expand Up @@ -545,7 +551,11 @@ 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
Expand All @@ -554,10 +564,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,
Expand All @@ -567,8 +577,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.
Expand All @@ -586,16 +596,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
Expand All @@ -612,7 +624,6 @@ def centered_predict(inputs):
"example_model",
1,
centered_predict,
["example"],
"doubles",
num_containers=1)
"""
Expand Down Expand Up @@ -668,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)

Expand Down Expand Up @@ -797,7 +808,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
Expand Down Expand Up @@ -934,8 +947,9 @@ def add_container(self, model_name, model_version):
key=model_key,
db=REDIS_MODEL_DB_NUM),
capture=True)
print(result)

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))
Expand All @@ -954,7 +968,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,
Expand All @@ -963,6 +977,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
Expand Down Expand Up @@ -1058,6 +1074,45 @@ 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):
"""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))
if len(containers) > 0:
container_ids = [l.strip() for l in containers.split("\n")]
for container in container_ids:
# returns a string formatted as "<model_name>:<model_version>"
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))
return num_containers_removed

def stop_all(self):
"""Stops and removes all Clipper Docker containers on the host.
Expand Down
Empty file removed clipper_admin/tests/__init__.py
Empty file.
2 changes: 1 addition & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Clipper Manager API Reference
==============================

.. automodule:: clipper_admin.clipper_manager
.. autoclass:: clipper_admin.Clipper
:members:
:undoc-members:
:show-inheritance:
Expand Down
9 changes: 5 additions & 4 deletions examples/basic_query/README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@

# Basic Query Example Requirements

The examples in this directory depend on a few Python packages.
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.

+ [`requests`](http://docs.python-requests.org/en/master/)
+ [`numpy`](http://www.numpy.org/)

# Running the example query

1. Start Clipper locally
Expand Down
9 changes: 3 additions & 6 deletions examples/basic_query/example_client.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -24,11 +22,10 @@ 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"],
"doubles")
clipper.register_external_model("example_model", 1, "doubles")
time.sleep(1.0)
uid = 0
while True:
Expand Down
18 changes: 12 additions & 6 deletions examples/tutorial/tutorial_part_one.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -121,17 +121,24 @@
"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",
"Starting Clipper will have the following effect on your setup: <img src=\"img/start_clipper.png\" style=\"width: 350px;\"/>\n",
"\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)`."
]
},
{
Expand All @@ -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()"
]
Expand Down
Loading

0 comments on commit fc7e2be

Please sign in to comment.