Fun with Kubernetes and Tensorflow Serving

{GUEST POST} About the Author: Samuel Cozannet is a Strategic Cloud Expert with strong technical background (OpenStack, Kubernetes, Big Data, public cloud, etc) from 10+ years of experience in product management, operations, architecture and DevOps.

There are multiple walk-throughs available for Tensorflow Serving, to run on K8s or otherwise. The vast majority gets you to the point where you still need to use the Tensorflow Python client to publish images you want to analyze.

While you can find data about performance optimisation of Tensorflow itself from the docs, it does not really cover some operational details which can be interesting when you intend to run the service. For an inception model, should you for example use PNG images? JPEG files? Is performance better if you use a high quality images or should you downscale them? Is accuracy better if you use a high quality images?

To start answering these questions, I teamed up with Kontron, who provided access to hardware, and Ronan Delacroix, the best Python guru I have ever known.

We did:

Build from source TF Serving binaries for CPU and GPU, in both optimized and non optimized fashion, with parameters taken from the docs;

Build Docker images with these fresh binaries;

Look at how to prepare the inception model to be served by TF Serving;

Build a Kubernetes manifest to deploy TF Serving with various resource constraints;

Build a web app with a simple API through which we can publish an image, run a simple pre-processing pipeline and compare the results;

Draw some early stage conclusions.

And now we want to share that awesome experience! And collect your feedback to iterate on it.

Prerequisites

To replicate this post, you will need access to a Kubernetes 1.6+ cluster, preferably with GPUs in it.

Building TF Serving “just right” is no easy task, especially as it is a fast moving target. This exercise was based on TF 1.5, and I do not guarantee that it will be reproducible. I spent a LOT of time getting it right, and I am still not happy with the whole thing.

Development Docker Image

First of all we need to build a base image that we can then use to build the various versions of our model server. We could use multistage Docker images, but for the sake of understanding what we are doing, I am just do it manually here.

The base "FROM" is from the latest CUDA image, so that we can do both CPU and GPU builds, and you can find the source in the repo.

In the src folder for this repo, you will find a nice Dockerfile for this. Build with:

Now we need to download Tensorflow Serving. There was a big change between 1.4 and 1.5 where submodules were removed. This change was not completely taken care of in docs and build systems, so we will need to hack it a little bit here.

This takes a LOT of time. On my Xeon E5 24 cores, 521 seconds (9min) for the non optimized CPU version, and up to 1427 seconds (22min) for the optimized GPU version. At the end of the process, you will have these 4 binaries in /usr/local/bin/ of your docker container.

Now we also need to build another dev binary for the purpose of serving Inception. If you have your own models already you do not need to do this step:

On a side note, if you run the build on tensorflow_serving/… you will build many more examples, though it will take longer. May be useful for some use cases.

On a second side note, this is supposed to work, but there is bug reported here that prevents it from working properly. If you really need this, the good news is that the bitnami docker image bitnami/tensorflow-inception:latest contains a working binary at /opt/bitnami/tensorflow-inception/bazel-bin/tensorflow_serving/example/inception_saved_model

Extracting binaries

All the binaries are built, you can now extract them from another shell with:

$ mkdir bin $ for type in gpu cpu; do for version in standard optimized; do docker cp tf-dev:/usr/local/bin/tensorflow_model_server.${type}.${version} ./bin/ done done

In order to save the tool that we will use to unpack the mode, we commit our docker image with

$ docker commit tf-dev ${USER}/tf

You do not have to do this if you intend to use the bitnami image.

Building TF Serving Images

Now that you have all the binaries ready, use them to build relatively small Docker images

Again you will find the sources in the src folder of this repository. You can adapt the files to run the non optimized versions as well

Strangely enough when you come from a more ops background, models that you download cannot be used out of the box. First you need to convert them into a frozen model, add a file for variables, then and only then can you serve them. Not very practical but well, it is what it is…

resize it in several versions from fitting in a 2048x2048 box to fitting in a 128x128 box (we actually did down to 32x32 but the results were not good enough)

Transcode each version in PNG format and in JPEG format

Publish each of the versions to 3 Tensorflow servers at the same time

Display a comparison graph of the results

The code for this app is here and it is packaged as a docker image you can download with

docker pull ronhanson/tensorflow-image-resizer

Deploying in Kubernetes

Deploying the model to nodes

OK so I did not have the chance to have any storage available, and I did not want to have issues of bandwidth and IO for my demo. So I used hostPath to store the model (yes I know, shame on me)

You should be able to do something like

$ scp -r model-data <remote-instance>:model-data

For each of your worker nodes, then SSH into each of them and :

remote-instance:$ sudo mv model-data /
remote-instance:$ exit

OK at this stage you have your model shared on all the nodes. Obviously, if you are running in the cloud, use storage classes and publish the content into it. Message me if you are struggling with this step.

Deploying TF

In the src folder, you will find 3 manifests to deploy 3 Tensorflow Serving instances with a GPU, 1 CPU and finally 8 CPU cores. Adapt them to your needs (change the TF image and the constraints as needed) then deploy with

You will find also in src a manifest to deploy the Python frontend to K8s. Note that for practical reasons we do the resizing server side in this example, which is CPU intensive. You may change the resource requests to improve response time. More on this in the conclusion.

Also note we are using a nodePort, also for practical reasons as we only run a bare metal cluster. You may change that to something more suited for your use case such as a Load Balancer for example.

kubectl create -f src/manifest-tensorflow-image-resizer.yaml

Playing with our cluster

Great!! Now you may connect on any of the nodes IP address on port 30501 (http://<node-ip>:30501) and look at the UI:

(analysis of a 24MP image of a cat)

(analysis of a 14MP image of another cat)

Conclusion

To be honest, some of the conclusions we drew make sense, other are less intuitive. Note that they are findings made on the inception model, and you may have different findings on your own model. We believe this is easy enough to replicate and that you can run (and share!!) your results and thoughts.

Image size (hence bandwidth) has a very low impact on prediction accuracy. This means it is almost always beneficial to resize the image to 1024x1024 or even 512x512 before submitting it to the Tensorflow Serving Model Server. You lose less than 5% accuracy but get a 99% bandwidth saving!! So, If you run a publicly available system, you may work on resizing the image client side to optimize your pipeline. This is easy enough with javascript (actually Ronan’s code has it disabled, but you can very easily switch it on) If you run a datacenter or edge compute, with M2M only, then having a resizing/transcoding pipeline will improve overall performance and save bandwidth.

Image Type DOES matter. You do not see that in the UI directly but you can collect the JSON output from the API, and notice that the same image with the same size is 4 to 5 times longer to analyze in PNG than it is in JPEG. This tells us that clearly compression is good, and that color space is probably not so much of a problem for the performance of the inception model. We would need more research and file types to nail this.

Image size does not have so much impact on the duration of the prediction, at least once we start resizing. The raw sources we used (over 10M pixel each) would take a long time to run, but when we are lower than 4MP, the prediction duration is about constant. This one was pretty weird as I expected the prediction to be much much faster with “smaller” images. Well it is not.

1x GPU (nVidia P4) is roughly equivalent to 8x CPU HT cores of the Intel Xeon once the images have been resized, but it is waaaaay better if you are looking at full scale ones. So I guess here it is a question of use case. So, if you are looking for absolute precision, you will need a GPU to get a decent performance and use large images. If you are looking for speed at the expense of precision/accuracy, then a traditional high density approach with CPUs will probably make more sense.

If you are looking to run this in a real customer scenario, the below could help you improve performance:

If you are looking into replicating and extending this post, note that the application has a full API which you can use to automate testing and processing the JSON output. Details are in the readme. You are welcome to test and publish:

More compilation options;

Other GPU performance metrics. P4 in our case was used because it has a low power requirement and a friendly form factor for the highly modular MSP platform.

K8s vs. other solutions? (in effect, all this can run outside of Docker and K8s, it was just easier for us to set it up that way)

Anyway, we hope you liked this as much as we loved building it! And if you have other use cases or research, Ronan and myself are available to get you started on Kubernetes as well.