mirror of https://github.com/kubeflow/examples.git
Add the web-ui for the mnist example (#473)
* Add the web-ui for the mnist example Copy the mnist web app from https://github.com/googlecodelabs/kubeflow-introduction * Update the web app * Change "server-name" argument to "model-name" because this is what is. * Update the prediction client code; The prediction code was copied from https://github.com/googlecodelabs/kubeflow-introduction and that model used slightly different values for the input names and outputs. * Add a test for the mnist_client code; currently it needs to be run manually. * Fix the label selector for the mnist service so that it matches the TFServing deployment. * Delete the old copy of mnist_client.py; we will go with the copy in ewb-ui from https://github.com/googlecodelabs/kubeflow-introduction * Delete model-deploy.yaml, model-train.yaml, and tf-user.yaml. The K8s resources for training and deploying the model are now in ks_app. * Fix tensorboard; tensorboard only partially works behind Ambassador. It seems like some requests don't work behind a reverse proxy. * Fix lint.
This commit is contained in:
parent
b3f06c204d
commit
6770b4adcc
|
|
@ -1 +1,2 @@
|
||||||
build/**
|
build/**
|
||||||
|
web-ui/static/tmp/**
|
||||||
120
mnist/README.md
120
mnist/README.md
|
|
@ -464,14 +464,32 @@ There are various ways to monitor workflow/training job. In addition to using `k
|
||||||
|
|
||||||
TODO: This section needs to be updated
|
TODO: This section needs to be updated
|
||||||
|
|
||||||
Tensorboard is deployed just before training starts. To connect:
|
Configure TensorBoard to point to your model location
|
||||||
|
|
||||||
```
|
```
|
||||||
PODNAME=$(kubectl get pod -l app=tensorboard-${JOB_NAME} -o jsonpath='{.items[0].metadata.name}')
|
ks param set tensorboard --env=${KSENV} logDir ${LOGDIR}
|
||||||
kubectl port-forward ${PODNAME} 6006:6006
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Tensorboard can now be accessed at [http://127.0.0.1:6006](http://127.0.0.1:6006).
|
Assuming you followed the directions above if you used GCS you can use the following value
|
||||||
|
|
||||||
|
```
|
||||||
|
LOGDIR=gs://${BUCKET}/${MODEL_PATH}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then you can deploy tensorboard
|
||||||
|
|
||||||
|
```
|
||||||
|
ks apply ${KSENV} -c tensorboard
|
||||||
|
```
|
||||||
|
|
||||||
|
To access tensorboard using port-forwarding
|
||||||
|
|
||||||
|
```
|
||||||
|
kubectl -n jlewi port-forward service/tensorboard-tb 8090:80
|
||||||
|
```
|
||||||
|
Tensorboard can now be accessed at [http://127.0.0.1:8090](http://127.0.0.1:8090).
|
||||||
|
|
||||||
|
|
||||||
## Serving the model
|
## Serving the model
|
||||||
|
|
||||||
|
|
@ -534,96 +552,34 @@ TODO: Add instructions
|
||||||
|
|
||||||
TODO: Add instructions
|
TODO: Add instructions
|
||||||
|
|
||||||
### Create the K8s service
|
## Web Front End
|
||||||
|
|
||||||
Next we need to create a K8s service to route traffic to our model
|
The example comes with a simple web front end that can be used with your model.
|
||||||
|
|
||||||
|
To deploy the web front end
|
||||||
|
|
||||||
```
|
```
|
||||||
ks apply jlewi -c mnist-service
|
ks apply ${ENV} -c web-ui
|
||||||
```
|
```
|
||||||
|
|
||||||
By default the workflow deploys our model via Tensorflow Serving. Included in this example is a client that can query your model and provide results:
|
### Connecting via port forwarding
|
||||||
|
|
||||||
|
To connect to the web app via port-forwarding
|
||||||
|
|
||||||
```
|
```
|
||||||
POD_NAME=$(kubectl get pod -l=app=mnist-${JOB_NAME} -o jsonpath='{.items[0].metadata.name}')
|
kubectl -n ${NAMESPACE} port-forward svc/web-ui 8080:80
|
||||||
kubectl port-forward ${POD_NAME} 9000:9000 &
|
|
||||||
TF_MNIST_IMAGE_PATH=data/7.png python mnist_client.py
|
|
||||||
```
|
```
|
||||||
|
|
||||||
This should result in output similar to this, depending on how well your model was trained:
|
You should now be able to open up the web app at [http://localhost:8080](http://localhost:8080).
|
||||||
|
|
||||||
|
### Using IAP on GCP
|
||||||
|
|
||||||
|
If you are using GCP and have set up IAP then you can access the web UI at
|
||||||
|
|
||||||
```
|
```
|
||||||
outputs {
|
https://${DEPLOYMENT}.endpoints.${PROJECT}.cloud.goog/${NAMESPACE}/mnist/
|
||||||
key: "classes"
|
|
||||||
value {
|
|
||||||
dtype: DT_UINT8
|
|
||||||
tensor_shape {
|
|
||||||
dim {
|
|
||||||
size: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
int_val: 7
|
|
||||||
}
|
|
||||||
}
|
|
||||||
outputs {
|
|
||||||
key: "predictions"
|
|
||||||
value {
|
|
||||||
dtype: DT_FLOAT
|
|
||||||
tensor_shape {
|
|
||||||
dim {
|
|
||||||
size: 1
|
|
||||||
}
|
|
||||||
dim {
|
|
||||||
size: 10
|
|
||||||
}
|
|
||||||
}
|
|
||||||
float_val: 0.0
|
|
||||||
float_val: 0.0
|
|
||||||
float_val: 0.0
|
|
||||||
float_val: 0.0
|
|
||||||
float_val: 0.0
|
|
||||||
float_val: 0.0
|
|
||||||
float_val: 0.0
|
|
||||||
float_val: 1.0
|
|
||||||
float_val: 0.0
|
|
||||||
float_val: 0.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
............................
|
|
||||||
............................
|
|
||||||
............................
|
|
||||||
............................
|
|
||||||
............................
|
|
||||||
............................
|
|
||||||
............................
|
|
||||||
..............@@@@@@........
|
|
||||||
..........@@@@@@@@@@........
|
|
||||||
........@@@@@@@@@@@@........
|
|
||||||
........@@@@@@@@.@@@........
|
|
||||||
........@@@@....@@@@........
|
|
||||||
................@@@@........
|
|
||||||
...............@@@@.........
|
|
||||||
...............@@@@.........
|
|
||||||
...............@@@..........
|
|
||||||
..............@@@@..........
|
|
||||||
..............@@@...........
|
|
||||||
.............@@@@...........
|
|
||||||
.............@@@............
|
|
||||||
............@@@@............
|
|
||||||
............@@@.............
|
|
||||||
............@@@.............
|
|
||||||
...........@@@..............
|
|
||||||
..........@@@@..............
|
|
||||||
..........@@@@..............
|
|
||||||
..........@@................
|
|
||||||
............................
|
|
||||||
Your model says the above number is... 7!
|
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also omit `TF_MNIST_IMAGE_PATH`, and the client will pick a random number from the mnist test data. Run it repeatedly and see how your model fares!
|
|
||||||
|
|
||||||
|
|
||||||
## Conclusion and Next Steps
|
## Conclusion and Next Steps
|
||||||
|
|
||||||
This is an example of what your machine learning can look like. Feel free to play with the tunables and see if you can increase your model's accuracy (increasing `model-train-steps` can go a long way).
|
This is an example of what your machine learning can look like. Feel free to play with the tunables and see if you can increase your model's accuracy (increasing `model-train-steps` can go a long way).
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,12 @@
|
||||||
contextDir: "."
|
contextDir: "."
|
||||||
},
|
},
|
||||||
|
|
||||||
steps: modelSteps.steps + ksonnetSteps.steps,
|
local uiSteps = subGraphTemplate {
|
||||||
images: modelSteps.images + ksonnetSteps.images,
|
name: "web-ui",
|
||||||
|
dockerFile: "./web-ui/Dockerfile",
|
||||||
|
contextDir: "./web-ui"
|
||||||
|
},
|
||||||
|
|
||||||
|
steps: modelSteps.steps + ksonnetSteps.steps + uiSteps.steps,
|
||||||
|
images: modelSteps.images + ksonnetSteps.images + uiSteps.images,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -52,10 +52,23 @@
|
||||||
"mnist-service": {
|
"mnist-service": {
|
||||||
enablePrometheus: 'true',
|
enablePrometheus: 'true',
|
||||||
injectIstio: 'false',
|
injectIstio: 'false',
|
||||||
modelName: 'null',
|
modelName: 'mnist',
|
||||||
name: 'mnist-service',
|
name: 'mnist-service',
|
||||||
serviceType: 'ClusterIP',
|
serviceType: 'ClusterIP',
|
||||||
trafficRule: 'v1:100',
|
trafficRule: 'v1:100',
|
||||||
},
|
},
|
||||||
|
"tensorboard": {
|
||||||
|
image: "tensorflow/tensorflow:1.11.0",
|
||||||
|
logDir: "gs://example/to/model/logdir",
|
||||||
|
name: "tensorboard",
|
||||||
|
},
|
||||||
|
"web-ui": {
|
||||||
|
containerPort: 5000,
|
||||||
|
image: "gcr.io/kubeflow-examples/mnist/web-ui:v20190112-v0.2-142-g3b38225",
|
||||||
|
name: "web-ui",
|
||||||
|
replicas: 1,
|
||||||
|
servicePort: 80,
|
||||||
|
type: "ClusterIP",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
// TODO: Generalize to use S3. We can follow the pattern of training that
|
||||||
|
// takes parameters to specify environment variables and secret which can be customized
|
||||||
|
// for GCS, S3 as needed.
|
||||||
|
local env = std.extVar("__ksonnet/environments");
|
||||||
|
local params = std.extVar("__ksonnet/params").components.tensorboard;
|
||||||
|
|
||||||
|
local k = import "k.libsonnet";
|
||||||
|
|
||||||
|
local name = params.name;
|
||||||
|
local namespace = env.namespace;
|
||||||
|
local service = {
|
||||||
|
apiVersion: "v1",
|
||||||
|
kind: "Service",
|
||||||
|
metadata: {
|
||||||
|
name: name + "-tb",
|
||||||
|
namespace: env.namespace,
|
||||||
|
annotations: {
|
||||||
|
"getambassador.io/config":
|
||||||
|
std.join("\n", [
|
||||||
|
"---",
|
||||||
|
"apiVersion: ambassador/v0",
|
||||||
|
"kind: Mapping",
|
||||||
|
"name: " + name + "_mapping",
|
||||||
|
"prefix: /" + env.namespace + "/tensorboard/mnist",
|
||||||
|
"rewrite: /",
|
||||||
|
"service: " + name + "-tb." + namespace,
|
||||||
|
"---",
|
||||||
|
"apiVersion: ambassador/v0",
|
||||||
|
"kind: Mapping",
|
||||||
|
"name: " + name + "_mapping_data",
|
||||||
|
"prefix: /" + env.namespace + "/tensorboard/mnist/data/",
|
||||||
|
"rewrite: /data/",
|
||||||
|
"service: " + name + "-tb." + namespace,
|
||||||
|
]),
|
||||||
|
}, //annotations
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
ports: [
|
||||||
|
{
|
||||||
|
name: "http",
|
||||||
|
port: 80,
|
||||||
|
targetPort: 80,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
selector: {
|
||||||
|
app: "tensorboard",
|
||||||
|
"tb-job": name,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
local deployment = {
|
||||||
|
apiVersion: "apps/v1beta1",
|
||||||
|
kind: "Deployment",
|
||||||
|
metadata: {
|
||||||
|
name: name + "-tb",
|
||||||
|
namespace: env.namespace,
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
replicas: 1,
|
||||||
|
template: {
|
||||||
|
metadata: {
|
||||||
|
labels: {
|
||||||
|
app: "tensorboard",
|
||||||
|
"tb-job": name,
|
||||||
|
},
|
||||||
|
name: name,
|
||||||
|
namespace: namespace,
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
containers: [
|
||||||
|
{
|
||||||
|
command: [
|
||||||
|
"/usr/local/bin/tensorboard",
|
||||||
|
"--logdir=" + params.logDir,
|
||||||
|
"--port=80",
|
||||||
|
],
|
||||||
|
image: params.image,
|
||||||
|
name: "tensorboard",
|
||||||
|
ports: [
|
||||||
|
{
|
||||||
|
containerPort: 80,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
env: [
|
||||||
|
{
|
||||||
|
name: "GOOGLE_APPLICATION_CREDENTIALS",
|
||||||
|
value: "/secret/gcp-credentials/user-gcp-sa.json",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
volumeMounts: [
|
||||||
|
{
|
||||||
|
mountPath: "/secret/gcp-credentials",
|
||||||
|
name: "gcp-credentials",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
volumes: [
|
||||||
|
{
|
||||||
|
name: "gcp-credentials",
|
||||||
|
secret: {
|
||||||
|
secretName: "user-gcp-sa",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
std.prune(k.core.v1.list.new([service, deployment]))
|
||||||
|
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
local env = std.extVar("__ksonnet/environments");
|
||||||
|
local params = std.extVar("__ksonnet/params").components["web-ui"];
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"apiVersion": "v1",
|
||||||
|
"kind": "Service",
|
||||||
|
"metadata": {
|
||||||
|
"name": params.name,
|
||||||
|
"namespace": env.namespace,
|
||||||
|
annotations: {
|
||||||
|
"getambassador.io/config":
|
||||||
|
std.join("\n", [
|
||||||
|
"---",
|
||||||
|
"apiVersion: ambassador/v0",
|
||||||
|
"kind: Mapping",
|
||||||
|
"name: " + params.name + "_mapping",
|
||||||
|
"prefix: /" + env.namespace + "/mnist/",
|
||||||
|
"rewrite: /",
|
||||||
|
"service: " + params.name + "." + env.namespace,
|
||||||
|
]),
|
||||||
|
}, //annotations
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"ports": [
|
||||||
|
{
|
||||||
|
"port": params.servicePort,
|
||||||
|
"targetPort": params.containerPort
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"selector": {
|
||||||
|
"app": params.name
|
||||||
|
},
|
||||||
|
"type": params.type
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"apiVersion": "apps/v1beta2",
|
||||||
|
"kind": "Deployment",
|
||||||
|
"metadata": {
|
||||||
|
"name": params.name,
|
||||||
|
"namespace": env.namespace,
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"replicas": params.replicas,
|
||||||
|
"selector": {
|
||||||
|
"matchLabels": {
|
||||||
|
"app": params.name
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"template": {
|
||||||
|
"metadata": {
|
||||||
|
"labels": {
|
||||||
|
"app": params.name
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"containers": [
|
||||||
|
{
|
||||||
|
"image": params.image,
|
||||||
|
"name": params.name,
|
||||||
|
"ports": [
|
||||||
|
{
|
||||||
|
"containerPort": params.containerPort
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
@ -22,9 +22,10 @@ local envParams = params + {
|
||||||
namespace: 'jlewi',
|
namespace: 'jlewi',
|
||||||
},
|
},
|
||||||
"mnist-service"+: {
|
"mnist-service"+: {
|
||||||
name: 'jlewi-deploy-test',
|
|
||||||
namespace: 'jlewi',
|
namespace: 'jlewi',
|
||||||
modelBasePath: 'gs://kubeflow-ci_temp/mnist-jlewi/export',
|
},
|
||||||
|
tensorboard+: {
|
||||||
|
logDir: 'gs://kubeflow-ci_temp/mnist-jlewi/',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
#!/usr/bin/env python2.7
|
|
||||||
|
|
||||||
import os
|
|
||||||
import random
|
|
||||||
import numpy
|
|
||||||
|
|
||||||
from PIL import Image
|
|
||||||
|
|
||||||
import tensorflow as tf
|
|
||||||
from tensorflow.examples.tutorials.mnist import input_data
|
|
||||||
from tensorflow_serving.apis import predict_pb2
|
|
||||||
from tensorflow_serving.apis import prediction_service_pb2
|
|
||||||
|
|
||||||
from grpc.beta import implementations
|
|
||||||
|
|
||||||
from mnist import MNIST # pylint: disable=no-name-in-module
|
|
||||||
|
|
||||||
TF_MODEL_SERVER_HOST = os.getenv("TF_MODEL_SERVER_HOST", "127.0.0.1")
|
|
||||||
TF_MODEL_SERVER_PORT = int(os.getenv("TF_MODEL_SERVER_PORT", 9000))
|
|
||||||
TF_DATA_DIR = os.getenv("TF_DATA_DIR", "/tmp/data/")
|
|
||||||
TF_MNIST_IMAGE_PATH = os.getenv("TF_MNIST_IMAGE_PATH", None)
|
|
||||||
TF_MNIST_TEST_IMAGE_NUMBER = int(os.getenv("TF_MNIST_TEST_IMAGE_NUMBER", -1))
|
|
||||||
|
|
||||||
if TF_MNIST_IMAGE_PATH != None:
|
|
||||||
raw_image = Image.open(TF_MNIST_IMAGE_PATH)
|
|
||||||
int_image = numpy.array(raw_image)
|
|
||||||
image = numpy.reshape(int_image, 784).astype(numpy.float32)
|
|
||||||
elif TF_MNIST_TEST_IMAGE_NUMBER > -1:
|
|
||||||
test_data_set = input_data.read_data_sets(TF_DATA_DIR, one_hot=True).test
|
|
||||||
image = test_data_set.images[TF_MNIST_TEST_IMAGE_NUMBER]
|
|
||||||
else:
|
|
||||||
test_data_set = input_data.read_data_sets(TF_DATA_DIR, one_hot=True).test
|
|
||||||
image = random.choice(test_data_set.images)
|
|
||||||
|
|
||||||
channel = implementations.insecure_channel(
|
|
||||||
TF_MODEL_SERVER_HOST, TF_MODEL_SERVER_PORT)
|
|
||||||
stub = prediction_service_pb2.beta_create_PredictionService_stub(channel)
|
|
||||||
|
|
||||||
request = predict_pb2.PredictRequest()
|
|
||||||
request.model_spec.name = "mnist"
|
|
||||||
request.model_spec.signature_name = "serving_default"
|
|
||||||
request.inputs['x'].CopyFrom(
|
|
||||||
tf.contrib.util.make_tensor_proto(image, shape=[1, 28, 28]))
|
|
||||||
|
|
||||||
result = stub.Predict(request, 10.0) # 10 secs timeout
|
|
||||||
|
|
||||||
print(result)
|
|
||||||
print(MNIST.display(image, threshold=0))
|
|
||||||
print("Your model says the above number is... %d!" %
|
|
||||||
result.outputs["classes"].int_val[0])
|
|
||||||
|
|
@ -1,144 +0,0 @@
|
||||||
apiVersion: argoproj.io/v1alpha1
|
|
||||||
kind: Workflow
|
|
||||||
metadata:
|
|
||||||
generateName: tf-workflow-
|
|
||||||
spec:
|
|
||||||
entrypoint: deploy-model
|
|
||||||
# Parameters can be passed/overridden via the argo CLI.
|
|
||||||
# To override the printed message, run `argo submit` with the -p option:
|
|
||||||
# $ argo submit examples/arguments-parameters.yaml -p message="goodbye world"
|
|
||||||
arguments:
|
|
||||||
parameters:
|
|
||||||
- name: workflow
|
|
||||||
value: workflow-name
|
|
||||||
templates:
|
|
||||||
- name: deploy-model
|
|
||||||
steps:
|
|
||||||
- - name: get-workflow-info
|
|
||||||
template: get-workflow-info
|
|
||||||
- - name: serve-model
|
|
||||||
template: tf-inference
|
|
||||||
arguments:
|
|
||||||
parameters:
|
|
||||||
- name: s3-model-url
|
|
||||||
value: "{{steps.get-workflow-info.outputs.parameters.s3-model-url}}"
|
|
||||||
- name: s3-exported-url
|
|
||||||
value: "{{steps.get-workflow-info.outputs.parameters.s3-exported-url}}"
|
|
||||||
- name: aws-secret
|
|
||||||
value: "{{steps.get-workflow-info.outputs.parameters.aws-secret}}"
|
|
||||||
- name: namespace
|
|
||||||
value: "{{steps.get-workflow-info.outputs.parameters.namespace}}"
|
|
||||||
- name: aws-region
|
|
||||||
value: "{{steps.get-workflow-info.outputs.parameters.aws-region}}"
|
|
||||||
- name: s3-endpoint
|
|
||||||
value: "{{steps.get-workflow-info.outputs.parameters.s3-endpoint}}"
|
|
||||||
- name: s3-use-https
|
|
||||||
value: "{{steps.get-workflow-info.outputs.parameters.s3-use-https}}"
|
|
||||||
- name: s3-verify-ssl
|
|
||||||
value: "{{steps.get-workflow-info.outputs.parameters.s3-verify-ssl}}"
|
|
||||||
- name: job-name
|
|
||||||
value: "{{steps.get-workflow-info.outputs.parameters.job-name}}"
|
|
||||||
- name: tf-serving-image
|
|
||||||
value: "{{steps.get-workflow-info.outputs.parameters.tf-serving-image}}"
|
|
||||||
- name: model-serving-servicetype
|
|
||||||
value: "{{steps.get-workflow-info.outputs.parameters.model-serving-servicetype}}"
|
|
||||||
- name: model-serving-ks-url
|
|
||||||
value: "{{steps.get-workflow-info.outputs.parameters.model-serving-ks-url}}"
|
|
||||||
- name: model-serving-ks-tag
|
|
||||||
value: "{{steps.get-workflow-info.outputs.parameters.model-serving-ks-tag}}"
|
|
||||||
- name: model-name
|
|
||||||
value: "{{steps.get-workflow-info.outputs.parameters.model-name}}"
|
|
||||||
- name: get-workflow-info
|
|
||||||
container:
|
|
||||||
image: nervana/circleci:master
|
|
||||||
imagePullPolicy: Always
|
|
||||||
command: ["bash", "-c", "-x", 'for var in s3-model-url s3-exported-url; do kubectl get workflow {{workflow.parameters.workflow}} -o json | jq -r ".status.nodes[] | select(.name|contains(\"get-workflow-info\")) | .outputs.parameters[] | select(.name == \"${var}\") | .value" > /tmp/${var} ; done; for var in job-name namespace aws-secret aws-region s3-endpoint s3-use-https s3-verify-ssl tf-serving-image model-serving-servicetype model-serving-ks-url model-serving-ks-tag model-name; do kubectl get workflow {{workflow.parameters.workflow}} -o jsonpath="{.spec.arguments.parameters[?(.name==\"${var}\")].value}" > /tmp/${var}; done']
|
|
||||||
outputs:
|
|
||||||
parameters:
|
|
||||||
- name: s3-model-url
|
|
||||||
valueFrom:
|
|
||||||
path: /tmp/s3-model-url
|
|
||||||
- name: s3-exported-url
|
|
||||||
valueFrom:
|
|
||||||
path: /tmp/s3-exported-url
|
|
||||||
- name: aws-secret
|
|
||||||
valueFrom:
|
|
||||||
path: /tmp/aws-secret
|
|
||||||
- name: namespace
|
|
||||||
valueFrom:
|
|
||||||
path: /tmp/namespace
|
|
||||||
- name: aws-region
|
|
||||||
valueFrom:
|
|
||||||
path: /tmp/aws-region
|
|
||||||
- name: s3-endpoint
|
|
||||||
valueFrom:
|
|
||||||
path: /tmp/s3-endpoint
|
|
||||||
- name: s3-use-https
|
|
||||||
valueFrom:
|
|
||||||
path: /tmp/s3-use-https
|
|
||||||
- name: s3-verify-ssl
|
|
||||||
valueFrom:
|
|
||||||
path: /tmp/s3-verify-ssl
|
|
||||||
- name: job-name
|
|
||||||
valueFrom:
|
|
||||||
path: /tmp/job-name
|
|
||||||
- name: tf-serving-image
|
|
||||||
valueFrom:
|
|
||||||
path: /tmp/tf-serving-image
|
|
||||||
- name: model-serving-servicetype
|
|
||||||
valueFrom:
|
|
||||||
path: /tmp/model-serving-servicetype
|
|
||||||
- name: model-serving-ks-url
|
|
||||||
valueFrom:
|
|
||||||
path: /tmp/model-serving-ks-url
|
|
||||||
- name: model-serving-ks-tag
|
|
||||||
valueFrom:
|
|
||||||
path: /tmp/model-serving-ks-tag
|
|
||||||
- name: model-name
|
|
||||||
valueFrom:
|
|
||||||
path: /tmp/model-name
|
|
||||||
- name: tf-inference
|
|
||||||
inputs:
|
|
||||||
parameters:
|
|
||||||
- name: s3-model-url
|
|
||||||
- name: s3-exported-url
|
|
||||||
- name: aws-secret
|
|
||||||
- name: namespace
|
|
||||||
- name: aws-region
|
|
||||||
- name: s3-endpoint
|
|
||||||
- name: s3-use-https
|
|
||||||
- name: s3-verify-ssl
|
|
||||||
- name: job-name
|
|
||||||
- name: tf-serving-image
|
|
||||||
- name: model-serving-servicetype
|
|
||||||
- name: model-serving-ks-url
|
|
||||||
- name: model-serving-ks-tag
|
|
||||||
- name: model-name
|
|
||||||
script:
|
|
||||||
image: elsonrodriguez/ksonnet:0.10.1
|
|
||||||
command: ["/ksonnet-entrypoint.sh"]
|
|
||||||
source: |
|
|
||||||
ks init my-model-server
|
|
||||||
cd my-model-server
|
|
||||||
ks registry add kubeflow {{inputs.parameters.model-serving-ks-url}}
|
|
||||||
ks pkg install kubeflow/tf-serving@{{inputs.parameters.model-serving-ks-tag}}
|
|
||||||
ks env add default
|
|
||||||
# TODO change mnist name to be specific to a job. Right now mnist name is required to serve the model.
|
|
||||||
ks generate tf-serving {{inputs.parameters.model-name}} --name=mnist-{{inputs.parameters.job-name}} --namespace={{inputs.parameters.namespace}} --model_path={{inputs.parameters.s3-exported-url}}
|
|
||||||
ks param set {{inputs.parameters.model-name}} model_server_image {{inputs.parameters.tf-serving-image}}
|
|
||||||
ks param set {{inputs.parameters.model-name}} model_name {{inputs.parameters.model-name}}
|
|
||||||
ks param set {{inputs.parameters.model-name}} namespace {{inputs.parameters.namespace}}
|
|
||||||
ks param set {{inputs.parameters.model-name}} service_type {{inputs.parameters.model-serving-servicetype}}
|
|
||||||
ks param set {{inputs.parameters.model-name}} s3_create_secret false
|
|
||||||
ks param set {{inputs.parameters.model-name}} s3_secret_name {{inputs.parameters.aws-secret}}
|
|
||||||
ks param set {{inputs.parameters.model-name}} s3_secret_accesskeyid_key_name awsAccessKeyID
|
|
||||||
ks param set {{inputs.parameters.model-name}} s3_secret_secretaccesskey_key_name awsSecretAccessKey
|
|
||||||
ks param set {{inputs.parameters.model-name}} s3_aws_region {{inputs.parameters.aws-region}}
|
|
||||||
ks param set {{inputs.parameters.model-name}} s3_endpoint {{inputs.parameters.s3-endpoint}}
|
|
||||||
ks param set {{inputs.parameters.model-name}} s3_use_https {{inputs.parameters.s3-use-https}} --as-string
|
|
||||||
ks param set {{inputs.parameters.model-name}} s3_verify_ssl {{inputs.parameters.s3-verify-ssl}} --as-string
|
|
||||||
ks apply default -c {{inputs.parameters.model-name}}
|
|
||||||
#FIXME This doesn't actually work in the current version of argo. We're using a default of `tf-user` in the container entrypoint for now.
|
|
||||||
env:
|
|
||||||
- name: SERVICE_ACCOUNT
|
|
||||||
value: tf-user
|
|
||||||
|
|
@ -1,364 +0,0 @@
|
||||||
apiVersion: argoproj.io/v1alpha1
|
|
||||||
kind: Workflow
|
|
||||||
metadata:
|
|
||||||
generateName: tf-workflow-
|
|
||||||
spec:
|
|
||||||
entrypoint: tests
|
|
||||||
onExit: exit-handler
|
|
||||||
# Parameters can be passed/overridden via the argo CLI.
|
|
||||||
# To override the printed message, run `argo submit` with the -p option:
|
|
||||||
# $ argo submit examples/arguments-parameters.yaml -p message="goodbye world"
|
|
||||||
arguments:
|
|
||||||
parameters:
|
|
||||||
- name: tf-worker # number of tf workers
|
|
||||||
value: 1
|
|
||||||
- name: tf-ps # number of tf parameter servers
|
|
||||||
value: 2
|
|
||||||
- name: tf-model-image
|
|
||||||
value: elsonrodriguez/mytfmodel:1.7
|
|
||||||
- name: tf-serving-image #FIXME this image is a mirror of a private kubeflow-ci image, once we're building images swap this out. https://github.com/kubeflow/kubeflow/blob/dcf4adfe2dd1cec243647f3dd05d7c26246fddb1/components/k8s-model-server/images/Dockerfile.cpu
|
|
||||||
value: elsonrodriguez/model-server:1.6
|
|
||||||
- name: tf-tensorboard-image
|
|
||||||
value: tensorflow/tensorflow:1.7.0
|
|
||||||
- name: ks-image
|
|
||||||
value: elsonrodriguez/ksonnet:0.10.1
|
|
||||||
- name: model-name
|
|
||||||
value: mnist
|
|
||||||
- name: model-hidden-units
|
|
||||||
value: 100
|
|
||||||
- name: model-train-steps
|
|
||||||
value: 200
|
|
||||||
- name: model-batch-size
|
|
||||||
value: 100
|
|
||||||
- name: model-learning-rate
|
|
||||||
value: 0.01
|
|
||||||
- name: model-serving
|
|
||||||
value: true
|
|
||||||
- name: model-serving-servicetype
|
|
||||||
value: ClusterIP
|
|
||||||
- name: model-serving-ks-url
|
|
||||||
value: github.com/kubeflow/kubeflow/tree/master/kubeflow
|
|
||||||
- name: model-serving-ks-tag
|
|
||||||
value: 1f474f30
|
|
||||||
- name: job-name
|
|
||||||
value: myjob
|
|
||||||
- name: namespace
|
|
||||||
value: default
|
|
||||||
- name: s3-data-url
|
|
||||||
value: s3://mybucket/data/mnist/
|
|
||||||
- name: s3-train-base-url
|
|
||||||
value: s3://mybucket/models
|
|
||||||
- name: aws-endpoint-url
|
|
||||||
value: https://s3.us-west-1.amazonaws.com
|
|
||||||
- name: s3-endpoint
|
|
||||||
value: s3.us-west-1.amazonaws.com
|
|
||||||
- name: s3-use-https
|
|
||||||
value: true
|
|
||||||
- name: s3-verify-ssl
|
|
||||||
value: true
|
|
||||||
- name: aws-region
|
|
||||||
value: us-west-1
|
|
||||||
- name: aws-secret
|
|
||||||
value: aws-creds
|
|
||||||
volumes:
|
|
||||||
- name: training-data
|
|
||||||
emptyDir: {}
|
|
||||||
- name: training-output
|
|
||||||
templates:
|
|
||||||
- name: tests
|
|
||||||
steps:
|
|
||||||
- - name: get-workflow-info
|
|
||||||
template: get-workflow-info
|
|
||||||
- - name: tensorboard
|
|
||||||
template: tf-tensorboard
|
|
||||||
arguments:
|
|
||||||
parameters:
|
|
||||||
- name: s3-model-url
|
|
||||||
value: "{{steps.get-workflow-info.outputs.parameters.s3-model-url}}"
|
|
||||||
- - name: train-model
|
|
||||||
template: tf-train
|
|
||||||
arguments:
|
|
||||||
parameters:
|
|
||||||
- name: s3-model-url
|
|
||||||
value: "{{steps.get-workflow-info.outputs.parameters.s3-model-url}}"
|
|
||||||
- - name: serve-model
|
|
||||||
template: tf-inference
|
|
||||||
arguments:
|
|
||||||
parameters:
|
|
||||||
- name: s3-exported-url
|
|
||||||
value: "{{steps.get-workflow-info.outputs.parameters.s3-exported-url}}"
|
|
||||||
when: "{{workflow.parameters.model-serving}} == true"
|
|
||||||
- name: exit-handler
|
|
||||||
steps:
|
|
||||||
- - name: cleanup
|
|
||||||
template: clean
|
|
||||||
- name: get-workflow-info
|
|
||||||
container:
|
|
||||||
image: nervana/circleci:master
|
|
||||||
imagePullPolicy: Always
|
|
||||||
command: ["bash", "-c", "echo '{{workflow.parameters.s3-train-base-url}}/{{workflow.parameters.job-name}}/' | tr -d '[:space:]' > /tmp/s3-model-url; echo '{{workflow.parameters.s3-train-base-url}}/{{workflow.parameters.job-name}}/export/{{workflow.parameters.model-name}}/' | tr -d '[:space:]' > /tmp/s3-exported-url"]
|
|
||||||
outputs:
|
|
||||||
parameters:
|
|
||||||
- name: s3-model-url
|
|
||||||
valueFrom:
|
|
||||||
path: /tmp/s3-model-url
|
|
||||||
- name: s3-exported-url
|
|
||||||
valueFrom:
|
|
||||||
path: /tmp/s3-exported-url
|
|
||||||
- name: tf-train
|
|
||||||
inputs:
|
|
||||||
parameters:
|
|
||||||
- name: s3-model-url
|
|
||||||
resource:
|
|
||||||
action: apply
|
|
||||||
# NOTE: need to detect master node complete
|
|
||||||
successCondition: status.tfReplicaStatuses.Master.succeeded == 1
|
|
||||||
manifest: |
|
|
||||||
apiVersion: "kubeflow.org/v1alpha2"
|
|
||||||
kind: "TFJob"
|
|
||||||
metadata:
|
|
||||||
name: {{workflow.parameters.job-name}}
|
|
||||||
namespace: {{workflow.parameters.namespace}}
|
|
||||||
spec:
|
|
||||||
tfReplicaSpecs:
|
|
||||||
Master:
|
|
||||||
replicas: 1
|
|
||||||
template:
|
|
||||||
spec:
|
|
||||||
serviceAccountName: tf-job-operator
|
|
||||||
containers:
|
|
||||||
- image: {{workflow.parameters.tf-model-image}}
|
|
||||||
name: tensorflow
|
|
||||||
imagePullPolicy: Always
|
|
||||||
env:
|
|
||||||
- name: TF_MODEL_DIR
|
|
||||||
value: {{inputs.parameters.s3-model-url}}
|
|
||||||
- name: TF_EXPORT_DIR
|
|
||||||
value: {{workflow.parameters.model-name}}
|
|
||||||
- name: TF_TRAIN_STEPS
|
|
||||||
value: "{{workflow.parameters.model-train-steps}}"
|
|
||||||
- name: TF_BATCH_SIZE
|
|
||||||
value: "{{workflow.parameters.model-batch-size}}"
|
|
||||||
- name: TF_LEARNING_RATE
|
|
||||||
value: "{{workflow.parameters.model-learning-rate}}"
|
|
||||||
- name: AWS_ACCESS_KEY_ID
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: {{workflow.parameters.aws-secret}}
|
|
||||||
key: awsAccessKeyID
|
|
||||||
- name: AWS_SECRET_ACCESS_KEY
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: {{workflow.parameters.aws-secret}}
|
|
||||||
key: awsSecretAccessKey
|
|
||||||
- name: AWS_DEFAULT_REGION
|
|
||||||
value: {{workflow.parameters.aws-region}}
|
|
||||||
- name: AWS_REGION
|
|
||||||
value: {{workflow.parameters.aws-region}}
|
|
||||||
- name: S3_REGION
|
|
||||||
value: {{workflow.parameters.aws-region}}
|
|
||||||
- name: S3_USE_HTTPS
|
|
||||||
value: "{{workflow.parameters.s3-use-https}}"
|
|
||||||
- name: S3_VERIFY_SSL
|
|
||||||
value: "{{workflow.parameters.s3-verify-ssl}}"
|
|
||||||
- name: S3_ENDPOINT
|
|
||||||
value: {{workflow.parameters.s3-endpoint}}
|
|
||||||
restartPolicy: OnFailure
|
|
||||||
Worker:
|
|
||||||
replicas: {{workflow.parameters.tf-worker}}
|
|
||||||
template:
|
|
||||||
spec:
|
|
||||||
serviceAccountName: tf-job-operator
|
|
||||||
containers:
|
|
||||||
- image: {{workflow.parameters.tf-model-image}}
|
|
||||||
name: tensorflow
|
|
||||||
imagePullPolicy: Always
|
|
||||||
env:
|
|
||||||
- name: TF_MODEL_DIR
|
|
||||||
value: {{inputs.parameters.s3-model-url}}
|
|
||||||
- name: TF_EXPORT_DIR
|
|
||||||
value: {{workflow.parameters.model-name}}
|
|
||||||
- name: TF_TRAIN_STEPS
|
|
||||||
value: "{{workflow.parameters.model-train-steps}}"
|
|
||||||
- name: TF_BATCH_SIZE
|
|
||||||
value: "{{workflow.parameters.model-batch-size}}"
|
|
||||||
- name: TF_LEARNING_RATE
|
|
||||||
value: "{{workflow.parameters.model-learning-rate}}"
|
|
||||||
- name: AWS_ACCESS_KEY_ID
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: {{workflow.parameters.aws-secret}}
|
|
||||||
key: awsAccessKeyID
|
|
||||||
- name: AWS_SECRET_ACCESS_KEY
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: {{workflow.parameters.aws-secret}}
|
|
||||||
key: awsSecretAccessKey
|
|
||||||
- name: AWS_DEFAULT_REGION
|
|
||||||
value: {{workflow.parameters.aws-region}}
|
|
||||||
- name: AWS_REGION
|
|
||||||
value: {{workflow.parameters.aws-region}}
|
|
||||||
- name: S3_REGION
|
|
||||||
value: {{workflow.parameters.aws-region}}
|
|
||||||
- name: S3_USE_HTTPS
|
|
||||||
value: "{{workflow.parameters.s3-use-https}}"
|
|
||||||
- name: S3_VERIFY_SSL
|
|
||||||
value: "{{workflow.parameters.s3-verify-ssl}}"
|
|
||||||
- name: S3_ENDPOINT
|
|
||||||
value: {{workflow.parameters.s3-endpoint}}
|
|
||||||
restartPolicy: OnFailure
|
|
||||||
Ps:
|
|
||||||
replicas: {{workflow.parameters.tf-ps}}
|
|
||||||
template:
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- image: {{workflow.parameters.tf-model-image}}
|
|
||||||
name: tensorflow
|
|
||||||
imagePullPolicy: Always
|
|
||||||
env:
|
|
||||||
- name: TF_MODEL_DIR
|
|
||||||
value: {{inputs.parameters.s3-model-url}}
|
|
||||||
- name: TF_EXPORT_DIR
|
|
||||||
value: {{workflow.parameters.model-name}}
|
|
||||||
- name: TF_TRAIN_STEPS
|
|
||||||
value: "{{workflow.parameters.model-train-steps}}"
|
|
||||||
- name: TF_BATCH_SIZE
|
|
||||||
value: "{{workflow.parameters.model-batch-size}}"
|
|
||||||
- name: TF_LEARNING_RATE
|
|
||||||
value: "{{workflow.parameters.model-learning-rate}}"
|
|
||||||
- name: AWS_ACCESS_KEY_ID
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: {{workflow.parameters.aws-secret}}
|
|
||||||
key: awsAccessKeyID
|
|
||||||
- name: AWS_SECRET_ACCESS_KEY
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: {{workflow.parameters.aws-secret}}
|
|
||||||
key: awsSecretAccessKey
|
|
||||||
- name: AWS_DEFAULT_REGION
|
|
||||||
value: {{workflow.parameters.aws-region}}
|
|
||||||
- name: AWS_REGION
|
|
||||||
value: {{workflow.parameters.aws-region}}
|
|
||||||
- name: S3_REGION
|
|
||||||
value: {{workflow.parameters.aws-region}}
|
|
||||||
- name: S3_USE_HTTPS
|
|
||||||
value: "{{workflow.parameters.s3-use-https}}"
|
|
||||||
- name: S3_VERIFY_SSL
|
|
||||||
value: "{{workflow.parameters.s3-verify-ssl}}"
|
|
||||||
- name: S3_ENDPOINT
|
|
||||||
value: {{workflow.parameters.s3-endpoint}}
|
|
||||||
restartPolicy: OnFailure
|
|
||||||
- name: tf-tensorboard
|
|
||||||
inputs:
|
|
||||||
parameters:
|
|
||||||
- name: s3-model-url
|
|
||||||
resource:
|
|
||||||
action: apply
|
|
||||||
manifest: |
|
|
||||||
apiVersion: extensions/v1beta1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: tensorboard-{{workflow.parameters.job-name}}
|
|
||||||
name: tensorboard-{{workflow.parameters.job-name}}
|
|
||||||
namespace: {{workflow.parameters.namespace}}
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: tensorboard-{{workflow.parameters.job-name}}
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: tensorboard-{{workflow.parameters.job-name}}
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: tensorboard-{{workflow.parameters.job-name}}
|
|
||||||
image: {{workflow.parameters.tf-tensorboard-image}}
|
|
||||||
imagePullPolicy: Always
|
|
||||||
command:
|
|
||||||
- /usr/local/bin/tensorboard
|
|
||||||
args:
|
|
||||||
- --logdir
|
|
||||||
- {{inputs.parameters.s3-model-url}}
|
|
||||||
env:
|
|
||||||
- name: AWS_ACCESS_KEY_ID
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
key: awsAccessKeyID
|
|
||||||
name: {{workflow.parameters.aws-secret}}
|
|
||||||
- name: AWS_SECRET_ACCESS_KEY
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
key: awsSecretAccessKey
|
|
||||||
name: {{workflow.parameters.aws-secret}}
|
|
||||||
- name: AWS_REGION
|
|
||||||
value: {{workflow.parameters.aws-region}}
|
|
||||||
- name: S3_REGION
|
|
||||||
value: {{workflow.parameters.aws-region}}
|
|
||||||
- name: S3_USE_HTTPS
|
|
||||||
value: "{{workflow.parameters.s3-use-https}}"
|
|
||||||
- name: S3_VERIFY_SSL
|
|
||||||
value: "{{workflow.parameters.s3-verify-ssl}}"
|
|
||||||
- name: S3_ENDPOINT
|
|
||||||
value: {{workflow.parameters.s3-endpoint}}
|
|
||||||
ports:
|
|
||||||
- containerPort: 6006
|
|
||||||
protocol: TCP
|
|
||||||
dnsPolicy: ClusterFirst
|
|
||||||
restartPolicy: Always
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: tensorboard-{{workflow.parameters.job-name}}
|
|
||||||
name: tensorboard-{{workflow.parameters.job-name}}
|
|
||||||
namespace: {{workflow.parameters.namespace}}
|
|
||||||
spec:
|
|
||||||
ports:
|
|
||||||
- port: 80
|
|
||||||
protocol: TCP
|
|
||||||
targetPort: 6006
|
|
||||||
selector:
|
|
||||||
app: tensorboard-{{workflow.parameters.job-name}}
|
|
||||||
sessionAffinity: None
|
|
||||||
type: ClusterIP
|
|
||||||
- name: tf-inference
|
|
||||||
inputs:
|
|
||||||
parameters:
|
|
||||||
- name: s3-exported-url
|
|
||||||
script:
|
|
||||||
image: "{{workflow.parameters.ks-image}}"
|
|
||||||
command: ["/ksonnet-entrypoint.sh"]
|
|
||||||
source: |
|
|
||||||
ks init my-model-server
|
|
||||||
cd my-model-server
|
|
||||||
ks registry add kubeflow {{workflow.parameters.model-serving-ks-url}}
|
|
||||||
ks pkg install kubeflow/tf-serving@{{workflow.parameters.model-serving-ks-tag}}
|
|
||||||
ks env add default
|
|
||||||
# TODO change mnist name to be specific to a job. Right now mnist name is required to serve the model.
|
|
||||||
ks generate tf-serving {{workflow.parameters.model-name}} --name=mnist-{{workflow.parameters.job-name}} --namespace={{workflow.parameters.namespace}} --model_path={{inputs.parameters.s3-exported-url}}
|
|
||||||
ks param set {{workflow.parameters.model-name}} model_server_image {{workflow.parameters.tf-serving-image}}
|
|
||||||
ks param set {{workflow.parameters.model-name}} model_name {{workflow.parameters.model-name}}
|
|
||||||
ks param set {{workflow.parameters.model-name}} namespace {{workflow.parameters.namespace}}
|
|
||||||
ks param set {{workflow.parameters.model-name}} service_type {{workflow.parameters.model-serving-servicetype}}
|
|
||||||
ks param set {{workflow.parameters.model-name}} s3_create_secret false
|
|
||||||
ks param set {{workflow.parameters.model-name}} s3_secret_name {{workflow.parameters.aws-secret}}
|
|
||||||
ks param set {{workflow.parameters.model-name}} s3_secret_accesskeyid_key_name awsAccessKeyID
|
|
||||||
ks param set {{workflow.parameters.model-name}} s3_secret_secretaccesskey_key_name awsSecretAccessKey
|
|
||||||
ks param set {{workflow.parameters.model-name}} s3_aws_region {{workflow.parameters.aws-region}}
|
|
||||||
ks param set {{workflow.parameters.model-name}} s3_endpoint {{workflow.parameters.s3-endpoint}}
|
|
||||||
ks param set {{workflow.parameters.model-name}} s3_use_https {{workflow.parameters.s3-use-https}} --as-string
|
|
||||||
ks param set {{workflow.parameters.model-name}} s3_verify_ssl {{workflow.parameters.s3-verify-ssl}} --as-string
|
|
||||||
ks apply default -c {{workflow.parameters.model-name}}
|
|
||||||
#FIXME This doesn't actually work in the current version of argo. We're using a default of `tf-user` in the container entrypoint for now.
|
|
||||||
env:
|
|
||||||
- name: SERVICE_ACCOUNT
|
|
||||||
value: tf-user
|
|
||||||
- name: clean
|
|
||||||
container:
|
|
||||||
image: nervana/circleci:master
|
|
||||||
imagePullPolicy: Always
|
|
||||||
command: ["bash", "-c", "kubectl delete tfjob {{workflow.parameters.job-name}} || true"]
|
|
||||||
|
|
@ -1,97 +0,0 @@
|
||||||
apiVersion: rbac.authorization.k8s.io/v1
|
|
||||||
kind: Role
|
|
||||||
metadata:
|
|
||||||
name: tf-user
|
|
||||||
rules:
|
|
||||||
- apiGroups:
|
|
||||||
- ""
|
|
||||||
resources:
|
|
||||||
- pods
|
|
||||||
- pods/exec
|
|
||||||
verbs:
|
|
||||||
- create
|
|
||||||
- get
|
|
||||||
- list
|
|
||||||
- watch
|
|
||||||
- update
|
|
||||||
- patch
|
|
||||||
- apiGroups:
|
|
||||||
- ""
|
|
||||||
resources:
|
|
||||||
- configmaps
|
|
||||||
- serviceaccounts
|
|
||||||
- secrets
|
|
||||||
verbs:
|
|
||||||
- get
|
|
||||||
- watch
|
|
||||||
- list
|
|
||||||
- apiGroups:
|
|
||||||
- ""
|
|
||||||
resources:
|
|
||||||
- persistentvolumeclaims
|
|
||||||
verbs:
|
|
||||||
- create
|
|
||||||
- delete
|
|
||||||
- apiGroups:
|
|
||||||
- ""
|
|
||||||
resources:
|
|
||||||
- services
|
|
||||||
verbs:
|
|
||||||
- create
|
|
||||||
- get
|
|
||||||
- list
|
|
||||||
- watch
|
|
||||||
- update
|
|
||||||
- patch
|
|
||||||
- apiGroups:
|
|
||||||
- apps
|
|
||||||
- extensions
|
|
||||||
resources:
|
|
||||||
- deployments
|
|
||||||
verbs:
|
|
||||||
- create
|
|
||||||
- get
|
|
||||||
- list
|
|
||||||
- watch
|
|
||||||
- update
|
|
||||||
- patch
|
|
||||||
- delete
|
|
||||||
- apiGroups:
|
|
||||||
- argoproj.io
|
|
||||||
resources:
|
|
||||||
- workflows
|
|
||||||
verbs:
|
|
||||||
- get
|
|
||||||
- list
|
|
||||||
- watch
|
|
||||||
- update
|
|
||||||
- patch
|
|
||||||
- apiGroups:
|
|
||||||
- kubeflow.org
|
|
||||||
resources:
|
|
||||||
- tfjobs
|
|
||||||
verbs:
|
|
||||||
- create
|
|
||||||
- get
|
|
||||||
- list
|
|
||||||
- watch
|
|
||||||
- update
|
|
||||||
- patch
|
|
||||||
- delete
|
|
||||||
---
|
|
||||||
apiVersion: rbac.authorization.k8s.io/v1
|
|
||||||
kind: RoleBinding
|
|
||||||
metadata:
|
|
||||||
name: tf-user
|
|
||||||
roleRef:
|
|
||||||
apiGroup: rbac.authorization.k8s.io
|
|
||||||
kind: Role
|
|
||||||
name: tf-user
|
|
||||||
subjects:
|
|
||||||
- kind: ServiceAccount
|
|
||||||
name: tf-user
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: ServiceAccount
|
|
||||||
metadata:
|
|
||||||
name: tf-user
|
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
FROM ubuntu:16.04
|
||||||
|
MAINTAINER "Daniel Sanche"
|
||||||
|
|
||||||
|
# add TF dependencies
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
build-essential \
|
||||||
|
curl \
|
||||||
|
libfreetype6-dev \
|
||||||
|
libpng12-dev \
|
||||||
|
libzmq3-dev \
|
||||||
|
pkg-config \
|
||||||
|
python3 \
|
||||||
|
python-dev \
|
||||||
|
rsync \
|
||||||
|
software-properties-common \
|
||||||
|
unzip \
|
||||||
|
&& \
|
||||||
|
apt-get clean && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# add python dependencies
|
||||||
|
RUN curl -O https://bootstrap.pypa.io/get-pip.py && \
|
||||||
|
python get-pip.py && \
|
||||||
|
rm get-pip.py
|
||||||
|
|
||||||
|
RUN pip --no-cache-dir install \
|
||||||
|
Pillow \
|
||||||
|
h5py \
|
||||||
|
ipykernel \
|
||||||
|
numpy \
|
||||||
|
tensorflow \
|
||||||
|
tensorflow-serving-api \
|
||||||
|
flask \
|
||||||
|
&& \
|
||||||
|
python -m ipykernel.kernelspec
|
||||||
|
|
||||||
|
# show python logs as they occur
|
||||||
|
ENV PYTHONUNBUFFERED=0
|
||||||
|
|
||||||
|
# add project files
|
||||||
|
ADD *.py /home/
|
||||||
|
ADD templates/* /home/templates/
|
||||||
|
ADD static/styles /home/static/styles/
|
||||||
|
RUN mkdir /home/static/tmp/
|
||||||
|
ADD static/scripts/ /home/static/scripts/
|
||||||
|
|
||||||
|
# start server on port 5000
|
||||||
|
WORKDIR /home/
|
||||||
|
EXPOSE 5000
|
||||||
|
ENTRYPOINT python flask_server.py
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,14 @@
|
||||||
|
# web-ui
|
||||||
|
|
||||||
|
The files in this folder define a web interface that can be used to interact with a TensorFlow server
|
||||||
|
|
||||||
|
- flask_server.py
|
||||||
|
- main server code. Handles incoming requests, and renders HTML from template
|
||||||
|
- mnist_client.py
|
||||||
|
- code to interact with TensorFlow model server
|
||||||
|
- takes in an image and server details, and returns the server's response
|
||||||
|
- Dockerfile
|
||||||
|
- builds a runnable container out of the files in this directory
|
||||||
|
|
||||||
|
---
|
||||||
|
This is not an officially supported Google product
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
'''
|
||||||
|
Copyright 2018 Google LLC
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
'''
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from threading import Timer
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from flask import Flask, render_template, request
|
||||||
|
from mnist_client import get_prediction, random_mnist
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# handle requests to the server
|
||||||
|
@app.route("/")
|
||||||
|
def main():
|
||||||
|
# get url parameters for HTML template
|
||||||
|
name_arg = request.args.get('name', 'mnist')
|
||||||
|
addr_arg = request.args.get('addr', 'mnist-service')
|
||||||
|
port_arg = request.args.get('port', '9000')
|
||||||
|
args = {"name": name_arg, "addr": addr_arg, "port": port_arg}
|
||||||
|
logging.info("Request args: %s", args)
|
||||||
|
|
||||||
|
output = None
|
||||||
|
connection = {"text": "", "success": False}
|
||||||
|
img_id = str(uuid.uuid4())
|
||||||
|
img_path = "static/tmp/" + img_id + ".png"
|
||||||
|
try:
|
||||||
|
# get a random test MNIST image
|
||||||
|
x, y, _ = random_mnist(img_path)
|
||||||
|
# get prediction from TensorFlow server
|
||||||
|
pred, scores, ver = get_prediction(x,
|
||||||
|
server_host=addr_arg,
|
||||||
|
server_port=int(port_arg),
|
||||||
|
server_name=name_arg,
|
||||||
|
timeout=10)
|
||||||
|
# if no exceptions thrown, server connection was a success
|
||||||
|
connection["text"] = "Connected (model version: " + str(ver) + ")"
|
||||||
|
connection["success"] = True
|
||||||
|
# parse class confidence scores from server prediction
|
||||||
|
scores_dict = []
|
||||||
|
for i in range(0, 10):
|
||||||
|
scores_dict += [{"index": str(i), "val": scores[i]}]
|
||||||
|
output = {"truth": y, "prediction": pred,
|
||||||
|
"img_path": img_path, "scores": scores_dict}
|
||||||
|
except Exception as e: # pylint: disable=broad-except
|
||||||
|
logging.info("Exception occured: %s", e)
|
||||||
|
# server connection failed
|
||||||
|
connection["text"] = "Exception making request: {0}".format(e)
|
||||||
|
# after 10 seconds, delete cached image file from server
|
||||||
|
t = Timer(10.0, remove_resource, [img_path])
|
||||||
|
t.start()
|
||||||
|
# render results using HTML template
|
||||||
|
return render_template('index.html', output=output,
|
||||||
|
connection=connection, args=args)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_resource(path):
|
||||||
|
"""
|
||||||
|
attempt to delete file from path. Used to clean up MNIST testing images
|
||||||
|
|
||||||
|
:param path: the path of the file to delete
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
os.remove(path)
|
||||||
|
print("removed " + path)
|
||||||
|
except OSError:
|
||||||
|
print("no file at " + path)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
logging.basicConfig(level=logging.INFO,
|
||||||
|
format=('%(levelname)s|%(asctime)s'
|
||||||
|
'|%(pathname)s|%(lineno)d| %(message)s'),
|
||||||
|
datefmt='%Y-%m-%dT%H:%M:%S',
|
||||||
|
)
|
||||||
|
logging.getLogger().setLevel(logging.INFO)
|
||||||
|
logging.info("Starting flask.")
|
||||||
|
app.run(debug=True, host='0.0.0.0')
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
#!/usr/bin/env python2.7
|
||||||
|
'''
|
||||||
|
Copyright 2018 Google LLC
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from grpc.beta import implementations
|
||||||
|
import numpy as np
|
||||||
|
import tensorflow as tf
|
||||||
|
from tensorflow.examples.tutorials.mnist import input_data
|
||||||
|
from tensorflow_serving.apis import predict_pb2
|
||||||
|
from tensorflow_serving.apis import prediction_service_pb2
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
|
||||||
|
def get_prediction(image, server_host='127.0.0.1', server_port=9000,
|
||||||
|
server_name="server", timeout=10.0):
|
||||||
|
"""
|
||||||
|
Retrieve a prediction from a TensorFlow model server
|
||||||
|
|
||||||
|
:param image: a MNIST image represented as a 1x784 array
|
||||||
|
:param server_host: the address of the TensorFlow server
|
||||||
|
:param server_port: the port used by the server
|
||||||
|
:param server_name: the name of the server
|
||||||
|
:param timeout: the amount of time to wait for a prediction to complete
|
||||||
|
:return 0: the integer predicted in the MNIST image
|
||||||
|
:return 1: the confidence scores for all classes
|
||||||
|
:return 2: the version number of the model handling the request
|
||||||
|
"""
|
||||||
|
|
||||||
|
print("connecting to:%s:%i" % (server_host, server_port))
|
||||||
|
# initialize to server connection
|
||||||
|
channel = implementations.insecure_channel(server_host, server_port)
|
||||||
|
stub = prediction_service_pb2.beta_create_PredictionService_stub(channel)
|
||||||
|
|
||||||
|
# build request
|
||||||
|
request = predict_pb2.PredictRequest()
|
||||||
|
request.model_spec.name = server_name
|
||||||
|
request.model_spec.signature_name = 'serving_default'
|
||||||
|
request.inputs['x'].CopyFrom(
|
||||||
|
tf.contrib.util.make_tensor_proto(image, shape=image.shape))
|
||||||
|
|
||||||
|
# retrieve results
|
||||||
|
result = stub.Predict(request, timeout)
|
||||||
|
resultVal = result.outputs["classes"].int_val[0]
|
||||||
|
scores = result.outputs['predictions'].float_val
|
||||||
|
version = result.outputs["classes"].int_val[0]
|
||||||
|
return resultVal, scores, version
|
||||||
|
|
||||||
|
|
||||||
|
def random_mnist(save_path=None):
|
||||||
|
"""
|
||||||
|
Pull a random image out of the MNIST test dataset
|
||||||
|
Optionally save the selected image as a file to disk
|
||||||
|
|
||||||
|
:param savePath: the path to save the file to. If None, file is not saved
|
||||||
|
:return 0: a 1x784 representation of the MNIST image
|
||||||
|
:return 1: the ground truth label associated with the image
|
||||||
|
:return 2: a bool representing whether the image file was saved to disk
|
||||||
|
"""
|
||||||
|
|
||||||
|
mnist = input_data.read_data_sets("MNIST_data/", one_hot=True)
|
||||||
|
batch_size = 1
|
||||||
|
batch_x, batch_y = mnist.test.next_batch(batch_size)
|
||||||
|
saved = False
|
||||||
|
if save_path is not None:
|
||||||
|
# save image file to disk
|
||||||
|
try:
|
||||||
|
data = (batch_x * 255).astype(np.uint8).reshape(28, 28)
|
||||||
|
img = Image.fromarray(data, 'L')
|
||||||
|
img.save(save_path)
|
||||||
|
saved = True
|
||||||
|
except Exception as e: # pylint: disable=broad-except
|
||||||
|
logging.error("There was a problem saving the image; %s", e)
|
||||||
|
return batch_x, np.argmax(batch_y), saved
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
"""Test mnist_client.
|
||||||
|
|
||||||
|
This file tests that we can send predictions to the model.
|
||||||
|
|
||||||
|
It is an integration test as it depends on having access to
|
||||||
|
a deployed model.
|
||||||
|
|
||||||
|
Python Path Requirements:
|
||||||
|
kubeflow/testing/py - https://github.com/kubeflow/testing/tree/master/py
|
||||||
|
* Provides utilities for testing
|
||||||
|
|
||||||
|
Manually running the test
|
||||||
|
1. Configure your KUBECONFIG file to point to the desired cluster
|
||||||
|
2. Use kubectl port-forward to forward a local port
|
||||||
|
to the gRPC port of TFServing; e.g.
|
||||||
|
kubectl -n ${NAMESPACE} port-forward service/mnist-service 9000:9000
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
import mnist_client
|
||||||
|
|
||||||
|
from py import test_runner
|
||||||
|
|
||||||
|
from kubeflow.testing import test_util
|
||||||
|
|
||||||
|
class MnistClientTest(test_util.TestCase):
|
||||||
|
def __init__(self, args):
|
||||||
|
self.args = args
|
||||||
|
super(MnistClientTest, self).__init__(class_name="MnistClientTest",
|
||||||
|
name="MnistClientTest")
|
||||||
|
|
||||||
|
def test_predict(self): # pylint: disable=no-self-use
|
||||||
|
this_dir = os.path.dirname(__file__)
|
||||||
|
data_dir = os.path.join(this_dir, "..", "data")
|
||||||
|
img_path = os.path.abspath(data_dir)
|
||||||
|
|
||||||
|
x, _, _ = mnist_client.random_mnist(img_path)
|
||||||
|
|
||||||
|
server_host = "localhost"
|
||||||
|
server_port = 9000
|
||||||
|
model_name = "mnist"
|
||||||
|
# get prediction from TensorFlow server
|
||||||
|
pred, scores, _ = mnist_client.get_prediction(
|
||||||
|
x, server_host=server_host, server_port=server_port,
|
||||||
|
server_name=model_name, timeout=10)
|
||||||
|
|
||||||
|
if pred < 0 or pred >= 10:
|
||||||
|
raise ValueError("Prediction {0} is not in the range [0, 9]".format(pred))
|
||||||
|
|
||||||
|
if len(scores) != 10:
|
||||||
|
raise ValueError("Scores should have dimension 10. Got {0}".format(
|
||||||
|
scores))
|
||||||
|
# TODO(jlewi): Should we do any additional validation?
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_runner.main(module=__name__)
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,238 @@
|
||||||
|
/**
|
||||||
|
* Copyright 2015 Google Inc. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
font-family: 'Roboto', 'Helvetica', sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.mdl-demo .mdl-layout__header-row {
|
||||||
|
padding-left: 40px;
|
||||||
|
}
|
||||||
|
.mdl-demo .mdl-layout.is-small-screen .mdl-layout__header-row h3 {
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
.mdl-demo .mdl-layout__tab-bar-button {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.mdl-demo .mdl-layout.is-small-screen .mdl-layout__tab-bar .mdl-button {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.mdl-demo .mdl-layout:not(.is-small-screen) .mdl-layout__tab-bar,
|
||||||
|
.mdl-demo .mdl-layout:not(.is-small-screen) .mdl-layout__tab-bar-container {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
.mdl-demo .mdl-layout__tab-bar-container {
|
||||||
|
height: 64px;
|
||||||
|
}
|
||||||
|
.mdl-demo .mdl-layout__tab-bar {
|
||||||
|
padding: 0;
|
||||||
|
padding-left: 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.mdl-demo .mdl-layout__tab-bar .mdl-layout__tab {
|
||||||
|
height: 64px;
|
||||||
|
line-height: 64px;
|
||||||
|
}
|
||||||
|
.mdl-demo .mdl-layout__tab-bar .mdl-layout__tab.is-active::after {
|
||||||
|
background-color: white;
|
||||||
|
height: 4px;
|
||||||
|
}
|
||||||
|
.mdl-demo main > .mdl-layout__tab-panel {
|
||||||
|
padding: 8px;
|
||||||
|
padding-top: 48px;
|
||||||
|
}
|
||||||
|
.mdl-demo .mdl-card {
|
||||||
|
height: auto;
|
||||||
|
display: -webkit-flex;
|
||||||
|
display: -ms-flexbox;
|
||||||
|
display: flex;
|
||||||
|
-webkit-flex-direction: column;
|
||||||
|
-ms-flex-direction: column;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.mdl-demo .mdl-card > * {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
.mdl-demo .mdl-card .mdl-card__supporting-text {
|
||||||
|
margin: 40px;
|
||||||
|
-webkit-flex-grow: 1;
|
||||||
|
-ms-flex-positive: 1;
|
||||||
|
flex-grow: 1;
|
||||||
|
padding: 0;
|
||||||
|
color: inherit;
|
||||||
|
width: calc(100% - 80px);
|
||||||
|
}
|
||||||
|
.mdl-demo.mdl-demo .mdl-card__supporting-text h4 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.mdl-demo .mdl-card__actions {
|
||||||
|
margin: 0;
|
||||||
|
padding: 4px 40px;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
.mdl-demo .mdl-card__actions a {
|
||||||
|
color: #00BCD4;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.mdl-demo .mdl-card__actions a:hover,
|
||||||
|
.mdl-demo .mdl-card__actions a:active {
|
||||||
|
color: inherit;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
.mdl-demo .mdl-card__supporting-text + .mdl-card__actions {
|
||||||
|
border-top: 1px solid rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
.mdl-demo #add {
|
||||||
|
position: absolute;
|
||||||
|
right: 40px;
|
||||||
|
top: 36px;
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mdl-demo .mdl-layout__content section:not(:last-of-type) {
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 48px;
|
||||||
|
}
|
||||||
|
.mdl-demo section.section--center {
|
||||||
|
max-width: 860px;
|
||||||
|
}
|
||||||
|
.mdl-demo #features section.section--center {
|
||||||
|
max-width: 620px;
|
||||||
|
}
|
||||||
|
.mdl-demo section > header{
|
||||||
|
display: -webkit-flex;
|
||||||
|
display: -ms-flexbox;
|
||||||
|
display: flex;
|
||||||
|
-webkit-align-items: center;
|
||||||
|
-ms-flex-align: center;
|
||||||
|
align-items: center;
|
||||||
|
-webkit-justify-content: center;
|
||||||
|
-ms-flex-pack: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.mdl-demo section > .section__play-btn {
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
.mdl-demo section > header > .material-icons {
|
||||||
|
font-size: 3rem;
|
||||||
|
}
|
||||||
|
.mdl-demo section > button {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 99;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
}
|
||||||
|
.mdl-demo section .section__circle {
|
||||||
|
display: -webkit-flex;
|
||||||
|
display: -ms-flexbox;
|
||||||
|
display: flex;
|
||||||
|
-webkit-align-items: center;
|
||||||
|
-ms-flex-align: center;
|
||||||
|
align-items: center;
|
||||||
|
-webkit-justify-content: flex-start;
|
||||||
|
-ms-flex-pack: start;
|
||||||
|
justify-content: flex-start;
|
||||||
|
-webkit-flex-grow: 0;
|
||||||
|
-ms-flex-positive: 0;
|
||||||
|
flex-grow: 0;
|
||||||
|
-webkit-flex-shrink: 1;
|
||||||
|
-ms-flex-negative: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
}
|
||||||
|
.mdl-demo section .section__text {
|
||||||
|
-webkit-flex-grow: 1;
|
||||||
|
-ms-flex-positive: 1;
|
||||||
|
flex-grow: 1;
|
||||||
|
-webkit-flex-shrink: 0;
|
||||||
|
-ms-flex-negative: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding-top: 8px;
|
||||||
|
}
|
||||||
|
.mdl-demo section .section__text h5 {
|
||||||
|
font-size: inherit;
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
.mdl-demo section .section__text a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.mdl-demo section .section__circle-container > .section__circle-container__circle {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
border-radius: 32px;
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
.mdl-demo section.section--footer .section__circle--big {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
border-radius: 50px;
|
||||||
|
margin: 8px 32px;
|
||||||
|
}
|
||||||
|
.mdl-demo .is-small-screen section.section--footer .section__circle--big {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border-radius: 25px;
|
||||||
|
margin: 8px 16px;
|
||||||
|
}
|
||||||
|
.mdl-demo section.section--footer {
|
||||||
|
padding: 64px 0;
|
||||||
|
margin: 0 -8px -8px -8px;
|
||||||
|
}
|
||||||
|
.mdl-demo section.section--center .section__text:not(:last-child) {
|
||||||
|
border-bottom: 1px solid rgba(0,0,0,.13);
|
||||||
|
}
|
||||||
|
.mdl-demo .mdl-card .mdl-card__supporting-text > h3:first-child {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.mdl-demo .mdl-layout__tab-panel:not(#overview) {
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
.mdl-demo #features section {
|
||||||
|
margin-bottom: 72px;
|
||||||
|
}
|
||||||
|
.mdl-demo #features h4, #features h5 {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.mdl-demo .toc {
|
||||||
|
border-left: 4px solid #C1EEF4;
|
||||||
|
margin: 24px;
|
||||||
|
padding: 0;
|
||||||
|
padding-left: 8px;
|
||||||
|
display: -webkit-flex;
|
||||||
|
display: -ms-flexbox;
|
||||||
|
display: flex;
|
||||||
|
-webkit-flex-direction: column;
|
||||||
|
-ms-flex-direction: column;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.mdl-demo .toc h4 {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
.mdl-demo .toc a {
|
||||||
|
color: #4DD0E1;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 28px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.mdl-demo .mdl-menu__container {
|
||||||
|
z-index: 99;
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,115 @@
|
||||||
|
<!--
|
||||||
|
Copyright 2018 Google LLC
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
|
||||||
|
<title>Kubeflow UI</title>
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:regular,bold,italic,thin,light,bolditalic,black,medium&lang=en">
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
|
||||||
|
<link rel="stylesheet" href="static/styles/material.deep_purple-pink.min.css">
|
||||||
|
<link rel="stylesheet" href="static/styles/demo.css">
|
||||||
|
<script src="static/scripts/material.min.js"></script>
|
||||||
|
</head>
|
||||||
|
<body class="mdl-demo mdl-color--grey-100 mdl-color-text--grey-700 mdl-base">
|
||||||
|
<div class="mdl-layout mdl-js-layout mdl-layout--fixed-header">
|
||||||
|
<header class="mdl-layout__header mdl-layout__header--scroll mdl-color--primary">
|
||||||
|
<div class="mdl-layout--large-screen-only mdl-layout__header-row"></div>
|
||||||
|
<div class="mdl-layout__header-row">
|
||||||
|
<h3>Kubeflow Codelab UI</h3>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main class="mdl-layout__content">
|
||||||
|
<!-- render server connection status -->
|
||||||
|
<section class="section--center mdl-grid mdl-grid--no-spacing mdl-shadow--2dp">
|
||||||
|
<div class="mdl-card mdl-cell mdl-cell--12-col">
|
||||||
|
<div class="mdl-card__supporting-text">
|
||||||
|
<h4>MNIST Model Server</h4>
|
||||||
|
<form action="/">
|
||||||
|
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label" style="width:250px">
|
||||||
|
<input class="mdl-textfield__input" type="text" id="server-name" name="name" value="{{ args.name }}">
|
||||||
|
<label class="mdl-textfield__label" for="sample1">Model Name</label>
|
||||||
|
</div>
|
||||||
|
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label" style="width:250px">
|
||||||
|
<input class="mdl-textfield__input" type="text" id="server-address" name="addr" value="{{ args.addr }}">
|
||||||
|
<label class="mdl-textfield__label" for="sample1">Server Address</label>
|
||||||
|
</div>
|
||||||
|
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label" style="width:100px">
|
||||||
|
<input class="mdl-textfield__input" type="text" name="port"
|
||||||
|
pattern="^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$"
|
||||||
|
id="server-port" value="{{ args.port }}">
|
||||||
|
<label class="mdl-textfield__label" for="sample2">Port</label>
|
||||||
|
<span class="mdl-textfield__error">Input is not a valid port</span>
|
||||||
|
</div>
|
||||||
|
<button class="mdl-button mdl-js-button">Connect</button>
|
||||||
|
</form>
|
||||||
|
{% if connection.success %}
|
||||||
|
<h6><font color="#388E3C">✓ {{ connection.text }}</font></h6>
|
||||||
|
{% else %}
|
||||||
|
<h6><font color="#C62828">❗ {{ connection.text }}</font></h6>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<!-- if connected to server, render testing results -->
|
||||||
|
{% if output %}
|
||||||
|
<section class="section--center mdl-grid mdl-grid--no-spacing mdl-shadow--2dp">
|
||||||
|
<div class="mdl-card mdl-cell mdl-cell--12-col">
|
||||||
|
<div class="mdl-card__supporting-text">
|
||||||
|
<h4>Test Results</h4>
|
||||||
|
<img src={{ output.img_path }}
|
||||||
|
style="width:128px;height:128px;display:block;margin:auto;">
|
||||||
|
<br><br>
|
||||||
|
<table class="mdl-data-table mdl-js-data-table mdl-shadow--2dp" style="margin:auto;width:40%">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="mdl-data-table__cell--non-numeric"><b>Truth</b></td>
|
||||||
|
<td><b>{{ output.truth }}</b></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="mdl-data-table__cell--non-numeric"><b>Prediction</b></td>
|
||||||
|
<td><b> {{ output.prediction }}</b></td>
|
||||||
|
</tr>
|
||||||
|
{% for score in output.scores %}
|
||||||
|
<tr>
|
||||||
|
<td class="mdl-data-table__cell--non-numeric">Probability {{ score.index }}:</td>
|
||||||
|
<td>
|
||||||
|
<div id="progressbar{{ score.index }}"
|
||||||
|
class="mdl-progress mdl-js-progress"
|
||||||
|
style="width:120;"></div>
|
||||||
|
<script language = "javascript">
|
||||||
|
document.querySelector('#progressbar{{ score.index }}').addEventListener('mdl-componentupgraded',
|
||||||
|
function() { this.MaterialProgress.setProgress({{ score.val * 100 }}); })
|
||||||
|
</script>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<br><br>
|
||||||
|
<button type="button"
|
||||||
|
class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-color--accent mdl-color-text--accent-contrast"
|
||||||
|
onClick="window.location.reload()" style="margin:auto;display:block">Test Random Image</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Reference in New Issue