<jupyter_start><jupyter_text>Deploying a Multi-Stage RecSys into Production with Merlin Systems and Triton Inference ServerThis notebook is created using the latest stable [merlin-tensorflow-inference](https://catalog.ngc.nvidia.com/orgs/nvidia/teams/merlin/containers/merlin-tensorflow-inference/tags) container. At this point, when you reach out to this notebook, we expect that you have already executed the first notebook `01-Building-Recommender-Systems-with-Merlin.ipynb` and exported all the required files and models. We are going to generate recommended items for a given user query (user_id) by following the steps described in the figure below. ![tritonensemble](../images/triton_ensemble.png) Merlin Systems library have the set of operators to be able to serve multi-stage recommender systems built with Tensorflow on [Triton Inference Server](https://github.com/triton-inference-server/server)(TIS) easily and efficiently. Below, we will go through these operators and demonstrate their usage in serving a multi-stage system on Triton.  Import required libraries and functions At this step, we assume you already installed the tensorflow-gpu, feast and faiss-gpu libraries when running the first notebook `01-Building-Recommender-Systems-with-Merlin.ipynb`. In case you need to install them, execute the following script in a cell.```%pip install tensorflow "feast<0.20" faiss-gpu```<jupyter_code>import os
import numpy as np
import pandas as pd
import feast
import faiss
import seedir as sd
from nvtabular import ColumnSchema, Schema

from merlin.systems.dag.ensemble import Ensemble
from merlin.systems.dag.ops.session_filter import FilterCandidates
from merlin.systems.dag.ops.softmax_sampling import SoftmaxSampling
from merlin.systems.dag.ops.tensorflow import PredictTensorflow
from merlin.systems.dag.ops.unroll_features import UnrollFeatures
from merlin.systems.triton.utils import run_triton_server, run_ensemble_on_tritonserver<jupyter_output><empty_output><jupyter_text>Register our features on feature store The Feast feature registry is a central catalog of all the feature definitions and their related metadata(read more [here](https://docs.feast.dev/getting-started/architecture-and-components/registry)). We have defined our user and item features definitions in the `user_features.py` and  `item_features.py` files. With FeatureView() users can register data sources in their organizations into Feast, and then use those data sources for both training and online inference. In the `user_features.py` and `item_features.py` files, we are telling Feast where to find user and item features.Before we move on to the next steps, we need to perform `feast apply`command as directed below.  With that, we register our features, we can apply the changes to create our feature registry and store all entity and feature view definitions in a local SQLite online store called `online_store.db`.<jupyter_code>BASE_DIR = os.environ.get("BASE_DIR", "/Merlin/examples/Building-and-deploying-multi-stage-RecSys/")

# define feature repo path
feast_repo_path = BASE_DIR + "feature_repo/"
%cd $feast_repo_path
!feast apply<jupyter_output>/Merlin/examples/Building-and-deploying-multi-stage-RecSys/feature_repo
/usr/local/lib/python3.8/dist-packages/feast/feature_view.py:100: DeprecationWarning: The argument 'input' is being deprecated. Please use 'batch_source' instead. Feast 0.13 and onwards will not support the argument 'input'.
  warnings.warn(
Created entity [1m[32mitem_id[0m
Created entity [1m[32muser_id[0m
Created feature view [1m[32mitem_features[0m
Created feature view [1m[32muser_features[0m

Created sqlite table [1m[32mfeature_repo_item_features[0m
Created sqlite table [1m[32mfeature_repo_user_features[0m<jupyter_text>Loading features from offline store into an online store After we execute `apply` and registered our features and created our online local store, now we need to perform [materialization](https://docs.feast.dev/how-to-guides/running-feast-in-production) operation. This is done to keep our online store up to date and get it ready for prediction. For that we need to run a job that loads feature data from our feature view sources into our online store. As we add new features to our offline stores, we can continuously materialize them to keep our online store up to date by finding the latest feature values for each user. When you run the `feast materialize ..` command below, you will see a message Materializing 2 feature views from 1995-01-01 01:01:01+00:00 to 2025-01-01 01:01:01+00:00 into the sqlite online store   will be printed out.Note that materialization step takes some time..<jupyter_code>!feast materialize 1995-01-01T01:01:01 2025-01-01T01:01:01<jupyter_output>Materializing [1m[32m2[0m feature views from [1m[32m1995-01-01 01:01:01+00:00[0m to [1m[32m2025-01-01 01:01:01+00:00[0m into the [1m[32msqlite[0m online store.

[1m[32mitem_features[0m:
100%|███████████████████████████████████████████████████████████| 448/448 [00:00<00:00, 4689.41it/s]
[1m[32muser_features[0m:
100%|███████████████████████████████████████████████████████████| 458/458 [00:00<00:00, 1477.71it/s]<jupyter_text>Now, let's check our feature_repo structure again after we ran `apply` and `materialize` commands.<jupyter_code># set up the base dir to for feature store
feature_repo_path = os.path.join(BASE_DIR, 'feature_repo')
sd.seedir(feature_repo_path, style='lines', itemlimit=10, depthlimit=5, exclude_folders=['.ipynb_checkpoints', '__pycache__'], sort=True)<jupyter_output>feature_repo/
├─__init__.py
├─data/
│ ├─item_features.parquet
│ ├─online_store.db
│ ├─registry.db
│ └─user_features.parquet
├─feature_store.yaml
├─item_features.py
└─user_features.py<jupyter_text>Set up Faiss index, create feature store client and objects for the Triton ensemble Create a folder for faiss index path<jupyter_code>if not os.path.isdir(os.path.join(BASE_DIR + 'faiss_index')):
    os.makedirs(os.path.join(BASE_DIR + 'faiss_index'))<jupyter_output><empty_output><jupyter_text>Define paths for ranking model, retrieval model, and faiss index path<jupyter_code>faiss_index_path = BASE_DIR + 'faiss_index' + "/index.faiss"
retrieval_model_path = BASE_DIR + "query_tower/"
ranking_model_path = BASE_DIR + "dlrm/"<jupyter_output><empty_output><jupyter_text>Create a request schema that we are going to use when sending a request to Triton Infrence Server (TIS).<jupyter_code>request_schema = Schema(
    [
        ColumnSchema("user_id", dtype=np.int32),
    ]
)<jupyter_output><empty_output><jupyter_text>`QueryFaiss` operator creates an interface between a FAISS Approximate Nearest Neighbors (ANN) Index and Triton Infrence Server. For a given input query vector, we do an ANN search query to find the ids of top-k nearby nodes in the index.`setup_faiss` is  a utility function that will create a Faiss index from an embedding vector with using L2 distance.<jupyter_code>from merlin.systems.dag.ops.faiss import QueryFaiss, setup_faiss 

item_embeddings = np.ascontiguousarray(
    pd.read_parquet(BASE_DIR + "item_embeddings.parquet").to_numpy()
)
setup_faiss(item_embeddings, faiss_index_path)<jupyter_output>WARNING clustering 448 points to 32 centroids: please provide at least 1248 training points<jupyter_text>Create feature store client.<jupyter_code>feature_store = feast.FeatureStore(feast_repo_path)<jupyter_output><empty_output><jupyter_text>Fetch user features with `QueryFeast` operator from the feature store. `QueryFeast` operator is responsible for ensuring that our feast feature store can communicate correctly with tritonserver for the ensemble feast feature look ups.<jupyter_code>from merlin.systems.dag.ops.feast import QueryFeast 

user_features = ["user_id"] >> QueryFeast.from_feature_view(
    store=feature_store,
    view="user_features",
    column="user_id",
    include_id=True,
)<jupyter_output>/systems/merlin/systems/dag/ops/feast.py:15: DeprecationWarning: `np.float` is a deprecated alias for the builtin `float`. To silence this warning, use `float` by itself. Doing this will not modify any behavior and is safe. If you specifically wanted the numpy scalar type, use `np.float64` here.
Deprecated in NumPy 1.20; for more details and guidance: https://numpy.org/devdocs/release/1.20.0-notes.html#deprecations
  ValueType.FLOAT: (np.float, False, False),<jupyter_text>Retrieve top-K candidate items using `retrieval model` that are relevant for a given user. We use `PredictTensorflow()` operator that takes a tensorflow model and packages it correctly for TIS to run with the tensorflow backend.<jupyter_code># prevent TF to claim all GPU memory
from merlin.models.loader.tf_utils import configure_tensorflow

configure_tensorflow()
topk_retrieval = 100
retrieval = (
    user_features
    >> PredictTensorflow(retrieval_model_path)
    >> QueryFaiss(faiss_index_path, topk=topk_retrieval)
)<jupyter_output>2022-05-05 14:38:32.696154: I tensorflow/core/platform/cpu_feature_guard.cc:151] This TensorFlow binary is optimized with oneAPI Deep Neural Network Library (oneDNN) to use the following CPU instructions in performance-critical operations:  AVX2 AVX512F FMA
To enable them in other operations, rebuild TensorFlow with the appropriate compiler flags.
2022-05-05 14:38:34.750160: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1525] Created device /job:localhost/replica:0/task:0/device:GPU:0 with 30667 MB memory:  -> device: 0, name: Quadro GV100, pci bus id: 0000:15:00.0, compute capability: 7.0
2022-05-05 14:38:34.750858: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1525] Created device /job:localhost/replica:0/task:0/device:GPU:1 with 30315 MB memory:  -> device: 1, name: Quadro GV100, pci bus id: 0000:2d:00.0, compute capability: 7.0
05/05/2022 02:38:36 PM WARNING:No training configuration found in save file, so the model was *not* compiled. Compile it manually.<jupyter_text>Fetch item features for the candidate items that are retrieved from the retrieval step above from the feature store.<jupyter_code>item_features = retrieval["candidate_ids"] >> QueryFeast.from_feature_view(
    store=feature_store,
    view="item_features",
    column="candidate_ids",
    output_prefix="item",
    include_id=True,
)<jupyter_output><empty_output><jupyter_text>Merge the user features and items features to create the all set of combined features that were used in model training using `UnrollFeatures` operator which takes a target column and joins the "unroll" columns to the target. This helps when broadcasting a series of user features to a set of items.<jupyter_code>user_features_to_unroll = [
    "user_id",
    "user_shops",
    "user_profile",
    "user_group",
    "user_gender",
    "user_age",
    "user_consumption_2",
    "user_is_occupied",
    "user_geography",
    "user_intentions",
    "user_brands",
    "user_categories",
]

combined_features = item_features >> UnrollFeatures(
    "item_id", user_features[user_features_to_unroll]
)<jupyter_output><empty_output><jupyter_text>Rank the combined features using the trained ranking model, which is a DLRM model for this example. We feed the path of the ranking model to `PredictTensorflow()` operator.<jupyter_code>ranking = combined_features >> PredictTensorflow(ranking_model_path)<jupyter_output><empty_output><jupyter_text>For the ordering we use `SoftmaxSampling()` operator. This operator sorts all inputs in descending order given the input ids and prediction introducing some randomization into the ordering by sampling items from the softmax of the predicted relevance scores, and finally returns top-k ordered items.<jupyter_code>top_k=10
ordering = combined_features["item_id"] >> SoftmaxSampling(
    relevance_col=ranking["output_1"], topk=top_k, temperature=20.0
)<jupyter_output><empty_output><jupyter_text>Export Graph as EnsembleThe last step is to create the ensemble artifacts that TIS can consume. To make these artifacts import the Ensemble class. This class  represents an entire ensemble consisting of multiple models that run sequentially in TIS initiated by an inference request. It is responsible with interpreting the graph and exporting the correct files for TIS.When we create an Ensemble object we feed the graph and a schema representing the starting input of the graph.  After we create the ensemble object, we export the graph, supplying an export path for the `ensemble.export()` function. This returns an ensemble config which represents the entire inference pipeline and a list of node-specific configs. Create the folder to export the models and config files.<jupyter_code>if not os.path.isdir(os.path.join(BASE_DIR + 'poc_ensemble')):
    os.makedirs(os.path.join(BASE_DIR + 'poc_ensemble'))
# define the path where all the models and config files exported to
export_path = os.path.join(BASE_DIR + 'poc_ensemble')

ensemble = Ensemble(ordering, request_schema)
ens_config, node_configs = ensemble.export(export_path)<jupyter_output><empty_output><jupyter_text>Let's check our export_path structure<jupyter_code>sd.seedir(export_path, style='lines', itemlimit=10, depthlimit=5, exclude_folders=['.ipynb_checkpoints', '__pycache__'], sort=True)<jupyter_output>poc_ensemble/
├─0_queryfeast/
│ ├─1/
│ │ └─model.py
│ └─config.pbtxt
├─1_predicttensorflow/
│ ├─1/
│ │ └─model.savedmodel/
│ │   ├─assets/
│ │   ├─keras_metadata.pb
│ │   ├─saved_model.pb
│ │   └─variables/
│ │     ├─variables.data-00000-of-00001
│ │     └─variables.index
│ └─config.pbtxt
├─2_queryfaiss/
│ ├─1/
│ │ ├─index.faiss/
│ │ │ └─index.faiss
│ │ └─model.py
│ └─config.pbtxt
├─3_queryfeast/
│ ├─1/
│ │ └─model.py
│ └─config.pbtxt
├─4_unrollfeatures/
│ ├─1/
│ │ └─model.py
│ └─config.pbtxt
├─5_predicttensorflow/
│ ├─1/
│ │ └─model.savedmodel/
│ │   ├─assets/
│ │   ├─keras_metadata.pb
│ │   ├─saved_model.pb
│ │   └─variables/
│ │     ├─variables.data-00000-of-00001
│ │     └─variables.index
│ └─config.pbtxt
├─6_softmaxsampling/
│ ├─1/
│ │ └─model.py
│ └─config.pbtxt
└─ensemble_model/
  ├─1/
  └─config.pbtxt<jupyter_text>Starting Triton Server It is time to deploy all the models as an ensemble model to Triton Inference Serve [TIS](https://github.com/triton-inference-server). After we export the ensemble, we are ready to start the TIS. You can start triton server by using the following command on your terminal:```tritonserver --model-repository=/ensemble_export_path/ --backend-config=tensorflow,version=2```For the `--model-repository` argument, specify the same path as the `export_path` that you specified previously in the `ensemble.export` method. This command will launch the server and load all the models to the server. Once all the models are loaded successfully, you should see `READY` status printed out in the terminal for each loaded model.  Retrieving Recommendations from Triton Once our models are successfully loaded to the TIS, we can now easily send a request to TIS and get a response for our query with `send_triton_request` utility function.<jupyter_code>from merlin.systems.triton.utils import send_triton_request
from merlin.core.dispatch import make_df

# create a request to be sent to TIS
request = make_df({"user_id": [1]})
request["user_id"] = request["user_id"].astype(np.int32)

outputs = ensemble.graph.output_schema.column_names
response = send_triton_request(request, outputs)
response<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.09860071591278881, 'fraction_numerical': 0.02753010087862024, 'mean_word_length': 5.784105960264901, 'pattern_counts': {'":': 1, '<': 69, '<?xml version=': 0, '>': 82, 'https://': 6, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '18', 'dir': 'jupyter-structured-clean-dedup', 'id': '518622', 'license': 'Apache-2.0', 'n_tokens_mistral': 5239, 'n_tokens_neox': 4814, 'n_words': 1597, 'path': 'examples/Building-and-deploying-multi-stage-RecSys/02-Deploying-multi-stage-RecSys-with-Merlin-Systems.ipynb', 'repo_name': 'mikemckiernan/Merlin', 'size': '15365'}