Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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::
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Extra colon after input

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's an intentional pydoc markup symbol (https://docs.python.org/devguide/documenting.html#source-code)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, thanks for clarifying!


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(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the reasoning for dropping into shell commands over using libraries? e.g. https://docker-py.readthedocs.io/en/stable/containers.html#docker.models.containers.ContainerCollection.list

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simplicity and wanting to use the same commands locally and remotely. As we refactor clipper_manager.py we should definitely look into the Python SDK.

"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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why don't we check for docker here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because this example doesn't start Clipper, it assumes there is already a Clipper instance running and model connected. We don't check for Docker so that the example works even if you aren't using Docker to run Clipper.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it! The key part is the docker-independent functionality.

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