diff --git a/CMake/mlpack.cmake b/CMake/mlpack.cmake index e598222a92..998b8d921e 100644 --- a/CMake/mlpack.cmake +++ b/CMake/mlpack.cmake @@ -603,13 +603,13 @@ macro(fetch_mlpack COMPILE_OPENBLAS) find_package(BLAS PATHS ${CMAKE_BINARY_DIR}) if (NOT BLAS_FOUND OR (NOT BLAS_LIBRARIES)) get_deps(https://github.com/xianyi/OpenBLAS/releases/download/v${OPENBLAS_VERSION}/OpenBLAS-${OPENBLAS_VERSION}.tar.gz - OpenBLAS OpenBLAS-${OPENBLAS_VERSION}.tar.gz) + OpenBLAS OpenBLAS-${OPENBLAS_VERSION}.tar.gz) if (NOT COMPILE_OPENBLAS) message(WARNING "OpenBLAS is downloaded but not compiled. Please compile OpenBLAS before compiling mlpack") else() - execute_process(COMMAND make NO_SHARED=1 WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/deps/OpenBLAS-${version}) - file(GLOB OPENBLAS_LIBRARIES "${CMAKE_BINARY_DIR}/deps/OpenBLAS-${version}/libopenblas.a") + execute_process(COMMAND make NO_SHARED=1 WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/deps/OpenBLAS-${OPENBLAS_VERSION}) + file(GLOB OPENBLAS_LIBRARIES "${CMAKE_BINARY_DIR}/deps/OpenBLAS-${OPENBLAS_VERSION}/libopenblas.a") set(BLAS_openblas_LIBRARY ${OPENBLAS_LIBRARIES}) set(LAPACK_openblas_LIBRARY ${OPENBLAS_LIBRARIES}) set(BLAS_FOUND ON) diff --git a/doc/img/pipeline-narrow.svg b/doc/img/pipeline-narrow.svg index e9514687fb..3f33e5645b 100644 --- a/doc/img/pipeline-narrow.svg +++ b/doc/img/pipeline-narrow.svg @@ -212,14 +212,14 @@ Compile an mlpack program - + + Deploy to a Docker container + - Cross-compile to a Raspberry + Cross-compile to a Raspberry Pi - Deploy mlpack on Windows + Deploy mlpack on Windows diff --git a/doc/img/pipeline-wide.svg b/doc/img/pipeline-wide.svg index 9ba0a2276a..7c832e6aa9 100644 --- a/doc/img/pipeline-wide.svg +++ b/doc/img/pipeline-wide.svg @@ -211,17 +211,15 @@ Compile an mlpack program - - Cross-compile to a Raspberry + Cross-compile to a Raspberry Pi - Deploy mlpack on Windows + Deploy mlpack on Windows diff --git a/doc/sidebar.html b/doc/sidebar.html index d2041a275b..85bb8edffa 100644 --- a/doc/sidebar.html +++ b/doc/sidebar.html @@ -178,6 +178,9 @@ when the sidebar is built for each page.
  • RPlusPlusTree + +
  • +
  • HilbertRTree @@ -471,6 +474,11 @@ when the sidebar is built for each page. Deploying mlpack on Windows
  • +
  • + + Deploying with Docker + +
  • diff --git a/doc/user/core/trees/r_plus_tree.md b/doc/user/core/trees/r_plus_tree.md index 3c628c0ca1..4cc86fd58c 100644 --- a/doc/user/core/trees/r_plus_tree.md +++ b/doc/user/core/trees/r_plus_tree.md @@ -28,7 +28,7 @@ functionality is required, then the R+-tree or other variants of * [`RectangleTree`](rectangle_tree.md) * [R+-Tree on Wikipedia](https://en.wikipedia.org/wiki/R%2B_tree) - * [The R+-tree: A Dynamic Index for Multi-Dimensional Objects (pdf)](https://kilthub.cmu.edu/articles/journal_contribution/The_R_-Tree_A_Dynamic_Index_for_Multi-Dimensional_Objects/6610748/files/12102908.pdf) + * [The R+-tree: A Dynamic Index for Multi-Dimensional Objects (pdf)](https://www.inf.ed.ac.uk/teaching/courses/adbs/extras/sellis87rtree.pdf) * [Tree-Independent Dual-Tree Algorithms (pdf)](https://www.ratml.org/pub/pdf/2013tree.pdf) ## Template parameters diff --git a/doc/user/deploy_docker.md b/doc/user/deploy_docker.md new file mode 100644 index 0000000000..7749928bfb --- /dev/null +++ b/doc/user/deploy_docker.md @@ -0,0 +1,382 @@ +# Deploy to a Docker container + +In many machine learning applications, it can be useful to deploy a model as a +standalone Docker container that can return predictions. This tutorial shows +how to build a Docker container with an mlpack model serving predictions. Here +the model returns predictions in a very primitive way: directly as input from a +terminal, but it would be straightforward to adapt the container to provide a +full REST API or similar. + +mlpack applications inside of Docker containers can be built in a way that the +resulting container is extremely small---sometimes even less than 1 MB! + +*See also*: + + - [Installing mlpack](install.md) + - [Compile an mlpack program](compile.md) + - [Deploying mlpack on Windows](deploy_windows.md) + - [Setting up an mlpack cross-compilation environment](../embedded/supported_boards.md) + +## General workflow + +To make a Docker container that serves predictions, we must first train a model. +Therefore, our workflow to build this container will be: + + * [Decide on the problem to solve](#problem-statement) + * [Write a program to train the model](#model-training-program) + * [Write a program to make predictions with the trained model](#prediction-program) + * [Build the Docker container with the prediction program](#building-the-container) + * [Run the Docker container](#run-the-container) + +## Problem statement + +For this simple example, we will solve a problem from the cybersecurity world: +[DGA detection](https://en.wikipedia.org/wiki/Domain_generation_algorithm). +The example here is based on (and heavily uses the code from) mlpack's +[DGA detection LSTM example](https://github.com/mlpack/examples/tree/master/cpp/lstm/dga_detection). + +Malware authors often write malware that communicates with a centralized +command-and-control server. This can allow the malware to update itself, or to +receive commands from an operator (e.g., 'start Bitcoin mining', or 'lock system +and display ransomware message'). + +The malware author cannot hardcode a domain name into their malware, because +this would be easily blocked by any antivirus software. So, instead, a *domain +generation algorithm* is used to generate a series of domain names. The malware +will try to contact a server at each of these domain names. The individual DGA +domain names can look very random; for instance, some DGA domains generated by +the `matsnu` malware family are: + + * `brothernerveplacebringconsult.com` + * `screencatchdishtellproposed.com` + * `balladoptwelladdinfluence.com` + * `capitalhuntdealsmokeboxclue.com` + +Other malware families may generate very random looking names, like +`i828ywu0ywqs.net` or similar. Since the set of domain names that can be +generated by a DGA is huge, blocking individual domain names is not a realistic +mitigation strategy. However, we can use machine learning techniques to detect +DGA-generated domain names with a high level of accuracy! + +This strategy has been shown to be effective in +[some previous work](https://www.arxiv.org/pdf/1611.00791). + +Adapting that approach for simplicity, we will train simple recurrent neural +networks with LSTMs to detect benign domains and DGA domains, and then the +Docker container application will read domain names as input and output a score +indicating the likelihood that the domain name was generated by a DGA. + +## Model training program + +Training a model can be done as a separate standalone program; since our goal is +just to provide a container that produces predictions, the program in the +container does not need to support training. + +The C++ code for training a DGA detector is available as the standalone program +[`lstm_dga_detection_train.cpp`](https://github.com/mlpack/examples/blob/master/cpp/lstm/dga_detection/lstm_dga_detection_train.cpp) +We can compile it with a call to `g++`, following the instructions from +[the compilation guide](compile.md): + +```sh +g++ -O3 -o lstm_dga_detection_train lstm_dga_detection_train.cpp -fopenmp -larmadillo +``` + +Some modification of the command above may be necessary if mlpack is installed +on your system in a nonstandard location, or if you are not using the Armadillo +wrapper. See [Configuring mlpack with compile-time +definitions](compile.md#configuring-mlpack-with-compile-time-definitions) and +[Linking without the Armadillo +wrapper](compile.md#linking-without-the-armadillo-wrapper) for more details. + +Once the program is compiled, we can train on a +[dataset of domain names](https://datasets.mlpack.org/dga_domains.csv.gz). The +commands below will download the prepared dataset from the mlpack website, and +then run the training process. + +```sh +wget https://datasets.mlpack.org/dga_domains.csv.gz +gunzip dga_domains.csv.gz +./lstm_dga_detection_train dga_domains.csv +``` + +The training process may take a while, but when it is finished, performance +statistics about the models will be printed, and the model will be saved to +`lstm_dga_detector.bin`. The models should achieve 99%+ accuracy on the +held-out test data. + +## Prediction program + +The examples repository also provides the standalone prediction program +[`lstm_dga_detection_predict.cpp`](https://github.com/mlpack/examples/blob/master/cpp/lstm/dga_detection/lstm_dga_detection_predict.cpp). +We can also compile this with a call to `g++`, following the instructions from +[the compilation guide](compile.md): + +```sh +g++ -O3 -o lstm_dga_detection_predict lstm_dga_detection_predict.cpp -fopenmp -larmadillo -static +``` + +As with the training program [above](#model-training-program), some modification +of the compilation command may be necessary depending on your configuration. + +We also specified the `-static` option here, so that the produced program is +statically linked. This will help us deploy into a Docker container, since we +can just run the program directly and do not need to ensure that supporting +libraries are available. Note that you will need a statically-compiled version +of Armadillo available to link against, or instead define the compiler option +`-DARMA_DONT_USE_WRAPPER` and link with static OpenBLAS using `-lopenblas`. + +The prediction program reads from stdin, and once a domain is entered, a +prediction is computed and the word `malicious` or `benign` is emitted, along +with a score indicating the model's "confidence". A sample transcript of the +program is below: + +```text +$ ./lstm_dga_detection_predict lstm_dga_detector_benign.bin lstm_dga_detector_malicious.bin +www.mlpack.org +benign (score 44.5417) +mdfvkejbqoxg.ru +malicious (score 22.2786) +arma.sourceforge.net +benign (score 19.4883) +11b5n854ublnv152.net +malicious (score 7.89951) +``` + +## Building the container + +Building a Docker container that runs `lstm_dga_detection_predict` is very +simple; we only need to put the prediction program and model in the container. +In fact, to save additional time, we can use a +[distroless](https://github.com/GoogleContainerTools/distroless) container. +This code can be used as the `Dockerfile`: + +```dockerfile +FROM gcr.io/distroless/static-debian12 + +ADD lstm_dga_detection_predict . +ADD lstm_dga_detector_benign.bin . +ADD lstm_dga_detector_malicious.bin . + +ENTRYPOINT ["./lstm_dga_detection_predict", \ + "lstm_dga_detector_benign.bin", \ + "lstm_dga_detector_malicious.bin"] +``` + +Building the container is simple (and nearly instantaneous): + +```sh +docker build -t lstm_dga_detector . +``` + +And once it is built, it is easy to see that the container is relatively small: + +```text +$ docker images | grep -B 1 lstm_dga_detector +REPOSITORY TAG IMAGE ID CREATED SIZE +lstm_dga_detector latest 9fc931a2cdae 35 seconds ago 33.7MB +``` + +However, we haven't even tried to optimize for size---see the [Reducing the size +of the container further](#reducing-the-size-of-the-container-further) section + +## Run the container + +The container can now be deployed or run in any standard Docker environment +(including on Kubernetes, although that seems like overkill for such a simple +example). + +Running the container locally (and interacting with the prediction service) is a +simple command: + +```sh +docker run --rm -it lstm_dga_detector +``` + +Once the container has started, simply type a domain name, hit enter, and a +prediction (plus score) will be printed. + +To build the example into a more complex application, you can use +`lstm_dga_detection_predict.cpp` as a starting point. + +## Reducing the size of the container further + +Although 33.7 MB for a container is already orders of magnitude smaller than an +equivalent unoptimized Python container, it is readily possible via compilation +options and a few other tricks to get the size to be significantly smaller. The +vast majority of the size of the container is just the size of the compiled +`lstm_dga_detection_predict`: + +```text +$ ls -lh +-rwxrwxr-x 1 ryan ryan 31M Mar 6 20:46 lstm_dga_detection_predict +-rw-rw-r-- 1 ryan ryan 80K Mar 5 19:10 lstm_dga_detector_benign.bin +-rw-rw-r-- 1 ryan ryan 80K Mar 5 19:10 lstm_dga_detector_malicious.bin +``` + +31 MB for a compiled program is quite large! But, as it turns out, most of this +is due to dependencies. We can see this by running a command that does not +perform linking, like this: + +```sh +g++ -O3 -c -o lstm_dga_detection_predict.o lstm_dga_detection_predict.cpp +``` + +This compiled (but not linked) object file is much smaller: + +```text +$ ls -lh lstm_dga_detection_predict.o +-rw-rw-r-- 1 ryan ryan 1.6M Mar 6 20:48 lstm_dga_detection_predict.o +``` + +That implies that our primary size issue is not with our own code, but instead +with our dependencies. Specifically, on most systems, OpenBLAS is compiled to +support any architecture and this results in the statically-linked OpenBLAS +library being *quite* large. On a Debian system: + +```text +$ cd /usr/lib/x86_64-linux-gnu/openblas-pthread/ +$ ls -lh libopenblasp-r0.3.28.a +-rw-r--r-- 1 root root 61M Nov 20 05:52 libopenblasp-r0.3.28.a +``` + +61 MB is very large! Although not all of OpenBLAS is used by our DGA detection +program, a significant portion is, and this is the primary culprit for our large +program size. Two alternatives to reduce this size are: + +#### Use reference BLAS and LAPACK instead + +The reference BLAS and LAPACK implementations are often slower, but much smaller +in size. + + * On a Debian or Ubuntu system, install `libblas-dev` and `liblapack-dev` + instead of `libopenblas-dev`. + + * This step alone reduces the size of the statically-linked + `lstm_dga_detection_predict` to ***3.4 MB***, but the use of reference BLAS + and LAPACK is likely to be slower than OpenBLAS. + +#### Compile OpenBLAS manually + +Another option is to compile OpenBLAS manually +[from source](https://github.com/OpenMathLib/OpenBLAS) to reduce its size. +OpenBLAS is compiled with a `make` command; options for this can be found in the +[OpenBLAS manual](http://www.openmathlib.org/OpenBLAS/docs/build_system/#important-variables). + +As an example (***note:*** do not use this command directly on your system, see +the description of the options and decide which is right for your system), the +following command produces an OpenBLAS library that is only 16MB. + +```sh +make \ + TARGET=NEHALEM \ + DYNAMIC_ARCH=0 \ + COMMON_OPTS="-Os -ffunction-sections -fdata-sections" \ + NO_SHARED=1 \ + BUILD_DOUBLE=0 \ + BUILD_COMPLEX=0 \ + BUILD_COMPLEX16=0 \ + BUILD_BFLOAT16=0 +``` + +Looking at each option: + + * `TARGET=NEHALEM` and `DYNAMIC_ARCH=0` specifies that this version of OpenBLAS + can ***only*** be run on + [Nehalem](https://en.wikipedia.org/wiki/Nehalem_(microarchitecture)) or newer + Intel processors. + - ***This causes the code to be significantly less portable.*** + - However, the `DYNAMIC_ARCH=0` option *significantly* reduces the size of + OpenBLAS and therefore downstream code too by compiling only for the + processor of interest. + - For other `TARGET` options, see `TargetList.txt` in the OpenBLAS source + code. + + * `COMMON_OPTS="-Os -ffunction-sections -fdata-sections"` specifies that the + compiler should aim to keep the compiled code as small as possible, and + include section information for later stripping and further size minimization + of code. + + * `NO_SHARED=1` specifies that only the static version of OpenBLAS + (`libopenblas.a`) should be built. + + * `BUILD_DOUBLE=0 BUILD_COMPLEX=0 BUILD_COMPLEX16=0 BUILD_BFLOAT16=0` disables + all OpenBLAS functions for data types we are not using in our program. + - The `lstm_dga_detection_predict.cpp` code only uses `arma::fmat` (e.g. + matrices with `float`), so we can omit the other functions. + +With this stripped-down version of OpenBLAS, the size of +`lstm_dga_detection_predict` with no further compilation modifications is +reduced to ***3.5 MB***. + +***Note:*** when compiling `lstm_dga_detection_predict`, linking against the +hand-compiled OpenBLAS will require specifying the `-L/path/to/openblas/` option +so that the linker finds the manually-compiled OpenBLAS version. + +### Compilation options for further size reduction + +We have gone from 31 MB to roughly 3 MB just by replacing our OpenBLAS +implementation with either reference BLAS/LAPACK or a hand-compiled version. +However, we can specify some additional compiler options to reduce the size +further. Assuming that we have placed the manually-compiled `libopenblas.a` in +the same directory as `lstm_dga_detection_predict.cpp`, we can compile with the +following command to reduce size even further: + +```sh +g++ -o lstm_dga_detection_predict lstm_dga_detection_predict.cpp \ + -Os \ + -DNDEBUG \ + -DARMA_DONT_USE_WRAPPER \ + -ffunction-sections -fdata-sections \ + -static \ + -L. -lopenblas \ + -Wl,--gc-sections \ + -Wl,--strip-all +``` + +Looking at each option: + + * `-Os` tells the compiler to optimize each function to have a small size. + * `-DNDEBUG` removes any debugging symbols or code paths. + * `-DARMA_DONT_USE_WRAPPER` is an Armadillo directive that tells Armadillo not + to link against the Armadillo runtime library but instead directly against + OpenBLAS. + - This is why we use `-lopenblas` later instead of `-larmadillo`. + * `-ffunction-sections -fdata-sections` are used so that the linker can later + remove unused sections from the code. + * `-Wl,--gc-sections -Wl,--strip-all` tells the linker to strip all unused code + from the final program. + * Note that we have *omitted* `-fopenmp`; this will cause compiled code to be + somewhat smaller, but it will execute serially. Given that our prediction + program only predicts a single domain at a time, this is acceptable. + +This shaves off more than 1 MB: + +```text +$ ls -lh lstm_dga_detection_predict +-rwxrwxr-x 1 ryan ryan 1.9M Mar 6 21:37 lstm_dga_detection_predict +``` + +That is a size reduction of basically ***16x*** entirely obtained through +compiler options. When the Docker container is recompiled with the new version +of `lstm_dga_detection_predict`, the size is significantly improved: + +```text +$ docker images | grep -B 1 lstm +REPOSITORY TAG IMAGE ID CREATED SIZE +lstm_dga_detector latest 0be5a677fd9c 6 seconds ago 4.09MB +``` + +But there is still room for additional size reduction, either through +further compiler options or more intrusive code modifications. Likely the +largest contributor to code size after the optimizations above are +serialization and the standard libraries: + + * Avoiding the use of `data::Load()` (and thus the + [Cereal](https://uscilab.github.io/cereal/) serialization library) by + directly saving the weights of the RNN using Armadillo's built-in save + functionality would be effective. + + * Replacing the standard `libc` and `libstdc++` implementation with lightweight + replacements, like [`musl`](https://musl.libc.org/) and others. + +These further optimizations, however, are beyond the scope of this tutorial. diff --git a/doc/user/deployment.md b/doc/user/deployment.md index 1a0161265a..ba94fbbc14 100644 --- a/doc/user/deployment.md +++ b/doc/user/deployment.md @@ -23,3 +23,7 @@ relatively simple environments. * [***Deploying mlpack on Windows***](deploy_windows.md): build a Windows application that uses mlpack. + + * [***Deploying mlpack to a Docker container***](deploy_docker.md): package an + mlpack application inside of a lightweight Docker container for local usage + or deployment in a cloud environment.