From 89a326e038a84e125094f5fad22ebbb8bf568cfe Mon Sep 17 00:00:00 2001 From: Song Jiaming Date: Fri, 15 Oct 2021 07:50:15 +0000 Subject: [PATCH] bigdl 2.0 serving doc (#4995) --- .../cluster-serving-http-example.ipynb | 858 ++++++++++++++++++ .../source/doc/Serving/Example/example.md | 119 +++ .../keras-to-cluster-serving-example.ipynb | 720 +++++++++++++++ .../Example/l08c08_forecasting_with_lstm.py | 75 ++ ..._nlp_constructing_text_generation_model.py | 75 ++ .../tf1-to-cluster-serving-example.ipynb | 573 ++++++++++++ .../doc/Serving/Example/transfer_learning.py | 40 + .../readthedocs/source/doc/Serving/FAQ/faq.md | 53 ++ .../Overview/cluster_serving_overview.jpg | Bin 0 -> 106923 bytes .../Overview/cluster_serving_steps.jpg | Bin 0 -> 62426 bytes .../doc/Serving/Overview/serving-overview.md | 28 + .../ProgrammingGuide/serving-inference.md | 185 ++++ .../ProgrammingGuide/serving-installation.md | 154 ++++ .../Serving/ProgrammingGuide/serving-start.md | 87 ++ .../Serving/QuickStart/serving-quickstart.md | 49 + 15 files changed, 3016 insertions(+) create mode 100644 docs/readthedocs/source/doc/Serving/Example/cluster-serving-http-example.ipynb create mode 100644 docs/readthedocs/source/doc/Serving/Example/example.md create mode 100644 docs/readthedocs/source/doc/Serving/Example/keras-to-cluster-serving-example.ipynb create mode 100644 docs/readthedocs/source/doc/Serving/Example/l08c08_forecasting_with_lstm.py create mode 100644 docs/readthedocs/source/doc/Serving/Example/l10c03_nlp_constructing_text_generation_model.py create mode 100644 docs/readthedocs/source/doc/Serving/Example/tf1-to-cluster-serving-example.ipynb create mode 100644 docs/readthedocs/source/doc/Serving/Example/transfer_learning.py create mode 100644 docs/readthedocs/source/doc/Serving/FAQ/faq.md create mode 100644 docs/readthedocs/source/doc/Serving/Overview/cluster_serving_overview.jpg create mode 100644 docs/readthedocs/source/doc/Serving/Overview/cluster_serving_steps.jpg create mode 100644 docs/readthedocs/source/doc/Serving/Overview/serving-overview.md create mode 100644 docs/readthedocs/source/doc/Serving/ProgrammingGuide/serving-inference.md create mode 100644 docs/readthedocs/source/doc/Serving/ProgrammingGuide/serving-installation.md create mode 100644 docs/readthedocs/source/doc/Serving/ProgrammingGuide/serving-start.md create mode 100644 docs/readthedocs/source/doc/Serving/QuickStart/serving-quickstart.md diff --git a/docs/readthedocs/source/doc/Serving/Example/cluster-serving-http-example.ipynb b/docs/readthedocs/source/doc/Serving/Example/cluster-serving-http-example.ipynb new file mode 100644 index 00000000..231cff66 --- /dev/null +++ b/docs/readthedocs/source/doc/Serving/Example/cluster-serving-http-example.ipynb @@ -0,0 +1,858 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this example, we will use tensorflow.keras package to create a keras image classification application using model MobileNetV2, and transfer the application to Cluster Serving step by step." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Original Keras application\n", + "We will first show an original Keras application, which download the data and preprocess it, then create the MobileNetV2 model to predict." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import tensorflow as tf\n", + "import os\n", + "import PIL" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'2.4.1'" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "tf.__version__" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Found 1000 images belonging to 2 classes.\n" + ] + } + ], + "source": [ + "# Obtain data from url:\"https://storage.googleapis.com/mledu-datasets/cats_and_dogs_filtered.zip\"\n", + "zip_file = tf.keras.utils.get_file(origin=\"https://storage.googleapis.com/mledu-datasets/cats_and_dogs_filtered.zip\",\n", + " fname=\"cats_and_dogs_filtered.zip\", extract=True)\n", + "\n", + "# Find the directory of validation set\n", + "base_dir, _ = os.path.splitext(zip_file)\n", + "test_dir = os.path.join(base_dir, 'validation')\n", + "# Set images size to 160x160x3\n", + "image_size = 160\n", + "\n", + "# Rescale all images by 1./255 and apply image augmentation\n", + "test_datagen = tf.keras.preprocessing.image.ImageDataGenerator(rescale=1./255)\n", + "\n", + "# Flow images using generator to the test_generator\n", + "test_generator = test_datagen.flow_from_directory(\n", + " test_dir,\n", + " target_size=(image_size, image_size),\n", + " batch_size=1,\n", + " class_mode='binary')" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "# Create the base model from the pre-trained model MobileNet V2\n", + "IMG_SHAPE=(160,160,3)\n", + "model = tf.keras.applications.MobileNetV2(input_shape=IMG_SHAPE,\n", + " include_top=False,\n", + " weights='imagenet')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In keras, input could be ndarray, or generator. We could just use `model.predict(test_generator)`. But to simplify, here we just input the first record to model." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[[[[0. 0. 0. ... 0. 0.\n", + " 0. ]\n", + " [0. 0. 0. ... 0. 0.\n", + " 0. ]\n", + " [0. 0. 0. ... 0. 0.\n", + " 0. ]\n", + " [0. 0. 0. ... 0. 0.\n", + " 0. ]\n", + " [0. 0. 0. ... 0. 0.\n", + " 0. ]]\n", + "\n", + " [[0. 0. 0. ... 0. 0.\n", + " 0. ]\n", + " [0.997349 0. 0. ... 0. 0.96874905\n", + " 0. ]\n", + " [1.8385804 0.3380084 2.4926844 ... 0. 0.14267397\n", + " 0. ]\n", + " [0. 0. 3.576158 ... 0. 0.\n", + " 0. ]\n", + " [0. 0. 0. ... 0. 0.\n", + " 0. ]]\n", + "\n", + " [[0. 0. 0. ... 0. 0.\n", + " 0. ]\n", + " [0. 0.0062952 0. ... 0. 0.15311003\n", + " 0. ]\n", + " [0. 1.7324333 1.1691046 ... 0. 0.9847245\n", + " 0. ]\n", + " [0. 0.84404707 3.2351522 ... 0. 0.\n", + " 0. ]\n", + " [0. 0. 0. ... 0. 0.\n", + " 0. ]]\n", + "\n", + " [[0. 0. 0. ... 0. 0.\n", + " 0.3681116 ]\n", + " [0. 3.3440204 0.5372138 ... 0. 0.\n", + " 0.79515934]\n", + " [0. 3.0932055 3.5937624 ... 0. 0.\n", + " 0.66862965]\n", + " [0. 1.4007983 0. ... 0. 0.\n", + " 2.8901892 ]\n", + " [0. 0. 0. ... 0. 0.\n", + " 0. ]]\n", + "\n", + " [[0. 0. 0. ... 0. 0.\n", + " 0.73307323]\n", + " [0. 0. 0. ... 0. 0.\n", + " 2.9129057 ]\n", + " [0. 0. 0.6134901 ... 0. 0.\n", + " 2.7102432 ]\n", + " [0. 0. 0. ... 0. 0.\n", + " 1.8489733 ]\n", + " [0. 0. 0. ... 0. 0.\n", + " 0.22623205]]]]\n" + ] + } + ], + "source": [ + "prediction=model.predict(test_generator.next()[0])\n", + "print(prediction)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Great! Now the Keras application is completed. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Export TensorFlow Saved Model\n", + "Next, we transfer the application to Cluster Serving. The first step is to save the model to SavedModel format." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "WARNING:tensorflow:From /home/user/anaconda3/envs/rec/lib/python3.6/site-packages/tensorflow/python/ops/resource_variable_ops.py:1817: calling BaseResourceVariable.__init__ (from tensorflow.python.ops.resource_variable_ops) with constraint is deprecated and will be removed in a future version.\n", + "Instructions for updating:\n", + "If using Keras pass *_constraint arguments to layers.\n", + "INFO:tensorflow:Assets written to: /tmp/transfer_learning_mobilenetv2/assets\n", + "assets\tsaved_model.pb\tvariables\n" + ] + } + ], + "source": [ + "# Save trained model to ./transfer_learning_mobilenetv2\n", + "model.save('/tmp/transfer_learning_mobilenetv2')\n", + "! ls /tmp/transfer_learning_mobilenetv2" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Deploy Cluster Serving\n", + "After model prepared, we start to deploy it on Cluster Serving.\n", + "\n", + "First install Cluster Serving" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Looking in indexes: http://10.239.45.10:8081/repository/pypi-group/simple, https://pypi.tuna.tsinghua.edu.cn/simple\n", + "Requirement already satisfied: bigdl-serving in /home/user/anaconda3/envs/rec/lib/python3.6/site-packages (0.9.0)\n", + "Requirement already satisfied: opencv-python in /home/user/anaconda3/envs/rec/lib/python3.6/site-packages (from bigdl-serving) (4.5.1.48)\n", + "Requirement already satisfied: httpx in /home/user/anaconda3/envs/rec/lib/python3.6/site-packages (from bigdl-serving) (0.16.1)\n", + "Requirement already satisfied: pyarrow in /home/user/.local/lib/python3.6/site-packages (from bigdl-serving) (1.0.1)\n", + "Requirement already satisfied: redis in /home/user/anaconda3/envs/rec/lib/python3.6/site-packages (from bigdl-serving) (3.5.3)\n", + "Requirement already satisfied: pyyaml in /home/user/anaconda3/envs/rec/lib/python3.6/site-packages (from bigdl-serving) (5.4.1)\n", + "Requirement already satisfied: rfc3986[idna2008]<2,>=1.3 in /home/user/anaconda3/envs/rec/lib/python3.6/site-packages (from httpx->bigdl-serving) (1.4.0)\n", + "Requirement already satisfied: certifi in /home/user/anaconda3/envs/rec/lib/python3.6/site-packages (from httpx->bigdl-serving) (2020.12.5)\n", + "Requirement already satisfied: httpcore==0.12.* in /home/user/anaconda3/envs/rec/lib/python3.6/site-packages (from httpx->bigdl-serving) (0.12.3)\n", + "Requirement already satisfied: sniffio in /home/user/anaconda3/envs/rec/lib/python3.6/site-packages (from httpx->bigdl-serving) (1.2.0)\n", + "Requirement already satisfied: h11==0.* in /home/user/anaconda3/envs/rec/lib/python3.6/site-packages (from httpcore==0.12.*->httpx->bigdl-serving) (0.12.0)\n", + "Requirement already satisfied: contextvars>=2.1 in /home/user/anaconda3/envs/rec/lib/python3.6/site-packages (from sniffio->httpx->bigdl-serving) (2.4)\n", + "Requirement already satisfied: immutables>=0.9 in /home/user/anaconda3/envs/rec/lib/python3.6/site-packages (from contextvars>=2.1->sniffio->httpx->bigdl-serving) (0.14)\n", + "Requirement already satisfied: idna in /home/user/anaconda3/envs/rec/lib/python3.6/site-packages (from rfc3986[idna2008]<2,>=1.3->httpx->bigdl-serving) (2.10)\n", + "Requirement already satisfied: numpy>=1.13.3 in /home/user/anaconda3/envs/rec/lib/python3.6/site-packages (from opencv-python->bigdl-serving) (1.19.2)\n", + "\u001b[33mWARNING: You are using pip version 20.3.3; however, version 21.0.1 is available.\n", + "You should consider upgrading via the '/home/user/anaconda3/envs/rec/bin/python -m pip install --upgrade pip' command.\u001b[0m\n" + ] + } + ], + "source": [ + "! pip install bigdl-serving" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Cluster Serving has been properly set up.\n", + "You did not specify ANALYTICS_ZOO_VERSION, will download 0.9.0\n", + "ANALYTICS_ZOO_VERSION is 0.9.0\n", + "BIGDL_VERSION is 0.12.1\n", + "SPARK_VERSION is 2.4.3\n", + "2.4\n", + "--2021-02-07 10:01:46-- https://repo1.maven.org/maven2/com/intel/analytics/bigdl/bigdl-bigdl_0.12.1-spark_2.4.3/0.9.0/bigdl-bigdl_0.12.1-spark_2.4.3-0.9.0-serving.jar\n", + "Resolving child-prc.intel.com (child-prc.intel.com)... You are installing Cluster Serving by pip, downloading...\n", + "\n", + "SIGHUP received.\n", + "Redirecting output to ‘wget-log.2’.\n" + ] + } + ], + "source": [ + "# we go to a new directory and initialize the environment\n", + "! mkdir cluster-serving\n", + "os.chdir('cluster-serving')\n", + "! cluster-serving-init" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " 2150K .......... .......... .......... .......... .......... 0% 27.0K 5h37m\r\n", + " 2200K .......... .......... .......... .......... .......... 0% 33.6K 5h36m\r\n", + " 2250K .......... .......... .......... .......... .......... 0% 27.3K 5h37m\r\n", + " 2300K .......... .......... .......... .......... .......... 0% 30.3K 5h36m\r\n", + " 2350K .......... .......... .......... .......... .......... 0% 29.7K 5h36m\r\n", + " 2400K .......... .......... .......... .......... .......... 0% 23.7K 5h38m\r\n", + " 2450K .......... .......... .......... .......... .......... 0% 23.4K 5h39m\r\n", + " 2500K .......... .......... .......... .......... .......... 0% 23.4K 5h41m\r\n", + " 2550K .......... .......... .......... .......... .......... 0% 22.3K 5h43m\r\n", + " 2600K .......... .......... .......... ....." + ] + } + ], + "source": [ + "! tail wget-log.2" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# if you encounter slow download issue like above, you can just use following command to download\n", + "# ! wget https://repo1.maven.org/maven2/com/intel/analytics/bigdl/bigdl-bigdl_0.12.1-spark_2.4.3/0.9.0/bigdl-bigdl_0.12.1-spark_2.4.3-0.9.0-serving.jar\n", + "\n", + "# if you are using wget to download, call mv *serving.jar bigdl.jar again after downloaded." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "config.yaml bigdl.jar\r\n" + ] + } + ], + "source": [ + "# After initialization finished, check the directory\n", + "! ls" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We config the model path in `config.yaml` to following (the detail of config is at [Cluster Serving Configuration](https://github.com/intel-analytics/bigdl/blob/master/docs/docs/ClusterServingGuide/ProgrammingGuide.md#2-configuration))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "## BigDL Cluster Serving\n", + "\n", + "model:\n", + " # model path must be provided\n", + " path: /tmp/transfer_learning_mobilenetv2" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "## BigDL Cluster Serving\r\n", + "\r\n", + "model:\r\n", + " # model path must be provided\r\n", + " path: /tmp/transfer_learning_mobilenetv2\r\n", + " # name, default is serving_stream, you need to specify if running multiple servings\r\n", + " name:\r\n", + "data:\r\n", + " # default, localhost:6379\r\n", + " src:\r\n" + ] + } + ], + "source": [ + "! head config.yaml" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Start Cluster Serving\n", + "\n", + "Cluster Serving requires Flink and Redis installed, and corresponded environment variables set, check [Cluster Serving Installation Guide](https://github.com/intel-analytics/bigdl/blob/master/docs/docs/ClusterServingGuide/ProgrammingGuide.md#1-installation) for detail.\n", + "\n", + "Flink cluster should start before Cluster Serving starts, if Flink cluster is not started, call following to start a local Flink cluster." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Starting cluster.\n", + "Starting standalonesession daemon on host my-PC.\n", + "Starting taskexecutor daemon on host my-PC.\n" + ] + } + ], + "source": [ + "! $FLINK_HOME/bin/start-cluster.sh" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "After configuration, start Cluster Serving by `cluster-serving-start` (the detail is at [Cluster Serving Programming Guide](https://github.com/intel-analytics/bigdl/blob/master/docs/docs/ClusterServingGuide/ProgrammingGuide.md#3-launching-service))" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "model_path=\"/tmp/transfer_learning_mobilenetv2\"\n", + "redis_timeout=\"5000\"\n", + "Redis maxmemory is not set, using default value 8G\n", + "redis server started, please check log in redis.log\n", + "OK\n", + "OK\n", + "OK\n", + "redis config maxmemory set to 8G\n", + "OK\n", + "OK\n", + "SLF4J: Class path contains multiple SLF4J bindings.\n", + "SLF4J: Found binding in [jar:file:/home/user/dep/flink-1.11.2/lib/bigdl-bigdl_0.12.0-spark_2.4.3-0.9.0-SNAPSHOT-serving.jar!/org/slf4j/impl/StaticLoggerBinder.class]\n", + "SLF4J: Found binding in [jar:file:/home/user/dep/flink-1.11.2/lib/log4j-slf4j-impl-2.12.1.jar!/org/slf4j/impl/StaticLoggerBinder.class]\n", + "SLF4J: Found binding in [jar:file:/home/user/dep/flink-1.11.2/lib/slf4j-log4j12-1.7.5.jar!/org/slf4j/impl/StaticLoggerBinder.class]\n", + "SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.\n", + "SLF4J: Actual binding is of type [org.slf4j.impl.Log4jLoggerFactory]\n", + "log4j:WARN No appenders could be found for logger (org.apache.flink.client.cli.CliFrontend).\n", + "log4j:WARN Please initialize the log4j system properly.\n", + "log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.\n", + "Starting new Cluster Serving job.\n", + "Cluster Serving job submitted, check log in log-cluster_serving-serving_stream.txt\n", + "To list Cluster Serving job status, use cluster-serving-cli list\n", + "SLF4J: Class path contains multiple SLF4J bindings.\n", + "SLF4J: Found binding in [jar:file:/home/user/dep/flink-1.11.2/lib/bigdl-bigdl_0.12.0-spark_2.4.3-0.9.0-SNAPSHOT-serving.jar!/org/slf4j/impl/StaticLoggerBinder.class]\n", + "SLF4J: Found binding in [jar:file:/home/user/dep/flink-1.11.2/lib/log4j-slf4j-impl-2.12.1.jar!/org/slf4j/impl/StaticLoggerBinder.class]\n", + "SLF4J: Found binding in [jar:file:/home/user/dep/flink-1.11.2/lib/slf4j-log4j12-1.7.5.jar!/org/slf4j/impl/StaticLoggerBinder.class]\n", + "SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.\n", + "SLF4J: Actual binding is of type [org.slf4j.impl.Log4jLoggerFactory]\n", + "log4j:WARN No appenders could be found for logger (org.apache.flink.client.cli.CliFrontend).\n", + "log4j:WARN Please initialize the log4j system properly.\n", + "log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.\n", + "[Full GC (Metadata GC Threshold) 32304K->20432K(1030144K), 0.0213821 secs]\n" + ] + } + ], + "source": [ + "! cluster-serving-start" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Prediction using Cluster Serving\n", + "Next we start Cluster Serving code at python client." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "redis group exist, will not create new one\n", + "redis group exist, will not create new one\n" + ] + } + ], + "source": [ + "from bigdl.serving.client import InputQueue, OutputQueue\n", + "input_queue = InputQueue()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In Cluster Serving, only NdArray is supported as input. Thus, we first transform the generator to ndarray (If you do not know how to transform your input to NdArray, you may get help at [data transform guide](https://github.com/intel-analytics/bigdl/tree/master/docs/docs/ClusterServingGuide/OtherFrameworkUsers#data))" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([[[[0.41176474, 0.50980395, 0.5882353 ],\n", + " [0.42352945, 0.47450984, 0.50980395],\n", + " [0.4901961 , 0.5058824 , 0.5019608 ],\n", + " ...,\n", + " [0.5764706 , 0.6392157 , 0.7019608 ],\n", + " [0.454902 , 0.5176471 , 0.5803922 ],\n", + " [0.3647059 , 0.427451 , 0.4784314 ]],\n", + "\n", + " [[0.31764707, 0.38431376, 0.4156863 ],\n", + " [0.35686275, 0.38431376, 0.40784317],\n", + " [0.34509805, 0.34509805, 0.3529412 ],\n", + " ...,\n", + " [0.5803922 , 0.64705884, 0.6862745 ],\n", + " [0.48627454, 0.5529412 , 0.5921569 ],\n", + " [0.48235297, 0.54509807, 0.59607846]],\n", + "\n", + " [[0.4039216 , 0.4431373 , 0.44705886],\n", + " [0.35686275, 0.36078432, 0.37647063],\n", + " [0.46274513, 0.4431373 , 0.47058827],\n", + " ...,\n", + " [0.53333336, 0.6 , 0.6313726 ],\n", + " [0.47450984, 0.5411765 , 0.5686275 ],\n", + " [0.5137255 , 0.5764706 , 0.627451 ]],\n", + "\n", + " ...,\n", + "\n", + " [[0.44705886, 0.5019608 , 0.54509807],\n", + " [0.42352945, 0.48627454, 0.5372549 ],\n", + " [0.37647063, 0.43921572, 0.49803925],\n", + " ...,\n", + " [0.69411767, 0.69411767, 0.69411767],\n", + " [0.6745098 , 0.6745098 , 0.68235296],\n", + " [0.6392157 , 0.63529414, 0.6666667 ]],\n", + "\n", + " [[0.3647059 , 0.41960788, 0.454902 ],\n", + " [0.35686275, 0.427451 , 0.47450984],\n", + " [0.3254902 , 0.3921569 , 0.454902 ],\n", + " ...,\n", + " [0.5647059 , 0.5647059 , 0.5647059 ],\n", + " [0.627451 , 0.627451 , 0.63529414],\n", + " [0.7176471 , 0.70980394, 0.76470596]],\n", + "\n", + " [[0.34117648, 0.40784317, 0.43529415],\n", + " [0.29803923, 0.37254903, 0.427451 ],\n", + " [0.31764707, 0.3921569 , 0.45882356],\n", + " ...,\n", + " [0.454902 , 0.454902 , 0.46274513],\n", + " [0.5803922 , 0.57254905, 0.6156863 ],\n", + " [0.5137255 , 0.5019608 , 0.58431375]]]], dtype=float32)" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "arr = test_generator.next()[0]\n", + "arr" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Write to Redis successful\n", + "redis group exist, will not create new one\n", + "Write to Redis successful\n" + ] + } + ], + "source": [ + "# Use async api to put and get, you have pass a name arg and use the name to get\n", + "input_queue.enqueue('my-input', t=arr)\n", + "output_queue = OutputQueue()\n", + "prediction = output_queue.query('my-input')\n", + "# Use sync api to predict, this will block until the result is get or timeout\n", + "prediction = input_queue.predict(arr)" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([[[0. , 0. , 0. , ..., 0. ,\n", + " 0. , 0. ],\n", + " [0. , 0. , 0. , ..., 0. ,\n", + " 0. , 0. ],\n", + " [0. , 0. , 0. , ..., 0. ,\n", + " 0. , 0. ],\n", + " [0. , 0. , 0. , ..., 1.3543907 ,\n", + " 0. , 0. ],\n", + " [0. , 0. , 0. , ..., 4.1898136 ,\n", + " 0. , 0. ]],\n", + "\n", + " [[0. , 0. , 0. , ..., 0. ,\n", + " 0. , 0. ],\n", + " [0. , 3.286649 , 0. , ..., 0. ,\n", + " 0. , 0. ],\n", + " [0. , 4.0817494 , 0. , ..., 0. ,\n", + " 0. , 0. ],\n", + " [0. , 3.3224926 , 0. , ..., 1.4220613 ,\n", + " 0. , 0. ],\n", + " [0. , 0. , 0. , ..., 4.9100547 ,\n", + " 0. , 0. ]],\n", + "\n", + " [[0. , 0. , 0. , ..., 0. ,\n", + " 0. , 0. ],\n", + " [0. , 1.5577714 , 0. , ..., 0. ,\n", + " 0. , 0. ],\n", + " [0. , 1.767426 , 0. , ..., 0. ,\n", + " 0. , 0. ],\n", + " [0. , 2.3534465 , 0. , ..., 0. ,\n", + " 0. , 0. ],\n", + " [0. , 0. , 0. , ..., 0.21401057,\n", + " 0. , 0. ]],\n", + "\n", + " [[0. , 0. , 0. , ..., 0. ,\n", + " 0.34797698, 0. ],\n", + " [0. , 1.4496232 , 0. , ..., 0. ,\n", + " 1.6221215 , 0. ],\n", + " [0. , 0.6171873 , 0. , ..., 0. ,\n", + " 0. , 0. ],\n", + " [0. , 0. , 0. , ..., 0. ,\n", + " 0. , 0. ],\n", + " [0. , 0. , 0. , ..., 0. ,\n", + " 0. , 0. ]],\n", + "\n", + " [[0. , 0. , 0. , ..., 0. ,\n", + " 0. , 0. ],\n", + " [0. , 0. , 0. , ..., 0. ,\n", + " 1.192298 , 0. ],\n", + " [0. , 0. , 0. , ..., 0. ,\n", + " 0. , 0. ],\n", + " [0. , 0. , 0. , ..., 0. ,\n", + " 0. , 0. ],\n", + " [0. , 0. , 0. , ..., 0. ,\n", + " 0. , 0. ]]], dtype=float32)" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "prediction" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If everything works well, the result `prediction` would be the exactly the same NdArray object with the output of original Keras model." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next is the way to use http service through python." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# start the http server via jar\n", + "# ! java -jar bigdl-bigdl_0.10.0-spark_2.4.3-0.9.0-SNAPSHOT-http.jar" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If you do not know how to find the jar or other http service, you may get help at [Cluster Serving http guide](https://github.com/intel-analytics/bigdl/blob/master/docs/docs/ClusterServingGuide/ProgrammingGuide.md#3-launching-service)" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "welcome to BigDL web serving frontend" + ] + } + ], + "source": [ + "! curl http://localhost:10020" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Cluster Serving provides an Python util `http_response_to_ndarray` which let user parse http response directly to ndarray, as following." + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [], + "source": [ + "import json\n", + "import requests\n", + "import numpy as np\n", + "from bigdl.serving.client import http_response_to_ndarray" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([[[0. , 0. , 0. , ..., 0. ,\n", + " 0. , 0. ],\n", + " [0. , 0. , 0. , ..., 0. ,\n", + " 0. , 0. ],\n", + " [0. , 0. , 0. , ..., 0. ,\n", + " 0. , 0. ],\n", + " [0. , 0. , 0. , ..., 0. ,\n", + " 0. , 0. ],\n", + " [0. , 0. , 0. , ..., 0. ,\n", + " 0. , 0. ]],\n", + "\n", + " [[0. , 0. , 0. , ..., 0. ,\n", + " 0. , 0. ],\n", + " [0. , 0. , 0. , ..., 0. ,\n", + " 0. , 0. ],\n", + " [0. , 0. , 0. , ..., 0. ,\n", + " 0. , 0. ],\n", + " [0. , 0. , 0. , ..., 0. ,\n", + " 0. , 0. ],\n", + " [0. , 0. , 0. , ..., 0. ,\n", + " 0. , 0. ]],\n", + "\n", + " [[0. , 0. , 0. , ..., 0. ,\n", + " 0. , 0. ],\n", + " [0. , 0. , 0. , ..., 0. ,\n", + " 0. , 0. ],\n", + " [0. , 0. , 0. , ..., 0. ,\n", + " 0. , 0. ],\n", + " [0. , 0. , 0. , ..., 0. ,\n", + " 0. , 0. ],\n", + " [0. , 0. , 0. , ..., 0. ,\n", + " 0. , 0. ]],\n", + "\n", + " [[0. , 0. , 0. , ..., 0. ,\n", + " 0. , 0. ],\n", + " [0. , 0. , 0.7070324 , ..., 0. ,\n", + " 0. , 0. ],\n", + " [0. , 0. , 1.9520156 , ..., 0. ,\n", + " 0. , 0. ],\n", + " [0. , 0. , 0. , ..., 0. ,\n", + " 0. , 0. ],\n", + " [0. , 0. , 0. , ..., 0. ,\n", + " 0. , 0. ]],\n", + "\n", + " [[0. , 0. , 0. , ..., 0. ,\n", + " 0. , 0. ],\n", + " [0. , 0. , 0. , ..., 0. ,\n", + " 0. , 0.45007578],\n", + " [0. , 0. , 0. , ..., 0. ,\n", + " 0. , 0. ],\n", + " [0. , 0. , 0. , ..., 0. ,\n", + " 0. , 0. ],\n", + " [0. , 0. , 0. , ..., 0. ,\n", + " 0. , 0. ]]])" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "url = 'http://localhost:10020/predict'\n", + "d = json.dumps({\"instances\":[{\"floatTensor\": arr.tolist()}]})\n", + "r = requests.post(url, data=d)\n", + "\n", + "http_prediction = http_response_to_ndarray(r)\n", + "http_prediction" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [], + "source": [ + "# don't forget to delete the model you save for this tutorial\n", + "! rm -rf /tmp/transfer_learning_mobilenetv2" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This is the end of this tutorial. If you have any question, you could raise an issue at [BigDL Github](https://github.com/intel-analytics/bigdl/issues)." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.12" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/readthedocs/source/doc/Serving/Example/example.md b/docs/readthedocs/source/doc/Serving/Example/example.md new file mode 100644 index 00000000..94ec156b --- /dev/null +++ b/docs/readthedocs/source/doc/Serving/Example/example.md @@ -0,0 +1,119 @@ +# BigDL Cluster Serving Example + +There are some examples provided for new user or existing Tensorflow user. +## End-to-end Example +### TFDataSet: +[l08c08_forecasting_with_lstm.py](https://github.com/intel-analytics/bigdl/tree/master/docs/docs/ClusterServingGuide/OtherFrameworkUsers/l08c08_forecasting_with_lstm.py) +### Tokenizer: +[l10c03_nlp_constructing_text_generation_model.py](https://github.com/intel-analytics/bigdl/tree/master/docs/docs/ClusterServingGuide/OtherFrameworkUsers/l10c03_nlp_constructing_text_generation_model.py) +### ImageDataGenerator: +[transfer_learning.py](https://github.com/intel-analytics/bigdl/tree/master/docs/docs/ClusterServingGuide/OtherFrameworkUsers/transfer_learning.py) + +## Model/Data Convert Guide +This guide is for users who: + +* have written local code of Tensorflow, Pytorch(to be added) +* have used specified data type of a specific framework, e.g. TFDataSet +* want to deploy the local code on Cluster Serving but do not know how to write client code (Cluster Serving takes Numpy Ndarray as input, other types need to transform in advance). + +**If you have the above needs but fail to find the solution below, please [create issue here](https://github.com/intel-analytics/bigdl/issues) + +## Tensorflow + +Model - includes savedModel, Frozen Graph (savedModel is recommended). + +Data - includes [TFDataSet](#tfdataset), [Tokenizer](#tokenizer), [ImageDataGenerator](#imagedatagenerator) + +Notes - includes tips to notice, includes [savedModel tips](#notes---use-savedmodel) + +### Model - ckpt to savedModel +#### tensorflow all version +This method works in all version of TF + +You need to create the graph, get the output layer, create place holder for input, load the ckpt then save the model +``` +# --- code you need to write +input_layer = tf.placeholder(...) +model = YourModel(...) +output_layer = model.your_output_layer() +# --- code you need to write +with tf.Session() as sess: + saver = tf.train.Saver() + saver.restore(sess, tf.train.latest_checkpoint(FLAGS.ckpt_path)) + tf.saved_model.simple_save(sess, + FLAGS.export_path, + inputs={ + 'input_layer': input_layer + }, + outputs={"output_layer": output_layer}) +``` + +#### tensorflow >= 1.15 +This method works if you are familiar with savedModel signature, and tensorflow >= 1.15 + +model graph could be load via `.meta`, and load ckpt then save the model, signature_def_map is required to provide +``` +# provide signature first +inputs = tf.placeholder(...) +outputs = tf.add(inputs, inputs) +tensor_info_input = tf.saved_model.utils.build_tensor_info(inputs) +tensor_info_output = tf.saved_model.utils.build_tensor_info(outputs) + +prediction_signature = ( + tf.saved_model.signature_def_utils.build_signature_def( + inputs={'x_input': tensor_info_input}, + outputs={'y_output': tensor_info_output}, + method_name=tf.saved_model.signature_constants.PREDICT_METHOD_NAME)) + + +# Your ckpt file is prefix.meta, prefix.index, etc +ckpt_prefix = 'model/model.ckpt-xxxx' +export_dir = 'saved_model' + +loaded_graph = tf.Graph() +with tf.Session(graph=loaded_graph) as sess: + # load + loader = tf.train.import_meta_graph(ckpt_prefix + '.meta') + loader.restore(sess, ckpt_prefix) + + # export + builder = tf.saved_model.builder.SavedModelBuilder(export_dir) + builder.add_meta_graph_and_variables(sess, + [tf.saved_model.tag_constants.TRAINING, tf.saved_model.tag_constants.SERVING],signature_def_map={ + tf.saved_model.signature_constants.DEFAULT_SERVING_SIGNATURE_DEF_KEY: + prediction_signature + } + ) + builder.save() +``` +### Model - Keras to savedModel +#### tensorflow > 2.0 +``` +model = tf.keras.models.load_model("./model.h5") +tf.saved_model.save(model, "saved_model") +``` +### Model - ckpt to Frozen Graph +[freeze checkpoint example](https://github.com/intel-analytics/bigdl/tree/master/pyzoo/bigdl/examples/tensorflow/freeze_checkpoint) +### Notes - Use SavedModel +If model has single tensor input, then nothing to notice. + +**If model has multiple input, please notice following.** + +When export, savedModel would store the inputs in alphabetical order. Use `saved_model_cli show --dir . --all` to see the order. e.g. +``` +signature_def['serving_default']: + The given SavedModel SignatureDef contains the following input(s): + inputs['id1'] tensor_info: + dtype: DT_INT32 + shape: (-1, 512) + name: id1:0 + inputs['id2'] tensor_info: + dtype: DT_INT32 + shape: (-1, 512) + name: id2:0 + +``` + +when enqueue to Cluster Serving, follow this order +### Data +To transform following data type to Numpy Ndarray, following examples are provided diff --git a/docs/readthedocs/source/doc/Serving/Example/keras-to-cluster-serving-example.ipynb b/docs/readthedocs/source/doc/Serving/Example/keras-to-cluster-serving-example.ipynb new file mode 100644 index 00000000..dab5e65c --- /dev/null +++ b/docs/readthedocs/source/doc/Serving/Example/keras-to-cluster-serving-example.ipynb @@ -0,0 +1,720 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this example, we will use tensorflow.keras package to create a keras image classification application using model MobileNetV2, and transfer the application to Cluster Serving step by step." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Original Keras application\n", + "We will first show an original Keras application, which download the data and preprocess it, then create the MobileNetV2 model to predict." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import tensorflow as tf\n", + "import os\n", + "import PIL" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'2.2.0'" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "tf.__version__" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Found 1000 images belonging to 2 classes.\n" + ] + } + ], + "source": [ + "# Obtain data from url:\"https://storage.googleapis.com/mledu-datasets/cats_and_dogs_filtered.zip\"\n", + "zip_file = tf.keras.utils.get_file(origin=\"https://storage.googleapis.com/mledu-datasets/cats_and_dogs_filtered.zip\",\n", + " fname=\"cats_and_dogs_filtered.zip\", extract=True)\n", + "\n", + "# Find the directory of validation set\n", + "base_dir, _ = os.path.splitext(zip_file)\n", + "test_dir = os.path.join(base_dir, 'validation')\n", + "# Set images size to 160x160x3\n", + "image_size = 160\n", + "\n", + "# Rescale all images by 1./255 and apply image augmentation\n", + "test_datagen = tf.keras.preprocessing.image.ImageDataGenerator(rescale=1./255)\n", + "\n", + "# Flow images using generator to the test_generator\n", + "test_generator = test_datagen.flow_from_directory(\n", + " test_dir,\n", + " target_size=(image_size, image_size),\n", + " batch_size=1,\n", + " class_mode='binary')" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "# Create the base model from the pre-trained model MobileNet V2\n", + "IMG_SHAPE=(160,160,3)\n", + "model = tf.keras.applications.MobileNetV2(input_shape=IMG_SHAPE,\n", + " include_top=False,\n", + " weights='imagenet')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In keras, input could be ndarray, or generator. We could just use `model.predict(test_generator)`. But to simplify, here we just input the first record to model." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[[[[0. 0. 0. ... 0. 0.\n", + " 0. ]\n", + " [0. 0. 0. ... 0. 0.\n", + " 0. ]\n", + " [0. 0. 0. ... 0. 0.\n", + " 0. ]\n", + " [0. 0. 0.8406992 ... 0. 0.\n", + " 0. ]\n", + " [0. 0. 0. ... 0. 0.\n", + " 0. ]]\n", + "\n", + " [[0. 0. 0. ... 0. 0.\n", + " 0. ]\n", + " [0. 0. 0. ... 0. 0.81465054\n", + " 0. ]\n", + " [0. 0. 0. ... 0. 0.6572695\n", + " 0.23970175]\n", + " [0. 0. 0. ... 0. 1.2423501\n", + " 0.8024192 ]\n", + " [0. 0. 0. ... 0. 0.\n", + " 0. ]]\n", + "\n", + " [[0. 0. 0. ... 0. 0.\n", + " 0. ]\n", + " [0. 0. 0. ... 0. 5.185735\n", + " 0.21723604]\n", + " [0. 0. 0. ... 0. 4.6399093\n", + " 0.40124178]\n", + " [0.3284886 0. 0. ... 0. 5.295811\n", + " 3.4133787 ]\n", + " [0. 0. 0. ... 0. 0.\n", + " 0. ]]\n", + "\n", + " [[0. 0. 0. ... 0. 0.\n", + " 0. ]\n", + " [0. 0. 0. ... 0. 0.52712107\n", + " 0.20341969]\n", + " [0. 0. 0. ... 0. 0.8279238\n", + " 0.42696333]\n", + " [0. 0. 0. ... 0. 1.0344229\n", + " 1.5225778 ]\n", + " [0. 0. 0. ... 0. 0.\n", + " 0. ]]\n", + "\n", + " [[0. 0. 0. ... 0. 0.\n", + " 0. ]\n", + " [0. 0. 0. ... 0. 0.\n", + " 1.3237557 ]\n", + " [0. 0. 0. ... 0. 0.\n", + " 1.3395147 ]\n", + " [0. 0. 0. ... 0. 0.\n", + " 0. ]\n", + " [0. 0. 0. ... 0. 0.\n", + " 0. ]]]]\n" + ] + } + ], + "source": [ + "prediction=model.predict(test_generator.next()[0])\n", + "print(prediction)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Great! Now the Keras application is completed. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Export TensorFlow SavedModel\n", + "Next, we transfer the application to Cluster Serving. The first step is to save the model to SavedModel format." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "WARNING:tensorflow:From /home/user/anaconda3/envs/rec/lib/python3.6/site-packages/tensorflow/python/ops/resource_variable_ops.py:1817: calling BaseResourceVariable.__init__ (from tensorflow.python.ops.resource_variable_ops) with constraint is deprecated and will be removed in a future version.\n", + "Instructions for updating:\n", + "If using Keras pass *_constraint arguments to layers.\n", + "INFO:tensorflow:Assets written to: /tmp/transfer_learning_mobilenetv2/assets\n", + "assets\tsaved_model.pb\tvariables\n" + ] + } + ], + "source": [ + "# Save trained model to ./transfer_learning_mobilenetv2\n", + "model.save('/tmp/transfer_learning_mobilenetv2')\n", + "! ls /tmp/transfer_learning_mobilenetv2" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Deploy Cluster Serving\n", + "After model prepared, we start to deploy it on Cluster Serving.\n", + "\n", + "First install Cluster Serving" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Looking in indexes: http://10.239.45.10:8081/repository/pypi-group/simple, https://pypi.tuna.tsinghua.edu.cn/simple\n", + "Requirement already satisfied: bigdl-serving in /home/user/anaconda3/envs/rec/lib/python3.6/site-packages (0.9.0)\n", + "Requirement already satisfied: opencv-python in /home/user/anaconda3/envs/rec/lib/python3.6/site-packages (from bigdl-serving) (4.5.1.48)\n", + "Requirement already satisfied: httpx in /home/user/anaconda3/envs/rec/lib/python3.6/site-packages (from bigdl-serving) (0.16.1)\n", + "Requirement already satisfied: pyarrow in /home/user/.local/lib/python3.6/site-packages (from bigdl-serving) (1.0.1)\n", + "Requirement already satisfied: redis in /home/user/anaconda3/envs/rec/lib/python3.6/site-packages (from bigdl-serving) (3.5.3)\n", + "Requirement already satisfied: pyyaml in /home/user/anaconda3/envs/rec/lib/python3.6/site-packages (from bigdl-serving) (5.4.1)\n", + "Requirement already satisfied: rfc3986[idna2008]<2,>=1.3 in /home/user/anaconda3/envs/rec/lib/python3.6/site-packages (from httpx->bigdl-serving) (1.4.0)\n", + "Requirement already satisfied: certifi in /home/user/anaconda3/envs/rec/lib/python3.6/site-packages (from httpx->bigdl-serving) (2020.12.5)\n", + "Requirement already satisfied: httpcore==0.12.* in /home/user/anaconda3/envs/rec/lib/python3.6/site-packages (from httpx->bigdl-serving) (0.12.3)\n", + "Requirement already satisfied: sniffio in /home/user/anaconda3/envs/rec/lib/python3.6/site-packages (from httpx->bigdl-serving) (1.2.0)\n", + "Requirement already satisfied: h11==0.* in /home/user/anaconda3/envs/rec/lib/python3.6/site-packages (from httpcore==0.12.*->httpx->bigdl-serving) (0.12.0)\n", + "Requirement already satisfied: contextvars>=2.1 in /home/user/anaconda3/envs/rec/lib/python3.6/site-packages (from sniffio->httpx->bigdl-serving) (2.4)\n", + "Requirement already satisfied: immutables>=0.9 in /home/user/anaconda3/envs/rec/lib/python3.6/site-packages (from contextvars>=2.1->sniffio->httpx->bigdl-serving) (0.14)\n", + "Requirement already satisfied: idna in /home/user/anaconda3/envs/rec/lib/python3.6/site-packages (from rfc3986[idna2008]<2,>=1.3->httpx->bigdl-serving) (2.10)\n", + "Requirement already satisfied: numpy>=1.13.3 in /home/user/anaconda3/envs/rec/lib/python3.6/site-packages (from opencv-python->bigdl-serving) (1.19.2)\n", + "\u001b[33mWARNING: You are using pip version 20.3.3; however, version 21.0.1 is available.\n", + "You should consider upgrading via the '/home/user/anaconda3/envs/rec/bin/python -m pip install --upgrade pip' command.\u001b[0m\n" + ] + } + ], + "source": [ + "! pip install bigdl-serving" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Cluster Serving has been properly set up.\n", + "You did not specify ANALYTICS_ZOO_VERSION, will download 0.9.0\n", + "ANALYTICS_ZOO_VERSION is 0.9.0\n", + "BIGDL_VERSION is 0.12.1\n", + "SPARK_VERSION is 2.4.3\n", + "2.4\n", + "--2021-02-07 10:01:46-- https://repo1.maven.org/maven2/com/intel/analytics/bigdl/bigdl-bigdl_0.12.1-spark_2.4.3/0.9.0/bigdl-bigdl_0.12.1-spark_2.4.3-0.9.0-serving.jar\n", + "Resolving child-prc.intel.com (child-prc.intel.com)... You are installing Cluster Serving by pip, downloading...\n", + "\n", + "SIGHUP received.\n", + "Redirecting output to ‘wget-log.2’.\n" + ] + } + ], + "source": [ + "# we go to a new directory and initialize the environment\n", + "! mkdir cluster-serving\n", + "os.chdir('cluster-serving')\n", + "! cluster-serving-init" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " 2150K .......... .......... .......... .......... .......... 0% 27.0K 5h37m\r\n", + " 2200K .......... .......... .......... .......... .......... 0% 33.6K 5h36m\r\n", + " 2250K .......... .......... .......... .......... .......... 0% 27.3K 5h37m\r\n", + " 2300K .......... .......... .......... .......... .......... 0% 30.3K 5h36m\r\n", + " 2350K .......... .......... .......... .......... .......... 0% 29.7K 5h36m\r\n", + " 2400K .......... .......... .......... .......... .......... 0% 23.7K 5h38m\r\n", + " 2450K .......... .......... .......... .......... .......... 0% 23.4K 5h39m\r\n", + " 2500K .......... .......... .......... .......... .......... 0% 23.4K 5h41m\r\n", + " 2550K .......... .......... .......... .......... .......... 0% 22.3K 5h43m\r\n", + " 2600K .......... .......... .......... ....." + ] + } + ], + "source": [ + "! tail wget-log.2" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# if you encounter slow download issue like above, you can just use following command to download\n", + "# ! wget https://repo1.maven.org/maven2/com/intel/analytics/bigdl/bigdl-bigdl_0.12.1-spark_2.4.3/0.9.0/bigdl-bigdl_0.12.1-spark_2.4.3-0.9.0-serving.jar\n", + "\n", + "# if you are using wget to download, or get \"bigdl-xxx-serving.jar\" after \"ls\", please call mv *serving.jar bigdl.jar after downloaded." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "config.yaml bigdl.jar\r\n" + ] + } + ], + "source": [ + "# After initialization finished, check the directory\n", + "! ls" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We config the model path in `config.yaml` to following (the detail of config is at [Cluster Serving Configuration](https://github.com/intel-analytics/bigdl/blob/master/docs/docs/ClusterServingGuide/ProgrammingGuide.md#2-configuration))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "## BigDL Cluster Serving\n", + "\n", + "model:\n", + " # model path must be provided\n", + " path: /tmp/transfer_learning_mobilenetv2" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "## BigDL Cluster Serving\r\n", + "\r\n", + "model:\r\n", + " # model path must be provided\r\n", + " path: /tmp/transfer_learning_mobilenetv2\r\n", + " # name, default is serving_stream, you need to specify if running multiple servings\r\n", + " name:\r\n", + "data:\r\n", + " # default, localhost:6379\r\n", + " src:\r\n" + ] + } + ], + "source": [ + "! head config.yaml" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Start Cluster Serving\n", + "\n", + "Cluster Serving requires Flink and Redis installed, and corresponded environment variables set, check [Cluster Serving Installation Guide](https://github.com/intel-analytics/bigdl/blob/master/docs/docs/ClusterServingGuide/ProgrammingGuide.md#1-installation) for detail.\n", + "\n", + "Flink cluster should start before Cluster Serving starts, if Flink cluster is not started, call following to start a local Flink cluster." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Starting cluster.\n", + "Starting standalonesession daemon on host my-PC.\n", + "Starting taskexecutor daemon on host my-PC.\n" + ] + } + ], + "source": [ + "! $FLINK_HOME/bin/start-cluster.sh" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "After configuration, start Cluster Serving by `cluster-serving-start` (the detail is at [Cluster Serving Programming Guide](https://github.com/intel-analytics/bigdl/blob/master/docs/docs/ClusterServingGuide/ProgrammingGuide.md#3-launching-service))" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "model_path=\"/tmp/transfer_learning_mobilenetv2\"\n", + "redis_timeout=\"5000\"\n", + "Redis maxmemory is not set, using default value 8G\n", + "redis server started, please check log in redis.log\n", + "OK\n", + "OK\n", + "OK\n", + "redis config maxmemory set to 8G\n", + "OK\n", + "OK\n", + "SLF4J: Class path contains multiple SLF4J bindings.\n", + "SLF4J: Found binding in [jar:file:/home/user/dep/flink-1.11.2/lib/bigdl-bigdl_0.12.0-spark_2.4.3-0.9.0-SNAPSHOT-serving.jar!/org/slf4j/impl/StaticLoggerBinder.class]\n", + "SLF4J: Found binding in [jar:file:/home/user/dep/flink-1.11.2/lib/log4j-slf4j-impl-2.12.1.jar!/org/slf4j/impl/StaticLoggerBinder.class]\n", + "SLF4J: Found binding in [jar:file:/home/user/dep/flink-1.11.2/lib/slf4j-log4j12-1.7.5.jar!/org/slf4j/impl/StaticLoggerBinder.class]\n", + "SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.\n", + "SLF4J: Actual binding is of type [org.slf4j.impl.Log4jLoggerFactory]\n", + "log4j:WARN No appenders could be found for logger (org.apache.flink.client.cli.CliFrontend).\n", + "log4j:WARN Please initialize the log4j system properly.\n", + "log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.\n", + "Starting new Cluster Serving job.\n", + "Cluster Serving job submitted, check log in log-cluster_serving-serving_stream.txt\n", + "To list Cluster Serving job status, use cluster-serving-cli list\n", + "SLF4J: Class path contains multiple SLF4J bindings.\n", + "SLF4J: Found binding in [jar:file:/home/user/dep/flink-1.11.2/lib/bigdl-bigdl_0.12.0-spark_2.4.3-0.9.0-SNAPSHOT-serving.jar!/org/slf4j/impl/StaticLoggerBinder.class]\n", + "SLF4J: Found binding in [jar:file:/home/user/dep/flink-1.11.2/lib/log4j-slf4j-impl-2.12.1.jar!/org/slf4j/impl/StaticLoggerBinder.class]\n", + "SLF4J: Found binding in [jar:file:/home/user/dep/flink-1.11.2/lib/slf4j-log4j12-1.7.5.jar!/org/slf4j/impl/StaticLoggerBinder.class]\n", + "SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.\n", + "SLF4J: Actual binding is of type [org.slf4j.impl.Log4jLoggerFactory]\n", + "log4j:WARN No appenders could be found for logger (org.apache.flink.client.cli.CliFrontend).\n", + "log4j:WARN Please initialize the log4j system properly.\n", + "log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.\n", + "[Full GC (Metadata GC Threshold) 32304K->20432K(1030144K), 0.0213821 secs]\n" + ] + } + ], + "source": [ + "! cluster-serving-start" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Prediction using Cluster Serving\n", + "Next we start Cluster Serving code at python client." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "redis group exist, will not create new one\n", + "redis group exist, will not create new one\n" + ] + } + ], + "source": [ + "from bigdl.serving.client import InputQueue, OutputQueue\n", + "input_queue = InputQueue()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In Cluster Serving, only NdArray is supported as input. Thus, we first transform the generator to ndarray (If you do not know how to transform your input to NdArray, you may get help at [data transform guide](https://github.com/intel-analytics/bigdl/tree/master/docs/docs/ClusterServingGuide/OtherFrameworkUsers#data))" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([[[[0.12156864, 0.11764707, 0.10980393],\n", + " [0.12156864, 0.11764707, 0.10980393],\n", + " [0.11764707, 0.1137255 , 0.10588236],\n", + " ...,\n", + " [0.28627452, 0.29803923, 0.22352943],\n", + " [0.24705884, 0.25882354, 0.18431373],\n", + " [0.24705884, 0.24705884, 0.20000002]],\n", + "\n", + " [[0.15686275, 0.15294118, 0.14509805],\n", + " [0.13725491, 0.13333334, 0.1254902 ],\n", + " [0.09803922, 0.09411766, 0.08627451],\n", + " ...,\n", + " [0.31764707, 0.3254902 , 0.27450982],\n", + " [0.31764707, 0.3254902 , 0.27058825],\n", + " [0.2784314 , 0.2784314 , 0.2392157 ]],\n", + "\n", + " [[0.21960786, 0.21568629, 0.20784315],\n", + " [0.23137257, 0.227451 , 0.21960786],\n", + " [0.24705884, 0.24313727, 0.23529413],\n", + " ...,\n", + " [0.29411766, 0.29803923, 0.27450982],\n", + " [0.26666668, 0.27058825, 0.2392157 ],\n", + " [0.30588236, 0.30588236, 0.26666668]],\n", + "\n", + " ...,\n", + "\n", + " [[0.35686275, 0.3019608 , 0.15686275],\n", + " [0.38431376, 0.29803923, 0.14509805],\n", + " [0.36862746, 0.25490198, 0.12156864],\n", + " ...,\n", + " [0.1764706 , 0.08627451, 0.01568628],\n", + " [0.16862746, 0.08627451, 0.00392157],\n", + " [0.1764706 , 0.08627451, 0.03137255]],\n", + "\n", + " [[0.30980393, 0.2784314 , 0.13333334],\n", + " [0.3529412 , 0.29411766, 0.14117648],\n", + " [0.3529412 , 0.26666668, 0.12156864],\n", + " ...,\n", + " [0.1764706 , 0.08627451, 0.01568628],\n", + " [0.17254902, 0.08235294, 0.01176471],\n", + " [0.18039216, 0.09019608, 0.03529412]],\n", + "\n", + " [[0.30588236, 0.27450982, 0.13333334],\n", + " [0.33333334, 0.28627452, 0.12941177],\n", + " [0.3372549 , 0.26666668, 0.11764707],\n", + " ...,\n", + " [0.19607845, 0.09411766, 0.03529412],\n", + " [0.18039216, 0.07843138, 0.02745098],\n", + " [0.1764706 , 0.08627451, 0.03137255]]]], dtype=float32)" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "arr = test_generator.next()[0]\n", + "arr" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Write to Redis successful\n", + "redis group exist, will not create new one\n", + "Write to Redis successful\n" + ] + } + ], + "source": [ + "# Use async api to put and get, you have pass a name arg and use the name to get\n", + "input_queue.enqueue('my-input', t=arr)\n", + "output_queue = OutputQueue()\n", + "prediction = output_queue.query('my-input')\n", + "# Use sync api to predict, this will block until the result is get or timeout\n", + "prediction = input_queue.predict(arr)" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([[[0. , 0. , 0. , ..., 0. ,\n", + " 0. , 0. ],\n", + " [0. , 0. , 0. , ..., 0. ,\n", + " 0. , 0. ],\n", + " [0. , 0. , 0. , ..., 0. ,\n", + " 0. , 0. ],\n", + " [0. , 0. , 0. , ..., 1.3543907 ,\n", + " 0. , 0. ],\n", + " [0. , 0. , 0. , ..., 4.1898136 ,\n", + " 0. , 0. ]],\n", + "\n", + " [[0. , 0. , 0. , ..., 0. ,\n", + " 0. , 0. ],\n", + " [0. , 3.286649 , 0. , ..., 0. ,\n", + " 0. , 0. ],\n", + " [0. , 4.0817494 , 0. , ..., 0. ,\n", + " 0. , 0. ],\n", + " [0. , 3.3224926 , 0. , ..., 1.4220613 ,\n", + " 0. , 0. ],\n", + " [0. , 0. , 0. , ..., 4.9100547 ,\n", + " 0. , 0. ]],\n", + "\n", + " [[0. , 0. , 0. , ..., 0. ,\n", + " 0. , 0. ],\n", + " [0. , 1.5577714 , 0. , ..., 0. ,\n", + " 0. , 0. ],\n", + " [0. , 1.767426 , 0. , ..., 0. ,\n", + " 0. , 0. ],\n", + " [0. , 2.3534465 , 0. , ..., 0. ,\n", + " 0. , 0. ],\n", + " [0. , 0. , 0. , ..., 0.21401057,\n", + " 0. , 0. ]],\n", + "\n", + " [[0. , 0. , 0. , ..., 0. ,\n", + " 0.34797698, 0. ],\n", + " [0. , 1.4496232 , 0. , ..., 0. ,\n", + " 1.6221215 , 0. ],\n", + " [0. , 0.6171873 , 0. , ..., 0. ,\n", + " 0. , 0. ],\n", + " [0. , 0. , 0. , ..., 0. ,\n", + " 0. , 0. ],\n", + " [0. , 0. , 0. , ..., 0. ,\n", + " 0. , 0. ]],\n", + "\n", + " [[0. , 0. , 0. , ..., 0. ,\n", + " 0. , 0. ],\n", + " [0. , 0. , 0. , ..., 0. ,\n", + " 1.192298 , 0. ],\n", + " [0. , 0. , 0. , ..., 0. ,\n", + " 0. , 0. ],\n", + " [0. , 0. , 0. , ..., 0. ,\n", + " 0. , 0. ],\n", + " [0. , 0. , 0. , ..., 0. ,\n", + " 0. , 0. ]]], dtype=float32)" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "prediction" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If everything works well, the result `prediction` would be the exactly the same NdArray object with the output of original Keras model." + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [], + "source": [ + "# don't forget to delete the model you save for this tutorial\n", + "! rm -rf /tmp/transfer_learning_mobilenetv2" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This is the end of this tutorial. If you have any question, you could raise an issue at [BigDL Github](https://github.com/intel-analytics/bigdl/issues)." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.10" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/readthedocs/source/doc/Serving/Example/l08c08_forecasting_with_lstm.py b/docs/readthedocs/source/doc/Serving/Example/l08c08_forecasting_with_lstm.py new file mode 100644 index 00000000..612017b8 --- /dev/null +++ b/docs/readthedocs/source/doc/Serving/Example/l08c08_forecasting_with_lstm.py @@ -0,0 +1,75 @@ +# Related url: https://github.com/tensorflow/examples/blob/master/courses/udacity_intro_to_tensorflow_for_deep_learning/l08c08_forecasting_with_lstm.ipynb +# Forecasting with LSTM +import numpy as np +import tensorflow as tf +import tensorflow.keras as keras + +# Get the trend with time and slope +def trend(time, slope=0): + return slope * time + + +# Get a specific pattern, which can be customerized +def seasonal_pattern(season_time): + return np.where(season_time < 0.4, + np.cos(season_time * 2 * np.pi), + 1 / np.exp(3 * season_time)) + +# Repeats the same pattern at each period +def seasonality(time, period, amplitude=1, phase=0): + season_time = ((time + phase) % period) / period + return amplitude * seasonal_pattern(season_time) + +# Obtain a random white noise +def white_noise(time, noise_level=1, seed=None): + rnd = np.random.RandomState(seed) + return rnd.randn(len(time)) * noise_level + +# Convert the series to dataset form +def ndarray_to_dataset(ndarray): + return tf.data.Dataset.from_tensor_slices(ndarray) + +# Convert the series to dataset with some modifications +def sequential_window_dataset(series, window_size): + series = tf.expand_dims(series, axis=-1) + ds = ndarray_to_dataset(series) + ds = ds.window(window_size + 1, shift=window_size, drop_remainder=True) + ds = ds.flat_map(lambda window: window.batch(window_size + 1)) + ds = ds.map(lambda window: (window[:-1], window[1:])) + return ds.batch(1).prefetch(1) + +# Convert dataset form to ndarray +def dataset_to_ndarray(dataset): + array=list(dataset.as_numpy_iterator()) + return np.ndarray(array) + +# Generate some raw test data +time_range=4 * 365 + 1 +time = np.arange(time_range) + +slope = 0.05 +baseline = 10 +amplitude = 40 +series = baseline + trend(time, slope) + seasonality(time, period=365, amplitude=amplitude) + +noise_level = 5 +noise = white_noise(time, noise_level, seed=42) + +series += noise + +# Modify the raw test data with DataSet form +tf.random.set_seed(42) +np.random.seed(42) + +window_size = 30 +test_set = sequential_window_dataset(series, window_size) + +# Convert the DataSet form data to ndarry +#pre_in=series[np.newaxis, :, np.newaxis] +test_array=dataset_to_ndarray(test_set) + +# Load the saved LSTM model +model=tf.keras.models.load_model("path/to/model") + +# Predict with LSTM model +rnn_forecast_nd = model.predict(test_array) diff --git a/docs/readthedocs/source/doc/Serving/Example/l10c03_nlp_constructing_text_generation_model.py b/docs/readthedocs/source/doc/Serving/Example/l10c03_nlp_constructing_text_generation_model.py new file mode 100644 index 00000000..3d27b9a0 --- /dev/null +++ b/docs/readthedocs/source/doc/Serving/Example/l10c03_nlp_constructing_text_generation_model.py @@ -0,0 +1,75 @@ +# Related url: https://github.com/tensorflow/examples/blob/master/courses/udacity_intro_to_tensorflow_for_deep_learning/l10c03_nlp_constructing_text_generation_model.ipynb +# Generating some new lyrics from the trained model + +import tensorflow as tf +from tensorflow.keras.preprocessing.text import Tokenizer +from tensorflow.keras.preprocessing.sequence import pad_sequences + +# Other imports for processing data +import string +import numpy as np +import pandas as pd + +# DATA PREPROCESSING +# First to get the dataset of the Song Lyrics dataset on Kaggle by: +# !wget --no-check-certificate \ +# https://drive.google.com/uc?id=1LiJFZd41ofrWoBtW-pMYsfz1w8Ny0Bj8 \ +# -O /tmp/songdata.csv + +# Then to generate a tokenizer with the songdata.csv +def tokenize_corpus(corpus, num_words=-1): + # Fit a Tokenizer on the corpus + if num_words > -1: + tokenizer = Tokenizer(num_words=num_words) + else: + tokenizer = Tokenizer() + tokenizer.fit_on_texts(corpus) + return tokenizer + +def create_lyrics_corpus(dataset, field): + # Remove all other punctuation + dataset[field] = dataset[field].str.replace('[{}]'.format(string.punctuation), '') + # Make it lowercase + dataset[field] = dataset[field].str.lower() + # Make it one long string to split by line + lyrics = dataset[field].str.cat() + corpus = lyrics.split('\n') + # Remove any trailing whitespace + for l in range(len(corpus)): + corpus[l] = corpus[l].rstrip() + # Remove any empty lines + corpus = [l for l in corpus if l != ''] + + return corpus + +# Read the dataset from csv +dataset = pd.read_csv('/tmp/songdata.csv', dtype=str) +# Create the corpus using the 'text' column containing lyrics +corpus = create_lyrics_corpus(dataset, 'text') +# Tokenize the corpus +tokenizer = tokenize_corpus(corpus) + +# Get the uniform input length (max_sequence_len) of the model +max_sequence_len=0 +for line in corpus: + token_list = tokenizer.texts_to_sequences([line])[0] + max_sequence_len=max(max_sequence_len,len(token_list)) + +# Load the saved model which is trained on the Song Lyrics dataset +model=tf.keras.models.load_model("path/to/model") + +# Generate new lyrics with some "seed text" +seed_text = "im feeling chills" # seed text can be customerized +next_words = 100 # this defined the length of the new lyrics + +for _ in range(next_words): + token_list = tokenizer.texts_to_sequences([seed_text])[0] # convert the seed text to ndarray + token_list = pad_sequences([token_list], maxlen=max_sequence_len - 1, padding='pre') # pad the input for equal length + predicted = np.argmax(model.predict(token_list), axis=-1) # get the predicted word index + output_word = "" + for word, index in tokenizer.word_index.items(): + if index == predicted: + output_word = word + break + seed_text += " " + output_word # add the predicted word to the seed text +print(seed_text) diff --git a/docs/readthedocs/source/doc/Serving/Example/tf1-to-cluster-serving-example.ipynb b/docs/readthedocs/source/doc/Serving/Example/tf1-to-cluster-serving-example.ipynb new file mode 100644 index 00000000..53e0b188 --- /dev/null +++ b/docs/readthedocs/source/doc/Serving/Example/tf1-to-cluster-serving-example.ipynb @@ -0,0 +1,573 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "reported-geometry", + "metadata": {}, + "source": [ + "In this example, we will use tensorflow v1 (version 1.15) to create a simple MLP model, and transfer the application to Cluster Serving step by step.\n", + "\n", + "This tutorial is recommended for Tensorflow v1 user only. If you are not Tensorflow v1 user, the keras tutorial [here](#keras-to-cluster-serving-example.ipynb) is more recommended." + ] + }, + { + "cell_type": "markdown", + "id": "athletic-trance", + "metadata": {}, + "source": [ + "### Original Tensorflow v1 Application" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "olive-dutch", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'1.15.0'" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import tensorflow as tf\n", + "tf.__version__" + ] + }, + { + "cell_type": "markdown", + "id": "vertical-recall", + "metadata": {}, + "source": [ + "We first define the Tensorflow graph, and create some data." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "tropical-clinton", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "WARNING:tensorflow:From :24: where (from tensorflow.python.ops.array_ops) is deprecated and will be removed in a future version.\n", + "Instructions for updating:\n", + "Use tf.where in 2.0, which has the same broadcast rule as np.where\n" + ] + } + ], + "source": [ + "g = tf.Graph()\n", + "with g.as_default():\n", + " \n", + " # Graph Inputs\n", + " features = tf.placeholder(dtype=tf.float32, \n", + " shape=[None, 2], name='features')\n", + " targets = tf.placeholder(dtype=tf.float32, \n", + " shape=[None, 1], name='targets')\n", + "\n", + " # Model Parameters\n", + " weights = tf.Variable(tf.zeros(shape=[2, 1], \n", + " dtype=tf.float32), name='weights')\n", + " bias = tf.Variable([[0.]], dtype=tf.float32, name='bias')\n", + " \n", + "\n", + " \n", + " # Forward Pass\n", + " linear = tf.add(tf.matmul(features, weights), bias, name='linear')\n", + " ones = tf.ones(shape=tf.shape(linear)) \n", + " zeros = tf.zeros(shape=tf.shape(linear))\n", + " prediction = tf.where(condition=tf.less(linear, 0.),\n", + " x=zeros, \n", + " y=ones, \n", + " name='prediction')\n", + " \n", + " # Backward Pass\n", + " errors = targets - prediction\n", + " weight_update = tf.assign_add(weights, \n", + " tf.reshape(errors * features, (2, 1)),\n", + " name='weight_update')\n", + " bias_update = tf.assign_add(bias, errors,\n", + " name='bias_update')\n", + " \n", + " train = tf.group(weight_update, bias_update, name='train')\n", + " \n", + " saver = tf.train.Saver(name='saver')\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "legislative-boutique", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "((3, 2), (3,))" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import numpy as np\n", + "x_train, y_train = np.array([[1,2],[3,4],[1,3]]), np.array([1,2,1])\n", + "x_train.shape, y_train.shape" + ] + }, + { + "cell_type": "markdown", + "id": "coated-grill", + "metadata": {}, + "source": [ + "### Export TensorFlow SavedModel\n", + "Then, we train the graph and in the `with tf.Session`, we save the graph to SavedModel. The detailed code is following, and we could see the prediction result is `[1]` with input `[1,2]`." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "detailed-message", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Model parameters:\n", + "\n", + "Weights:\n", + " [[15.]\n", + " [20.]]\n", + "Bias: [[5.]]\n", + "[[1.]\n", + " [1.]\n", + " [1.]]\n", + "WARNING:tensorflow:From :26: simple_save (from tensorflow.python.saved_model.simple_save) is deprecated and will be removed in a future version.\n", + "Instructions for updating:\n", + "This function will only be available through the v1 compatibility library as tf.compat.v1.saved_model.simple_save.\n", + "WARNING:tensorflow:From /home/user/anaconda3/envs/tf1/lib/python3.7/site-packages/tensorflow_core/python/saved_model/signature_def_utils_impl.py:201: build_tensor_info (from tensorflow.python.saved_model.utils_impl) is deprecated and will be removed in a future version.\n", + "Instructions for updating:\n", + "This function will only be available through the v1 compatibility library as tf.compat.v1.saved_model.utils.build_tensor_info or tf.compat.v1.saved_model.build_tensor_info.\n", + "INFO:tensorflow:Assets added to graph.\n", + "INFO:tensorflow:No assets to write.\n", + "INFO:tensorflow:SavedModel written to: /tmp/mlp_tf1/saved_model.pb\n" + ] + } + ], + "source": [ + "with tf.Session(graph=g) as sess:\n", + " \n", + " sess.run(tf.global_variables_initializer())\n", + " \n", + " for epoch in range(5):\n", + " for example, target in zip(x_train, y_train):\n", + " feed_dict = {'features:0': example.reshape(-1, 2),\n", + " 'targets:0': target.reshape(-1, 1)}\n", + " _ = sess.run(['train'], feed_dict=feed_dict)\n", + "\n", + "\n", + " w, b = sess.run(['weights:0', 'bias:0']) \n", + " print('Model parameters:\\n')\n", + " print('Weights:\\n', w)\n", + " print('Bias:', b)\n", + "\n", + " saver.save(sess, save_path='perceptron')\n", + " \n", + " pred = sess.run('prediction:0', feed_dict={features: x_train})\n", + " print(pred)\n", + " \n", + " # in this session, save the model to savedModel format\n", + " inputs = dict([(features.name, features)])\n", + " outputs = dict([(prediction.name, prediction)])\n", + " inputs, outputs\n", + " tf.saved_model.simple_save(sess, \"/tmp/mlp_tf1\", inputs, outputs)" + ] + }, + { + "cell_type": "markdown", + "id": "consolidated-newport", + "metadata": {}, + "source": [ + "### Deploy Cluster Serving\n", + "After model prepared, we start to deploy it on Cluster Serving.\n", + "\n", + "First install Cluster Serving" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "inner-texas", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Looking in indexes: http://10.239.45.10:8081/repository/pypi-group/simple, https://pypi.tuna.tsinghua.edu.cn/simple\n", + "Collecting bigdl-serving\n", + " Downloading http://10.239.45.10:8081/repository/pypi-group/packages/bigdl-serving/0.9.0/analytics_zoo_serving-0.9.0-20201216-py2.py3-none-any.whl (17 kB)\n", + "Requirement already satisfied: httpx in /home/user/anaconda3/envs/tf1/lib/python3.7/site-packages (from bigdl-serving) (0.17.1)\n", + "Requirement already satisfied: pyarrow in /home/user/anaconda3/envs/tf1/lib/python3.7/site-packages (from bigdl-serving) (3.0.0)\n", + "Requirement already satisfied: pyyaml in /home/user/anaconda3/envs/tf1/lib/python3.7/site-packages (from bigdl-serving) (5.4.1)\n", + "Requirement already satisfied: redis in /home/user/anaconda3/envs/tf1/lib/python3.7/site-packages (from bigdl-serving) (3.5.3)\n", + "Requirement already satisfied: opencv-python in /home/user/anaconda3/envs/tf1/lib/python3.7/site-packages (from bigdl-serving) (4.5.1.48)\n", + "Requirement already satisfied: certifi in /home/user/anaconda3/envs/tf1/lib/python3.7/site-packages (from httpx->bigdl-serving) (2020.12.5)\n", + "Requirement already satisfied: httpcore<0.13,>=0.12.1 in /home/user/anaconda3/envs/tf1/lib/python3.7/site-packages (from httpx->bigdl-serving) (0.12.3)\n", + "Requirement already satisfied: rfc3986[idna2008]<2,>=1.3 in /home/user/anaconda3/envs/tf1/lib/python3.7/site-packages (from httpx->bigdl-serving) (1.4.0)\n", + "Requirement already satisfied: sniffio in /home/user/anaconda3/envs/tf1/lib/python3.7/site-packages (from httpx->bigdl-serving) (1.2.0)\n", + "Requirement already satisfied: h11==0.* in /home/user/anaconda3/envs/tf1/lib/python3.7/site-packages (from httpcore<0.13,>=0.12.1->httpx->bigdl-serving) (0.12.0)\n", + "Requirement already satisfied: idna in /home/user/anaconda3/envs/tf1/lib/python3.7/site-packages (from rfc3986[idna2008]<2,>=1.3->httpx->bigdl-serving) (3.1)\n", + "Requirement already satisfied: numpy>=1.14.5 in /home/user/anaconda3/envs/tf1/lib/python3.7/site-packages (from opencv-python->bigdl-serving) (1.20.1)\n", + "Installing collected packages: bigdl-serving\n", + "Successfully installed bigdl-serving-0.9.0\n" + ] + } + ], + "source": [ + "! pip install bigdl-serving" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "working-terrorism", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Trying to find config file in /home/user/anaconda3/envs/tf1/lib/python3.7/site-packages/bigdl/conf/config.yaml\r\n", + "Config file found in pip package, copying...\r\n", + "Config file ready.\r\n", + "Cluster Serving has been properly set up.\r\n", + "You did not specify ANALYTICS_ZOO_VERSION, will download 0.9.0\r\n", + "ANALYTICS_ZOO_VERSION is 0.9.0\r\n", + "BIGDL_VERSION is 0.12.1\r\n", + "SPARK_VERSION is 2.4.3\r\n", + "2.4\r\n", + "You are installing Cluster Serving by pip, downloading...\r\n" + ] + } + ], + "source": [ + "import os\n", + "! mkdir cluster-serving\n", + "os.chdir('cluster-serving')\n", + "! cluster-serving-init" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "excited-exception", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " 2800K .......... .......... .......... .......... .......... 0% 11.8M 19m20s\r\n", + " 2850K .......... .......... .......... .......... .......... 0% 11.3M 19m1s\r\n", + " 2900K .......... .......... .......... .......... .......... 0% 8.60M 18m43s\r\n", + " 2950K .......... .......... .......... .......... .......... 0% 11.9M 18m25s\r\n", + " 3000K .......... .......... .......... .......... .......... 0% 11.8M 18m7s\r\n", + " 3050K .......... .......... .......... .......... .......... 0% 674K 18m4s\r\n", + " 3100K .......... .......... .......... .......... .......... 0% 418K 18m9s\r\n", + " 3150K .......... .......... .......... .......... .......... 0% 1.05M 18m0s\r\n", + " 3200K .......... .......... .......... .......... .......... 0% 750K 17m56s\r\n", + " 3250K .......... .......... .......... ...." + ] + } + ], + "source": [ + "! tail wget-log" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "casual-premium", + "metadata": {}, + "outputs": [], + "source": [ + "# if you encounter slow download issue like above, you can just use following command to download\n", + "# ! wget https://repo1.maven.org/maven2/com/intel/analytics/bigdl/bigdl-bigdl_0.12.1-spark_2.4.3/0.9.0/bigdl-bigdl_0.12.1-spark_2.4.3-0.9.0-serving.jar\n", + "\n", + "# if you are using wget to download, or get \"bigdl-xxx-serving.jar\" after \"ls\", please call mv *serving.jar bigdl.jar after downloaded." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "ruled-bermuda", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "bigdl-bigdl_0.12.1-spark_2.4.3-0.9.0-serving.jar config.yaml wget-log\r\n" + ] + } + ], + "source": [ + "# After initialization finished, check the directory\n", + "! ls" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "computational-rehabilitation", + "metadata": {}, + "outputs": [], + "source": [ + "# Call mv *serving.jar bigdl.jar as mentioned above\n", + "! mv *serving.jar bigdl.jar" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "personal-central", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "config.yaml wget-log bigdl.jar\r\n" + ] + } + ], + "source": [ + "! ls" + ] + }, + { + "cell_type": "markdown", + "id": "combined-stability", + "metadata": {}, + "source": [ + "We config the model path in `config.yaml` to following (the detail of config is at [Cluster Serving Configuration](https://github.com/intel-analytics/bigdl/blob/master/docs/docs/ClusterServingGuide/ProgrammingGuide.md#2-configuration))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "received-hayes", + "metadata": {}, + "outputs": [], + "source": [ + "## BigDL Cluster Serving\n", + "\n", + "model:\n", + " # model path must be provided\n", + " path: /tmp/mlp_tf1" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "satellite-honey", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "## BigDL Cluster Serving\r\n", + "\r\n", + "model:\r\n", + " # model path must be provided\r\n", + " path: /tmp/mlp_tf1\r\n", + " # name, default is serving_stream, you need to specify if running multiple servings\r\n", + " name:\r\n", + "data:\r\n", + " # default, localhost:6379\r\n", + " src:\r\n" + ] + } + ], + "source": [ + "! head config.yaml" + ] + }, + { + "cell_type": "markdown", + "id": "planned-hometown", + "metadata": {}, + "source": [ + "### Start Cluster Serving\n", + "\n", + "Cluster Serving requires Flink and Redis installed, and corresponded environment variables set, check [Cluster Serving Installation Guide](https://github.com/intel-analytics/bigdl/blob/master/docs/docs/ClusterServingGuide/ProgrammingGuide.md#1-installation) for detail.\n", + "\n", + "Flink cluster should start before Cluster Serving starts, if Flink cluster is not started, call following to start a local Flink cluster." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "antique-melbourne", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Starting cluster.\n", + "Starting standalonesession daemon on host user-PC.\n", + "Starting taskexecutor daemon on host user-PC.\n" + ] + } + ], + "source": [ + "! $FLINK_HOME/bin/start-cluster.sh" + ] + }, + { + "cell_type": "markdown", + "id": "interested-bench", + "metadata": {}, + "source": [ + "After configuration, start Cluster Serving by `cluster-serving-start` (the detail is at [Cluster Serving Programming Guide](https://github.com/intel-analytics/bigdl/blob/master/docs/docs/ClusterServingGuide/ProgrammingGuide.md#3-launching-service))" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "modern-monster", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "model_path=\"/tmp/mlp_tf1\"\n", + "redis_timeout=\"5000\"\n", + "Redis maxmemory is not set, using default value 8G\n", + "redis server started, please check log in redis.log\n", + "OK\n", + "OK\n", + "OK\n", + "redis config maxmemory set to 8G\n", + "OK\n", + "OK\n", + "Starting new Cluster Serving job.\n", + "Cluster Serving job submitted, check log in log-cluster_serving-serving_stream.txt\n", + "To list Cluster Serving job status, use cluster-serving-cli list\n", + "{maxmem=null, timeout=5000}timeout getted: 5000\n" + ] + } + ], + "source": [ + "! cluster-serving-start" + ] + }, + { + "cell_type": "markdown", + "id": "improved-rough", + "metadata": {}, + "source": [ + "### Prediction using Cluster Serving\n", + "Next we start Cluster Serving code at python client." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "immune-madness", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "redis group exist, will not create new one\n", + "redis group exist, will not create new one\n", + "Write to Redis successful\n", + "redis group exist, will not create new one\n", + "Write to Redis successful\n" + ] + } + ], + "source": [ + "from bigdl.serving.client import InputQueue, OutputQueue\n", + "input_queue = InputQueue()\n", + "# Use async api to put and get, you have pass a name arg and use the name to get\n", + "arr = np.array([1,2])\n", + "input_queue.enqueue('my-input', t=arr)\n", + "output_queue = OutputQueue()\n", + "prediction = output_queue.query('my-input')\n", + "# Use sync api to predict, this will block until the result is get or timeout\n", + "prediction = input_queue.predict(arr)" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "signal-attention", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([1.], dtype=float32)" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "prediction" + ] + }, + { + "cell_type": "markdown", + "id": "suitable-selection", + "metadata": {}, + "source": [ + "The `prediction` result would be the same as using Tensorflow.\n", + "\n", + "This is the end of this tutorial. If you have any question, you could raise an issue at [BigDL Github](https://github.com/intel-analytics/bigdl/issues)." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/readthedocs/source/doc/Serving/Example/transfer_learning.py b/docs/readthedocs/source/doc/Serving/Example/transfer_learning.py new file mode 100644 index 00000000..9777ea70 --- /dev/null +++ b/docs/readthedocs/source/doc/Serving/Example/transfer_learning.py @@ -0,0 +1,40 @@ +# Related url: https://github.com/tensorflow/docs/blob/master/site/en/r1/tutorials/images/transfer_learning.ipynb +# Categorize image to cat or dog +import os +import tensorflow.compat.v1 as tf +from tensorflow import keras + +# Obtain data from url:"https://storage.googleapis.com/mledu-datasets/cats_and_dogs_filtered.zip" +zip_file = tf.keras.utils.get_file(origin="https://storage.googleapis.com/mledu-datasets/cats_and_dogs_filtered.zip", + fname="cats_and_dogs_filtered.zip", extract=True) + +# Find the directory of validation set +base_dir, _ = os.path.splitext(zip_file) +test_dir = os.path.join(base_dir, 'validation') + +# Set images size to 160x160x3 +image_size = 160 + +# Rescale all images by 1./255 and apply image augmentation +test_datagen = keras.preprocessing.image.ImageDataGenerator(rescale=1./255) + +# Flow images using generator to the test_generator +test_generator = test_datagen.flow_from_directory( + test_dir, + target_size=(image_size, image_size), + batch_size=1, + class_mode='binary') + +# Convert the next data of ImageDataGenerator to ndarray +def convert_to_ndarray(ImageGenerator): + return ImageGenerator.next()[0] + +# Load model from its path +model=tf.keras.models.load_model("path/to/model") + +# Convert each image in test_generator to ndarray and predict with model +max_length=test_generator.__len__() +for i in range(max_length): # number of image to predict can be altered + test_input=convert_to_ndarray(test_generator) + prediction=model.predict(test_input) + diff --git a/docs/readthedocs/source/doc/Serving/FAQ/faq.md b/docs/readthedocs/source/doc/Serving/FAQ/faq.md new file mode 100644 index 00000000..812d07e3 --- /dev/null +++ b/docs/readthedocs/source/doc/Serving/FAQ/faq.md @@ -0,0 +1,53 @@ +# BigDL Cluster Serving FAQ + +## General Debug Guide +You could use following guide to debug if serving is not working properly. + +### Check if Cluster Serving environment is ready +Run following commands in terminal +``` +echo $FLINK_HOME +echo $REDIS_HOME +``` +the output directory +``` +/path/to/flink-version +/path/to/redis-version +``` + +should be displayed, otherwise, go to [Programming Guide](ProgrammingGuide.md) **Installation** section. + +### Check if Flink Cluster is working +Run following commands in terminal +``` +netstat -tnlp +``` +output like following should be displayed, `6123,8081` is Flink default port usage. +``` +tcp6 0 0 :::6123 :::* LISTEN xxxxx/java +tcp6 0 0 :::8081 :::* LISTEN xxxxx/java +``` +if not, run `$FLINK_HOME/bin/start-cluster.sh` to start Flink cluster. + +After that, check Flink log in `$FLINK_HOME/log/`, check the log file of `flink-xxx-standalone-xxx.log` and `flink-xxx-taskexecutor-xxx.log` to make sure there is no error. + +If the port could not bind in this step, kill the program which use the port, and `$FLINK_HOME/bin/stop-cluster.sh && $FLINK_HOME/bin/start-cluster.sh` to restart Flink cluster. +### Check if Cluster Serving is running +``` +$FLINK_HOME/bin/flink list +``` +output of Cluster Serving job information should be displayed, if not, go to [Programming Guide](ProgrammingGuide.md) **Launching Service** section to make sure you call `cluster-serving-start` correctly. + + + +### Troubleshooting + +1. `Duplicate registration of device factory for type XLA_CPU with the same priority 50` + +This error is caused by Flink ClassLoader. Please put cluster serving related jars into `${FLINK_HOME}/lib`. + +2. `servable Manager config dir not exist` + +Check if `servables.yaml` exists in current directory. If not, download from [github](https://github.com/intel-analytics/bigdl/blob/master/ppml/trusted-realtime-ml/scala/docker-graphene/servables.yaml). +### Still, I get no result +If you still get empty result, raise issue [here](https://github.com/intel-analytics/bigdl/issues) and post the output/log of your serving job. diff --git a/docs/readthedocs/source/doc/Serving/Overview/cluster_serving_overview.jpg b/docs/readthedocs/source/doc/Serving/Overview/cluster_serving_overview.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6edbc9c90ed24db12714774e046982e9ca85f671 GIT binary patch literal 106923 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

M+7h$9$4gO5a|W>9|7V`7BjqBH{G>dBE#$ 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 QhyNXi|2OXd{-5#x1JbEO=>Px# literal 0 HcmV?d00001 diff --git a/docs/readthedocs/source/doc/Serving/Overview/cluster_serving_steps.jpg b/docs/readthedocs/source/doc/Serving/Overview/cluster_serving_steps.jpg new file mode 100644 index 0000000000000000000000000000000000000000..74fb2752af094e286235d85e8394fa49e1076d8a GIT binary patch literal 62426 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

H%Xw$;wfEc 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 literal 0 HcmV?d00001 diff --git a/docs/readthedocs/source/doc/Serving/Overview/serving-overview.md b/docs/readthedocs/source/doc/Serving/Overview/serving-overview.md new file mode 100644 index 00000000..f2d8757b --- /dev/null +++ b/docs/readthedocs/source/doc/Serving/Overview/serving-overview.md @@ -0,0 +1,28 @@ +# BigDL Cluster Serving Overview +BigDL Cluster Serving is a lightweight distributed, real-time serving solution that supports a wide range of deep learning models (such as TensorFlow, PyTorch, Caffe, BigDL and OpenVINO models). It provides a simple pub/sub API, so that the users can easily send their inference requests to the input queue (using a simple Python API); Cluster Serving will then automatically manage the scale-out and real-time model inference across a large cluster (using distributed streaming frameworks such as Apache Spark Streaming, Apache Flink, etc.) + +The overall architecture of BigDL Cluster Serving solution is illustrated as below: + +![overview](cluster_serving_overview.jpg) + +## Workflow Overview +The figure below illustrates the simple 3-step "Prepare-Launch-Inference" workflow for Cluster Serving. + +![steps](cluster_serving_steps.jpg) + +#### 1. Install and prepare Cluster Serving environment on a local node: + +- Copy a previously trained model to the local node; currently TensorFlow, PyTorch, Caffe, BigDL and OpenVINO models are supported. +- Install BigDL Cluster Serving on the local node (e.g., using a single pip install command) +- Configure Cluster Server on the local node, including the file path to the trained model and the address of the cluster (such as Apache Hadoop YARN cluster, K8s cluster, etc.). +Please note that you only need to deploy the Cluster Serving solution on a single local node, and NO modifications are needed for the (YARN or K8s) cluster. + +#### 2. Launch the Cluster Serving service + +You can launch the Cluster Serving service by running the startup script on the local node. Under the hood, Cluster Serving will automatically deploy the trained model and serve the model inference requests across the cluster in a distributed fashion. You may monitor its runtime status (such as inference throughput) using TensorBoard. + +#### 3. Distributed, real-time (streaming) inference + +Cluster Serving provides a simple pub/sub API to the users, so that you can easily send the inference requests to an input queue (currently Redis Streams is used) using a simple Python API. + +Cluster Serving will then read the requests from the Redis stream, run the distributed real-time inference across the cluster (using Flink), and return the results back through Redis. As a result, you may get the inference results again using a simple Python API. diff --git a/docs/readthedocs/source/doc/Serving/ProgrammingGuide/serving-inference.md b/docs/readthedocs/source/doc/Serving/ProgrammingGuide/serving-inference.md new file mode 100644 index 00000000..097d1e00 --- /dev/null +++ b/docs/readthedocs/source/doc/Serving/ProgrammingGuide/serving-inference.md @@ -0,0 +1,185 @@ +# BigDL Cluster Serving Programming Guide + +## Model Inference +Once you finish the installation and service launch, you could do inference using Cluster Serving client API. + +We support Python API and HTTP RESTful API for conducting inference with Data Pipeline in Cluster Serving. + +### Python API +For Python API, the requirements of python packages are `opencv-python`(for raw image only), `pyyaml`, `redis`. You can use `InputQueue` and `OutputQueue` to connect to data pipeline by providing the pipeline url, e.g. `my_input_queue = InputQueue(host, port)` and `my_output_queue = OutputQueue(host, port)`. If parameters are not provided, default url `localhost:6379` would be used. + +We provide some basic usages here, for more details, please see [API Guide](APIGuide.md). + +To input data to queue, you need a `InputQueue` instance, and using `enqueue` method, for each input, give a key correspond to your model or give arbitrary key if your model does not care about it. + +To enqueue an image +``` +from bigdl.serving.client import InputQueue +input_api = InputQueue() +input_api.enqueue('my-image1', user_define_key={"path: 'path/to/image1'}) +``` +To enqueue an instance containing 1 image and 2 ndarray +``` +from bigdl.serving.client import InputQueue +import numpy as np +input_api = InputQueue() +t1 = np.array([1,2]) +t2 = np.array([[1,2], [3,4]]) +input_api.enqueue('my-instance', img={"path": 'path/to/image'}, tensor1=t1, tensor2=t2) +``` +There are 4 types of inputs in total, string, image, tensor, sparse tensor, which could represents nearly all types of models. For more details of usage, go to [API Guide](APIGuide.md) + +To get data from queue, you need a `OutputQueue` instance, and using `query` or `dequeue` method. The `query` method takes image uri as parameter and returns the corresponding result. The `dequeue` method takes no parameter and just returns all results and also delete them in data queue. See following example. +``` +from bigdl.serving.client import OutputQueue +output_api = OutputQueue() +img1_result = output_api.query('img1') +all_result = output_api.dequeue() # the output queue is empty after this code +``` +Consider the code above, +``` +img1_result = output_api.query('img1') +``` +##### Sync API +Python API is a pub-sub schema async API. Specifically, thread would not block once you call `enqueue` method. If you want the thread to block, see this section. + +To use sync API, create a `InputQueue` instance with `sync=True` and `frontend_url=frontend_server_url` argument. +``` +from bigdl.serving.client import InputQueue +input_api = InputQueue(sync=True, frontend_url=frontend_server_url) +response = input_api.predict(request_json_string) +print(response.text) +``` +example of `request_json_string` is +``` +'{ + "instances" : [ { + "ids" : [ 100.0, 88.0 ] + }] +}' +``` +This API is also a python support of [Restful API](#restful-api) section, so for more details of input format, refer to it. +### RESTful API +RESTful API uses serving HTTP server. +This part describes API endpoints and end-to-end examples on usage. +The requests and responses are in JSON format. The composition of them depends on the requests type or verb. See the APIs for details. +In case of error, all APIs will return a JSON object in the response body with error as key and the error message as the value: +``` +{ + "error": +} +``` +#### Predict API +URL +``` +POST http://host:port/predict +``` +Request Example for images as inputs: +``` +curl -d \ +'{ + "instances": [ + { + "image": "/9j/4AAQSkZJRgABAQEASABIAAD/7RcEUGhvdG9za..." + }, + { + "image": "/9j/4AAQSkZJRgABAQEASABIAAD/7RcEUGhvdG9za..." + } + ] +}' \ +-X POST http://host:port/predict +``` +Response Example +``` +{ + "predictions": [ + "{value=[[903,0.1306194]]}", + "{value=[[903,0.1306194]]}" + ] +} +``` +Request Example for tensor as inputs: +``` +curl -d \ +'{ + "instances" : [ { + "ids" : [ 100.0, 88.0 ] + }, { + "ids" : [ 100.0, 88.0 ] + } ] +}' \ +-X POST http://host:port/predict +``` +Response Example +``` +{ + "predictions": [ + "{value=[[1,0.6427843]]}", + "{value=[[1,0.6427842]]}" + ] +} +``` +Another request example for composition of scalars and tensors. +``` +curl -d \ + '{ + "instances" : [ { + "intScalar" : 12345, + "floatScalar" : 3.14159, + "stringScalar" : "hello, world. hello, arrow.", + "intTensor" : [ 7756, 9549, 1094, 9808, 4959, 3831, 3926, 6578, 1870, 1741 ], + "floatTensor" : [ 0.6804766, 0.30136853, 0.17394465, 0.44770062, 0.20275897, 0.32762378, 0.45966738, 0.30405098, 0.62053126, 0.7037923 ], + "stringTensor" : [ "come", "on", "united" ], + "intTensor2" : [ [ 1, 2 ], [ 3, 4 ], [ 5, 6 ] ], + "floatTensor2" : [ [ [ 0.2, 0.3 ], [ 0.5, 0.6 ] ], [ [ 0.2, 0.3 ], [ 0.5, 0.6 ] ] ], + "stringTensor2" : [ [ [ [ "come", "on", "united" ], [ "come", "on", "united" ], [ "come", "on", "united" ], [ "come", "on", "united" ] ], [ [ "come", "on", "united" ], [ "come", "on", "united" ], [ "come", "on", "united" ], [ "come", "on", "united" ] ] ], [ [ [ "come", "on", "united" ], [ "come", "on", "united" ], [ "come", "on", "united" ], [ "come", "on", "united" ] ], [ [ "come", "on", "united" ], [ "come", "on", "united" ], [ "come", "on", "united" ], [ "come", "on", "united" ] ] ] ] + }] +}' \ +-X POST http://host:port/predict +``` +Another request example for composition of sparse and dense tensors. +``` +curl -d \ +'{ + "instances" : [ { + "sparseTensor" : { + "shape" : [ 100, 10000, 10 ], + "data" : [ 0.2, 0.5, 3.45, 6.78 ], + "indices" : [ [ 1, 1, 1 ], [ 2, 2, 2 ], [ 3, 3, 3 ], [ 4, 4, 4 ] ] + }, + "intTensor2" : [ [ 1, 2 ], [ 3, 4 ], [ 5, 6 ] ] + }] +}' \ +-X POST http://host:port/predict +``` + + +#### Metrics API +URL +``` +GET http://host:port/metrics +``` +Response example: +``` +[ + { + name: "bigdl.serving.redis.get", + count: 810, + meanRate: 12.627772820651845, + min: 0, + max: 25, + mean: 0.9687099303718213, + median: 0.928579, + stdDev: 0.8150031623593447, + _75thPercentile: 1.000047, + _95thPercentile: 1.141443, + _98thPercentile: 1.268665, + _99thPercentile: 1.608387, + _999thPercentile: 25.874584 + } +] +``` +## Logs and Visualization +To see outputs/logs, go to FLink UI -> job -> taskmanager, (`localhost:8081` by default), or go to `${FLINK_HOME}/logs` + +To visualize the statistics, e.g. performance, go to Flink UI -> job -> metrics, and select the statistic to monitor diff --git a/docs/readthedocs/source/doc/Serving/ProgrammingGuide/serving-installation.md b/docs/readthedocs/source/doc/Serving/ProgrammingGuide/serving-installation.md new file mode 100644 index 00000000..70029f7a --- /dev/null +++ b/docs/readthedocs/source/doc/Serving/ProgrammingGuide/serving-installation.md @@ -0,0 +1,154 @@ +# BigDL Cluster Serving Programming Guide + +## Installation +It is recommended to install Cluster Serving by pulling the pre-built Docker image to your local node, which have packaged all the required dependencies. Alternatively, you may also manually install Cluster Serving (through either pip or direct downloading), Redis on the local node. +#### Docker +``` +docker pull intelanalytics/bigdl-cluster-serving +``` +then, (or directly run `docker run`, it will pull the image if it does not exist) +``` +docker run --name cluster-serving -itd --net=host intelanalytics/bigdl-cluster-serving:0.9.0 +``` +Log into the container +``` +docker exec -it cluster-serving bash +``` +`cd ./cluster-serving`, you can see all the environments prepared. + +#### Manual installation + +##### Requirements +Non-Docker users need to install [Flink 1.10.0+](https://archive.apache.org/dist/flink/flink-1.10.0/), 1.10.0 by default, [Redis 5.0.0+](https://redis.io/topics/quickstart), 5.0.5 by default. + +For users do not have above dependencies, we provide following command to quickly set up. + +Redis +``` +$ export REDIS_VERSION=5.0.5 +$ wget http://download.redis.io/releases/redis-${REDIS_VERSION}.tar.gz && \ + tar xzf redis-${REDIS_VERSION}.tar.gz && \ + rm redis-${REDIS_VERSION}.tar.gz && \ + cd redis-${REDIS_VERSION} && \ + make +``` + +Flink +``` +$ export FLINK_VERSION=1.11.2 +$ wget https://archive.apache.org/dist/flink/flink-${FLINK_VERSION}/flink-${FLINK_VERSION}-bin-scala_2.11.tgz && \ + tar xzf flink-${FLINK_VERSION}-bin-scala_2.11.tgz && \ + rm flink-${FLINK_VERSION}-bin-scala_2.11.tgz.tgz +``` + +After preparing dependencies above, make sure the environment variable `$FLINK_HOME` (/path/to/flink-FLINK_VERSION-bin), `$REDIS_HOME`(/path/to/redis-REDIS_VERSION) is set before following steps. + +#### Install release version +``` +pip install bigdl-serving +``` +#### Install nightly version +Download package from [here](https://sourceforge.net/projects/bigdl/files/cluster-serving-py/), run following command to install Cluster Serving +``` +pip install analytics_zoo_serving-*.whl +``` +For users who need to deploy and start Cluster Serving, run `cluster-serving-init` to download and prepare dependencies. + +For users who need to do inference, aka. predict data only, the environment is ready. + +## Configuration +### Set up cluster +Cluster Serving uses Flink cluster, make sure you have it according to [Installation](#1-installation). + +For docker user, the cluster should be already started. You could use `netstat -tnlp | grep 8081` to check if Flink REST port is working, if not, call `$FLINK_HOME/bin/start-cluster.sh` to start Flink cluster. + +If you need to start Flink on yarn, refer to [Flink on Yarn](https://ci.apache.org/projects/flink/flink-docs-stable/deployment/resource-providers/yarn.html), or K8s, refer to [Flink on K8s](https://ci.apache.org/projects/flink/flink-docs-stable/deployment/resource-providers/standalone/kubernetes.html) at Flink official documentation. + +If you use Flink standalone, call `$FLINK_HOME/bin/start-cluster.sh` to start Flink cluster. + + + +### Configuration file +After [Installation](#1-installation), you will see a config file `config.yaml` in your current working directory. This file contains all the configurations that you can customize for your Cluster Serving. See an example of `config.yaml` below. +``` +## BigDL Cluster Serving Config Example +# model path must be provided +modelPath: /path/to/model +``` + +### Preparing Model +Currently BigDL Cluster Serving supports TensorFlow, OpenVINO, PyTorch, BigDL, Caffe models. Supported types are listed below. + +You need to put your model file into a directory with layout like following according to model type, note that only one model is allowed in your directory. Then, set in `config.yaml` file with `modelPath:/path/to/dir`. + +**Tensorflow** +***Tensorflow SavedModel*** +``` +|-- model + |-- saved_model.pb + |-- variables + |-- variables.data-00000-of-00001 + |-- variables.index +``` +***Tensorflow Frozen Graph*** +``` +|-- model + |-- frozen_inference_graph.pb + |-- graph_meta.json +``` +**note:** `.pb` is the weight file which name must be `frozen_inference_graph.pb`, `.json` is the inputs and outputs definition file which name must be `graph_meta.json`, with contents like `{"input_names":["input:0"],"output_names":["output:0"]}` + +***Tensorflow Checkpoint*** +Please refer to [freeze checkpoint example](https://github.com/intel-analytics/bigdl/tree/master/pyzoo/bigdl/examples/tensorflow/freeze_checkpoint) + +**Pytorch** + +``` +|-- model + |-- xx.pt +``` +Running Pytorch model needs extra dependency and config. Refer to [here](https://github.com/intel-analytics/bigdl/blob/master/pyzoo/bigdl/examples/pytorch/train/README.md) to install dependencies, and set environment variable `$PYTHONHOME` to your python, e.g. python could be run by `$PYTHONHOME/bin/python` and library is at `$PYTHONHOME/lib/`. + +**OpenVINO** + +``` +|-- model + |-- xx.xml + |-- xx.bin +``` +**BigDL** + +``` +|--model + |-- xx.model +``` +**Caffe** + +``` +|-- model + |-- xx.prototxt + |-- xx.caffemodel +``` + + +### Other Configuration +The field `params` contains your inference parameter configuration. + +* core_number: the **batch size** you use for model inference, usually the core number of your machine is recommended. Thus you could just provide your machine core number at this field. We recommend this value to be not smaller than 4 and not larger than 512. In general, using larger batch size means higher throughput, but also increase the latency between batches accordingly. + +### High Performance Configuration Recommended +#### Tensorflow, Pytorch +1 <= thread_per_model <= 8, in config +``` +# default: number of models used in serving +# modelParallelism: core_number of your machine / thread_per_model +``` +environment variable +``` +export OMP_NUM_THREADS=thread_per_model +``` +#### OpenVINO +environment variable +``` +export OMP_NUM_THREADS=core_number of your machine +``` diff --git a/docs/readthedocs/source/doc/Serving/ProgrammingGuide/serving-start.md b/docs/readthedocs/source/doc/Serving/ProgrammingGuide/serving-start.md new file mode 100644 index 00000000..195355c1 --- /dev/null +++ b/docs/readthedocs/source/doc/Serving/ProgrammingGuide/serving-start.md @@ -0,0 +1,87 @@ +# BigDL Cluster Serving Programming Guide + +## Launching Service of Serving + +Before do inference (predict), you have to start serving service. This section shows how to start/stop the service. + +### Start +You can use following command to start Cluster Serving. +``` +cluster-serving-start +``` + +Normally, when calling `cluster-serving-start`, your `config.yaml` should be in current directory. You can also use `cluster-serving-start -c config_path` to pass config path `config_path` to Cluster Serving manually. + +### Stop +You can use Flink UI in `localhost:8081` by default, to cancel your Cluster Serving job. + +Or you can use `${FLINK_HOME}/bin/flink list` to get serving job ID and call `${FLINK_HOME|/bin/flink cancel $ID`. + +### Shut Down +You can use following command to shutdown Cluster Serving. This operation will stop all Cluster Serving jobs and Redis server. Note that your data in Redis will be removed when you shutdown. +``` +cluster-serving-shutdown +``` +If you are using Docker, you could also run `docker rm` to shutdown Cluster Serving. +### Start Multiple Serving +To run multiple Cluster Serving job, e.g. the second job name is `serving2`, then use following configuration +``` +# model path must be provided +# modelPath: /path/to/model + +# name, default is serving_stream, you need to specify if running multiple servings +# jobName: serving2 +``` +then call `cluster-serving-start` in this directory would start another Cluster Serving job with this new configuration. + +Then, in Python API, pass `name=serving2` argument during creating object, e.g. +``` +input_queue=InputQueue(name=serving2) +output_queue=OutputQueue(name=serving2) +``` +Then the Python API would interact with job `serving2`. + +### HTTP Server +If you want to use sync API for inference, you should start a provided HTTP server first. User can submit HTTP requests to the HTTP server through RESTful APIs. The HTTP server will parse the input requests and pub them to Redis input queues, then retrieve the output results and render them as json results in HTTP responses. + +#### Prepare +User can download a bigdl-${VERSION}-http.jar from the Nexus Repository with GAVP: +``` +com.intel.analytics.bigdl +bigdl-bigdl_${BIGDL_VERSION}-spark_${SPARK_VERSION} +${ZOO_VERSION} +``` +User can also build from the source code: +``` +mvn clean package -P spark_2.4+ -Dmaven.test.skip=true +``` +#### Start the HTTP Server +User can start the HTTP server with following command. +``` +java -jar bigdl-bigdl_${BIGDL_VERSION}-spark_${SPARK_VERSION}-${ZOO_VERSION}-http.jar +``` +And check the status of the HTTP server with: +``` +curl http://${BINDED_HOST_IP}:${BINDED_HOST_PORT}/ +``` +If you get a response like "welcome to BigDL web serving frontend", that means the HTTP server is started successfully. +#### Start options +User can pass options to the HTTP server when start it: +``` +java -jar bigdl-bigdl_${BIGDL_VERSION}-spark_${SPARK_VERSION}-${ZOO_VERSION}-http.jar --redisHost="172.16.0.109" +``` +All the supported parameter are listed here: +* **interface**: the binded server interface, default is "0.0.0.0" +* **port**: the binded server port, default is 10020 +* **redisHost**: the host IP of redis server, default is "localhost" +* **redisPort**: the host port of redis server, default is 6379 +* **redisInputQueue**: the input queue of redis server, default is "serving_stream" +* **redisOutputQueue**: the output queue of redis server, default is "result:" +* **parallelism**: the parallelism of requests processing, default is 1000 +* **timeWindow**: the timeWindow wait to pub inputs to redis, default is 0 +* **countWindow**: the timeWindow wait to ub inputs to redis, default is 56 +* **tokenBucketEnabled**: the switch to enable/disable RateLimiter, default is false +* **tokensPerSecond**: the rate of permits per second, default is 100 +* **tokenAcquireTimeout**: acquires a permit from this RateLimiter if it can be obtained without exceeding the specified timeout(ms), default is 100 + +**User can adjust these options to tune the performance of the HTTP server.** diff --git a/docs/readthedocs/source/doc/Serving/QuickStart/serving-quickstart.md b/docs/readthedocs/source/doc/Serving/QuickStart/serving-quickstart.md new file mode 100644 index 00000000..a25e67bd --- /dev/null +++ b/docs/readthedocs/source/doc/Serving/QuickStart/serving-quickstart.md @@ -0,0 +1,49 @@ +# BigDL Cluster Serving Quick Start + +This section provides a quick start example for you to run BigDL Cluster Serving. To simplify the example, we use docker to run Cluster Serving. If you do not have docker installed, [install docker](https://docs.docker.com/install/) first. The quick start example contains all the necessary components so the first time users can get it up and running within minutes: + +* A docker image for BigDL Cluster Serving (with all dependencies installed) +* A sample configuration file +* A sample trained TensorFlow model, and sample data for inference +* A sample Python client program + +Use one command to run Cluster Serving container. (We provide quick start model in older version of docker image, for newest version, please refer to following sections and we remove the model to reduce the docker image size). +``` +docker run --name cluster-serving -itd --net=host intelanalytics/bigdl-cluster-serving:0.9.1 +``` +Log into the container using `docker exec -it cluster-serving bash`, and run +``` +cd cluster-serving +cluster-serving-init +``` +`bigdl.jar` and `config.yaml` is in your directory now. + +Also, you can see prepared TensorFlow frozen ResNet50 model in `resources/model` directory with following structure. + +``` +cluster-serving | + -- | model + -- frozen_graph.pb + -- graph_meta.json +``` +Modify `config.yaml` and add following to `model` config +``` +model: + path: resources/model +``` + +Start Cluster Serving using `cluster-serving-start`. + +Run python program `python3 image_classification_and_object_detection_quick_start.py -i resources/test_image` to push data into queue and get inference result. + +Then you can see the inference output in console. +``` +cat prediction layer shape: (1000,) +the class index of prediction of cat image result: 292 +cat prediction layer shape: (1000,) +``` +Wow! You made it! + +Note that the Cluster Serving quick start example will run on your local node only. Check the [Deploy Your Own Cluster Serving](#deploy-your-own-cluster-serving) section for how to configure and run Cluster Serving in a distributed fashion. + +For more details, refer to following sections.