Online Inference#

This tutorial shows how to use trained PyTorch, TensorFlow, and ONNX (format) models, written in Python, directly in HPC workloads written in Fortran, C, C++ and Python.

The example simulation here is written in Python for brevity, however, the inference API in SmartRedis is the same (besides extra parameters for compiled langauges) across all clients.

Installing the ML backends#

In order to use the Orchestrator database as an inference engine, the Machine Learning (ML) backends need to be built and supplied to the database at runtime.

To check which backends are built, a simple helper function is available in SmartSim as shown below.

[1]:
## Installing the ML backends
from smartsim._core.utils.helpers import installed_redisai_backends
print(installed_redisai_backends())
{'torch'}

As you can see, only the Torch backend is built. In order to use the TensorFlow and ONNX backends as well, they need to be built.

The smart command line interface can be used to build the backends using the smart build command. The output of smart build --help is shown below.

[2]:
!smart build --help
usage: smart build [-h] [-v] [--device {cpu,gpu}] [--only_python_packages]
                   [--no_pt] [--no_tf] [--onnx] [--torch_dir TORCH_DIR]
                   [--libtensorflow_dir LIBTENSORFLOW_DIR] [--keydb]

Build SmartSim dependencies (Redis, RedisAI, ML runtimes)

options:
  -h, --help            show this help message and exit
  -v                    Enable verbose build process
  --device {cpu,gpu}    Device to build ML runtimes for
  --only_python_packages
                        Only evaluate the python packages (i.e. skip building
                        backends)
  --no_pt               Do not build PyTorch backend
  --no_tf               Do not build TensorFlow backend
  --onnx                Build ONNX backend (off by default)
  --torch_dir TORCH_DIR
                        Path to custom <path>/torch/share/cmake/Torch/
                        directory (ONLY USE IF NEEDED)
  --libtensorflow_dir LIBTENSORFLOW_DIR
                        Path to custom libtensorflow directory (ONLY USE IF
                        NEEDED)
  --keydb               Build KeyDB instead of Redis

We use smart clean first to remove the previous build, and then call smart build to build the new backend set. For larger teams, CrayLabs will help setup your system so that the backends do not have to be built by each user.

By default, the PyTorch and TensorFlow backends are built. To build all three backends for use on CPU, we issue the following command.

[3]:
!smart clean && smart build --device cpu --onnx
[SmartSim] INFO Successfully removed existing RedisAI installation
[SmartSim] INFO Successfully removed ML runtimes
[SmartSim] INFO Running SmartSim build process...
[SmartSim] INFO Checking requested versions...
[SmartSim] INFO Checking for build tools...
[SmartSim] INFO Redis build complete!

ML Backends Requested
╒════════════╤════════╤══════╕
│ PyTorch    │ 2.0.1  │ True │
│ TensorFlow │ 2.13.1 │ True │
│ ONNX       │ 1.14.1 │ True │
╘════════════╧════════╧══════╛

Building for GPU support: False

[SmartSim] INFO Building RedisAI version 1.2.7 from https://github.com/RedisAI/RedisAI.git/
[SmartSim] INFO ML Backends and RedisAI build complete!
[SmartSim] INFO Tensorflow, Onnxruntime, Torch backend(s) built
[SmartSim] INFO SmartSim build complete!

Starting the Database for Inference#

SmartSim performs online inference by using the SmartRedis clients to call into the Machine Learning (ML) runtimes linked into the Orchestrator database. The Orchestrator is the name in SmartSim for a Redis or KeyDB database with a RedisAI module built into it with the ML runtimes.

Therefore, to perform inference, you must first create an Orchestrator database and launch it. There are two methods to couple the database to your application in order to add inference capability to your application. - standard (not colocated) - colocated

standard mode launches an optionally clustered (across many compute hosts) database instance that can be treated as a single storage device for many clients (possibly the many ranks of an MPI program) where there is a single address space for keys across all hosts.

colocated mode launches a orchestrator instance on each compute host used by a, possibly distributed, application. each instance contains their own address space for keys. In SmartSim, Model instances can be launched with a colocated orchetrator through Model.colocate_db_tcp or Model.colocate_db_udp. Colocated Models are used for highly scalable inference where global aggregations aren’t necessary for inference.

The code below launches the Orchestrator database using the standard deployment method.

[4]:
# some helper libraries for the tutorial
import io
import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'
import logging
import numpy as np

# import smartsim and smartredis
from smartredis import Client
from smartsim import Experiment
[5]:
exp = Experiment("Inference-Tutorial", launcher="local")
[6]:
db = exp.create_database(port=6780, interface="lo")
exp.start(db)

Using PyTorch#

The Orchestrator supports both PyTorch models and TorchScript functions and scripts in PyTorch.

Below, the code is shown to create, jit-trace (prepare for inference), set, and call a PyTorch Convolutional Neural Network (CNN) with SmartSim and SmartRedis

[7]:
import torch
import torch.nn as nn
import torch.nn.functional as F


class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(1, 32, 3, 1)
        self.conv2 = nn.Conv2d(32, 64, 3, 1)
        self.dropout1 = nn.Dropout(0.25)
        self.dropout2 = nn.Dropout(0.5)
        self.fc1 = nn.Linear(9216, 128)
        self.fc2 = nn.Linear(128, 10)

    def forward(self, x):
        x = self.conv1(x)
        x = F.relu(x)
        x = self.conv2(x)
        x = F.relu(x)
        x = F.max_pool2d(x, 2)
        x = self.dropout1(x)
        x = torch.flatten(x, 1)
        x = self.fc1(x)
        x = F.relu(x)
        x = self.dropout2(x)
        x = self.fc2(x)
        output = F.log_softmax(x, dim=1)
        return output

To set a PyTorch model, we create a function to “jit-trace” the model and save it to a buffer in memory.

If you aren’t familiar with the concept of tracing, take a look at the Torch documentation for trace

[8]:
# Initialize an instance of our CNN model
n = Net()
n.eval()

# prepare a sample input to trace on (random noise is fine)
example_forward_input = torch.rand(1, 1, 28, 28)

def create_torch_model(torch_module, example_forward_input):

    # perform the trace of the nn.Module.forward() method
    module = torch.jit.trace(torch_module, example_forward_input)

    # save the traced module to a buffer
    model_buffer = io.BytesIO()
    torch.jit.save(module, model_buffer)
    return model_buffer.getvalue()

traced_cnn = create_torch_model(n, example_forward_input)

Lastly, we use the SmartRedis Python client to

  1. Connect to the database

  2. Put a batch of 20 tensors into the database (put_tensor)

  3. Set the Torch model in the database (set_model)

  4. Run the model on the batch of tensors (run_model)

  5. Retrieve the result (get_tensor)

[9]:
client = Client(address=db.get_address()[0], cluster=False)

client.put_tensor("input", torch.rand(20, 1, 28, 28).numpy())

# put the PyTorch CNN in the database in GPU memory
client.set_model("cnn", traced_cnn, "TORCH", device="CPU")

# execute the model, supports a variable number of inputs and outputs
client.run_model("cnn", inputs=["input"], outputs=["output"])

# get the output
output = client.get_tensor("output")
print(f"Prediction: {output}")
Prediction: [[-2.1860428 -2.3318565 -2.2773128 -2.2742267 -2.2679536 -2.304159
  -2.423439  -2.3406057 -2.2474668 -2.3950338]
 [-2.1803837 -2.3286302 -2.2805855 -2.2874444 -2.261593  -2.3145547
  -2.4357762 -2.3169715 -2.2618299 -2.3798223]
 [-2.1833746 -2.3249795 -2.28497   -2.2851245 -2.2555952 -2.308204
  -2.4274755 -2.3441646 -2.2553194 -2.3779805]
 [-2.1843016 -2.3395848 -2.2619352 -2.294549  -2.2571433 -2.312943
  -2.4161577 -2.338785  -2.2538524 -2.3881512]
 [-2.1936755 -2.3315516 -2.2739122 -2.2832148 -2.2666094 -2.3038912
  -2.4211216 -2.3300066 -2.2564852 -2.3846986]
 [-2.1709712 -2.3271346 -2.280365  -2.286064  -2.2617233 -2.3227994
  -2.4253702 -2.3313646 -2.2593162 -2.383301 ]
 [-2.1948013 -2.3318067 -2.2713811 -2.2844    -2.2526758 -2.3178148
  -2.4255004 -2.3233378 -2.2388031 -2.4088087]
 [-2.17515   -2.3240736 -2.2818787 -2.2857373 -2.259629  -2.3184
  -2.425821  -2.3519678 -2.2413275 -2.385761 ]
 [-2.187554  -2.3335872 -2.2767708 -2.2818003 -2.2654893 -2.3097534
  -2.4182632 -2.3376188 -2.2509694 -2.384327 ]
 [-2.1793714 -2.340681  -2.271785  -2.287751  -2.2620957 -2.3163543
  -2.4111845 -2.3468175 -2.2472064 -2.3842056]
 [-2.1906679 -2.3483853 -2.2580595 -2.2923894 -2.25718   -2.2951608
  -2.431815  -2.3487022 -2.2326546 -2.3963163]
 [-2.1882055 -2.3293467 -2.2767649 -2.279892  -2.2527165 -2.3220086
  -2.4226239 -2.3364902 -2.2455037 -2.394776 ]
 [-2.1756573 -2.3318045 -2.2690601 -2.2737868 -2.264148  -2.3212118
  -2.4243867 -2.3421402 -2.2562728 -2.390894 ]
 [-2.1824148 -2.3317673 -2.2749603 -2.291667  -2.2524009 -2.3026595
  -2.42986   -2.3290846 -2.265264  -2.387787 ]
 [-2.1871543 -2.3408008 -2.2773213 -2.283908  -2.249834  -2.3159058
  -2.4251873 -2.339211  -2.245001  -2.3839695]
 [-2.1855574 -2.3216138 -2.2722392 -2.2826352 -2.2573392 -2.308948
  -2.4348576 -2.3421624 -2.2397952 -2.4060655]
 [-2.1876159 -2.330091  -2.2779942 -2.2849102 -2.2582757 -2.3122754
  -2.4250498 -2.333003  -2.250753  -2.3871331]
 [-2.182653  -2.3381891 -2.2795184 -2.287199  -2.2628696 -2.303869
  -2.413879  -2.3404965 -2.26254   -2.3739154]
 [-2.1733668 -2.3377435 -2.2724369 -2.28559   -2.2537165 -2.3127556
  -2.4249415 -2.3484716 -2.2515364 -2.3897333]
 [-2.1839535 -2.336417  -2.2839231 -2.285238  -2.2608624 -2.3198016
  -2.424396  -2.3165755 -2.2433887 -2.3935702]]

As we gave the CNN random noise, the predictions reflect that.

If running on GPU, be sure to change the argument in the set_model call above to device="GPU".

Using TorchScript#

In addition to PyTorch models, TorchScript scripts and functions can be set in the Orchestrator database and called from any of the SmartRedis languages. Functions can be set in the database in Python prior to application launch and then used directly in Fortran, C, and C++ simulations.

The example below uses the TorchScript Singular Value Decomposition (SVD) function. The function set in side the database and then called with a random input tensor.

[10]:
def calc_svd(input_tensor):
    # svd function from TorchScript API
    return input_tensor.svd()
[11]:
# connect a client to the database
client = Client(address=db.get_address()[0], cluster=False)

# test the SVD function
tensor = np.random.randint(0, 100, size=(5, 3, 2)).astype(np.float32)
client.put_tensor("input", tensor)
client.set_function("svd", calc_svd)
client.run_script("svd", "calc_svd", ["input"], ["U", "S", "V"])
U = client.get_tensor("U")
S = client.get_tensor("S")
V = client.get_tensor("V")
print(f"U: {U}\n\n, S: {S}\n\n, V: {V}\n")
U: [[[-0.31189808  0.86989427]
  [-0.48122275 -0.49140105]
  [-0.81923395 -0.0425336 ]]

 [[-0.5889101  -0.29554686]
  [-0.43949458 -0.66398275]
  [-0.6782547   0.68686163]]

 [[-0.61623317  0.05853765]
  [-0.6667615  -0.5695148 ]
  [-0.4191489   0.81989413]]

 [[-0.5424681   0.8400398 ]
  [-0.31990844 -0.2152339 ]
  [-0.77678    -0.49800384]]

 [[-0.43667376  0.8088193 ]
  [-0.70812154 -0.57906115]
  [-0.5548693   0.10246649]]]

, S: [[137.10924   25.710997]
 [131.49983   37.79937 ]
 [178.72423   24.792084]
 [125.13014   49.733784]
 [137.48834   53.57199 ]]

, V: [[[-0.8333395   0.5527615 ]
  [-0.5527615  -0.8333395 ]]

 [[-0.5085228  -0.8610485 ]
  [-0.8610485   0.5085228 ]]

 [[-0.8650402   0.5017025 ]
  [-0.5017025  -0.8650402 ]]

 [[-0.56953645  0.8219661 ]
  [-0.8219661  -0.56953645]]

 [[-0.6115895   0.79117525]
  [-0.79117525 -0.6115895 ]]]

[12]:
## TensorFlow and Keras
import tensorflow as tf
from tensorflow import keras
tf.get_logger().setLevel(logging.ERROR)

# create a simple Fully connected network in Keras
model = keras.Sequential(
    layers=[
        keras.layers.InputLayer(input_shape=(28, 28), name="input"),
        keras.layers.Flatten(input_shape=(28, 28), name="flatten"),
        keras.layers.Dense(128, activation="relu", name="dense"),
        keras.layers.Dense(10, activation="softmax", name="output"),
    ],
    name="FCN",
)

# Compile model with optimizer
model.compile(optimizer="adam",
              loss="sparse_categorical_crossentropy",
              metrics=["accuracy"])

Setting TensorFlow and Keras Models#

After a model is created (trained or not), the graph of the model is frozen and saved to file so the client method client.set_model_from_file can load it into the database.

SmartSim includes a utility to freeze the graph of a TensorFlow or Keras model in smartsim.ml.tf. To use TensorFlow or Keras in SmartSim, specify TF as the argument for backend in the call to client.set_model or client.set_model_from_file.

Note that TensorFlow and Keras, unlike the other ML libraries supported by SmartSim, requires an input and output argument in the call to set_model. These arguments correspond to the layer names of the created model. The smartsim.ml.tf.freeze_model utility returns these values for convenience as shown below.

[13]:
from smartsim.ml.tf import freeze_model

# SmartSim utility for Freezing the model and saving it to a file.
model_path, inputs, outputs = freeze_model(model, os.getcwd(), "fcn.pb")

# use the same client we used for PyTorch to set the TensorFlow model
# this time the method for setting a model from a saved file is shown.
# TensorFlow backed requires named inputs and outputs on graph
# this differs from PyTorch and ONNX.
client.set_model_from_file(
    "keras_fcn", model_path, "TF", device="CPU", inputs=inputs, outputs=outputs
)

# put random random input tensor into the database
input_data = np.random.rand(1, 28, 28).astype(np.float32)
client.put_tensor("input", input_data)

# run the Fully Connected Network model on the tensor we just put
# in and store the result of the inference at the "output" key
client.run_model("keras_fcn", "input", "output")

# get the result of the inference
pred = client.get_tensor("output")
print(pred)
[[0.05032112 0.06484107 0.03512685 0.14747524 0.14440396 0.02395445
  0.03395916 0.06222691 0.26738793 0.1703033 ]]

Using ONNX#

ONNX is a standard format for representing models. A number of different Machine Learning Libraries are supported by ONNX and can be readily used with SmartSim.

Some popular ones are:

As well as some that are not listed. There are also many tools to help convert models to ONNX.

And PyTorch has its own converter.

Below are some examples of a few models in Scikit-learn that are converted into ONNX format for use with SmartSim. To use ONNX in SmartSim, specify ONNX as the argument for backend in the call to client.set_model or client.set_model_from_file

Scikit-Learn K-means Cluster#

K-means clustering is an unsupervised ML algorithm. It is used to categorize data points into functional groups (“clusters”). Scikit Learn has a built in implementation of K-means clustering and it is easily converted to ONNX for use with SmartSim through skl2onnx.to_onnx

Since the KMeans model returns two outputs, we provide the client.run_model call with two output key names.

[14]:
from skl2onnx import to_onnx
from sklearn.cluster import KMeans
[15]:

X = np.arange(20, dtype=np.float32).reshape(10, 2) tr = KMeans(n_clusters=2) tr.fit(X) # save the trained k-means model in memory with skl2onnx kmeans = to_onnx(tr, X, target_opset=11) model = kmeans.SerializeToString() # random input data sample = np.arange(20, dtype=np.float32).reshape(10, 2) # use the same client from TensorFlow and Pytorch examples. client.put_tensor("input", sample) client.set_model("kmeans", model, "ONNX", device="CPU") client.run_model("kmeans", inputs="input", outputs=["labels", "transform"]) print(client.get_tensor("labels"))
[1 1 1 1 1 0 0 0 0 0]

Scikit-Learn Random Forest#

The Random Forest example uses the Iris dataset from Scikit Learn to train a RandomForestRegressor. As with the other examples, the skl2onnx function skl2onnx.to_onnx is used to convert the model to ONNX format.

[16]:
from sklearn.datasets import load_iris
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import train_test_split
[17]:
iris = load_iris()
X, y = iris.data, iris.target
X_train, X_test, y_train, _ = train_test_split(X, y, random_state=13)
clr = RandomForestRegressor(n_jobs=1, n_estimators=100)
clr.fit(X_train, y_train)

rf_model = to_onnx(clr, X_test.astype(np.float32), target_opset=11)

sample = np.array([[6.4, 2.8, 5.6, 2.2]]).astype(np.float32)
model = rf_model.SerializeToString()

client.put_tensor("input", sample)
client.set_model("rf_regressor", model, "ONNX", device="CPU")
client.run_model("rf_regressor", inputs="input", outputs="output")
print(client.get_tensor("output"))
[[1.9999987]]
[18]:
exp.stop(db)
[19]:
exp.summary(style="html")
[19]:
Name Entity-Type JobID RunID Time Status Returncode
0 orchestrator_0DBNode 31857 0 32.7161Cancelled0

Colocated Deployment#

A colocated Orchestrator is a special type of Orchestrator that is deployed on the same compute hosts an a Model instance defined by the user. In this deployment, the database is not connected together in a cluster and each shard of the database is addressed individually by the processes running on that compute host. This is particularly important for GPU-intensive workloads which require frequent communication with the database.

lattice

[20]:
# create colocated model
colo_settings = exp.create_run_settings(
    exe="python",
    exe_args="./colo-db-torch-example.py"
)

colo_model = exp.create_model("colocated_model", colo_settings)
colo_model.colocate_db_tcp(
    port=6780,
    db_cpus=1,
    debug=False,
    ifname="lo"
)
[21]:
exp.start(colo_model, summary=True)
21:18:06 C02G13RYMD6N SmartSim[30945] INFO

=== Launch Summary ===
Experiment: Inference-Tutorial
Experiment Path: /Users/smartsim/smartsim/tutorials/ml_inference/Inference-Tutorial
Launcher: local
Models: 1
Database Status: inactive

=== Models ===
colocated_model
Executable: /Users/smartsim/venv/bin/python
Executable Arguments: ./colo-db-torch-example.py
Co-located Database: True



21:18:09 C02G13RYMD6N SmartSim[30945] INFO colocated_model(31865): Completed
[22]:
exp.summary(style="html")
[22]:
Name Entity-Type JobID RunID Time Status Returncode
0 orchestrator_0 DBNode 31857 0 32.7161Cancelled0
1 colocated_modelModel 31865 0 3.5862 Completed0