Make it easier to demo serving and run in Katacoda (#107)

* Make it easier to demo serving and run in Katacoda

* Allow the model path to be specified via environment variables so that
  we could potentially load the model from PVC.

* Continue to bake the model into the image so that we don't need to train
  in order to serve.

* Parameterize download_data.sh so we could potentially fetch different sources.

* Update the Makefile so that we can build and set the image for the serving
  component.

* Fix lint.

* Update the serving docs.
This commit is contained in:
Jeremy Lewi 2018-04-28 08:11:18 -07:00 committed by k8s-ci-robot
parent 26d68ead6c
commit e12231bae3
11 changed files with 225 additions and 27 deletions

3
.gitignore vendored
View File

@ -42,3 +42,6 @@ examples/.ipynb_checkpoints/
# Data files
*.h5
*.dpkl
# Build directory
github_issue_summarization/build/

View File

@ -18,6 +18,12 @@ environments:
server: https://35.188.73.10
k8sVersion: v1.7.0
path: jlewi
kubecon-gh-demo-1:
destination:
namespace: kubeflow
server: https://35.231.60.188
k8sVersion: v1.7.0
path: kubecon-gh-demo-1
kind: ksonnet.io/app
libraries:
core:

View File

@ -1,7 +1,7 @@
// Run a job to download the data to a persistent volume.
//
local env = std.extVar("__ksonnet/environments");
local params = std.extVar("__ksonnet/params").components["data-pvc"];
local overrideParams = std.extVar("__ksonnet/params").components["data-pvc"];
local k = import "k.libsonnet";
@ -20,6 +20,13 @@ local scriptConfigMap = {
},
};
local params = {
// Default location for the data. Should be a directory on the PVC.
"dataPath": "/data",
"dataUrl": "https://storage.googleapis.com/kubeflow-examples/github-issue-summarization-data/github-issues.zip",
"pvcName": "data-pvc",
} + overrideParams;
local downLoader = {
apiVersion: "batch/v1",
kind: "Job",
@ -36,6 +43,8 @@ local downLoader = {
command: [
"/bin/ash",
"/scripts/download_data.sh",
params.dataPath,
params.dataUrl,
],
image: "busybox",
name: "downloader",
@ -62,7 +71,7 @@ local downLoader = {
{
name: "data",
persistentVolumeClaim: {
claimName: "data-pvc",
claimName: params.pvcName,
},
},
],

View File

@ -1,13 +1,20 @@
#!/bin/bash
#
# Script to download the data
# Usage
# download_data.sh <URL of data> <data_dir>
# e.g
# download_data.sh https://storage.googleapis.com/kubeflow-examples/github-issue-summarization-data/github-issues.zip /data
#
# Script expects data to be a zip file
set -ex
DATA_DIR=/data
URL=$1
DATA_DIR=$2
mkdir -p ${DATA_DIR}
wget --directory-prefix=${DATA_DIR} \
https://storage.googleapis.com/kubeflow-examples/github-issue-summarization-data/github-issues.zip
unzip -d ${DATA_DIR} ${DATA_DIR}/github-issues.zip
wget --directory-prefix=${DATA_DIR} ${URL}
TARGET=$(basename ${URL})
unzip -d ${DATA_DIR} ${DATA_DIR}/${TARGET}

View File

@ -3,16 +3,96 @@ local params = std.extVar("__ksonnet/params").components["issue-summarization-mo
local k = import "k.libsonnet";
local serve = import "kubeflow/seldon/serve-simple.libsonnet";
// updatedParams uses the environment namespace if
// the namespace parameter is not explicitly set
local updatedParams = params {
namespace: if params.namespace == "null" then env.namespace else params.namespace,
};
local name = params.name;
local image = params.image;
local namespace = updatedParams.namespace;
local namespace = env.namespace;
local replicas = params.replicas;
local endpoint = params.endpoint;
k.core.v1.list.new(serve.parts(namespace).serve(name, image, replicas, endpoint))
local modelVar =
if std.objectHas(params, "modelFile") then
{
name: "MODEL_FILE",
value: params.modelFile,
}
else
{};
local titleVar =
if std.objectHas(params, "titleFile") then
{
name: "TITLE_PP_FILE",
value: params.titleFile,
}
else
{};
local bodyVar =
if std.objectHas(params, "bodyFile") then
{
name: "BODY_PP_FILE",
value: params.bodyFile,
}
else
{};
local containerEnv = [modelVar, titleVar, bodyVar];
# We don't use our ksonnet component because we want to override environment
# variables and its just cleaner to copy paste the component.
# TODO(https://github.com/kubeflow/kubeflow/issues/403) We should rewrite
# our components to better support this.
local serve = {
apiVersion: "machinelearning.seldon.io/v1alpha1",
kind: "SeldonDeployment",
metadata: {
labels: {
app: "seldon",
},
name: params.name,
namespace: env.namespace,
},
spec: {
annotations: {
deployment_version: "v1",
project_name: params.name,
},
name: name,
predictors: [
{
annotations: {
predictor_version: "v1",
},
componentSpec: {
spec: {
containers: [
{
image: params.image,
imagePullPolicy: "Always",
name: params.name,
env: std.prune(containerEnv),
},
],
terminationGracePeriodSeconds: 1,
},
},
graph: {
children: [
],
endpoint: {
type: endpoint,
},
name: name,
type: "MODEL",
},
name: name,
replicas: replicas,
},
],
},
};
k.core.v1.list.new(serve)

View File

@ -39,7 +39,7 @@
},
"issue-summarization-model-serving": {
endpoint: "REST",
image: "null",
image: "gcr.io/kubeflow-examples/issue-summarization-model:v20180427-e2aa113",
name: "issue-summarization",
namespace: "null",
replicas: 2,

View File

@ -0,0 +1,7 @@
local base = import "base.libsonnet";
local k = import "k.libsonnet";
base + {
// Insert user-specified overrides here. For example if a component is named "nginx-deployment", you might have something like:
// "nginx-deployment"+: k.deployment.mixin.metadata.labels({foo: "bar"})
}

View File

@ -0,0 +1,10 @@
local params = import "../../components/params.libsonnet";
params + {
components +: {
// Insert component parameter overrides here. Ex:
// guestbook +: {
// name: "guestbook-dev",
// replicas: params.global.replicas,
// },
},
}

View File

@ -5,6 +5,8 @@ Uses trained model files to generate a prediction.
from __future__ import print_function
import os
import numpy as np
import dill as dpickle
from keras.models import load_model
@ -13,13 +15,21 @@ from seq2seq_utils import Seq2Seq_Inference
class IssueSummarization(object):
def __init__(self):
with open('body_pp.dpkl', 'rb') as body_file:
body_pp_file = os.getenv('BODY_PP_FILE', 'body_pp.dpkl')
print('body_pp file {0}'.format(body_pp_file))
with open(body_pp_file, 'rb') as body_file:
body_pp = dpickle.load(body_file)
with open('title_pp.dpkl', 'rb') as title_file:
title_pp_file = os.getenv('TITLE_PP_FILE', 'title_pp.dpkl')
print('title_pp file {0}'.format(title_pp_file))
with open(title_pp_file, 'rb') as title_file:
title_pp = dpickle.load(title_file)
model_file = os.getenv('MODEL_FILE', 'seq2seq_model_tutorial.h5')
print('model file {0}'.format(model_file))
self.model = Seq2Seq_Inference(encoder_preprocessor=body_pp,
decoder_preprocessor=title_pp,
seq2seq_model=load_model('seq2seq_model_tutorial.h5'))
seq2seq_model=load_model(model_file))
def predict(self, input_text, feature_names): # pylint: disable=unused-argument
return np.asarray([[self.model.generate_issue_title(body[0])[1]] for body in input_text])

View File

@ -31,17 +31,28 @@ else
TAG := $(shell date +v%Y%m%d)-$(shell git describe --tags --always --dirty)-$(shell git diff | shasum -a256 | cut -c -6)
endif
DIR := ${CURDIR}
DIR := $(shell pwd)
# Use a subdirectory of the root directory
# this way it will be excluded by git diff-files
BUILD_DIR := $(shell cd ../build/notebook_build && pwd)
MODEL_GCS := gs://kubeflow-examples-data/gh_issue_summarization/model/v20180426
# You can override this on the command line as
# make PROJECT=kubeflow-examples <target>
PROJECT := kubeflow-examples
IMG := gcr.io/$(PROJECT)/tf-job-issue-summarization
# gcr.io is prepended automatically by Seldon's builder.
MODEL_IMG_NAME := $(PROJECT)/issue-summarization-model
MODEL_IMG := gcr.io/$(MODEL_IMG_NAME)
echo:
@echo changed files $(CHANGED_FILES)
@echo tag $(TAG)
@echo BUILD_DIR=$(BUILD_DIR)
push: build
gcloud docker -- push $(IMG):$(TAG)
@ -53,4 +64,44 @@ set-image: push
# export DOCKER_BUILD_OPTS=--no-cache
build:
docker build ${DOCKER_BUILD_OPTS} -f Dockerfile -t $(IMG):$(TAG) ./
@echo Built $(IMG):$(TAG)
@echo Built $(IMG):$(TAG)
$(BUILD_DIR)/body_pp.dpkl:
mkdir -p model
gsutil cp $(MODEL_GCS)/body_pp.dpkl $(BUILD_DIR)/
$(BUILD_DIR)/title_pp.dpkl:
mkdir -p model
gsutil cp $(MODEL_GCS)/title_pp.dpkl $(BUILD_DIR)/
$(BUILD_DIR)/seq2seq_model_tutorial.h5:
mkdir -p model
gsutil cp $(MODEL_GCS)/seq2seq_model_tutorial.h5 $(BUILD_DIR)/
# Copy python files into the model directory
# so that we can mount a single directory into the container
$(BUILD_DIR)/% : %
cp -f $< $@
download-model: $(BUILD_DIR)/seq2seq_model_tutorial.h5 $(BUILD_DIR)/title_pp.dpkl $(BUILD_DIR)/body_pp.dpkl
# TODO(jlewi): This doesn't actually pick up changes to code. The problem is that docker run is not detecting when code changes
# and when it does copying it to $(BUILD_DIR)/build. Using --force fixes that problem but then it looks like docker build
# no longer uses the cache and rebuilds are slow. Manually copying the files doesn't work because the files end up
# owned by root because they were created inside the container.
# The work around now is to manually copy files (e.g. in a shell outside Make) as needed
build-model-image: download-model $(BUILD_DIR)/seq2seq_utils.py $(BUILD_DIR)/IssueSummarization.py $(BUILD_DIR)/requirements.txt
# The docker run comand creates a Dockerfile for the seldon image with required assets
docker run -v $(BUILD_DIR):/my_model seldonio/core-python-wrapper:0.7 /my_model IssueSummarization $(TAG) gcr.io --base-image=python:3.6 --image-name=$(MODEL_IMG_NAME)
# We don't use the script generated by Seldon because that script won't get updated by make to reflect the change
# in the desired tag.
docker build -t $(MODEL_IMG):$(TAG) -f $(BUILD_DIR)/build/Dockerfile $(BUILD_DIR)/build
@echo built $(MODEL_IMG):$(TAG)
push-model-image: build-model-image
echo pushing $(MODEL_IMG):$(TAG)
gcloud docker -- push $(MODEL_IMG):$(TAG)
set-model-image: push-model-image
# Set the image to use
cd ../ks-kubeflow && ks param set issue-summarization-model-serving image $(MODEL_IMG):$(TAG)

View File

@ -4,15 +4,20 @@ We are going to use [seldon-core](https://github.com/SeldonIO/seldon-core) to se
> The model is written in Keras and when exported as a TensorFlow model seems to be incompatible with TensorFlow Serving. So we're using seldon-core to serve this model since seldon-core allows you to serve any arbitrary model. More details [here](https://github.com/kubeflow/examples/issues/11#issuecomment-371005885).
# Prerequisites
# Building a model server
Ensure that you have the following files from the [training](training_the_model.md) step in your `notebooks` directory:
You have two options for getting a model server
* `seq2seq_model_tutorial.h5` - the keras model
* `body_pp.dpkl` - the serialized body preprocessor
* `title_pp.dpkl` - the serialized title preprocessor
1. You can use the public model server image `gcr.io/kubeflow-examples/issue-summarization-model`
# Wrap the model into a seldon-core microservice
* This server has a copy of the model and supporting assets baked into the container image
* So you can just run this image to get a pre-trained model
* Serving your own model using this server is discussed below
1. You can build your own model server as discussed below
## Wrap the model into a seldon-core microservice
cd into the notebooks directory and run the following docker command. This will create a build/ directory.
@ -36,6 +41,16 @@ You can push the image by running `gcloud docker -- push gcr.io/gcr-repository-n
> You can find more details about wrapping a model with seldon-core [here](https://github.com/SeldonIO/seldon-core/blob/master/docs/wrappers/python.md)
### Storing a model in the Docker image
If you want to store a copy of the model in the Docker image make sure the following files are available in the directory in which you run
the commands in the previous steps. These files are produced by the [training](training_the_model.md) step in your `notebooks` directory:
* `seq2seq_model_tutorial.h5` - the keras model
* `body_pp.dpkl` - the serialized body preprocessor
* `title_pp.dpkl` - the serialized title preprocessor
# Deploying the model to your kubernetes cluster
Now that we have an image with our model server, we can deploy it to our kubernetes cluster. We need to first deploy seldon-core to our cluster.