-
Notifications
You must be signed in to change notification settings - Fork 280
Assorted usability fixes #192
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
970f92c
18584cc
6964b85
e2e54e5
ca5a9d5
cdbeb85
ee90331
06cd6ce
60bb9dd
5826a19
54bbb80
614b51a
38f4fe4
f561c5c
872e0af
f52dbe4
8d624db
f60fcff
acf9c11
ab0d0e2
0bcd4ca
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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] | ||
|
|
@@ -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") | ||
|
|
||
|
|
@@ -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. | ||
|
|
||
|
|
@@ -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. | ||
|
|
@@ -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 | ||
|
|
@@ -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, | ||
|
|
@@ -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. | ||
|
|
||
|
|
@@ -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 | ||
|
|
@@ -612,7 +624,6 @@ def centered_predict(inputs): | |
| "example_model", | ||
| 1, | ||
| centered_predict, | ||
| ["example"], | ||
| "doubles", | ||
| num_containers=1) | ||
| """ | ||
|
|
@@ -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) | ||
|
|
||
|
|
@@ -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 | ||
|
|
@@ -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)) | ||
|
|
@@ -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, | ||
|
|
@@ -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 | ||
|
|
@@ -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( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
|
|
||
|
|
||
| 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 | ||
|
|
@@ -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) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why don't we check for docker here?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Extra colon after
inputThere was a problem hiding this comment.
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)
There was a problem hiding this comment.
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!